P7-5.2까지 읽으면 독자는 보통 이렇게 생각하기 쉽습니다. 문서가 있으면 답하고, 없으면 멈추면 된다. 하지만 실제 RAG 프로젝트에서는 그 경계가 그렇게 단순하지 않습니다.
문제가 되는 장면은 대체로 세 가지입니다.
장면
겉보기에는 비슷해 보이지만 실제로는 다른 점
문서에 없는 개념을 묻는 질문
검색 자체가 비어 있거나 억지 관련 문서를 끌고 온다
문서에 있는 주제를 과하게 일반화한 질문
검색은 되지만 답변을 강하게 단정하면 근거 밖으로 나간다
문서 표현과 맞닿게 다시 쓴 질문
같은 주제라도 문서 범위 안 답변으로 돌아온다
예를 들어 질문을 다시 썼더니 두 건이 근거 기반 답변으로 바뀌면, 빠르게는 검색기를 손대지 않아도 질문만 잘 쓰면 다 해결된다고 적고 싶어질 수 있습니다. 하지만 더 안전한 다음 판단은 재작성 성공만 보는 것이 아니라, 원래 질문이 문서 범위 밖이었는가, 과장 표현 때문에 위험했던 것인가, 재작성 뒤에도 여전히 근거 부족으로 남는 질문은 무엇인가를 먼저 나누는 것입니다. 그렇게 읽어야 질문 재작성으로 풀리는 문제와 문서 자체를 늘려야 하는 문제를 구분할 수 있습니다.
flowchart TD
A["문제 장면<br/>질문 재작성 뒤 두 건이 답변 가능으로 바뀜"]
B["빠른 판단<br/>질문만 잘 쓰면 대부분 해결된다"]
C["원래 상태 확인<br/>범위 밖이었나 과장 위험이었나"]
D["재작성 효과 확인<br/>무엇이 상태를 바꾸었나"]
E["잔여 문제 확인<br/>여전히 근거 부족인 질문은 무엇인가"]
F["더 안전한 판단<br/>질문 수정과 문서 보강을 구분"]
A --> B
A --> C --> D --> E --> F
즉, RAG 품질은 문서와 검색기만의 문제가 아니라 질문 문구와 멈춤 규칙의 문제이기도 합니다. 역할은 바로 이 지점을 독자가 직접 흔들어 보는 데 있습니다.
P7-5.1, P7-5.2와 같은 문서 집합을 그대로 사용합니다. 대신 질문 파일을 별도로 두어, 같은 지식베이스 위에서 질문 문구와 기대 상태만 바꾸며 결과를 비교합니다. 즉, 다른 프로젝트를 새로 여는 것이 아니라 같은 RAG 프로젝트 기록에서 질문 경계가 어떻게 품질 판정을 바꾸는지 더 분명히 드러내는 실습입니다.
# RAG 연습 질문을 재작성하기 전후로 근거 기반 답변 가능 상태와 상위 후보가 어떻게 바뀌는지 비교하는 예제입니다.importcsvfrompathlibimportPathdocument_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}"forindexinrange(1,7)}대표_case_ids={f"연습-{index:02d}"forindexinrange(1,7)}document_rows=[rowforrowindocument_rowsifrow["doc_id"]in대표_문서_ids]case_rows=[rowforrowincase_rowsifrow["case_id"]in대표_case_ids]documents={row["doc_id"]:row["text"]forrowindocument_rows}rewrite_map={"MCP는 왜 필요한가?":"최신 규칙이 필요한 서비스에서 검색 단계가 먼저 필요한 이유를 문서 범위 안에서 설명할 수 있는가?","문서 분할 재정렬만 있으면 모든 환각이 해결되는가?":"문서 분할과 재정렬은 검색 품질에 어떤 도움을 줄 수 있는가?",}defclean_token(token):token=token.replace("?","").replace(".","")forkeywordin["RAG","MCP"]:iftoken.startswith(keyword):returnkeywordreturntokendeftokenize(text):return{clean_token(token)fortokenintext.split()}defevaluate_question(question):question_tokens=tokenize(question)ranked=[]fordoc_id,textindocuments.items():overlap=len(question_tokens&tokenize(text))domain_bonus=sum(phraseinquestionandphraseintextforphrasein["문서 분할","재정렬","검색 후보","선택 근거","최신 규칙"])direct_bonus=1ifoverlap>0and("기록"intextor"구분"intextor"질문에 더 직접 답하는"intext)else0ranked.append({"doc_id":doc_id,"score":overlap+domain_bonus+direct_bonus,"overlap":overlap,"text":text,})ranked.sort(key=lambdarow:(row["score"],row["overlap"]),reverse=True)top=ranked[0]iftop["score"]==0:state="근거 부족"reason="문서 범위 밖 질문"elif"모든"inquestionor"항상"inquestion:state="답변 과장 위험"reason="검색은 됐지만 단정 표현이 문서 근거를 넘어간다"else:state="근거 기반 답변"reason="현재 문서 범위 안에서 보수적으로 답할 수 있다"return{"question":question,"state":state,"reason":reason,"top_doc":top["doc_id"],"top_score":top["score"],"candidates":ranked[:3],}exercise_records=[]forrowincase_rows:before=evaluate_question(row["question"])rewritten_question=rewrite_map.get(row["question"])after=evaluate_question(rewritten_question)ifrewritten_questionelseNoneexpected_state="근거 부족"ifrow["expected_state"]=="문서 범위 밖"elserow["expected_state"]exercise_records.append({"case_id":row["case_id"],"focus":row["focus"],"expected_state":expected_state,"before":before,"rewritten_question":rewritten_question,"after":after,})summary={"연습 수":len(exercise_records),"기대 상태와 일치한 수":sum(record["before"]["state"]==record["expected_state"]forrecordinexercise_records),"재작성으로 상태가 바뀐 수":sum(record["after"]isnotNoneandrecord["after"]["state"]!=record["before"]["state"]forrecordinexercise_records),"다시 쓸 가치가 큰 질문":[record["case_id"]forrecordinexercise_recordsifrecord["before"]["state"]!="근거 기반 답변"],}print("연습 요약 =",summary)print("읽은 문서 파일 =",str(document_path))print("읽은 연습 질문 파일 =",str(case_path))핵심_case_ids={"연습-02","연습-03"}print("핵심 연습 기록 =")forrowinexercise_records:ifrow["case_id"]in핵심_case_ids:print(row)
연습 요약 = {'연습 수': 6, '기대 상태와 일치한 수': 6, '재작성으로 상태가 바뀐 수': 2, '다시 쓸 가치가 큰 질문': ['연습-02', '연습-03', '연습-06']}
읽은 문서 파일 = docs/assets/part-07/chapter-05/p7-5-rag-documents.csv
읽은 연습 질문 파일 = docs/assets/part-07/chapter-05/p7-5-boundary-cases.csv
핵심 연습 기록 =
{'case_id': '연습-02', 'focus': '현재 문서 집합에 없는 주제를 멈추는 경우', 'expected_state': '근거 부족', 'before': {'question': 'MCP는 왜 필요한가?', 'state': '근거 부족', 'reason': '문서 범위 밖 질문', 'top_doc': '문서-1', 'top_score': 0, 'candidates': [{'doc_id': '문서-1', 'score': 0, 'overlap': 0, 'text': 'RAG는 외부 문서를 검색해 모델 입력에 넣고 그 근거 위에서 답변을 구성하는 구조다.'}, {'doc_id': '문서-2', 'score': 0, 'overlap': 0, 'text': '프롬프트만으로는 최신 문서 근거를 보장할 수 없으므로 최신 규칙이 필요한 서비스에서는 검색 단계가 먼저 필요하다.'}, {'doc_id': '문서-3', 'score': 0, 'overlap': 0, 'text': '검색 후보 점수가 높아도 질문에 직접 답하지 않는 문장이 섞일 수 있으므로 선택 근거를 따로 남겨야 한다.'}]}, 'rewritten_question': '최신 규칙이 필요한 서비스에서 검색 단계가 먼저 필요한 이유를 문서 범위 안에서 설명할 수 있는가?', 'after': {'question': '최신 규칙이 필요한 서비스에서 검색 단계가 먼저 필요한 이유를 문서 범위 안에서 설명할 수 있는가?', 'state': '근거 기반 답변', 'reason': '현재 문서 범위 안에서 보수적으로 답할 수 있다', 'top_doc': '문서-2', 'top_score': 9, 'candidates': [{'doc_id': '문서-2', 'score': 9, 'overlap': 8, 'text': '프롬프트만으로는 최신 문서 근거를 보장할 수 없으므로 최신 규칙이 필요한 서비스에서는 검색 단계가 먼저 필요하다.'}, {'doc_id': '문서-5', 'score': 3, 'overlap': 3, 'text': '문서 분할 chunking 은 검색 범위를 세밀하게 만들지만 너무 잘게 나누면 문맥이 끊길 수 있다.'}, {'doc_id': '문서-4', 'score': 3, 'overlap': 2, 'text': 'RAG 프로젝트 기록에는 질문 검색 후보 선택 근거 최종 답변을 분리해 남겨야 검색 실패와 답변 실패를 나중에 구분할 수 있다.'}]}}
{'case_id': '연습-03', 'focus': '검색은 됐지만 단정이 근거 밖인 경우', 'expected_state': '답변 과장 위험', 'before': {'question': '문서 분할 재정렬만 있으면 모든 환각이 해결되는가?', 'state': '답변 과장 위험', 'reason': '검색은 됐지만 단정 표현이 문서 근거를 넘어간다', 'top_doc': '문서-5', 'top_score': 3, 'candidates': [{'doc_id': '문서-5', 'score': 3, 'overlap': 2, 'text': '문서 분할 chunking 은 검색 범위를 세밀하게 만들지만 너무 잘게 나누면 문맥이 끊길 수 있다.'}, {'doc_id': '문서-2', 'score': 1, 'overlap': 1, 'text': '프롬프트만으로는 최신 문서 근거를 보장할 수 없으므로 최신 규칙이 필요한 서비스에서는 검색 단계가 먼저 필요하다.'}, {'doc_id': '문서-6', 'score': 1, 'overlap': 0, 'text': '재정렬 reranking 은 상위 후보의 순서를 다시 바꾸어 질문에 더 직접 답하는 근거를 앞으로 당기는 단계다.'}]}, 'rewritten_question': '문서 분할과 재정렬은 검색 품질에 어떤 도움을 줄 수 있는가?', 'after': {'question': '문서 분할과 재정렬은 검색 품질에 어떤 도움을 줄 수 있는가?', 'state': '근거 기반 답변', 'reason': '현재 문서 범위 안에서 보수적으로 답할 수 있다', 'top_doc': '문서-5', 'top_score': 4, 'candidates': [{'doc_id': '문서-5', 'score': 4, 'overlap': 3, 'text': '문서 분할 chunking 은 검색 범위를 세밀하게 만들지만 너무 잘게 나누면 문맥이 끊길 수 있다.'}, {'doc_id': '문서-2', 'score': 3, 'overlap': 3, 'text': '프롬프트만으로는 최신 문서 근거를 보장할 수 없으므로 최신 규칙이 필요한 서비스에서는 검색 단계가 먼저 필요하다.'}, {'doc_id': '문서-4', 'score': 3, 'overlap': 2, 'text': 'RAG 프로젝트 기록에는 질문 검색 후보 선택 근거 최종 답변을 분리해 남겨야 검색 실패와 답변 실패를 나중에 구분할 수 있다.'}]}}
문서 분할 재정렬만 있으면 모든 환각이 해결되는가?는 검색 후보는 있었지만 모든이라는 표현이 문서 근거보다 더 강해 답변 과장 위험으로 남았다. 같은 주제를 문서 분할과 재정렬은 검색 품질에 어떤 도움을 줄 수 있는가?로 다시 쓰자, 같은 문서 집합 안에서도 근거 기반 답변 상태로 바뀌었다. 따라서 다음 반복에서는 강한 일반화 질문을 그대로 답하기보다, 문서 범위에 맞는 질문으로 다시 쓰거나 답변 강도를 낮추는 규칙을 먼저 붙이는 편이 적절하다.