블로그 목록
ai

Claude Citations로 답변에 출처 박기 — LLM 환각을 근거로 잡기

문서 기반 챗봇을 만들면 항상 같은 질문을 받는다. "이 답이 진짜 우리 문서에 있는 내용이에요?"

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 에러가 난다. 인용이 필요한 답변은 자유 형식으로, 구조화가 필요한 부분은 별도 호출로 나누는 게 편했다.