
이 글은 AI 리더 제로 환각 Q&A의 엔지니어링 구현을 공유합니다. 답변은 현재 책 원문에 엄격히 기반하며, 핵심 논지는 한 번의 클릭으로 구체적인 단락까지 추적할 수 있습니다. AI 독서, 문서 QA, RAG 유형 애플리케이션을 개발 중이라면, 세 번의 반복을 거친 경험과 최종 아키텍처가 참고가 되길 바랍니다.
1. 실천 과정: 세 단계의 진화
제로 환각 Q&A는 처음부터 완벽하게 설계된 것이 아닙니다. 비용, 지연 시간, 정확도 사이의 긴장 속에서 점진적으로 진화했습니다. 아래는 시간 순서대로 세 단계를 돌아보며, 현재 아키텍처가 왜 이렇게 되었는지 이해하기 쉽게 정리한 내용입니다.
1단계: 전문을 Context에 직접 삽입 (가장 단순하지만 가장 먼저 문제가 드러남)
방법: 사용자가 책을 열고 질문할 때, 추출한 전체 본문을 System Prompt 또는 User 메시지에 넣고 대화 모델에 답변을 요청합니다. 책 전체가 약 40만 문자를 초과하면 하드 트렁케이션—앞부분만 남기고 이후 장은 모델에 보이지 않습니다.
장점:
- 구현 비용이 매우 낮고, 거의 전처리가 필요 없음;
- 짧은 책이나 구조가 단순한 문서에서는 효과가 괜찮음—모델이 실제로 「책 전체」를 「봤음」;
- 상호작용이 단순함: 질문하면 바로 답변, 「분석을 기다려 주세요」 같은 대기 상태 없음.
단점 (곧 용납하기 어려워짐):
- 응답이 느림: 매 질문마다 방대한 텍스트를 모델에 전송, 첫 Token 지연과 총 소요 시간이 책 길이에 비례해 악화;
- Token 비용이 높음: 같은 책에 질문할 때마다 전문 입력 비용을 반복 지불;
- 장편에서 심각한 왜곡: 40만 문자 초과 후 잘림, 후반부·부록·결론 장은 존재하지 않는 것과 같고, UI는 종종 잘림을 명확히 알리지 않음;
- 검색 세분화가 제로: 모델이 수십만 자 가운데 「바늘 찾기」를 해야 함—세부를 놓치기 쉽고, 그럴듯하지만 근거 없는 요약을 만들기 더 쉬움—독서 시나리오에서 가장 피해야 할 환각.
1단계는 MVP 검증에는 적합하지만, 제품 수준 방안으로는 부적합합니다.
2단계: 경량 LLM으로 핵심 문장 추출 (Context 압축, 하지만 너무 과하게 압축)
방법: 질문 전(또는 책을 처음 열 때) 비용이 더 낮은 모델로 본문을 한 번 전처리: Spine 기준으로 장별(또는 책 전체를 분할) 핵심 문장을 추출하고, 출력 시 [f파일-시작-끝] 형식의 위치 마커를 유지한 뒤, 발췌문을 짧은 텍스트로 이어 붙여 이후 Q&A의 Context로 사용.
전형적인 파이프라인은 Extract → Cache → Chat: 먼저 오프라인 또는 필요 시 추출을 실행하고 DB에 저장, 이후 매 질문마다 같은 「핵심 문장 모음」을 재사용. 많은 문서 QA 프로토타입의 「문서를 먼저 압축하고, 압축 결과로 QA」와 같은 사고이며, 2단계에서 실제로 사용했던 경로입니다.
장점:
- 매 질문마다 모델에 보내는 텍스트가 눈에 띄게 짧아짐, 1단계 대비 단일 Token 소비가 크게 감소;
- 전처리 결과를 캐시할 수 있어, 같은 책에 매번 추출을 다시 할 필요 없음;
- 위치 마커를 도입하여 이후 추적의 기반을 마련.
단점 (장편 시나리오에서도 여전히 버티기 어려움):
- 세부 정보가 대량 손실: 「핵심 문장」은 모델이 주관적으로 선별, 논증 사슬의 한정 조건·반례 등이 쉽게 빠짐, 답변이 「맞지만 편향적」이 되기 쉬움;
- 장편에서 Context가 여전히 큼: 대작이라도 핵심 문장만 남겨도 이어 붙인 입력은 여전히 상당함, 지연과 비용은 완화될 뿐 근본 해결은 아님;
- 이중 LLM 오차: 추출 단계에서 누락, Q&A 단계에서 발췌 오독—오류가 누적;
- 정적 Context: 사용자가 특정 장 세부를 묻든 전체 구조를 묻든, 모델에 들어가는 것은 동일한 사전 추출 텍스트—질문에 따라 동적으로 범위를 좁힐 수 없음.
이 단계의 교훈은 분명합니다: **문제는 「압축했느냐」가 아니라 「압축이 필요에 따라 이루어지고, 원문으로 돌아갈 수 있느냐」**입니다.
3단계: 세그먼트 인덱스 + Tool 필요 시 검색 + 원문 반환 (현재 방안)
방법: 기본 사상은 PageIndex를 참고했으며, 2단계 대비 핵심 변화는 세 가지입니다:
- 전처리 산출물은 구조화된 인덱스(목차 수준 요약 + 정확한 문자 span)이며, 발췌문을 Q&A Context로 직접 쓰지 않음;
- 매 질문마다 모델이 Tool Calling으로 필요 시 검색한 뒤, 위치 마커가 붙은 원문을 가져와 답변;
- System Prompt와 프론트엔드 연동으로 인용 형식을 제약하고, 클릭 시 각주로 이동·원문 하이라이트를 지원.
세 단계 비교:
| 차원 | 1단계 (전문 직접 삽입) | 2단계 (핵심 문장 추출) | 3단계 (현재) |
|---|---|---|---|
| 단일 질문 Context | 책 전체 (또는 잘린 앞부분) | 사전 추출 핵심 문장 모음 | 질문과 관련된 소량 원문 조각만 |
| 장편 정확도 | 40만 문자 초과 후 심각히 하락 | 추출 품질에 의존, 세부 손실 | 목차/span 검색, 전체 길이 하드 트렁케이션 없음 |
| 응답 속도 | 느림 | 다소 개선, 장편은 여전히 느림 | 검색 + 짧은 Context, 눈에 띄게 빠름 |
| Token 비용 | 매우 높음 | 중간~높음 | 전처리 분산 + 필요 시 과금 |
| 추적 능력 | 약함 (출처 표기 어려움) | 위치 마커 있으나 내용은 2차 선별 | 각주가 실제 원문 span에 대응 |
| 엔지니어링 복잡도 | 낮음 | 중간 | 높음 |
3단계에서 멈춘 이유: 독서 시나리오의 제로 환각에서 핵심은 「모델이 최대한 많은 글자를 보게 하는 것」이 아니라 **「답변 전에 질문과 관련된 원문 증거를 반드시 확보하는 것」**입니다. 1·2단계는 Context 용량을 다루었고, 3단계는 파이프라인을 **「인덱스(전처리) → 검색(Tool) → 증거 수집(원문) → 답변(제약 생성)」**으로 분리하여 정확도·비용·추적 가능성을 동시에 만족합니다.
아래에서는 3단계 구현 세부를 펼칩니다.
2. 문제 정의: 독서 시나리오에서 환각은 일반 Chat보다 더 치명적
일반 ChatBot의 가끔 오류는 사용자가 종종 용납합니다. 책 QA에서는 환각의 대가가 더 큽니다:
- 사용자는 이 책이 무엇이라고 했는지 묻는 것이지, 모델의 parametric memory를 묻는 것이 아님;
- 그럴듯한 「책의 관점」 한 마디가 노트·인용·2차 전파를 오도할 수 있음;
- 출처가 없으면 사용자가 검증할 수 없고, 제품 신뢰 구축이 어려움.
따라서 「제로 환각」은 엔지니어링에서 다음 세 가지 실행 가능한 규칙으로 구체화됩니다:
- 책 내 질문은 먼저 책을 조회: 현재 책과 관련될 수 있는 모든 질문은 모델이 먼저 검색(Tool)을 거친 뒤 답을 구성해야 함;
- 답변은 반드시 추적 가능: 핵심 결론에 원문 위치 마커를 붙이고, 프론트엔드가 파싱하여 이동·하이라이트 가능;
- 찾을 수 없으면 찾을 수 없다고 말함: 책에 없는 내용은 명확히 알리고, 일반 지식으로 「책의 관점」을 가장하지 않음.
아래에서는 3단계 데이터 흐름에 따라 위 규칙이 어떻게 구현되는지 설명합니다.
3. 전체 아키텍처: 전처리 → Tool 검색 → 제약 생성 → 클릭 가능한 추적
핵심 사상을 한 줄로: 모델이 「기억으로 답하지」 않게 하고, 「먼저 증거를 수집하고, 답한 뒤, 출처를 표시」하게 한다는 것입니다.
4. 전처리: 책 전체를 검색 가능한 「세그먼트 인덱스」로 변환
매 질문마다 1단계의 전문 Context를 쓰면 장편에서 Token이 폭발하고 검색 세분화도 너무 거칩니다. 3단계 해결책: 사용자가 특정 책에 대해 AI 대화를 처음 시작할 때, 백그라운드에서 세그먼트 요약 작업을 비동기 실행—목차 구조 또는 텍스트 길이로 책을 여러 Segment로 나누고, 각 조각에 요약을 생성한 뒤 로컬 IndexedDB에 영구 저장.
각 Segment는 데이터 구조상 요약과 본문 물리적 위치를 포함:
| 필드 | 의미 |
|---|---|
startFileIndex / endFileIndex | Spine 파일 인덱스 (PDF는 페이지당 하나) |
startOffset / endOffset | 문자 수준 시작/끝 오프셋 |
sequence | 선형 독서 순서 |
title | 해당 목차 제목 |
분할 전략은 정밀도와 비용을 균형 있게: 단일 목차 본문이 약 20KB 이하면 해당 노드만 요약; 동급 목차는 배치(15KB20KB)로 병합 후 LLM 호출; 목차 없는 큰 본문은 34만 문자 구간으로 분할.
요약 생성 시 System Prompt는 원문 위치 마커 유지를 요구(형식 [f숫자-숫자-숫자])하여 이후 Tool이 원문을 반환할 때 위치 정보가 spine 문자 오프셋과 일치하도록 함. 핵심 제약은 다음과 같습니다:
If summary content relates to a passage, keep the trailing position tag [fNumber-Number-Number] (e.g. [f1-90-109]).
Tags are atomic—do not alter, merge, or omit any character or digit.
전처리 완료 후 Q&A는 「책 전체 Context」가 아니라 구조화된 세그먼트 인덱스에 의존—장편 시나리오에서 제로 환각의 엔지니어링 전제입니다.
5. 위치 마커 체계: 「출처」를 텍스트에 인코딩
제로 환각은 내용이 원문에서 와야 할 뿐 아니라, 출처가 기계적으로 파싱 가능하고 UI에서 이동 가능해야 합니다. 인라인 위치 마커를 사용합니다:
[f{fileIndex}-{startChar}-{endChar}]
예: [f5-123-165]는 5번째 Spine 파일(0부터 시작)에서 문자 오프셋 123~165 구간의 텍스트를 의미합니다.
5.1 마커를 본문에 쓰는 방법
본문 추출 계층은 조각 출력 시 각 소단락 끝에 [f{fileIndex}-{start}-{end}]를 기록. 예시:
const position = `[f${fileIndex}-${absOffset}-${absOffset + segment.length}]`;
fileLines.push(segment.text.trim() + position);
전처리 요약이든 Tool이 반환한 원문 발췌든, 위치 정보는 Spine 문자 오프셋에 맞추며, 모델이 「페이지 번호를 추정」하지 않습니다.
5.2 모델 출력에 대한 제약
System Prompt 조립 시 **Position Citation Rules**를 별도로 약정, 핵심 다섯 가지:
- 표준 형식: 반드시
[f_fileIndex-startChar-endChar]사용, 세 구간 숫자 모두 필수; - 현재 출처만 인용: 각주는 이번 System/User 메시지 또는 Tool 반환 텍스트의 마커를 그대로 복사;
- 위조 금지: 위치를 직접 계산·수정·조작하지 않음;
- 부족함을 감수: 현재 컨텍스트에 유효한 마커가 없으면 정상적으로 답변하고, 위치 마커는 출력하지 않음;
- 논지 바로 뒤: 마커는 관련 문장 바로 뒤에, 글 끝에 인용 목록을 쌓지 않음.
프론트엔드 표시 전에는 모델이 가끔 출력하는 2구간 불법 마커(예: [f1-293])도 필터링하여 무효 각주가 UI에 들어가지 않게 합니다.

6. Tool Calling: 먼저 검색, 그다음 답변
대화가 특정 책에 바인딩되어 있을 때(resourceId 존재, chatType === 'chat'), 매 생성 전에 모델에 두 Tool을 등록하고 해당 executor를 마운트. 전체는 OpenAI 호환 function calling 루프를 따릅니다.
6.1 get_related_segment_summaries — 구체적 질문에 대한 세그먼트 조회
적용: 개념, 인물, 줄거리, 장 세부 등 명확한 검색 의도가 있는 질문.
흐름 요약:
- 모델이 사용자 구어를 책에 나올 법한 용어로 바꿈(System Prompt의 「Optimize Search Queries」);
- Tool 호출,
question전달; - 모든 세그먼트 요약을 Token 예산으로 배치(배치당 약 3만 Token, 최대 5배치);
- 배치마다 독립 LLM 요청으로
{ id, title, summary }목록에서 관련 세그먼트 ID 선택(최대 5개), JSON 반환, 형태{"Thinking":"...","answer":["1","3"]}; - 선택된 Segment의 span으로 Spine에서 위치 마커가 붙은 원문을 가져옴(요약 아님), Tool 결과로 반환.
핵심 설계: Tool은 요약이 아니라 원문을 반환. 모델은 실제 단락 + 인라인 [f…]를 보고 답하여 「요약 → 재요약」 drift를 피합니다.
6.2 get_full_book_segment_summaries — 책 전체 개요형 질문
적용: 「책 전체 요약」「이 책 평」「전체 구조/주제」 등 전역 시야가 필요한 질문.
독서 순서로 모든 세그먼트의 summary를 이어 반환하여, 단계별 관련도 선별만으로 핵심 장을 놓치지 않음.
6.3 System Prompt: 책 우선, Tool 우선
책이 바인딩되면 System Prompt에 Core Principles for Reading Assistant 주입, 핵심 세 가지:
1. Book First, Tool First
- 책과 관련될 수 있는 모든 질문은 먼저 Tool 검색을 호출해야 함;
- 답변은 주로 검색 결과에 근거해야 하며, 검색 없이 「책 내용」을 지어내지 말 것.
2. General Knowledge as Fallback Only
- 순수 잡담 / 사용자가 명시적으로 책을 쓰지 않음 / Tool 결과 없음일 때만 일반 지식 사용;
- 책에 없으면 먼저 「이 책에 해당 내용 없음」을 선언한 뒤 일반 지식 보충.
3. Direct Style
- 본론으로, 「제공된 자료에 따르면…」「종합하면…」 같은 상투어 금지.
생성 계층은 표준 Tool 루프 구현: tool_calls → executor 실행 → role: tool 추가 → 최종 텍스트까지 계속 요청. tools 활성화 시 thinking 채널은 끄고 function call 프로토콜 충돌을 피함.
7. 프론트엔드 추적: 각주에서 원문 하이라이트까지
모델 출력 [f5-123-165]는 그대로 표시하지 않고, 렌더링 계층에서 클릭 가능한 인용으로 변환.
7.1 각주 렌더링
표시 전 위치 마커를 Markdown 링크로 정규화, 예 [1]([f5-123-165]), 번호 각주로 렌더; 같은 위치가 여러 번 나오면 중복 제거하여 UI 중복 방지.
7.2 클릭 상호작용
- 첫 클릭:
[f…]파싱 → fileIndex와 문자 오프셋 → Spine 원문에서 텍스트 추출 → 미리보기 팝업(목차 제목 포함 가능); - 같은 각주 다시 클릭: 팝업 닫기;
- 이동 확인: 독서 뷰 열기, 문자 구간 하이라이트.
모델이 복사한 마커에서 사용자가 보는 원문까지, 중간에 LLM 2차 가공 없음, 추적 체인 전체가 확정적·재현 가능.
8. 경계 상황과 정직한 다운그레이드
제로 환각 ≠ 「항상 답이 있음」, 증거 없으면 지어내지 않음:
| 시나리오 | 동작 |
|---|---|
| 세그먼트 요약 아직 없음 | 먼저 전문 추출 후 요약 |
| Tool 검색 결과 없음 | (No relevant segment excerpts found…) 반환, 모델은 책에 언급 없음을 선언해야 함 |
| 모델이 불법 2구간 마커 출력 | 프론트엔드 필터, 무효 각주 미표시 |
| 사용자 순수 잡담 | System Prompt가 책에서 벗어나 일반 지식 답변 허용 |
| 대화 내보내기 | 각주를 리더 딥링크로 변환 가능, 공유·보관에 편리 |

9. 설계 트레이드오프: 왜 「벡터 RAG」를 쓰지 않는가?
문서 QA를 하는 동료들이 자주 묻습니다: 검색 증강을 한다면 왜 Embedding + 벡터 DB Top-K 표준 경로를 가지 않는가?
실제로 우리도 RAG를 하고 있습니다—매 답변 전에 책을 조회한 뒤 생성. 차이는: 커뮤니티 맥락의 RAG는 종종 벡터화와 유사도 검색을 전제로 하고, 현재 방안은 「세그먼트 인덱스 + Tool 필요 시 원문 가져오기」(3단계)로 벡터 계층을 의도적으로 도입하지 않음. 아래는 아키텍처 제약에 따른 선택이며, 벡터 RAG의 가치를 부정하는 것이 아닙니다.
범위 구분: 검색을 안 하는 게 아니라 「벡터 검색」을 안 함
- 넓은 의미 RAG: 관련 자료 검색 → 생성 → 우리는 함.
- 벡터 RAG: Embedding 유사도로 리콜 → 현재 버전에서는 안 함.
책 전체 전처리는 세그먼트 요약 인덱스; 질문 시 모델이 Tool로 구간 선택 후 원문 반환. 검색 증강은 있으나 별도 embedding 모델과 벡터 인덱스 유지에 의존하지 않음.
이유 1: 사용자 정의 LLM Provider 지원, 설정 체인은 짧게
제품은 사용자가 자체 API Key, 사용자 정의 Base URL, 로컬 Ollama를 자유롭게 연결 가능—대화 모델은 사용자 선택, 비용과 데이터 경로 통제. 많은 셀프 호스팅·다중 모델 비교 시나리오의 필수 요구.
전형적 벡터 RAG를 겹치면 통합 면이 눈에 띄게 넓어짐:
- Chat 모델 외에 보통 Embedding 모델도 필요(다른 model name, 때로는 다른 endpoint);
- Ollama 등 로컬 배포는 embedding 모델을 별도로 받고 차원·API 호환 처리;
- 장애 도메인 복잡: Chat은 정상인데 검색이 비어 있음—embedding, 인덱스, 차원 불일치 가능, 「단일 Provider 전체 체인」보다 디버깅 비용 높음.
현재 방안에서는 구간 선택과 답변이 동일 Provider 설정 공유—「Chat은 A, 인덱스는 B」 없음. 플러그형 LLM 애플리케이션에서는 리콜 몇 포인트보다 이게 더 중요한 경우가 많음.

이유 2: Embedding과 인덱스 강결합, Provider 전환 비용 높음
벡터 RAG에서 흔히 과소평가되는 점: 벡터는 범용 중간 형식이 아니라 특정 embedding 모델 아래 좌표. A로 인덱스, B로 쿼리하면 유사도는 보통 비교 불가—모델 교체는 책 전체 재벡터화를 의미하고, 모델마다 벡터 차원(768 / 1024 / 1536 …)이 저장 schema에 묶임.
3단계는 구조화 요약 + 문자 span을 영구 저장, 벡터 저장 안 함; Chat 모델 전환 시 인덱스 재구축 불필요, 증거 체인(원문 위치) 불변. 「언제든 다른 LLM 비교」 목표와 더 일치.
이유 3: 목차 있는 긴 문서, 구조화 라우팅만으로도 종종 충분
전자책·PDF는 보통 장 구조; 전처리가 구간 제목 + 요약 산출. 「특정 장이 무엇을 말하는지」「책이 개념을 어떻게 정의하는지」류 질문은 요약 목록에서 구간 선택 후 원문 회수가 실무에서 안정적; Tool 반환은 [f…]가 붙은 원문이라 제로 환각은 여전히 문자 span에 고정.
벡터 검색은 의미 모호, 다국어, 긴 구간 문자열 불일치 등에서 유리; 목차 + 전처리 + 강한 추적 리더에서는 복잡도를 Tool + 원문 반환 + 인용 제약에 두는 ROI가 보통 더 높음.
향후 방향: 하이브리드 리콜, 처음부터 다시 짓지 않음
향후 벡터 거친 리콜 추가 가능(예: embedding으로 Top-N 후보 장만 선별), 최종은 여전히 구간 선택 → 원문 반환 → 클릭 추적, 제로 환각 규칙 불변. 도입 시 Embedding 선택, 모델 변경 시 인덱스 재구축 명시적 안내—silent wrong retrieval 방지.
그 전까지 우선 보장: OpenAI 호환 Chat API만 있으면 동작, Chat 모델 교체 시 로컬 인덱스 재구축 불필요.
10. 요약
| 단계 | 수단 | 역할 |
|---|---|---|
| 전처리 | 목차/길이 분할 + 세그먼트 요약 캐시 | 장편 검색·위치 지정 가능 |
| 위치 마커 | [f파일-시작-끝] 원문에 기록 | 출처 기계 파싱 |
| Tool 검색 | 질문별 구간/전체 요약 조회, 원문 반환 | 답변 전 증거 강제 |
| System Prompt | 책 우선, 각주 위조 금지, 없으면 말함 | 생성 행위 제약 |
| 프론트엔드 추적 | 각주 → 미리보기 → 이동 하이라이트 | 사용자 증거 검증 |
| 벡터 검색 미사용 | 단일 Provider, Chat 모델 교체 시 인덱스 재구축 불필요 | 통합·마이그레이션 비용 절감 |
「제로 환각」은 모델이 절대 실수하지 않는다는 뜻이 아니라 엔지니어링 구조로 출력을 증거 체인에 고정하는 것: 검색 결과 없으면 책 내용을 가장하면 안 됨; 검색 결과 있으면 검증 가능한 원문 위치를 제시해야 함.
AI 독서나 문서 QA를 개발 중이라면 전문 직접 삽입 → 핵심 문장 추출 → Tool-First 필요 시 검색 진화 경로와 인라인 위치 마커 + 원문 반환 방식이 참고 구현이 되길 바랍니다.
위 내용은 Foxycape AI 리더 개발 실천에서 나온 것으로, 참고용입니다. 글 끝 다운로드 페이지에서 리더를 체험해 보세요.
