RAG 봇에게 환각(Hallucination)을 일으키지 말라고 말하는 것을 멈추세요. 환각이 불가능하게 만드세요.
요약
RAG 시스템에서 프롬프트만으로 환각을 방지하는 대신, 검색 단계에서 신뢰도 기준을 적용해 모델에게 잘못된 정보를 제공하지 않는 아키텍처 설계 방식을 제안합니다. MCP SDK Docs Assistant 사례를 통해 버전 관리와 거절 로직을 결합한 구현 방법을 설명합니다.
핵심 포인트
- 프롬프트 지시보다 검색 단계의 신뢰도 필터링이 환각 방지에 효과적임
- 모델이 답변할 재료(Context) 자체를 제공하지 않는 아키텍처 설계 필요
- 버전 정보를 검색의 일급 시민으로 취급하여 API 변경 대응
- 거절(Refusal)을 모델의 성격이 아닌 시스템의 속성으로 구현
모든 RAG 앱이 무시하는 제안
만약 당신이 검색 증강 생성 (RAG, Retrieval-Augmented Generation) 어시스턴트를 출시했다면, 시스템 프롬프트(System Prompt)에 다음과 같은 문구를 어떤 형태로든 작성해 보았을 것입니다:
"제공된 컨텍스트 (Context)에 답변이 없다면, 모른다고 말하세요. 지어내지 마세요."
그리고 모델이 압박을 받을 때 이 지시를 아주 쾌활하게 무시하는 모습을 지켜보았을 것입니다. 자신감 넘치는 질문이 들어오고, 검색 (Retrieval) 결과가 질문과 '유사한' 무언가를 반환하면, 모델은 그럴듯하고 유창하지만 틀린 답변을 짜깁기해냅니다. 언어 모델에게 환각 (Hallucination)을 일으키지 말라고 말하는 것은 하나의 '제안'일 뿐입니다. 그리고 이러한 제안은 도움이 되고자 하는 모델의 압도적인 사전 학습 (Prior) 성향에 패배하고 맙니다.
저는 프롬프트 문구로 이 문제와 싸우는 것에 지쳐서, Model Context Protocol TypeScript SDK를 위한 어시스턴트인 MCP SDK Docs Assistant를 구축하면서 다른 프레임워크를 시도해 보았습니다. 그 프레임워크는 다음과 같습니다: 모델에게 거절을 요청하지 마세요. 모델이 이야기를 지어낼 수 있는 능력 자체를 제거하세요.
프롬프트가 아닌 코드로 구현하는 거절
핵심 아이디어는 모델에게 환각을 일으킬 재료를 건네주지 않으면 모델은 환각을 일으킬 수 없다는 것입니다. 따라서 거절 결정은 모델이 무언가를 보기 전, 검색 도구 (Retrieval Tool) 단계에서 이루어집니다. 만약 어떤 것도 신뢰도 기준을 통과하지 못하면, 도구는 빈 결과 세트를 반환하며, 모델은 답변으로 만들어낼 소스 텍스트가 없는 상태가 됩니다.
실제로 도구는 대략 다음과 같은 형태를 띱니다:
const candidates = await hybridSearch(query, { version, limit: 12 });
if (!hasConfidentMatch(candidates)) { // 최적의 코사인 유사도 (Cosine Similarity) < 0.45
...
모델에게 행동을 '요청'하는 것이 아닙니다. 결과가 빈 상태로 돌아왔을 때, 유일하게 일관성 있는 다음 행동은 "문서에 이 내용이 포함되어 있지 않습니다"라고 말하는 것이 되도록 시스템을 설계하는 것입니다. 거절은 당신이 기대하는 성격적 특성이 아니라, 아키텍처 (Architecture)의 속성이 됩니다.
왜 특히 이 SDK에 이것이 필요했는가
이 어시스턴트가 해결해야 했던 두 번째 실패 모드(failure mode)가 있는데, 이는 빠르게 변화하는 라이브러리들에 특화된 문제입니다. MCP TypeScript SDK는 최근 메서드 이름 변경, 전송 방식(transport) 교체, 단일 패키지가 세 개로 분할되는 등 파괴적 API 변경(breaking API changes)을 포함하는 v1에서 v2로의 전환을 거쳤습니다. 일반적인 문서 봇들은 v1과 v2의 코드 스니펫(snippets)을 서로 섞어버리는 경향이 있어, 복사한 코드가 제대로 작동하지 않게 됩니다.
해결책은 버전을 검색(retrieval)의 일급 시민(first-class) 차원으로 취급하는 것입니다. 코퍼스(corpus) 내의 모든 청크(chunk)는 수집(ingestion)될 때 해당 버전 태그가 붙으며, 검색 시에는 적용 범위 내의 버전으로 필터링하고, 두 버전이 모두 관련이 있는 경우에는 답변 시 이를 라벨링하고 분리합니다. 거절 게이트(refusal gate)와 결합하면, 어시스턴트가 지켜야 할 세 가지 규율이 완성됩니다. 즉, 버전의 정확성을 유지하고, 모든 주장 뒤에 있는 정확한 소스 라인을 인용하며, 거절해야 할 때는 거절하는 것입니다.
검색 파이프라인(retrieval pipeline) 요약
질문에서 답변으로 이어지는 경로는 작은 파이프라인을 통과합니다. 하이브리드 검색(hybrid search)은 상호 순위 융합(Reciprocal Rank Fusion)을 사용하여 의미론적 유사성(pgvector를 통한 벡터 코사인 유사도)과 어휘 매칭(Postgres full-text)을 결합하므로, 정확한 용어의 정밀도를 놓치지 않으면서 임베딩(embeddings)의 재현율(recall)을 얻을 수 있습니다. 융합된 후보군들은 앞서 언급한 거절 게이트를 통과하며, 살아남은 후보들은 최종 세트를 좁히기 위해 LLM 교차 인코더(cross-encoder) 재순위화(rerank) 과정을 거칩니다. 그제서야 에이전트는 인용을 병행하며 답변을 생성합니다.
동일한 검색 코어가 하나의 코드베이스로부터 웹 채팅, CLI, MCP 서버라는 세 가지 인터페이스를 구동합니다. 이는 어시스턴트가 Cursor나 Claude Desktop이 호출할 수 있는 도구로서 _자기 자신_을 배포할 수 있음을 의미합니다. MCP SDK를 가르치는 MCP 도구라니, 참 재미있는 재귀(recursion)입니다.
평가 하네스(eval harness)가 제값을 한 부분
이 이야기는 이런 종류의 시스템을 구축하려는 모든 사람에게 가장 유용할 것이라고 생각합니다. 왜냐하면 이것은 당신이 작업이 끝났다고 생각한 _이후_에 벌어지는 일에 관한 것이기 때문입니다.
저는 18개의 케이스로 구성된 골든 세트(golden set)를 작성하여, 모든 질문을 라이브 에이전트(live agent)에 통과시키고 실제로 중요한 행동들을 점수화했습니다. 즉, 거절 정확도(refusal accuracy), 답변에 인용(citations)이 포함되었는지 여부, 그리고 버전 정확성(version-correctness)입니다. 초기 실행 결과는 83%였으며, 실패 사례들은 매우 유익한 종류였습니다. 실제로 답변 가능한 두 개의 질문이 '거짓 거절(false-refused)'되고 있었습니다. 검색(retrieval)을 통해 유사도가 0.70에서 0.72 정도로 강력한 검색 결과가 나타났고, 이는 임계값(gate)인 0.45를 여유롭게 상회했음에도 불구하고, 어시스턴트는 "문서에 해당 내용이 없습니다"라고 답했습니다.
검색 레이어(retrieval layer)는 제 역할을 다했습니다. 문제는 모델이 관련성(relevance)을 스스로 다시 판단하며, 도구가 이미 확인한 신호를 의심했다는 점입니다. 해결책은 모델이 이를 다시 논쟁하게 두는 대신, 거절 여부를 도구의 신호에 따르도록 만드는 것이었습니다. 재실행 결과, 테스트 스위트(suite)는 100%를 기록했으며 거절 정확도 또한 그대로 유지되었습니다. 즉, 이 수정 사항이 시스템을 무모하게 만들어 답변 커버리지(answer coverage)를 얻어낸 것이 아님을 의미합니다.
이 수치에는 주의할 점이 있으며, 이는 매우 중요합니다. 100%라는 수치는 이 리포지토리(repo) 자체에서 큐레이션한 골든 세트를 대상으로 한 결과라는 점입니다. 이는 시스템이 대표적인 질문 세트에 대해 계약(contract)을 준수한다는 것을 증명하는 것이지, 시스템이 전지전능하다는 뜻은 아닙니다. 테스트 하네스(harness)의 진정한 가치는 점수 그 자체가 아니라, 세트를 확장함으로써 사용자가 발견하기 전에 미래의 회귀(regressions)를 포착할 수 있다는 데 있습니다.
여기서 얻을 수 있는 교훈
RAG 시스템을 구축한다면, 전이 가능한 교훈은 "이 임계값을 사용하라"나 "이 스택을 사용하라"가 아닙니다. 그것은 보장(guarantees)을 어디에 두느냐에 대한 관점의 전환입니다. 거절, 인용, 버전 준수와 같이 당신이 진정으로 신경 쓰는 행동들은 프롬프트(prompt)에서 요청할 때보다 코드(code)로 강제할 때 더 신뢰할 수 있습니다. 프롬프트는 희망 사항일 뿐입니다. 검색 도구의 게이트(gate)는 제약 조건(constraint)입니다. 그리고 제약 조건은 정중한 지시사항이 결코 해내지 못하는 방식으로, 적대적인 질문(adversarial questions)과의 접촉 속에서도 살아남습니다.
오픈 소스입니다 — 함께 만들어 가세요
이 프로젝트는 MIT 라이선스이며, 여러분의 도움을 간절히 기다리고 있습니다. good first issue 티켓들이 친절하게 준비되어 있습니다. 다크 테마(dark theme) 적용, 코드 블록의 클립보드 복사(copy-to-clipboard) 버튼 추가, 선택한 엔진의 상태 유지(persisting the selected engine), 그리고 더 많은 범위를 벗어난 사례(out-of-scope cases)를 통해 평가 골든 셋(eval golden set)을 확장하는 작업 등이 포함됩니다 (이는 위에서 언급한 거절 보장(refusal guarantees)을 직접적으로 강화합니다).
만약 이러한 문제들이 여러분이 즐기는 종류의 작업이라면, 아래에 리포지토리(repo), 라이브 데모(live demo), 그리고 기여자 가이드(contributor guide) 링크를 모두 연결해 두었습니다. 이슈(Issues)와 풀 리퀘스트(PRs)를 환영합니다.
Repo: https://github.com/Kaydenletk/mcp-docs-assistant
Live demo: https://library-assisstant-ai.vercel.app
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기