RAG 심화 #7 실전 프로젝트: 문서 Q&A 봇 업그레이드
6편까지로 진단, 개선, 측정의 도구가 모두 모였습니다. 마지막 글에서는 LLM 앱 개발 실전 13편에서 만든 사내 문서 Q&A 봇을 이 시리즈의 기법으로 업그레이드합니다. 한꺼번에 다 바꾸는 것이 아니라, 한 번에 하나씩 적용하고 매번 측정하는 과정 자체가 이 글의 내용입니다. 실무에서 RAG를 개선하는 일은 정확히 이 모양이기 때문입니다.
출발점 — 13편의 봇과 기준선 #
13편의 봇은 이 시리즈 기준으로 보면 가장 단순한 구성입니다. 고정 크기 청킹, 벡터 검색 하나, 출처 표시 없음. 먼저 1편대로 실제 질문 로그에서 골든셋 30건을 만들고, 6편의 평가 파이프라인으로 기준선을 잽니다.
result = evaluate(GOLDEN, top_k=5)
# {'recall@k': 0.63, 'mrr': 0.44, 'accuracy': 0.57, 'hallucination_rate': 0.13}수치는 이 예제 문서셋에서의 결과이고, 여러분의 데이터에서는 다르게 나옵니다. 중요한 것은 절대값이 아니라 읽는 방법입니다. recall@k 0.63은 세 질문 중 하나는 정답 조각조차 못 가져온다는 뜻이므로, 검색부터 고치는 것이 순서입니다. 환각률 13%는 출처 없는 봇으로는 신뢰를 얻기 어려운 수준입니다.
1단계 — 청킹 교체 #
2편의 구조 기반 청킹으로 바꿉니다. 사내 문서가 마크다운이므로 헤딩 단위로 자르고, 표는 통째로 두고, 조각마다 출처와 섹션 메타데이터를 붙여 인덱스를 다시 빌드합니다.
chunks = []
for doc in load_documents("docs/"):
for c in chunk_by_heading(doc.text, max_chars=1500):
chunks.append({"text": c, "metadata": {"source": doc.name, "section": heading_of(c)}})
rebuild_index(chunks)
# 재평가: recall@k 0.63 → 0.77, accuracy 0.57 → 0.67검색 알고리즘은 한 줄도 안 바꿨는데 recall이 가장 크게 뛰었습니다. 진단에서 “표가 잘려 양쪽 조각 모두 쓸모없던” 실패들이 사라진 덕분입니다. 청킹을 시리즈 앞부분에 둔 이유가 이 순서에 있습니다. 토대를 고치지 않으면 뒤의 기법들이 제 효과를 못 냅니다.
2단계 — 하이브리드 검색 #
골든셋을 질문 유형별로 나눠 보니 남은 검색 실패의 다수가 제품 코드와 사내 약어 질문입니다. 3편의 BM25 + RRF 를 더합니다.
def search(question: str, top_k: int = 5) -> list:
vec = vector_search_ids(question, top_k=20)
kw = keyword_search(question, top_k=20)
return [chunks[i] for i in rrf([vec, kw], top_k=top_k)]
# 재평가: recall@k 0.77 → 0.87 (고유 명사 질문군 0.50 → 0.90)전체 수치보다 질문군별 수치가 더 많은 것을 말해 줍니다. 서술형 질문군은 거의 그대로이고 고유 명사 질문군이 크게 올랐습니다. 도입한 기법이 의도한 곳에서 일하고 있다는 확인입니다.
3단계 — 리랭킹 #
recall은 올라왔는데 MRR이 따라오지 않습니다. 정답 조각이 5개 안에는 있지만 4〜5등에 걸려 있는 경우가 많다는 뜻입니다. 4편의 크로스 인코더 리랭킹을 답니다. 후보를 30개로 넓히고 리랭커로 5개를 추립니다.
def search(question: str, top_k: int = 5) -> list:
candidates = hybrid_search(question, top_k=30)
pairs = [(question, c["text"]) for c in candidates]
scores = reranker.predict(pairs)
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
return [c for c, _ in ranked[:top_k]]
# 재평가: mrr 0.58 → 0.74, accuracy 0.70 → 0.77MRR이 오르자 accuracy가 따라 올랐습니다. 정답 조각이 컨텍스트의 위쪽에 또렷이 자리 잡으니 생성도 좋아지는, 검색과 생성이 이어져 있다는 증거입니다. 사내 봇이라 리랭킹이 더하는 1초 미만의 지연은 받아들이기로 합니다. 이 판단도 트레이드오프이고, 지연이 민감한 서비스라면 다른 결론이 날 수 있습니다.
4단계 — 인용과 모른다고 답하기 #
마지막으로 5편을 적용합니다. 시스템 프롬프트에 근거 제한과 “모른다고 답할 권리"를 넣고, 조각을 document 블록으로 바꿔 citations를 켜고, 인용 비율 게이트를 답니다.
def qa(question: str) -> str:
chunks = search(question, top_k=5)
response = answer_with_citations(question, chunks) # 5편의 구현
if not is_grounded(response):
return "관련 문서를 충분히 찾지 못했습니다. 질문을 바꿔 보시겠어요?"
return render(response)
# 재평가: hallucination_rate 0.10 → 0.03, accuracy 0.77 → 0.80환각률이 한 자릿수 아래로 내려왔고, 답마다 출처가 붙으면서 사용자가 직접 검증할 수 있게 됐습니다. 운영자 입장에서는 “찾지 못했습니다” 응답의 로그가 새 진단 자료가 됩니다. 그 질문들이 곧 다음에 보강할 문서와 골든셋 추가 후보입니다.
전체 여정 #
| 단계 | recall@5 | MRR | accuracy | 환각률 |
|---|---|---|---|---|
| 기준선 (13편 봇) | 0.63 | 0.44 | 0.57 | 0.13 |
| + 구조 기반 청킹 | 0.77 | 0.55 | 0.67 | 0.13 |
| + 하이브리드 검색 | 0.87 | 0.58 | 0.70 | 0.10 |
| + 리랭킹 | 0.87 | 0.74 | 0.77 | 0.10 |
| + 인용·게이트 | 0.87 | 0.74 | 0.80 | 0.03 |
표를 읽는 법이 곧 이 시리즈의 요약입니다. 어떤 행도 모든 칸을 올리지 않았습니다. 각 기법은 자기가 겨냥한 지표를 올렸고, 측정이 있었기에 그 사실을 알 수 있었습니다. 여기서 더 가려면 4편의 쿼리 변환(대화형 질문 대응)이 다음 후보이고, 문서가 계속 늘어난다면 6편의 평가를 정기 실행으로 자동화하는 것이 운영의 다음 단계입니다.
시리즈를 마치며 #
일곱 편을 돌아보면 이렇습니다.
- 개선은 진단에서 시작합니다. 실패를 검색과 생성으로 가르고, 골든셋으로 기준선을 만듭니다(1편).
- 검색 품질의 토대는 청킹입니다. 문서 구조를 따라 자르고 표는 통째로 둡니다(2편).
- 벡터와 키워드 검색을 RRF로 합쳐 서로의 약점을 메웁니다(3편).
- 질문은 리라이팅으로 다듬고, 넓게 가져온 후보는 리랭킹으로 추립니다(4편).
- 생성에는 근거 제한과 모른다고 답할 권리를 주고, citations로 검증 가능한 출처를 답니다(5편).
- recall@k, MRR, 정확도, 환각률 네 지표를 회귀 테스트로 돌립니다(6편).
- 그리고 적용은 한 번에 하나씩, 측정과 함께(7편).
RAG는 한 번 만들고 끝나는 시스템이 아니라 문서와 질문이 바뀌는 동안 계속 가꾸는 시스템입니다. 측정이 있으면 그 과정이 감이 아니라 공학이 됩니다. 이 시리즈가 그 전환점이 되기를 바랍니다. 다음에 또 만나요!