RAG 심화 #5 인용으로 환각 줄이기
4편까지로 검색을 다듬었습니다. 이제 1편에서 가른 실패의 나머지 절반, 생성 실패 차례입니다. 정답 조각을 컨텍스트에 넣어 줬는데도 답이 틀리는 경우, 그리고 더 고약하게는 조각에 없는 내용을 그럴듯하게 지어내는 경우입니다. 후자를 환각(hallucination, 모델이 근거 없는 내용을 사실처럼 생성하는 현상)이라고 부릅니다. RAG에서 환각은 신뢰의 문제라서, 열 번 잘 답해도 한 번의 그럴듯한 거짓말이 서비스를 무너뜨립니다.
근거 안에서만 답하게 하기 #
첫 번째 방어선은 시스템 프롬프트입니다. 핵심은 두 가지를 분명히 하는 것입니다. 근거 밖의 지식을 쓰지 말 것, 그리고 근거에 없으면 없다고 답할 것.
SYSTEM = """너는 사내 문서 기반 Q&A 봇이다.
규칙:
- 답은 반드시 제공된 문서 조각의 내용에만 근거한다. 일반 지식으로 보충하지 않는다.
- 문서 조각에 답이 없으면, 추측하지 말고 "제공된 문서에서 해당 내용을 찾지 못했습니다"라고 답한다.
- 문서 조각끼리 내용이 충돌하면, 충돌한다는 사실을 그대로 알린다.
"""두 번째 규칙이 특히 중요합니다. “모른다"가 허용되지 않으면 모델은 빈칸을 지어내서라도 채웁니다. 모른다고 답할 권리를 명시적으로 주는 것이 환각 억제의 절반입니다. 이 규칙이 있으면 부수 효과로 진단도 좋아집니다. “찾지 못했습니다"라는 답이 늘어난다면 그건 생성이 아니라 검색을 고치라는 신호입니다.
citations — 문장마다 출처 달기 #
프롬프트로 “출처를 표시하라"고 시킬 수도 있지만, 모델이 출처 표기 자체를 환각하는 역설이 생깁니다. Claude에는 이를 구조적으로 푸는 citations 기능이 있습니다. 조각을 일반 텍스트가 아니라 document 블록으로 넣고 인용을 켜면, 응답의 문장들이 어느 문서의 어느 대목에 근거했는지를 API가 구조화된 데이터로 함께 돌려줍니다.
def answer_with_citations(question: str, chunks: list):
documents = [
{
"type": "document",
"source": {"type": "text", "media_type": "text/plain", "data": c["text"]},
"title": f'{c["metadata"]["source"]} — {c["metadata"]["section"]}',
"citations": {"enabled": True},
}
for c in chunks
]
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=2048,
system=SYSTEM,
messages=[{"role": "user", "content": documents + [{"type": "text", "text": question}]}],
)
return response2편에서 조각에 붙여 둔 메타데이터가 여기서 title로 쓰입니다. 응답에서는 텍스트 블록마다 인용 정보가 따라옵니다.
def render(response) -> str:
out = []
for block in response.content:
if block.type != "text":
continue
out.append(block.text)
for cite in (block.citations or []):
out.append(f" [출처: {cite.document_title} — \"{cite.cited_text[:50]}…\"]")
return "\n".join(out)cited_text는 모델이 지어낸 표기가 아니라 실제 문서에서 그대로 가져온 원문 대목입니다. 사용자에게는 검증 가능한 답이 되고, 운영자에게는 환각 탐지기가 됩니다. 인용이 하나도 붙지 않은 단정적인 문장은 의심 대상입니다.
인용을 품질 게이트로 쓰기 #
인용 데이터는 보여 주는 용도로 끝나지 않습니다. 답을 내보내기 전의 검사 장치로 쓸 수 있습니다.
def is_grounded(response, min_ratio: float = 0.5) -> bool:
"""본문 텍스트 중 인용이 붙은 비율이 기준 미만이면 의심 답변으로 처리한다."""
cited, total = 0, 0
for block in response.content:
if block.type != "text":
continue
total += len(block.text)
if block.citations:
cited += len(block.text)
return total == 0 or cited / total >= min_ratio기준에 못 미치는 답은 그대로 내보내는 대신 “관련 문서를 충분히 찾지 못했습니다"로 바꾸거나, 더 보수적인 프롬프트로 재생성하는 분기를 둘 수 있습니다. 거친 장치지만, 가장 위험한 유형의 답(근거 없이 단정하는 답)을 걸러 내는 데에는 이 정도로도 효과가 있습니다.
조각이 많을 때의 주의점 #
생성 실패의 또 다른 원인은 컨텍스트에 넣는 조각의 양입니다. 관련도 낮은 조각까지 잔뜩 넣으면, 모델이 그 안에서 그럴듯한(그러나 틀린) 근거를 찾아내는 일이 생깁니다. 4편의 리랭킹이 여기서도 의미가 있습니다. 적지만 정확한 조각이 많지만 어중간한 조각보다 생성 품질에 유리합니다. citations를 켜면 이 문제도 관찰 가능해집니다. 엉뚱한 조각이 자꾸 인용된다면 검색 후보를 줄이거나 리랭킹 기준을 올릴 차례입니다.
흔히 걸려 넘어지는 곳 #
- 모른다는 답을 막는다 — “반드시 답하라"는 지시는 환각 제조기입니다. 모른다고 답하는 길을 열어 두고, 그 빈도를 검색 개선의 신호로 씁니다.
- 출처 표기를 프롬프트로만 시킨다 — 모델이 쓴 “[출처: …]” 문자열은 출처 자체가 환각일 수 있습니다. 검증 가능한 출처가 필요하면 citations처럼 구조화된 기능을 씁니다.
- 인용 표시를 그대로 노출한다 —
cited_text와 위치 정보는 원자료입니다. 사용자 화면에는 각주나 접힌 출처 목록처럼 읽기 좋은 형태로 가공해 보여 줍니다.
마무리 #
이번 글에서는 생성 실패와 환각을 다뤘습니다.
- 근거 밖 지식 금지와 “모른다고 답할 권리"를 시스템 프롬프트에 명시하는 것이 기본 방어선입니다.
- citations 기능은 문장마다 실제 원문 대목을 출처로 돌려줍니다. 사용자에게는 검증 수단, 운영자에게는 환각 탐지기입니다.
- 인용 비율을 품질 게이트로 쓰면 근거 없는 단정 답변을 내보내기 전에 거를 수 있습니다.
이제 개선 도구는 다 갖췄습니다. 남은 것은 이 모든 변경을 안심하고 반복할 수 있게 해 주는 토대, 평가의 체계화입니다. 다음 글인 “RAG 심화 #6 RAG 평가 파이프라인 만들기"에서 1편의 기준선을 본격적인 평가 체계로 키웁니다.