---
id: beacon-chat
type: project
title: Beacon Chat
url: https://auejin.com/ko/projects/beacon-chat/
lang: ko
alternate:
  en: https://auejin.com/en/projects/beacon-chat/
updated: '2026-08-11'
---

# Beacon Chat

*대화를 통해 인용 자료의 신뢰도를 능동적으로 끌어올리는 LLM 챗봇*

2026.08

- **무엇** — 건설 현장을 3D로 찍어 관리하는 SaaS 위에 올린 LLM 챗 서버와 패널 UI다.
- **왜** — "3층 안전 지적이 몇 건이냐"는 물음에는 숫자 하나로 공사가 멈추고 정산이 갈린다.
- **어떻게** — 검색 결과가 전수인지 표본인지를 코드가 먼저 등급으로 판정하고, 그 등급이 모델이 쓸 수 있는 문장의 형태를 제한한다. 등급이 낮으면 되물어 범위를 좁힌다.
- **새로움** — 모델이 확신을 흉내 내는 대신 대화로 확신을 얻는다. 표본 인용이 전수 인용으로 승격되는 경로가 대화 안에 있다.

## 역할 분담

챗 서버는 단독으로 구현했고, 제품 전체는 **기술 PM(TPM)** 으로 세 팀과 함께 이끌었다.

| 범위 | 담당 |
|---|---|
| 챗 서버 · 패널 UI · 에이전트 오케스트레이션 하네스 | 함어진 (단독) |
| MCP 서버 | Andrew (Algorithm 팀) |
| AWS Lambda 배포 환경 구축 | Jerry · Lyva (Backend 팀) |

## 요약

#### 모든 수치에 확정 등급을 매기고, 역질문으로 범위를 좁혀 **"최소 N건"을 "정확히 N건"으로, 표본 인용을 전수 인용으로** 승격시키는 건설 현장 검색 시스템

## **10장을 보여주면서 "10장을 찾았다"고 말하지 않는 챗봇입니다.**

건설 현장에서 "3층 안전 지적사항이 몇 건이냐"는 잡담이 아닙니다. 그 숫자로 작업이 중지되고 하도급 정산이 갈립니다.

그런데 LLM 챗봇이 숫자를 틀리는 가장 흔한 경로는 환각이 아닙니다. **아무도 거짓말하지 않았는데 결과적으로 거짓말이 되는 경로**가 따로 있습니다. 실제로 측정해 보니 이랬습니다.

1. 검색 엔진은 "조건에 맞는 게 전부 몇 건인지"를 응답에 담아 보냅니다.
2. 그 앞의 MCP 서버가 파싱 과정에서 그 필드를 버립니다. 그러고는 잘라내고 남은 개수에 `Total:`이라는 라벨을 붙입니다.
3. 우리 챗 서버는 모델이 요청한 개수가 10을 넘으면 조용히 10으로 줄입니다.
4. 모델은 받은 10건을 세어 "10건을 찾았습니다"라고 답합니다.

사용자는 12장을 요청했고, 10장을 봤고, 그게 전부라고 믿습니다.

이 프로젝트는 그 사슬을 **등급 체계**로 끊고, 거기서 한 걸음 더 나가 **대화로 등급을 올립니다.**

---

## 무엇을 만들었나

건설 현장을 3D로 촬영해 관리하는 B2B SaaS 위에 올린 **LLM 챗 서버(TypeScript)** 와, 두 개의 앱에 붙여 쓰는 **패널 UI(Angular)** 입니다. 사용자는 촬영된 현장 데이터를 자연어로 검색합니다.

모델은 **문장만 씁니다.** 개수·부재·실패에 대한 판단은 전부 코드가 합니다.

## 어디까지 검증했나

사내 데모를 3단계(CLI → 브라우저 → 전체 UI)로 나눠, dev·qa **실제 배포본에 실계정·실토큰으로** 왕복시켜 통과시켰습니다. 마지막 단계는 기능 구현과 내부 검증을 마쳤고 지금은 UI/UX를 다듬고 있습니다.

**아직 프로덕션 사용자 트래픽은 없습니다.** 이 문서의 어떤 숫자도 사용자 규모를 주장하지 않습니다.

## 규모

> **테스트 코드가 소스 코드의 2.14배** — 서버 소스 25,283줄 vs 테스트 54,227줄 (약 2,817 케이스)

패널 UI 10,284줄 · 브라우저 e2e 16개 · 상태 전이표 **116행**(금지된 전이 포함) · CLI 재현 스크립트 17개 · 외부 계약 경계 **103개** 등록 · **31일간 18번** 릴리스.

## 내가 한 일

서버·패널·검증 도구를 설계하고 구현했습니다. 그리고 **코드를 고칠 권한이 없는 외부 팀의 API를 측정 리포트로 두 번 바꿨습니다.**

---

## 흔한 LLM 챗봇과 뭐가 다른가

| # | 흔한 구현 (ChatGPT 클론 · RAG 튜토리얼 · MCP 툴 호출 데모) | 이 프로젝트 |
|---|---|---|
| 1 | 모델이 **받은 배열 길이를 세어** "N개를 찾았습니다"라고 답한다 | 말할 후보가 되는 모든 숫자를 **말하기 전에** 4등급(정확 / 하한 / 표시분 / 불명)으로 판정한다. 등급마다 쓸 수 있는 문장 형태가 정해져 있고, **불명이면 숫자를 아예 못 쓴다** |
| 2 | 애매하면 되묻는다 (프롬프트에 그렇게 써 둔다) | 되묻기가 **등급을 올리는 연산**이다. 좁힌 조건이 한 페이지에 들어가고 그 뒤가 없다는 게 확인되면, 화면의 카드가 **표본에서 전수로** 바뀌고 등급이 올라간다 |
| 3 | 검색 결과가 비면 "없습니다"라고 답한다 | **"없다"고 말하려면 자격이 필요하다.** 서버가 집계 정보를 붙여 "이건 내 검색 결과다"라고 표시한 응답에서만 부재를 주장한다. 자격이 없으면 **아무 말도 안 한다** |
| 4 | 툴이 실패했는지를 `isError` 하나로 판단한다 | HTTP 200으로 **위장해서 오는 실패**를 잡아낸다. 판단 근거는 에러 문구가 아니라 **정상 응답이면 반드시 붙는 메타데이터가 없다는 사실**이다 — 그래서 상대가 문구를 바꿔도 안 깨진다 |
| 5 | 권한을 시스템 프롬프트로 지시한다 | 툴을 **호출하기 전에** 막는다. 데이터가 아예 안 오니까 유출이 구조적으로 불가능하다. 게다가 플래너는 라이선스 관련 단어를 **일부러 모른다**(프롬프트에 그 단어가 있으면 테스트가 깨진다). 이 축에만 전용 테스트 14개 파일 · 131 케이스 |
| 6 | happy path 스크린샷이 데모 검증의 전부다 | **금지된 전이까지 표로 적은** 116행 + CLI 재현 17개 + 브라우저 e2e 16개. 게다가 **e2e가 붙는 가짜 서버 자체를 계약 테스트**로 검증한다 |
| 7 | 혼자 만들고 혼자 배포한다 | 외부 팀 API를 측정 리포트로 **두 번 바꿨다**(백엔드 조회 상한 30→200, 커서 페이지네이션 도입 · 도달 가능한 결과 16→38). 받은 구현이 요청과 다른 부분은 우리 쪽에서 방어 조건으로 막았다 |

---

## 대표 사례 셋

**① 검색 서버가 "이게 끝"이라고 해도 우리 코드가 그 말을 안 믿는다.**
데이터 조회 툴은 자연어를 SQL로 바꿔 실행하는데, 그때 `LIMIT 3000`을 자동으로 붙입니다. 결과가 3,000건에 딱 차면 전체 개수도 3,000으로 고정되고, 페이지를 다 넘겼을 때 "더 없음"이라고 답합니다. 파노라마가 7,961장인 프로젝트에서 실제로 그랬습니다. **서버가 자신 있게 틀린 겁니다.** 우리 판정 모듈은 이 포화를 감지해서 전체 개수를 "하한값"으로 다시 분류하고 "더 있음"으로 뒤집습니다. 이 재분류가 어느 쪽으로도 거짓이 되지 않는다는 증명을 코드 주석에 적어 뒀습니다.

**② 테스트는 계속 초록불인데 한 번도 동작한 적 없는 경로가 있었다.**
데모를 처음 돌렸더니 시나리오 4개가 전부 실패했습니다. 최근 회귀를 의심하고 커밋 이력을 문자열 단위로 뒤져 보니(`git log -S`), **릴리스 태그 6개 전부에 같은 결함이 있었습니다.** 회귀가 아니라 처음부터 죽어 있던 코드였죠. 그런데 그 경로 위의 작업 항목 21장이 이미 완료 처리돼 있었고, **규칙은 전부 지켜진 상태**였습니다(머지 검증 100% 통과). 그래서 버그를 고치는 대신 파이프라인을 바꿨습니다 — 양쪽을 이어 붙여 돌리는 테스트를 의무화하고, 계약 점검이 오래됐으면 릴리스를 거부하게 하고, 외부 계약 경계 103개를 목록으로 고정하고, **"게이트를 만들 때 그걸 누가 부르는지 같은 커밋에서 답하라"** 는 규칙을 세웠습니다. 이 규칙을 기존 자산에 적용하자마자 **죽어 있던 게이트 3개가 바로 나왔습니다.** 그중 하나는 배포 전 스모크 테스트였는데, 실행 런타임이 설치돼 있지 않아 **exit 127**로 죽으면서 clean install한 사람에게는 한 번도 돌지 않고 있었습니다.

**③ 실패가 성공인 척하고 도착했다.**
검색 API가 502로 죽었는데, 그게 `isError: false` + 평범한 텍스트 한 덩어리로 왔습니다. 우리 서버의 실패 감지기가 `isError` 하나뿐이라 **모델이 에러 문자열을 데이터로 받아서 그 위에 답을 지어냈고**, 사용자에게는 실패 신호가 아무 데도 안 떴습니다. 해법은 "0건이면 실패"가 아니었습니다 — 반대로 측정해 보니 **진짜 0건 응답에도 메타데이터가 붙어 있었기** 때문입니다. 그래서 판별 기준을 **"정상 응답이면 반드시 붙는 메타데이터가 없다"** 로 잡았고, 에러 문구 자체는 판정에 쓰지 않습니다. 상대가 문구를 바꿔도 안 깨지게요.

---

## 한계

- **응답 속도 목표 미달.** 목표는 첫 토큰까지 P95 3.5초인데, 실측은 콜드 스타트 약 30초, 웜 상태 20초 이내입니다. 원인의 절반은 아직 안 만든 모델 입력 압축이고, 그게 다음 우선순위입니다.
- **프로덕션 지표가 없습니다.** 계측 코드(메트릭 발행, 데이터 웨어하우스 적재)는 다 짜 뒀지만 집계할 트래픽이 아직 없습니다. 사용자 규모·에러율·비용은 **일부러 비워 뒀습니다.**
- **프롬프트로 지시한 규칙은 지켜진다는 보장이 없습니다.** 모델에게 지시하는 형태의 규칙은 "지시문이 프롬프트에 들어갔는지"만 테스트할 수 있습니다. 실제로 모델이 지시를 어긴 사례를 측정했고, 사용자에게 보이는 결과를 구한 건 프롬프트가 아니라 UI 쪽 중복 제거 코드였습니다.
- **권한 전수 테스트는 아직 안 만들었습니다.** 설계상 명세한 120셀 전수 검증은 미착수고, 지금은 14개 파일 · 131 케이스의 부분 커버리지입니다.

---

---

> **표기**: 코드명 Beacon · 2026-08 · 회사·제품·인명·내부 호스트는 일반화했고, 코드 심볼과 수치는 실제 값입니다.

상세 포트폴리오: https://auejin.com/ko/projects/beacon-chat/portfolio/

## 관련

- [소프트웨어 엔지니어 @ 주식회사 큐픽스](https://auejin.com/ko/experience/cupix/)
