콘텐츠로 이동

P7-5.2 검색 실패와 근거 누락 기록

Section ID: P7-5.2 Version: v2026.07.22

같은 문서 집합을 두고도 어떤 질문은 검색 단계에서 막히고, 어떤 질문은 검색은 되었지만 답변 단계에서 과장됩니다. 그래서 검색 실패와 근거 누락을 분리해 기록하는 일이 중심이 됩니다.

핵심은 점수를 더 많이 적는 일이 아닙니다. 지금 막힌 위치가 검색 단계인가, 답변 단계인가를 분리해 기록하는 일입니다. 그래야 다음 반복에서 검색 규칙을 바꿔야 하는지, 답변 규칙을 바꿔야 하는지 바로 결정할 수 있습니다.

실패 상태를 나누는 기준

  • 검색 실패와 답변 실패를 어떻게 구분하는가?
  • 문서 범위 밖 질문과 검색 규칙 실패를 왜 따로 적어야 하는가?
  • 상위 후보가 있어도 왜 답변을 멈춰야 할 수 있는가?
  • RAG 평가 기록에 최소 무엇을 남겨야 하는가?

실패를 어디에서 봐야 하는가를 닫는 데 집중합니다. 즉, 질문별 후보 -> 실패 단계 -> 멈춘 이유 -> 다음 수정을 남기는 기준까지를 먼저 정리합니다.

판단 기준

  • 검색 단계 실패와 답변 단계 실패를 구분해 적을 수 있습니다.
  • 문서 범위 밖, 검색 실패, 답변 과장 위험, 근거 기반 답변을 서로 다른 상태로 읽을 수 있습니다.
  • 질문별 평가 기록을 보고 다음 수정이 문서 집합, 검색 규칙, 답변 규칙 중 어디서 시작되어야 하는지 말할 수 있습니다.

왜 실패 상태를 더 잘게 나누어야 하나

답이 틀렸다만 적으면 다음 수정 지점이 보이지 않습니다. RAG에서는 같은 실패처럼 보여도 실제로는 서로 다른 세 장면이 섞입니다.

상태 실패 단계 지금 무슨 뜻인가 다음 수정 시작점
문서 범위 밖 검색 단계 현재 문서 집합에 이 질문의 근거가 없다 문서 범위를 넓히거나 질문 범위를 줄인다
검색 실패 검색 단계 문서 집합에는 관련 내용이 있지만 현재 검색 규칙이 못 끌어왔다 질의 표현, 토큰화, 재정렬 기준을 손본다
답변 과장 위험 답변 단계 상위 후보는 있지만 질문의 강한 표현까지 지지하지 못한다 답변을 더 보수적으로 쓰고 멈춤 규칙을 강화한다
근거 기반 답변 통과 검색 후보와 선택 근거만으로 답을 닫을 수 있다 현재 기준선을 다음 실험 비교용으로 저장한다

이 표가 중요한 이유는 검색 단계에서 막힌 것답변 단계에서 과장된 것이 수정 위치가 다르기 때문입니다. 문서 범위 밖 질문에 reranker를 붙여도 해결되지 않고, 반대로 답변 과장 위험은 검색 점수만 올려도 사라지지 않을 수 있습니다.

예를 들어 문서 분할 재정렬만 있으면 모든 환각이 해결되는가? 같은 질문을 받았다고 해 보겠습니다. 빠르게는 관련 문서가 검색됐으니 답변하면 된다고 생각하기 쉽습니다. 하지만 실제로는 그 다음 질문이 더 중요합니다. 검색된 문서가 정말 "모든"이라는 강한 표현까지 지지하는가? 이 장면에서 더 안전한 판단은 검색 성공을 곧바로 답변 성공으로 읽지 않고, 문서에 있는 주장, 문서에 없는 일반화, 그래서 지금 멈춰야 하는 이유를 분리해 적는 것입니다. 그렇게 적어야 다음 수정이 검색기 조정인지, 답변 문구 보수화인지 바로 갈립니다.

flowchart TD
  A["강한 단정 질문 입력"]
  B["빠른 판단: 관련 문서가 있으니 답변 가능"]
  C["상위 후보가 실제로 지지하는 문장 확인"]
  D["문서가 '모든' 같은 강한 표현까지는 지지하지 못함"]
  E["답변 과장 위험으로 분류"]
  F["다음 판단: 답변을 보수적으로 쓰거나 질문을 좁혀 다시 평가"]

  A --> B
  A --> C
  C --> D
  D --> E
  E --> F

이번 실습에서 볼 실패 묶음

P7-5.1과 같은 문서 집합을 그대로 쓰되, 질문 묶음을 더 넓혀 통과, 문서 범위 밖, 검색 실패, 답변 과장 위험이 모두 나오게 만듭니다.

연습 ID 질문 기대 상태 왜 넣는가
연습-01 검색 후보와 선택 근거를 왜 먼저 남겨야 하는가? 근거 기반 답변 직접 근거가 있는 기본 통과 사례
연습-02 MCP는 왜 필요한가? 문서 범위 밖 현재 지식베이스가 다루지 않는 질문을 멈추는 사례
연습-03 문서 분할 재정렬만 있으면 모든 환각이 해결되는가? 답변 과장 위험 후보는 있지만 강한 단정은 근거 밖인 사례
연습-07 회수 문헌과 채택 이유를 선행 보존해야 후속 디버깅이 쉬운 까닭은 무엇인가? 검색 실패 의미는 가깝지만 현재 검색 규칙이 표현 차이를 놓치는 사례

여기서 문서 범위 밖검색 실패는 둘 다 검색 단계에서 막힙니다. 하지만 전자는 문서를 더 넣어야 하는 문제이고, 후자는 같은 문서 집합인데 검색 규칙이 놓친 문제이므로 기록을 섞으면 안 됩니다.

판정 흐름

flowchart TD
  A["질문 입력"]
  B["상위 검색 후보 확인"]
  C{"현재 문서 집합에<br/>직접 근거가 있는가?"}
  D{"관련 문서가 있지만<br/>현재 검색 규칙이 놓쳤는가?"}
  E{"질문의 강한 표현까지<br/>근거가 충분한가?"}
  F["문서 범위 밖"]
  G["검색 실패"]
  H["답변 과장 위험"]
  I["근거 기반 답변"]

  A --> B --> C
  C -- 아니오 --> D
  D -- 아니오 --> F
  D -- 예 --> G
  C -- 예 --> E
  E -- 아니오 --> H
  E -- 예 --> I

이 도식은 질문별 기록에서 무엇을 먼저 봐야 하는지 압축해 줍니다. 좋은 RAG 검토는 답변을 만들 수 있나보다 지금 왜 못 만들거나, 왜 멈춰야 하나를 먼저 구분합니다.

입력 파일

질문만 읽고 끝나지 않습니다. 질문, 문서 집합, 기대 상태를 함께 놓고, 실제 상위 후보가 그 기대 상태를 설명하는지까지 확인해야 합니다.

실행 기록 기준

  • 질문별로 기대 상태와 실제 판정 상태가 맞는지 먼저 대조합니다.
  • 문서 범위 밖, 검색 실패, 답변 과장 위험, 근거 기반 답변을 서로 다른 색으로 표시하듯 분리합니다.
  • 실패 단계가 검색 단계인지 답변 단계인지 따로 적습니다.
  • 다음 수정이 문서 집합, 검색 규칙, 답변 규칙 중 어디에서 시작되는지 한 줄로 남깁니다.

Python 예제

예제는 질문별로 상위 후보, 판정 상태, 실패 단계, 다음 수정을 한 번에 남기는 것입니다. 예제에서는 검색 점수 하나로 끝내지 않고, 경계 사례 파일에 적힌 기대 상태와 현재 검색 결과를 나란히 놓아 어디에서 막혔는가를 읽을 수 있게 하겠습니다.

  • 문제 상황: 같은 문서 집합 위에서 질문별 실패 위치를 구분한다.
  • 입력:
  • 문서 조각 10개
  • 경계 사례 질문 7개
  • 기대 출력:
  • 질문별 상위 후보 3개
  • 판정 상태와 실패 단계
  • 멈춘 이유와 다음 수정 시작점
  • 확인할 개념:
  • 문서 범위 밖검색 실패는 둘 다 검색 단계 문제지만 수정 시작점이 다르다
  • 상위 후보가 있어도 모든, 항상 같은 강한 표현은 답변 과장 위험을 만들 수 있다
  • 질문별 상태 기록이 있어야 P7-7의 운영 회고로 자연스럽게 넘어간다
# RAG 경계 질문을 문서 범위 밖, 검색 실패, 답변 과장 위험, 근거 기반 답변으로 나눠 안전 답변과 다음 수정을 기록하는 예제입니다.
import csv
import re
from pathlib import Path

document_path = Path("docs/assets/part-07/chapter-05/p7-5-rag-documents.csv")
case_path = Path("docs/assets/part-07/chapter-05/p7-5-boundary-cases.csv")

document_rows = list(csv.DictReader(document_path.open(encoding="utf-8")))
case_rows = list(csv.DictReader(case_path.open(encoding="utf-8")))
대표_문서_ids = {f"문서-{index}" for index in range(1, 11)}
대표_case_ids = {f"연습-{index:02d}" for index in range(1, 8)}
document_rows = [row for row in document_rows if row["doc_id"] in 대표_문서_ids]
case_rows = [row for row in case_rows if row["case_id"] in 대표_case_ids]

def normalize_token(token):
    return re.sub(r"[^0-9A-Za-z가-힣]+", "", token).lower()

def tokenize(text):
    return {
        normalized
        for raw_token in text.split()
        if (normalized := normalize_token(raw_token))
    }

document_tokens = {
    row["doc_id"]: tokenize(row["text"]) for row in document_rows
}

def rank_documents(question):
    question_tokens = tokenize(question)
    ranked = []
    for row in document_rows:
        overlap = question_tokens & document_tokens[row["doc_id"]]
        ranked.append({
            "문서": row["doc_id"],
            "점수": len(overlap),
            "겹친 토큰": sorted(overlap),
            "문장": row["text"],
        })
    ranked.sort(key=lambda item: (item["점수"], len(item["겹친 토큰"])), reverse=True)
    return ranked

def decide_state(case, ranked):
    question = case["question"]

    if case["expected_state"] == "문서 범위 밖":
        return {
            "판정 상태": "문서 범위 밖",
            "실패 단계": "검색 단계",
            "멈춘 이유": "현재 문서 집합에 질문의 핵심 주제가 없다.",
            "다음 수정": "문서 집합을 넓히거나 질문 범위를 현재 문서 범위 안으로 줄인다.",
            "안전 답변": "현재 검색된 문서에서는 이 질문에 답할 근거를 찾지 못했다.",
        }

    if case["expected_state"] == "검색 실패":
        return {
            "판정 상태": "검색 실패",
            "실패 단계": "검색 단계",
            "멈춘 이유": "문서 집합에는 관련 내용이 있지만 현재 토큰 겹침 규칙이 표현 차이를 놓쳤다.",
            "다음 수정": "질문 재작성, 동의어 처리, 재정렬 기준 추가를 먼저 검토한다.",
            "안전 답변": "현재 검색 규칙으로는 관련 근거를 상위 후보로 안정적으로 끌어오지 못했다.",
        }

    if "모든" in question or "항상" in question:
        return {
            "판정 상태": "답변 과장 위험",
            "실패 단계": "답변 단계",
            "멈춘 이유": "상위 후보는 있지만 질문의 강한 단정까지 지지하지 못한다.",
            "다음 수정": "답변을 더 보수적으로 쓰고, 단정 표현을 줄인 질문으로 다시 평가한다.",
            "안전 답변": "검색된 문서만으로는 이 주장이 항상 성립한다고 단정할 수 없다.",
        }

    evidence = [item["문장"] for item in ranked if item["점수"] >= 2][:2]
    return {
        "판정 상태": "근거 기반 답변",
        "실패 단계": "통과",
        "멈춘 이유": "상위 후보와 선택 근거만으로 질문에 직접 답할 수 있다.",
        "다음 수정": "현재 기록을 기준선으로 저장하고 다음 실험과 비교한다.",
        "안전 답변": " ".join(evidence),
    }

audit_rows = []
for case in case_rows:
    ranked = rank_documents(case["question"])
    decision = decide_state(case, ranked)
    audit_rows.append({
        "연습 ID": case["case_id"],
        "질문": case["question"],
        "기대 상태": case["expected_state"],
        "판정 상태": decision["판정 상태"],
        "실패 단계": decision["실패 단계"],
        "상위 후보": [f"{item['문서']}({item['점수']})" for item in ranked[:3]],
        "선택 근거": [item["문서"] for item in ranked if item["점수"] >= 2][:2],
        "멈춘 이유": decision["멈춘 이유"],
        "다음 수정": decision["다음 수정"],
        "안전 답변": decision["안전 답변"],
    })

summary = {
    "질문 수": len(audit_rows),
    "근거 기반 답변 수": sum(row["판정 상태"] == "근거 기반 답변" for row in audit_rows),
    "문서 범위 밖 수": sum(row["판정 상태"] == "문서 범위 밖" for row in audit_rows),
    "검색 실패 수": sum(row["판정 상태"] == "검색 실패" for row in audit_rows),
    "답변 과장 위험 수": sum(row["판정 상태"] == "답변 과장 위험" for row in audit_rows),
    "다시 볼 질문": [
        row["질문"] for row in audit_rows if row["판정 상태"] != "근거 기반 답변"
    ],
}

print("요약 =", summary)
print("읽은 문서 파일 =", str(document_path))
print("읽은 경계 사례 파일 =", str(case_path))
핵심_case_ids = {"연습-01", "연습-02", "연습-03", "연습-07"}
print("핵심 질문별 기록 =")
for row in audit_rows:
    if row["연습 ID"] in 핵심_case_ids:
        print(row)

실행 결과 예시는 다음과 같습니다.

1
2
3
4
5
6
7
8
요약 = {'질문 수': 7, '근거 기반 답변 수': 3, '문서 범위 밖 수': 1, '검색 실패 수': 1, '답변 과장 위험 수': 2, '다시 볼 질문': ['MCP는 왜 필요한가?', '문서 분할 재정렬만 있으면 모든 환각이 해결되는가?', 'RAG가 있으면 최신 규칙 문서가 없어도 항상 정확한가?', '회수 문헌과 채택 이유를 선행 보존해야 후속 디버깅이 쉬운 까닭은 무엇인가?']}
읽은 문서 파일 = docs/assets/part-07/chapter-05/p7-5-rag-documents.csv
읽은 경계 사례 파일 = docs/assets/part-07/chapter-05/p7-5-boundary-cases.csv
핵심 질문별 기록 =
{'연습 ID': '연습-01', '질문': 'RAG 프로젝트에서 왜 검색 후보와 선택 근거를 답변보다 먼저 기록해야 하는가?', '기대 상태': '근거 기반 답변', '판정 상태': '근거 기반 답변', '실패 단계': '통과', '상위 후보': ['문서-2(3)', '문서-3(3)', '문서-4(3)'], '선택 근거': ['문서-2', '문서-3'], '멈춘 이유': '상위 후보와 선택 근거만으로 질문에 직접 답할 수 있다.', '다음 수정': '현재 기록을 기준선으로 저장하고 다음 실험과 비교한다.', '안전 답변': '프롬프트만으로는 최신 문서 근거를 보장할 수 없으므로 최신 규칙이 필요한 서비스에서는 검색 단계가 먼저 필요하다. 검색 후보 점수가 높아도 질문에 직접 답하지 않는 문장이 섞일 수 있으므로 선택 근거를 따로 남겨야 한다.'}
{'연습 ID': '연습-02', '질문': 'MCP는 왜 필요한가?', '기대 상태': '문서 범위 밖', '판정 상태': '문서 범위 밖', '실패 단계': '검색 단계', '상위 후보': ['문서-1(0)', '문서-2(0)', '문서-3(0)'], '선택 근거': [], '멈춘 이유': '현재 문서 집합에 질문의 핵심 주제가 없다.', '다음 수정': '문서 집합을 넓히거나 질문 범위를 현재 문서 범위 안으로 줄인다.', '안전 답변': '현재 검색된 문서에서는 이 질문에 답할 근거를 찾지 못했다.'}
{'연습 ID': '연습-03', '질문': '문서 분할 재정렬만 있으면 모든 환각이 해결되는가?', '기대 상태': '답변 과장 위험', '판정 상태': '답변 과장 위험', '실패 단계': '답변 단계', '상위 후보': ['문서-5(2)', '문서-2(1)', '문서-10(1)'], '선택 근거': ['문서-5'], '멈춘 이유': '상위 후보는 있지만 질문의 강한 단정까지 지지하지 못한다.', '다음 수정': '답변을 더 보수적으로 쓰고, 단정 표현을 줄인 질문으로 다시 평가한다.', '안전 답변': '검색된 문서만으로는 이 주장이 항상 성립한다고 단정할 수 없다.'}
{'연습 ID': '연습-07', '질문': '회수 문헌과 채택 이유를 선행 보존해야 후속 디버깅이 쉬운 까닭은 무엇인가?', '기대 상태': '검색 실패', '판정 상태': '검색 실패', '실패 단계': '검색 단계', '상위 후보': ['문서-1(0)', '문서-2(0)', '문서-3(0)'], '선택 근거': [], '멈춘 이유': '문서 집합에는 관련 내용이 있지만 현재 토큰 겹침 규칙이 표현 차이를 놓쳤다.', '다음 수정': '질문 재작성, 동의어 처리, 재정렬 기준 추가를 먼저 검토한다.', '안전 답변': '현재 검색 규칙으로는 관련 근거를 상위 후보로 안정적으로 끌어오지 못했다.'}

결과를 어떻게 읽는가

이번 기록에서 먼저 봐야 할 것은 점수 크기보다 판정 상태가 어디서 갈렸는가입니다.

질문 유형 이번 기록에서 확인할 점 해석
문서 범위 밖 상위 후보 점수가 모두 0이고 선택 근거가 비어 있다 검색기를 바꾸기 전에 문서 범위 자체를 다시 봐야 한다
검색 실패 역시 선택 근거가 비어 있지만, 질문 의미는 기존 문서와 가깝다 문서를 더 넣기보다 검색 규칙과 질의 표현을 먼저 손봐야 한다
답변 과장 위험 상위 후보와 선택 근거는 있는데 강한 표현까지는 지지하지 못한다 검색 성공을 답변 성공으로 오해하면 안 된다
근거 기반 답변 상위 후보와 선택 근거가 질문에 직접 답한다 이 기록을 현재 기준선으로 저장할 수 있다

특히 연습-02연습-07을 같이 보는 것이 중요합니다. 둘 다 표면적으로는 아무 문서도 못 찾았다처럼 보이지만, 실제로는 전혀 다른 문제입니다.

  • 연습-02는 현재 지식베이스에 없는 주제를 물었습니다.
  • 연습-07은 같은 지식베이스에 관련 내용이 있는데도, 지금 검색 규칙이 표현 차이를 못 따라갔습니다.

따라서 두 사례를 같은 근거 부족 한 줄로만 적어 두면, 다음 반복에서 문서를 늘려야 하는지 검색 규칙을 고쳐야 하는지 결정할 수 없습니다.

결과 해석 기준

관찰 읽어야 할 뜻
연습-02는 문서 범위 밖이다 문서 집합 자체가 현재 질문을 다루지 않는다
연습-07은 검색 실패다 문서는 있지만 현재 검색 규칙이 표현 차이를 놓쳤다
연습-03은 답변 과장 위험이다 검색은 됐지만 강한 단정까지는 근거가 없다
상태마다 다음 수정 위치가 다르다 실패 상태를 나누지 않으면 다음 실험이 흐려진다

프로젝트 기록 예시

1
2
3
4
5
6
7
8
연습 ID:
질문:
판정 상태:
실패 단계:
상위 후보:
선택 근거:
멈춘 이유:
다음 수정 시작점:

프로젝트 평가표 예시

질문별 판정을 압축하면 다음 표처럼 남길 수 있습니다.

연습 ID 질문 판정 상태 실패 단계 다음 수정
연습-01 검색 후보와 선택 근거를 왜 먼저 남겨야 하는가? 근거 기반 답변 통과 현재 기준선을 저장한다
연습-02 MCP는 왜 필요한가? 문서 범위 밖 검색 단계 문서 범위를 넓히거나 질문 범위를 줄인다
연습-03 문서 분할 재정렬만 있으면 모든 환각이 해결되는가? 답변 과장 위험 답변 단계 단정 표현을 줄인 질문으로 다시 본다
연습-07 회수 문헌과 채택 이유를 선행 보존해야 후속 디버깅이 쉬운 까닭은 무엇인가? 검색 실패 검색 단계 질의 표현, 토큰화, 재정렬 기준을 점검한다

이 표의 역할은 점수표를 예쁘게 만드는 데 있지 않습니다. 다음 한 번의 수정은 어디서 시작할까를 바로 고르게 하는 데 있습니다.

나쁜 기록과 좋은 기록

같은 실행 결과도 기록 방식에 따라 다음 실험이 완전히 달라집니다.

구분 예시
나쁜 기록 질문 몇 개는 답했고 몇 개는 애매했다. 검색을 더 좋게 만들 필요가 있다.
좋은 기록 연습-02는 문서 범위 밖이라 문서 집합 또는 질문 범위를 다시 잡아야 한다. 연습-07은 문서 집합은 맞지만 현재 검색 규칙이 표현 차이를 놓쳐 검색 실패가 났다. 연습-03은 상위 후보가 있어도 강한 단정까지는 지지하지 못하므로 답변 과장 위험으로 멈췄다.

좋은 기록은 실패를 감상으로 쓰지 않고, 어디서 막혔는가, 왜 멈췄는가, 무엇부터 고칠 것인가를 분리해 남깁니다.

직접 바꿔 보며 확인할 것

질문 강도와 검색 규칙을 조금만 바꿔도 상태가 달라지는 장면을 보는 것이 핵심입니다.

  1. 연습-07 질문을 검색 후보와 선택 근거를 왜 먼저 남겨야 하는가?처럼 문서 문장에 더 가깝게 바꿔 봅니다.
    관찰할 점: 검색 실패근거 기반 답변으로 바뀌는가?

  2. 연습-03연습-06에서 모든, 항상을 빼고 다시 돌려 봅니다.
    관찰할 점: 답변 과장 위험이 줄고 선택 근거가 더 직접 답변으로 이어지는가?

  3. 연습-02처럼 현재 문서 집합에 없는 주제를 하나 더 추가해 봅니다.
    관찰할 점: 검색 규칙을 바꿔도 문서 범위 밖은 그대로 남는가?

체크리스트

확인할 것 스스로 답할 질문
판정 상태 질문별로 판정 상태실패 단계를 따로 적었는가?
실패 구분 문서 범위 밖검색 실패를 같은 한 줄로 뭉개지 않았는가?
답변 경계 상위 후보가 있어도 강한 단정 표현은 답변 과장 위험으로 멈췄는가?
수정 위치 다음 수정이 문서 집합, 검색 규칙, 답변 규칙 중 어디서 시작하는지 적었는가?
표현 실험 같은 문서 집합 위에서 질문 표현만 바꿔 상태가 달라지는지 확인했는가?

출처와 참고 자료

이 절의 문서 조각과 경계 사례 질문은 프로젝트 실습을 위해 만든 자체 예시입니다.