Chain-of-Note: 검색된 패시지별 관련성 노트를 작성하고, 지원하는 정보만을 사용하여 답변하며, 정직하게 답변을 거부하기
요약
RAG 시스템의 고질적인 문제인 환각과 노이즈 문제를 해결하기 위해 각 검색 패시지에 대한 관련성 노트를 작성하는 Chain-of-Note 기법을 소개합니다. 모델이 답변 전 패시지의 유효성을 먼저 판단하게 하여, 정보가 없을 경우 정직하게 답변을 거부하도록 유도합니다.
핵심 포인트
- Naive RAG의 키워드 중복 기반 유사도 측정 한계 지적
- Chain-of-Note를 통한 패시지별 관련성 및 내용 요약 단계 삽입
- 지원하는 정보가 없을 경우 '모르겠습니다'라고 답변하는 거부 규칙 적용
- 프롬프트 엔지니어링만으로 구현 가능한 효율적인 RAG 개선 방식
제가 단순한 RAG (Retrieval-Augmented Generation)를 더 이상 신뢰하지 않게 된 결정적인 실패 사례는, 문서가 답할 수 없는 질문에 대해 단순히 키워드가 겹친다는 이유만으로 확신을 가지고 답변하는 모습을 목격했을 때였습니다. 검색 (Retrieval)은 유사도 (similarity)에 따라 순위를 매기지만, 유사도가 곧 진실은 아닙니다. 어떤 패시지 (passage)는 질문의 모든 단어와 중복될 수 있지만 정작 답은 포함하지 않을 수 있으며 (distractor, 방해 요소), 때로는 정답이 상위 K개 (top-K) 패시지 중 그 어디에도 없을 수도 있습니다. Chain-of-Note (Yu et al., 2023)는 읽기 단계 (reading step)를 삽입하여 이 두 가지 문제를 모두 해결합니다. 답변하기 전에 각 패시지별로 짧은 '노트 (note)'를 작성합니다. 즉, 이 패시지가 관련이 있는지, 그리고 실제로 무엇을 말하고 있는지를 기록하는 것입니다. 그런 다음 지원적인 노트로부터만 답변을 도출하며, 답변을 뒷받침하는 내용이 없을 때는 "모르겠습니다"라고 말합니다. 저는 검색 유사도, 노트 로직, 답변 추출, 그리고 답변 거부 규칙이 모두 실시간으로 실행되는 데모를 구축했습니다. 작동 방식은 다음과 같습니다.
Naive RAG는 덩어리를 통째로 집어넣고 한 번에 답변합니다
기본적인 방식은 누구나 가장 먼저 구현하는 방식입니다: 상위 K개를 검색하고, 그대로 붙여넣은 뒤, 답변합니다.
const docs = await retrieve(question, { k: 4 }); // 유사도에 따라 순위 지정
const answer = await llm(`Passages:\n${docs.map((d,i)=>`[${i+1}] ${d.text}`).join("\n")}\nQuestion: ${question}\nAnswer:`);
검색 결과가 깨끗할 때는 괜찮습니다. 하지만 모델은 검색기의 노이즈 (noise)를 그대로 물려받고 모든 내용을 하나의 나뉘지 않은 덩어리로 읽기 때문에, 표면적인 중복성만을 따라 잘못된 답변으로 직행하거나, 모르는 질문에 대해 답변을 지어냅니다.
두 가지 고장 나는 방식
제 데모에서 답변 가능한 시나리오는 "Nimbus가 첫 드론을 출시한 연도는 언제인가요?"라고 묻습니다. 실제 패시지는 드론이 2017년에 출하되었다고 말하지만, 방해 요소인 "2012년에 첫 출시 파티를 열었다"가 키워드 중복도에서 더 높은 점수를 받아 Naive RAG를 2012년으로 유도합니다. 모르는 시나리오는 가상의 국가의 수도를 묻습니다. 답변할 수 있는 패시지가 없지만, Naive RAG는 근처 패시지에서 도시 이름을 빌려와 환각 (hallucination)을 일으킵니다. 두 사례 모두 순위 (ranking)를 신뢰하는 데서 비롯됩니다.
노트는 패시지별 판결입니다
Chain-of-Note는 새로운 파이프라인 (pipeline)이 아니라 하나의 _프롬프트 (prompt)_입니다. 지시 사항은 단 한 단락으로 구성됩니다: 각 패시지 (passage)에 대해 노트 (note)를 작성하고 (관련이 있는가? 무엇을 말하고 있는가?), 관련이 있는 패시지만을 사용하여 답변하며, 답변을 뒷받침하는 내용이 없다면 "모르겠습니다"라고 답하는 것입니다. 데모에서 저는 관련성 (relevance)을 실제처럼 구현했습니다. 즉, 패시지가 후보 답변을 포함하고 있으며 필요한 개념들이 함께 등장 (co-occur)할 때만 지원적인 것으로 간주합니다:
const notes = docs.map(d => {
const answer = extractAnswer(d.text, question);
const relevant = answer != null &&
...
따라서 설립 연도에 관한 패시지는 중복되는 단어들로 가득 차 있지만 "출시 (launch)"와 "드론 (drone)"이 함께 등장하지 않으므로, 조용히 답변으로 채택되는 대신 _관련 없음 (not relevant)_으로 표시되어 제외됩니다. 이는 단순히 텍스트 덩어리 (blob)에 "확실하지 않으면 기권하라"는 힌트를 던져주는 것과는 다릅니다. 단순한 힌트는 각 패시지에 대한 판결 (verdict)을 근거로 삼을 수 없으므로, 방해 요소 (distractor)와 실제 답변을 깔끔하게 구분할 수 없습니다.
지원적인 노트로 필터링한 후 답변하거나, 기권하기
관련이 있다고 표시된 노트만 유지하며, 그 외의 모든 것은 답변 문맥 (context)에서 제외됩니다. 그런 다음 핵심적인 동작이 수행됩니다:
function chainOfNote(question, docs) {
const supportive = docs.map(d => noteFor(question, d)).filter(n => n.relevant);
if (supportive.length === 0) return { answer: "I don't know", grounded: false };
...
지원적인 노트가 하나도 없다는 것은 정직한 답변이 "모르겠습니다"임을 의미합니다. 답할 수 없는 질문에 대해 기권하는 것은 실패가 아닙니다. 특히 이해관계가 큰 질의응답 (high-stakes QA) 상황에서는 확신에 찬 오답이 지식의 공백을 인정하는 것보다 더 나쁩니다. 데모에서 실제 패시지를 끄면, 단순한 RAG (naive RAG)는 계속해서 가장 유사해 보이는 것을 잡아내는 반면, Chain-of-Note는 즉시 정직한 기권 상태로 전환됩니다.
이 기술이 가치를 발휘하는 곳
저는 검색 결과에 노이즈가 많거나 질문이 코퍼스 (Corpus) 범위를 벗어날 수 있는 경우마다 Chain-of-Note를 활용합니다. 이 기술은 다음과 같이 구성됩니다: 리랭킹 (Reranking) _이후_에 실행하여 (노트 작성을 위한 더 적고 깨끗한 패시지 확보), 검색 및 재검색 여부를 결정하는 Self-RAG / CRAG와 병행하여 사용합니다. 이때 CoN은 검색된 결과가 무엇인지 판단하고 언제 기권해야 하는지를 알고 있습니다. 이는 프롬프트 전용 (Prompt-only) 방식이기에 추가적인 리트리버 (Retriever)나 파인튜닝 (Fine-tune)이 필요 없으며, 바로 이 점 때문에 도입 비용이 저렴하고 거부하기 어렵습니다.
Chain-of-Note 스위치를 켜고 패시지를 전환하여 답변이 바뀌는 것을 확인해 보세요:
https://dev48v.infy.uk/prompt/day46-chain-of-note.html
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기