문서 기반 챗봇을 만들면 항상 같은 질문을 받는다. "이 답이 진짜 우리 문서에 있는 내용이에요?"
RAG로 관련 문단을 뽑아서 프롬프트에 넣고, LLM이 그럴듯한 답을 내놓는다. 문제는 그 답이 실제로 어느 문단에서 나왔는지 알 수가 없다는 거다. 모델이 문서를 참고했는지, 아니면 그냥 지어냈는지 구분이 안 된다. 답 끝에 "출처를 표시하세요"라고 프롬프트로 시켜봐도, 없는 페이지 번호를 만들어내는 경우가 생긴다. 환각(hallucination)을 프롬프트로 막으려는 시도 자체가 또 다른 환각을 부른다.
Claude의 Citations 기능은 이걸 API 레벨에서 해결한다. 문서를 document 블록으로 넣고 citations: { enabled: true }만 켜면, 모델이 답변의 각 문장이 원문의 어느 부분에서 나왔는지를 구조화된 데이터로 같이 돌려준다. 프롬프트로 부탁하는 게 아니라, 모델이 실제로 인용한 위치를 그대로 받는 방식이다.
써보기
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const response = await client.messages.create({
model: 'claude-opus-4-8',
max_tokens: 1024,
messages: [
{
role: 'user',
content: [
{
type: 'document',
source: {
type: 'text',
media_type: 'text/plain',
data: '환불은 구매일로부터 14일 이내에만 가능합니다. 배송비는 고객 부담입니다.',
},
title: '환불 정책',
citations: { enabled: true },
},
{ type: 'text', text: '환불 기한이 어떻게 되나요?' },
],
},
],
});
응답을 파싱하기
Citations를 켜면 응답의 content가 여러 개의 text 블록으로 쪼개진다. 인용이 붙은 블록에는 citations 배열이 함께 온다.
for (const block of response.content) {
if (block.type !== 'text') continue;
process.stdout.write(block.text);
for (const c of block.citations ?? []) {
// cited_text: 원문에서 실제로 인용된 구절
// document_title: 어느 문서인지
console.log(`\n └ 출처: "${c.cited_text}" (${c.document_title})`);
}
}
각 인용에는 cited_text(원문 그대로의 구절), document_index, document_title이 들어있다. PDF면 page_location으로 페이지 번호(1부터)가, 일반 텍스트면 char_location으로 글자 위치가 온다. 프론트에서 이 위치를 원문에 하이라이트로 연결하면, 사용자가 답변의 근거를 클릭 한 번으로 확인할 수 있다.
정리
RAG에서 "정확도"는 결국 신뢰의 문제다. 답이 맞아 보이는 것과, 그 답이 어디서 왔는지 보여주는 것은 완전히 다른 얘기다. Citations는 별도 베타 헤더도 필요 없고 enabled: true 한 줄이면 끝난다. 주의할 점 하나 — output_config.format(구조화 출력)과는 같이 못 쓴다. 400 에러가 난다. 인용이 필요한 답변은 자유 형식으로, 구조화가 필요한 부분은 별도 호출로 나누는 게 편했다.