14장. 근거 기반 답변
컨텍스트 주입과 출처 꼬리표 — 발췌를 어떻게 넣을 것인가
한 줄 요약
찾은 청크를 프롬프트에 넣는 일을 컨텍스트 주입(context injection) 이라고 합니다. 그냥 이어 붙이지 않고, 발췌마다 번호와 출처 꼬리표를 붙여 넣습니다. 모델은 답변에 사용한 발췌 번호를 고르고, 코드는 그 발췌의 메타데이터로 출처를 붙입니다.
1. 컨텍스트란
컨텍스트(context) 는 모델이 답을 쓸 때 참고하라고 프롬프트에 함께 넣어 주는 글입니다. RAG에서는 검색으로 찾은 청크들이 컨텍스트가 됩니다.
모델은 chroma_db/를 직접 들여다보지 못합니다. 모델이 볼 수 있는 것은 우리 코드가 프롬프트에 적어 준 글뿐입니다. 그러니 무엇을 어떤 모양으로 적어 주느냐가 답의 품질을 정합니다.
2. 나쁜 방법과 좋은 방법
나쁜 방법 — 그냥 이어 붙인다
context = "\n".join(d.page_content for d in docs)
청크 네 개가 한 덩어리 글이 됩니다. 어디까지가 한 발췌인지, 어느 문서의 몇 조인지 모델이 알 수 없습니다. 출처를 쓰라고 해도 쓸 이름이 없습니다.
좋은 방법 — 발췌마다 번호와 출처를 붙인다
context = "\n\n".join(
f"[발췌 {i+1}] ({d.metadata.get('doc_title', '정책')} · "
f"{d.metadata.get('article') or '조항 미상'})\n{d.page_content}"
for i, d in enumerate(docs)
)
이렇게 만든 컨텍스트는 다음과 같은 모양이 됩니다.
[발췌 1] (반품교환환불정책 · 제5조 (환불 처리 기간), 제6조 (환불 금액 산정), 제7조 (반품·교환 불가 사유))
계좌이체·무통장 계좌 환급 검수 후 3일 이내
(중략)
[발췌 2] (반품교환환불정책 · 제8조 (배송 지연 보상), 제9조 (분쟁 처리), 자주 묻는 질문 (FAQ))
서 제외된다.
제9조 (분쟁 처리)
(중략)
| 붙인 것 | 어디서 왔나 | 왜 붙이나 |
|---|---|---|
[발췌 1] |
코드가 매긴 번호 | 발췌 사이의 경계와 모델이 선택할 번호를 알려 줍니다 |
반품교환환불정책 |
메타데이터 doc_title |
어느 문서인지. 정책 문서가 두 개이므로 필요합니다 |
제5조 …, 제6조 …, 제7조 … |
메타데이터 article |
이 발췌에 어느 조항들이 담겨 있는지 |
3. 꼬리표는 12장에서 심어 둔 것이다
doc_title과 article은 이번 장에서 만든 것이 아닙니다. 12장에서 청크를 만들 때 메타데이터로 붙여 두었고, 13장에서 벡터와 함께 chroma_db/에 저장했습니다. 그것이 지금 검색 결과에 딸려 나온 것입니다.
| 장 | 메타데이터에 한 일 |
|---|---|
| 12장 | 청크마다 문서 이름과 조항을 붙였다 |
| 13장 | 벡터와 함께 저장했다 |
| 14장 | 검색 결과에서 꺼내 꼬리표로 썼다 |
색인할 때 붙여 두지 않았다면 지금 붙일 방법이 없습니다. 출처를 밝히는 답변은 색인 단계에서 이미 정해집니다.
4. 꼬리표에 조항이 여럿일 때
12장에서 본 대로 청크 하나에는 조항이 두세 개 들어 있고, 꼬리표 article에는 그 조항이 모두 적혀 있습니다.
| 청크 | 꼬리표 |
|---|---|
policy-007 |
제3조 (반품 신청 절차), 제4조 (교환), 제5조 (환불 처리 기간) |
policy-010 |
제8조 (배송 지연 보상), 제9조 (분쟁 처리), 자주 묻는 질문 (FAQ) |
모델은 발췌 본문을 읽고 답변에 사용한 발췌 번호를 evidence_ids로 반환합니다. 코드는 그 번호에 해당하는 청크에서 doc_title과 article을 꺼내 그대로 붙입니다. 발췌 번호는 이번 검색 결과의 순서이며, DB의 고정 청크 ID와는 다릅니다.
예를 들어 policy-010을 선택했다면 다음과 같이 표시합니다.
(근거: 반품교환환불정책 · 제8조 (배송 지연 보상), 제9조 (분쟁 처리), 자주 묻는 질문 (FAQ))
article 전체를 표시하므로 실제 답변에 사용한 조항보다 출처 범위가 넓을 수 있습니다. 모델이 조항 이름을 다시 쓰지 않아 이름의 오타나 문서명 누락을 줄일 수 있습니다. 다만 선택한 청크가 답변을 실제로 뒷받침하는지는 원문과 대조해야 합니다.
5. 최종 프롬프트의 모양
컨텍스트 뒤에 고객의 질문을 붙이면 모델에게 보낼 글이 완성됩니다.
prompt = f"[정책 발췌]\n{context}\n\n[고객 문의]\n{question}"
[정책 발췌]와 [고객 문의]라는 제목으로 참고 자료와 질문의 자리를 나눴습니다. 3장에서 시스템 프롬프트를 [응대 원칙], [형식]으로 나눈 것과 같은 요령입니다.
모델에게 가는 글 전체를 자리별로 정리하면 이렇습니다.
| 자리 | 내용 | 누가 만드나 |
|---|---|---|
| 시스템 프롬프트 | 역할과 세 가지 규칙 (다음 절) | 개발자가 미리 써 둔다 |
| 사용자 메시지 앞부분 | [정책 발췌] + 꼬리표 붙은 청크 4개 |
코드가 질문마다 새로 만든다 |
| 사용자 메시지 뒷부분 | [고객 문의] + 질문 |
고객 |
핵심 정리
- 컨텍스트는 모델이 참고하도록 프롬프트에 넣어 주는 글입니다. 모델은 그 글만 볼 수 있습니다.
- 발췌마다 번호와 출처 꼬리표를 붙여 넣습니다. 꼬리표가 인용할 이름이 됩니다.
- 꼬리표는 12장에서 심은 메타데이터입니다. 색인할 때 없었으면 지금도 없습니다.
- 모델은 근거 발췌 번호를 선택하고, 코드는
doc_title · article을 그대로 표시합니다. 청크에 담긴 조항 이름이 모두 표시됩니다. - 최종 프롬프트는
[정책 발췌]+[고객 문의]입니다.