WRITING · 2026-11-03

LLM이 붙인 [근거 N]을 코드로 다시 검사하기 — 국보 RAG 가이드의 인용 설계

근거를 인용하라고 프롬프트에 써도 LLM은 없는 번호를 붙입니다. 근거 번호를 검색 단계에서 고정하고, 답변의 인용을 코드로 파싱해 없는 번호는 지우고 실제로 인용한 근거만 보여 주는 구조를 만든 기록.

RAGLangGraphLLM할루시네이션SSAFY

SSAFY AI 코어 프로젝트 2에서 국보 입문 가이드를 혼자 만들었습니다. 국보를 고르고 질문하면 국보 메타데이터·한국어 위키백과·국가유산청 공식 설명에서 근거를 찾아, 문장마다 [근거 N]을 붙여 답하는 RAG 서비스입니다.

만들면서 가장 오래 붙잡고 있었던 문제는 "근거를 인용하라"고 시켜도 LLM이 그걸 지킨다는 보장이 없다는 점이었습니다.

문제: 인용은 형식일 뿐, 사실이 아니다

처음 버전은 검색한 문서를 프롬프트에 넣고 "근거에 없는 내용은 쓰지 말고 문장 끝에 [근거 N]을 붙여라"라고만 했습니다. 겉보기엔 잘 됐지만 답변을 하나씩 대조해 보니 이런 경우가 나왔습니다.

"근거를 보여 주는 서비스"인데 근거 표시가 틀리면 오히려 더 그럴듯한 오답이 됩니다. 그래서 인용을 LLM에게 맡기지 않고, 번호의 의미를 코드가 정하고 코드가 검사하는 쪽으로 구조를 바꿨습니다.

1단계 — 근거 번호를 검색 단계에서 고정한다

LangGraph로 retrieve → generate 두 노드를 만들고, 번호는 retrieve에서 정해서 넘깁니다.

retrieve:
  [근거 1]  = 선택한 국보의 메타데이터 청크 (검색과 무관하게 항상 첫 번째)
  [근거 2~] = 질문으로 검색한 외부 문서 청크 — 단, item_id 필터로 그 국보 범위 안에서만
  (+ 설정 시) 질문과 관련된 다른 국보 메타데이터
  → 청크 ID와 본문 지문(md5)으로 중복 제거 → 1부터 다시 번호 부여
generate:
  번호가 붙은 근거 블록만 보고 답변 작성

두 가지가 중요했습니다.

청크 ID도 국보ID:문서ID:순번으로 고정했습니다. 같은 위키 문서를 다른 국보가 다시 수집해도 ID가 겹치지 않고, 같은 국보를 다시 색인하면 덮어쓰기만 일어나 중복 청크가 생기지 않습니다.

2단계 — 답변의 인용을 파싱해서 없는 번호를 지운다

LLM 답변을 받으면 정규식으로 [근거 …] 묶음을 모두 찾고, 안의 숫자를 풀어냅니다.

그다음 근거 목록에 실제로 있는 번호만 남기고 묶음을 다시 씁니다. [근거 1, 9]인데 근거가 8개뿐이면 [근거 1]로, [근거 9]만 있으면 표기 자체를 지웁니다. 화면에 엉뚱한 번호가 노출될 일이 없습니다.

3단계 — 실제로 인용한 근거만 출처로 보여 준다

남은 번호를 기준으로 답변이 실제로 인용한 근거만 출처 목록에 표시합니다. 검색은 8건 했어도 답이 2건만 인용했다면 출처도 2건입니다. 화면에는 "답변에 사용한 근거 2건 · 검색된 근거 8건"처럼 두 숫자를 나눠 보여 줘서, 검색 결과와 실제 근거를 사용자가 구분할 수 있게 했습니다.

인용이 하나도 없으면 "답변에 인용된 근거가 없습니다"라는 안내를 따로 띄웁니다. 그리고 문서에서 확인되지 않는 질문에는 프롬프트 규칙으로 "제공된 문서에서 확인되지 않습니다"라는 고정 문장으로 답하게 해, 테스트에서 이 경우를 정확히 검사할 수 있게 했습니다.

같은 원칙을 다른 기능에도

인용만이 아니라 LLM이 고르는 모든 것을 같은 방식으로 다뤘습니다.

기능 LLM이 하는 일 코드가 막는 것
검색어 추출 위키 검색어를 JSON 배열로 제안 형식이 깨지면 국보 이름만으로 검색. 국보 자체 문서 검색어는 LLM 답과 무관하게 항상 첫 번째
문서 선별 검색 후보 중 관련 문서 번호 선택 범위를 벗어난 번호 무시, 형식이 깨지면 국보 이름이 제목에 들어간 후보만
조건으로 찾기 후보 국보 중 조건에 맞는 ID 선택 실제 후보에 있던 ID만 채택 → 없는 국보가 결과에 끼어들 수 없음
통설 판정 사실 / 일부 사실 / 다름 / 확인 불가 중 판정 판정 문구를 못 찾으면 "문서로 확인 불가"로 처리

수집 단계에서는 실패의 흔적이 다음 질문을 오염시키지 않게 했습니다. 선별 프롬프트가 실패하면 그 시도에서 메모리에 쌓인 기록(어떤 검색어를 처리했는지)을 버리고 디스크 상태로 되돌립니다. 그렇게 하지 않으면 "이미 처리한 검색어"로 잘못 기록돼, 같은 질문을 다시 해도 문서를 영영 수집하지 않습니다.

테스트 — LLM 없이 전 과정을 검증

이런 규칙은 실제 LLM으로는 재현이 어려워서, 시스템 프롬프트의 [작업] … 줄을 보고 정해진 답을 돌려주는 가짜 LLM을 만들었습니다. 일부러 [근거 99]를 섞어 답하게 하고, 화면과 API에서 그 번호가 사라지는지 검사합니다.

외부 호출 없이 50개 테스트가 10초 안에 끝나서, 프롬프트를 고칠 때마다 부담 없이 돌릴 수 있었습니다.

배운 것

ThRead 프로젝트에서 "LLM은 번호만 고르고 실제 삭제는 코드가 한다"고 했던 원칙을, 이번에는 답변의 출처 표시까지 넓힌 셈입니다.

SSAFY 16기 관통 프로젝트 — ThRead: LLM 댓글 분석 파이프라인 · AI 도서 서비스이 글에서 다룬 코드는 저장소에서 볼 수 있습니다.
GitHub 저장소 →포트폴리오로
다른 글겹쳐 있는 두 신호를 위상으로 가르기 — 파수 상관 필터링 석사 연구 정리중력과 자력 데이터를 같은 주파수 성분끼리 비교해, 함께 움직이는 성분과 반대로 움직이는 성분을 나눴다. 임계값을 감으로 정하지 않으려고 한 일들.점수를 지키면서 올리기 — VQA 챌린지에서 앙상블 후보를 고른 기준정확도가 조금 올라도 원래 맞히던 문제를 많이 틀리면 불안정한 개선이다. fixes와 breaks로 순기여를 따져 0.94608에서 시작한 점수를 쌓아 올린 과정.YAML 파일 하나로 포트폴리오·이력서·비공개 페이지까지 — 이 사이트를 만든 방식내용은 한 곳에만 쓰고, 웹 페이지·PDF 이력서·노션 문서는 빌드로 만든다. 공개하면 안 되는 내용은 저장소에 평문으로 남기지 않는다.데이터를 해석하기 전에 그 데이터가 만들어진 역사를 본다 — 학부 제1저자 논문 회고해외 대규모 CO2 저장소 8곳을 판구조 형성사와 탄성파 영상으로 비교했다. 처리 결과를 '그럴듯한지' 판단하는 기준을 어디서 가져올 것인가에 대한 이야기.길찾기에서 의사결정으로 — 배틀싸피 자동 플레이 봇을 세 번 고친 기록목표까지 가는 BFS 한 줄짜리 봇이, 사거리·장애물·탄약·보급·암호 해독까지 판단하는 봇이 되기까지. 매 턴 다시 계획하는 구조로 바꾼 이유.산불 4건으로 시공간 딥러닝 학습하기데이터가 35개 시점뿐일 때 어떻게 학습시켰고, 다시 보니 무엇이 부족했는지. U-LSTM 산불 확산 예측 모델 회고.LLM 응답을 믿지 않는 파이프라인 설계LLM에는 판단만 맡기고, 결과는 코드로 검증한다. 댓글 분석 파이프라인과 AI 도서 추천을 만들며 정한 네 가지 원칙.순차 학습이 성능을 떨어뜨린 이유 — catastrophic forgetting 실험기단어 단위 → 문맥 단위로 차례로 학습하면 더 좋아질 줄 알았는데, BERTScore가 0.47에서 0.32로 떨어졌다. FLAN-T5 LoRA 학습 전략 비교.