환각(Hallucination)을 잡아내기 위해 로컬 RAG에 검증 레이어를 추가했습니다. 제 자신의 코퍼스에 대해 제가 틀렸음을 두 번이나
요약
로컬 RAG 시스템의 환각 현상을 방지하기 위해 답변의 주장을 소스 데이터와 대조하는 검증 레이어를 구현했습니다. LLM-as-judge 방식을 활용하여 답변을 원자적 주장 단위로 분해하고, 각 주장의 근거성을 확인하여 잘못된 정보를 식별합니다.
핵심 포인트
- RAG 답변 생성 후 주장(claim) 단위로 분해하여 검증하는 프로세스 구축
- LLM-as-judge를 통해 소스 데이터와의 일치 여부를 판단하는 근거성(groundedness) 확인
- 로컬 8B 모델을 활용하여 추가적인 비용 없이 환각 및 수치 오류 탐지 가능
- 검증 과정에서 발생하는 추가적인 추론 시간과 정확도 사이의 트레이드오프 고려
저는 작고 완전히 로컬로 작동하는 연구 보조 도구를 만들고 있습니다. 즉, Ollama 위에서 실행되며 외부로 데이터가 전혀 나가지 않는 제 논문들에 대한 RAG (Retrieval-Augmented Generation)입니다. 실제로 저를 걱정하게 만드는 위험 요소는 속도나 비용이 아닙니다. 확신에 찬 말투로 잘못된 수치를 인용하는 연구 도구는 도구가 없는 것보다 더 나쁩니다. 왜냐하면 사용자가 그것을 믿어버릴 것이기 때문입니다.
Andrej Karpathy의 llm-wiki 노트에는 제가 계속 생각하게 만든 부분이 있었습니다. 매 쿼리마다 처음부터 다시 검색하는 대신, 모델이 지속적인 위키(wiki)를 구축하게 하고, 데이터 주입(ingest) 과정에서 린트(lint) 패스를 통해 페이지 간의 모순을 확인하는 방식입니다. 저는 답변 시점에 이와 유사한 무언가를 원했습니다. RAG가 답변 초안을 작성한 후, 이를 주장(claims) 단위로 나누고 각 주장을 소스(sources)와 대조하여, 소스가 실제로 지원하지 않는 부분을 표시하는 것입니다.
이 포스트가 부분적으로 인용 정확도(citation accuracy)에 관한 것이므로, 그것이 무엇인지 정확히 정의해야겠습니다. Karpathy의 린트는 데이터 주입 중에 위키 페이지들을 서로 비교합니다. 제가 만든 것은 답변 시점에 각 답변 주장(answer-claim)을 검색된 구절(retrieved passage)과 비교합니다. 이것은 근거성 (groundedness, 또는 faithfulness) 확인이며, RAGAS의 faithfulness 지표 및 다양한 셀프 체크 (self-check) 방법들과 같은 계열의 기술입니다. 이를 덧붙이겠다는 아이디어는 llm-wiki에서 얻었지만, 메커니즘은 소형 모델에서 로컬로 실행되는 표준적인 근거성 확인 방식입니다.
저는 이를 구축하고 측정했으며, 솔직한 결과는 깔끔한 승리보다 더 나은 결과였습니다. 여기에는 제 자신의 코퍼스(corpus)에 들어있는 내용에 대해 제가 두 번이나 틀렸던 부분도 포함되어 있습니다.
검증 레이어 (The verify layer)
기존 RAG 위에 약 80줄 정도의 코드를 추가했습니다. 일반적인 검색 및 답변(retrieve-and-answer) 단계 이후에 다음 과정을 거칩니다:
- 초안을 원자적 주장 (atomic claims)으로 분해합니다 (로컬 LLM 호출 1회).
- 각 주장에 대해, LLM-as-judge 호출을 통해
{supported, cite, why}를 반환받습니다. 특정 발췌문이 명시적으로 언급했을 때만 'supported'로 판정합니다. - 지원되지 않는 주장들을 표시(flag)합니다.
구체적인 예시를 보여드리기 위해 판결문 하나를 그대로 가져왔습니다. 주장은 의도적으로 왜곡된 "AUROC는 0.92이다"였으며, 0.804를 보고하는 구절과 대조되었습니다:
{ "supported": false,
"cite": null,
"why": "본문(passage)에는 AUROC가 0.804라고 명시되어 있으나, 0.92는 나타나지 않음" }
비용(Cost) 측면에서, 이는 로컬 8B (8-billion parameter) 컨텍스트이기에 중요한 문제입니다. 답변 한 번을 검증하기 위해 대략 N+2회의 호출(분해를 위한 1회, 각 주장(claim)을 확인하기 위한 N회)이 필요합니다. 제 1080 Ti에서 5개의 주장이 포함된 답변을 처리할 경우 약 15~20초의 추가 시간이 소요됩니다. 공짜는 아니지만, 감당할 만한 수준입니다.
첫 번째 평가, 그리고 잘못된 결론을 내릴 뻔한 순간
조작(fabrication)을 잡아낼 수 있는지 확인하기 위해, 제가 가진 3편의 논문 코퍼스(corpus)로는 답할 수 없다고 가정한 질문들을 작성했습니다. 그중 하나는 시너지 모델의 "홀드아웃 테스트 세트(held-out test set)에서의" AUROC를 묻는 것이었습니다. 베이스라인(baseline) 모델은 "0.804 [1]"라고 답했고, 제 검증 레이어(verify layer)는 이를 통과시켰습니다. 저는 이를 실패 사례로 기록했습니다: 검증 레이어가 조작된 통계치를 통과시켰다.
그 후 제 코퍼스에서 0.804를 검색(grep)해 보았습니다. 그 숫자는 일곱 번이나 등장했습니다. 그래서 저는 반대로 다시 작성했습니다. 숫자는 실제였고, 모델은 맞았으며, 검증 레이어는 올바르게 통과시켰다고 말이죠. 더 깔끔한 이야기였고, 저 역시 그 결론을 그대로 내보낼 뻔했습니다.
하지만 그 역시 틀렸습니다. 본문(passages)이 실제로 무엇을 말하고 있는지 보십시오. 모든 0.804는 GroupKFold 교차 검증(cross-validation) 결과로 보고되어 있으며, 한 줄에는 다음과 같이 명시되어 있습니다: "제한된 샘플 크기로 인해 별도의 홀드아웃 테스트 세트는 사용되지 않았습니다." 제 질문은 홀드아웃 테스트 AUROC를 물었습니다. 홀드아웃 테스트 세트는 존재하지 않습니다. 모델은 실제 교차 검증 수치를 가져와 존재하지 않는 평가 방식에 갖다 붙였고, 검증 레이어는 0.804라는 숫자가 컨텍스트(context)에 그대로 있었기 때문에 이를 통과시킨 것입니다.
결국 저는 제 코퍼스에 대해 서로 반대되는 방향으로 두 번이나 틀렸고, 그제야 무슨 일이 일어났는지 깨달았습니다: 주장 검증(claim-checking)을 그대로 통과해 버린, "숫자는 맞지만 컨텍스트는 틀린(right-number-wrong-context)" 환각(hallucination)이었습니다. 첫 번째 교훈은 어색하지만 분명하게 말할 가치가 있습니다: 정답(ground truth) 없이는 환각을 측정할 수 없으며, "숫자가 실제라는 것"이 "답변이 옳다는 것"과 같지는 않습니다.
제대로 수행하기
저는 분위기(vibe)에 의존하는 질문들을 버리고 통제된 벤치마크(benchmark)를 구축했습니다: 8쌍의 주장(claims)입니다. 각 쌍은 하나의 참인 주장(grep으로 확인한 사실)과 하나의 거짓 주장을 포함합니다. 거짓 주장은 동일한 문장이지만 숫자나 개체(entity)가 코퍼스(corpus)에 존재하지 않음을 확인한 무언가로 변조된 것입니다. AUROC 0.804 대 AUROC 0.92. "pathway membership and gene essentiality scores" 대 "patient age, BMI, and smoking status." "implicates the hippocampus, amygdala, prefrontal cortex" 대 "leaves the hippocampus and amygdala unaffected."
그 레이블링(labeling) 단계는 즉시 제 값을 했습니다. 제가 처음 초안으로 작성한 "거짓" 주장 중 3개는 (cerebellum, CRISPR, Loewe)와 같은 용어를 사용했는데, grep으로 확인해 보니 이 용어들은 실제로 코퍼스에 존재했으므로 전혀 거짓이 아니었습니다. AUROC와 마찬가지로, 수치에 반영되기 전에 오류를 잡아낸 것입니다.
그런 다음, 저는 참인 주장을 뒷받침하는 컨텍스트(context)를 검증기(verifier)에 입력하고, 동일한 컨텍스트를 바탕으로 두 주장을 모두 판단하도록 요청합니다.
벤치마크 결과
관대한 프롬프트(lenient prompt)와 더 엄격한 "숫자가 글자 그대로 일치해야 함" 프롬프트 모두 동일한 점수를 기록했습니다: 8개의 조작된 주장 중 8개 모두 적발, 8개의 참인 주장 중 잘못 표시된 것 0개.
정직하게 말하자면 두 가지 주의 사항이 있습니다. n=8인 경우 이는 점 추정치(point estimate)일 뿐 보증은 아닙니다. 8/8 결과에 대한 Wilson 95% 신뢰 구간은 약 67%에서 100% 사이이므로, 이를 "8번의 시도 중 실패가 없었음"으로 읽어야 합니다. 그리고 이 설정은 가능한 가장 쉬운 설정이라는 점에 유의하십시오: 뒷받침하는 구절이 존재함이 보장되어 있고, 변조된 값은 존재하지 않음이 보장되어 있습니다. 이는 파이프라인(pipeline)이 아니라, 검색(retrieval)이 이미 완벽할 때의 판사(judge)를 측정하는 것입니다. 엄격한 프롬프트가 아무것도 바꾸지 못했다고 해서 그것이 무용하다는 증거도 아닙니다. 왜냐하면 세트 내의 어떤 쌍도 두 프롬프트를 구분해내지 못했기 때문입니다. 모든 변조는 실제 값과 거리가 매우 멉니다. 0.804 대 0.81이 테스트가 되겠지만, 저에게는 그런 데이터가 없습니다.
결론적으로: 완벽한 컨텍스트와 명백한 변조가 주어졌을 때, 로컬 8B 모델은 신뢰할 수 있게 판단합니다. 알고 있으면 좋은 사실이지만, 이것이 어려운 사례는 아닙니다.
벤치마크가 도달할 수 없는 어려운 사례
해당 벤치마크는 제가 직접 작성한 허위 주장들을 사용했습니다. 이는 제가 공동 과학자(co-scientist)로서 실제로 관심을 두는 시나리오, 즉 모델 자체가 생성한 환각(hallucination)을 동일한 모델이자 동일한 사각지대를 공유할 수 있는 검증기(verifier)가 판단하는 상황을 우회해 버립니다.
그래서 저는 두 번째 평가(eval)를 실행했습니다. 정답(ground-truth)을 확인한, 잘못된 전제 또는 특정 정보가 누락된 6개의 질문을 준비했습니다. 모델이 작성한 초안 자체가 테스트 대상(unit under test)이며, 저는 각 초안을 두 번씩 판단합니다. 한 번은 동일한 모델(qwen3:8b)로, 다른 한 번은 다른 모델(gemma4 12B QAT)로 판단합니다. 한 가지 주의할 점을 말씀드리자면, 판단 모델이 답변 모델인 8B보다 더 크기 때문에, 여기서 발생하는 모델 간 성능 향상은 순수하게 "다름"에서 오는 것이 아니라 "다르고 더 큼"에서 오는 것입니다. 동일한 크기의 모델 간 교차 확인을 수행했다면 이를 분리해낼 수 있었겠지만, 저는 그렇게 하지 않았습니다.
6개 중 4개에 대해 모델은 올바르게 답변을 거부(abstain)했습니다. 메로페넴(meropenem)의 용량(코퍼스에 없음)이나, 메타 분석의 샘플 크기(해당 스트레스 논문은 샘플 크기가 없는 범위 검토(scoping review) 논문임)를 물었을 때, 모델은 컨텍스트에 해당 내용이 포함되어 있지 않다고 답했습니다. 간극이 명확할 때는 근거 제시(Grounding)가 제대로 작동합니다.
나머지 2개에 대해서는 모델이 내용을 꾸며냈으며, 두 사례 모두 "숫자는 맞지만 문맥이 틀린(right-number-wrong-context)" 경우였습니다.
- "홀드아웃 테스트 세트(held-out test set)에서의 AUROC" → 다시 "0.804"라고 답함. 질문이 전제하고 있는 해당 홀드아웃 AUROC는 존재하지 않습니다.
- "경로 멤버십 특징(pathway-membership features)을 제거한 후의 AUROC" → "0.627." 이 0.627은 실제 수치이지만, 이는 리보솜 타겟팅(ribosome-targeting) 조합을 제거했을 때의 수치로, 다른 절제 연구(ablation) 결과입니다. 모델이 근처에 있는 실제 값을 가져와서 잘못 할당한 것입니다.
동일 모델 판사(same-model judge)는 그 두 가지 중 어느 것도 잡아내지 못했습니다. 자신이 만들어낸 허구(fabrication) 두 건 모두에 대해 승인 도장을 찍어버린 것입니다. 교차 모델 판사(cross-model judge)는 한 건을 잡아냈습니다. Gemma가 "해당 AUROC는 리보솜 타겟팅 조합을 제거한 결과입니다"라는 이유를 들며 0.627을 지적했습니다. 다른 모델은 그 숫자가 무엇에 연결되어 있는지 확인했습니다. 원래 모델은 잘못된 할당(misattribution)을 생성한 후 이를 재검토하지 않았던 것입니다. 두 판사 모두 홀드아웃(held-out) 사례는 잡아내지 못했습니다. 숫자 0.804는 실제 값이므로, 숫자 자체에는 모순되는 내용이 없으며, 이를 잡아내려면 홀드아웃 세트가 존재하지 않는다는 사실을 알아야 합니다. 제가 확인했을 때, "별도의 홀드아웃 테스트 세트는 사용되지 않았습니다"라고 명시된 문장은 검색된 컨텍스트(retrieved context)에조차 없었습니다. 모델은 전제(premise)를 거짓으로 만드는 그 내용을 전혀 보지 못한 것입니다.
이것이 이번 실험의 측정된 핵심이며, 실패는 두 가지가 아닌 세 가지 유형으로 나뉩니다. 컨텍스트에 아예 존재하지 않는 값(0.92)은 동일 모델이 판정할 때조차 안정적으로 잡아냅니다. 실제 값이지만 잘못된 대상에 연결된 경우(다른 절제(ablation) 연구에서 나온 0.627)는 동일 모델 판사를 통과하며, 두 번째 판사에 의해서만 가끔 회복됩니다. 홀드아웃 세트가 존재하지 않는데 홀드아웃 AUROC라고 주장하는 것과 같은 잘못된 전제는 두 판사 모두를 통과하며, 이 경우는 반박하는 문장이 컨텍스트에 도달하지 못했기 때문에 검색(retrieval) 단계에서 막혔습니다. 제가 구축한 레이어는 첫 번째 유형만을 안정적으로 잡아냅니다. 자신의 출력을 심판하는 모델은 자신의 사각지대(blind spots)를 그대로 물려받습니다. 두 번째 모델은 잘못된 할당(misattribution)의 일부를 회복할 뿐, 전제 수준의 오류(premise-level error)를 회복하지는 못합니다.
플래그(flags)가 실제로 가리키고 있었던 것
엉망이었던 첫 번째 평가에서 발견된 또 다른 실마리입니다. 당시 검증기(verifier)는 몇 가지 참인 주장("전전두엽 피질(prefrontal cortex)", "유전자 필수성 점수(gene essentiality scores)")을 근거 없는 것으로 플래그를 표시했습니다. 검증기가 그런 실수를 전혀 하지 않았던 깨끗한 벤치마크 이후에 보니, 그 결과들은 일관성이 없어 보였습니다.
해결책은 다음과 같습니다: 검증기가 전체 코퍼스(corpus)가 아닌, 검색된(retrieved) 컨텍스트(context)를 바탕으로 주장(claims)을 검증하도록 하는 것입니다. 해당 주장들은 사실이었고 코퍼스 내에 존재했지만, 그 주장을 증명하는 구절이 해당 질문을 위해 검색된 청크(chunks) 중에는 없었습니다. 검증기는 "제공된 내용에는 없습니다"라고 올바르게 답변한 것입니다.
따라서 플래그(flag)는 세 가지 의미를 가질 수 있으며, 정답(ground truth) 없이는 답변 시점에 이를 구분할 수 없습니다: 검색 실패(retrieval miss), 실제 조작(fabrication), 또는 코퍼스 외부에 존재하는 사실입니다. 제가 실제로 확인할 수 있었던 첫 번째 평가 사례들에서 플래그는 검색 실패였으며, 이것이 바로 "주장을 삭제하라"는 것보다 "재검색 및 재검증"이 더 나은 기본 대응책인 이유입니다. 비율을 측정하지 않았으므로, 각 사례가 얼마나 자주 발생하는지에 대한 수치는 제시하지 않겠습니다.
핵심 요약 (Takeaways)
qwen3:8b를 사용하여 하나의 코퍼스에서 실제로 측정한 결과는 다음과 같습니다:
- 양질의 컨텍스트가 주어지고 명백하게 오염된 값이 있을 때, 검증기는 신뢰할 수 있습니다 (n=8이라는 주의사항이 있지만, 8/8 성공).
- 모델은 명백히 결여된 정보에 대해 답변을 유보합니다 (여기서는 4/6).
- 두 건의 조작(fabrications)은 모두 '실제 숫자가 잘못된 컨텍스트'와 관련되었습니다: 하나는 잘못 할당된 값(
0.627, 잘못된 어블레이션(ablation) 결과)이었고, 다른 하나는 잘못된 전제(홀드아웃(held-out) 세트가 존재하지 않는데 홀드아웃 AUROC를 언급함)였습니다. 동일 모델 판독기(same-model judge)는 둘 다 잡아내지 못했습니다 (0/2). 자신의 출력을 그대로 승인(rubber-stamped)해 버린 것입니다. - 교차 모델 판독기(cross-model judge)는 잘못 할당된 값은 잡아냈지만, 잘못된 전제는 잡아내지 못했습니다 (1/2). 전제 오류가 더 어려운 유형이었으며, 이를 반박할 문장이 검색조차 되지 않았기 때문입니다.
제가 측정하지는 않았지만 추측하는 바(따라서 인상 정도로만 받아들이세요)는 다음과 같습니다: 근거 기반 RAG(grounded RAG)에서 모델이 사실을 지어내는 것처럼 보이는 대부분의 현상은, 실제로는 검색이 구절을 표면화하지 못했거나 프롬프트(prompt)가 충분히 근거를 고정(grounding)하지 못했기 때문입니다. 만약 이것이 사실이라면, 해결의 핵심(leverage)은 더 큰 모델이 아니라 검색(retrieval)과 근거 설정(grounding)에 있습니다. 더 확신을 가지고 말하기 전에는 라벨링된 실행(labeled run) 데이터가 필요할 것 같습니다.
그리고 실질적인 버전은 다음과 같습니다: 주장 검증(claim-checking)은 컨텍스트(context)에 없는 값만을 안정적으로 잡아냅니다. 잘못된 질문에 붙은 실제 숫자를 놓치거나, 잘못된 전제(false premise)를 아예 놓치기도 합니다. 답변을 생성하는 모델과 판단하는 모델을 다르게 사용하세요. 이는 두 사례 중 하나가 회복된 것에 근거한 것이 아니라, 잘못된 귀속(misattribution)을 생성한 모델은 이를 재검토하지 않지만, 관련 없는 다른 모델은 해당 숫자가 무엇에 붙어 있는지를 교차 검증(cross-check)하기 때문입니다. 잘못된 전제가 아닌, 잘못 귀속된 일부 값들을 회복할 것으로 기대하십시오. 플래그(flag)가 발생하면 "삭제"가 아니라 "다시 검색(re-retrieve)" 하라는 신호로 취급하세요. 그리고 정답 라벨(ground-truth labels) 없이는 이 모든 것을 평가할 수 없습니다. 저는 제 자신의 코퍼스(corpus)에 대해 두 번이나 잘못된 발견을 출시할 뻔했으며, grep이 두 가지 모두를 해결해 주었습니다.
한계점들이 이곳의 정직함의 대부분을 차지합니다: 작은 규모의 코퍼스, 8B 규모의 답변 모델, 직접 만든 8개의 쌍(pairs)과 6개의 프로브(probes), 홀드아웃 점수 산정(held-out scoring)의 부재, 그리고 "중요한(significant)" 대 "트렌드인(trending)"과 같이 미묘하지만 실제적인 오염(corruptions)의 부재입니다. 이것은 첫 번째 측정일 뿐, 최종 판결이 아닙.
출처: 영감은 Karpathy의 llm-wiki 패턴과 nashsu/llm_wiki의 전체 데스크톱 구현에서 얻었습니다. 제가 구축한 것은 답변 시점에 실행되도록 옮겨진 단순한 근거 설정(groundedness) 확인이며, 로컬 환경에서 실행됩니다. 만약 이 설정의 허점을 찾고 싶다면, 코드와 평가 스크립트는 제 paper-rag repo에 있습니다. 꼭 확인해 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기