실무 Multi-Agent 오케스트레이션 14장 · 근거 기반 답변 7 / 7 ← 이전목차다음 → TechLead Cro

14장. 근거 기반 답변

실습문제와 해답

코드를 직접 돌려 보며 14장 내용을 손에 익힙니다. 먼저 스스로 해 본 뒤 해답을 펼쳐 보세요. 해답은 새 파일로 만들어 실행합니다. 세 문제 모두 chroma_db/가 있어야 합니다.

lesson14_rag_retriever.py는 질문을 돌리는 부분이 if __name__ == "__main__": 아래에 있어서, 다른 파일에서 import 해도 질문 네 개가 다시 실행되지 않습니다. 그래서 이번 문제들은 그 파일의 함수를 가져다 씁니다.


문제 1 — 규칙이 지켜졌는지 코드로 검사하기

규칙 1과 규칙 2는 형식을 정해 두었습니다. 그래서 지켜졌는지를 눈이 아니라 코드로 확인할 수 있습니다. 그 검사를 하는 파일 ex14_check.py를 만드세요.

  • from lesson14_rag_retriever import answer_policy_question으로 함수를 가져옵니다.
  • 질문 여섯 개를 돌립니다. 「따라하기」의 네 개에 아래 둘을 더합니다.
  • "하루클럽 골드 등급은 어떻게 되는 거예요?" (표준 질문 Q4의 앞부분)
  • "오늘 날씨 어때요?"
  • 답마다 기권 문구가 있는지, (근거: ...)가 있는지 검사해 넷 중 하나로 판정합니다: 근거 있는 답변 / 기권 / 기권인데 근거를 붙임 / 근거 없는 답변.
해답 보기
import re

from lesson14_rag_retriever import answer_policy_question

ABSTAIN = "정책 문서에서 확인되지 않아"            # 규칙 2의 기권 문구
CITATION = re.compile(r"^\(근거: .+\)$", re.MULTILINE)          # 규칙 1의 출처 표기

questions = [
    "단순 변심인데 반품 배송비 얼마예요?",
    "불량품이 왔는데 배송비를 제가 내야 하나요?",
    "환불 신청하면 카드 취소는 며칠 걸려요?",
    "하루클럽 골드 등급은 어떻게 되는 거예요?",
    "해외 배송도 반품 되나요?",
    "오늘 날씨 어때요?",
]

rows = []
for q in questions:
    answer = answer_policy_question(q)
    cited = bool(CITATION.search(answer))
    abstained = ABSTAIN in answer
    if abstained and cited:
        verdict = "기권인데 근거를 붙임"
    elif abstained:
        verdict = "기권"
    elif cited:
        verdict = "근거 있는 답변"
    else:
        verdict = "근거 없는 답변"
    rows.append((q, verdict))
    print(f"  [답변] {answer.strip()}\n")

print("=" * 60)
for q, verdict in rows:
    print(f"{verdict:<12} | {q}")
$ python ex14_check.py

이전 출처 표기 방식의 실행 기록 (골드 등급 질문만 싣고 나머지는 줄였습니다. 현재 코드는 선택한 청크의 doc_title · article 전체를 표시하므로 아래의 짧은 출처 표기와 다릅니다. 이 여섯 질문은 수정 후 다시 실행한 기록이 아닙니다.)

(중략)
  [검색] 4개 청크:
    - [제1조 (등급 체계와 산정 기준)] 하루마켓 HARU MARKET CS-POL-2026-011 · 고객경험팀 하루클럽 멤버십 운영 정책...
    - [제1조 (등급 체계와 산정 기준), 제2조 (등급별 혜택), 제3조 (하루포인트 적립)] [표 1] 등급 산정 기준 (최근 6개월 실결제액) 등급 산정 기준 산정 주기 일반 가입 시 기본 ...
    - [자주 묻는 질문 (FAQ), 부칙 — 개정 이력] “Q. 등급은 언제 바뀌나요?” A. 매월 1일에 최근 6개월 실결제액 기준으로 다시 산정됩니다. ...
    - [제3조 (하루포인트 적립), 제4조 (하루포인트 사용), 제5조 (하루포인트 유효기간과 소멸)] 제3조 (하루포인트 적립) ① 하루포인트는 구매확정 다음 날 등급별 적립률에 따라 자동 적립된다. ...
  [답변] 안녕하세요 하루마켓 고객지원 상담원 '하루'입니다. 하루클럽 골드 등급은 최근 6개월 실결제 금액이 30만원 이상일 때 산정됩니다. 등급은 매월 1일에 최근 6개월 실결제액 기준으로 다시 산정됩니다. (근거: 멤버십정책 · 제1조 (등급 체계와 산정 기준), 자주 묻는 질문 (FAQ))

(중략)
  [답변] 해당 내용은 정책 문서에서 확인되지 않아 상담원 확인이 필요합니다.
  [답변] 해당 내용은 정책 문서에서 확인되지 않아 상담원 확인이 필요합니다.

============================================================
근거 있는 답변     | 단순 변심인데 반품 배송비 얼마예요?
근거 있는 답변     | 불량품이 왔는데 배송비를 제가 내야 하나요?
근거 있는 답변     | 환불 신청하면 카드 취소는 며칠 걸려요?
근거 있는 답변     | 하루클럽 골드 등급은 어떻게 되는 거예요?
기권           | 해외 배송도 반품 되나요?
기권           | 오늘 날씨 어때요?

해설 — 세 가지를 봅니다.

첫째, 여섯 답이 모두 기대한 칸에 들어갔습니다. 문서에 답이 있는 네 질문은 "근거 있는 답변", 문서에 없는 두 질문은 "기권"입니다. "기권인데 근거를 붙임"과 "근거 없는 답변"은 한 건도 없습니다. 이 교재를 준비하며 세 번 실행했을 때 세 번 모두 판정표가 같았습니다.

둘째, 멤버십 질문은 멤버십 정책에서 찾았습니다. 골드 등급을 묻자 검색된 네 청크가 모두 멤버십 정책의 것이고, 출처에도 "멤버십정책"이 적혔습니다. 정책 문서가 두 개여도 검색이 가려냈습니다. 꼬리표에 문서 이름을 넣어 둔 것이 여기서 쓸모가 있습니다.

셋째, 날씨 질문은 정책과 상관이 없는데도 검색은 네 개를 돌려줬고, 모델은 기권 문구 한 문장으로 답했습니다. 규칙 2에 적은 그대로입니다.

이 검사는 출처 표기의 존재 여부를 확인합니다. 출처가 답변의 실제 근거인지까지 증명하지는 않습니다. 정규식은 조항 이름 내부의 괄호까지 포함하도록 출처 한 줄 전체를 검사합니다. 프롬프트를 고칠 때마다 이 파일을 돌리면 고친 뒤에도 여섯 칸이 그대로인지 바로 알 수 있습니다.


문제 2 — 거리를 보고 LLM을 부르지 않고 기권하기

정책과 전혀 상관없는 질문에도 지금은 검색 1번과 LLM 1번을 씁니다. 13장에서 본 거리를 써서, 가장 가까운 청크조차 멀면 LLM을 부르지 않고 기권 문구를 돌려주는 파일 ex14_precheck.py를 만드세요.

  • 질문 여섯 개의 1위 거리를 출력합니다. vectorstore.similarity_search_with_score(질문, k=1)을 씁니다.
  • 1위 거리가 기준(THRESHOLD = 0.7)보다 멀면 기권 문구를 곧바로 돌려주고, 아니면 answer_policy_question을 부르는 함수 answer_with_precheck를 만듭니다.
  • 뒤의 세 질문("해외 배송도 반품 되나요?", "오늘 날씨 어때요?", "점심 메뉴 추천해 주세요")을 그 함수로 돌립니다.

실행하기 전에 짐작해 보세요. "해외 배송도 반품 되나요?"의 1위 거리는 정책에 답이 있는 질문들보다 멀까요?

해답 보기
from lesson14_rag_retriever import answer_policy_question, vectorstore

ABSTAIN = "해당 내용은 정책 문서에서 확인되지 않아 상담원 확인이 필요합니다."
THRESHOLD = 0.7          # 1위 거리가 이보다 멀면 LLM 을 부르지 않는다 (사람이 정했다)

questions = [
    "단순 변심인데 반품 배송비 얼마예요?",
    "환불 신청하면 카드 취소는 며칠 걸려요?",
    "하루클럽 골드 등급은 어떻게 되는 거예요?",
    "해외 배송도 반품 되나요?",
    "오늘 날씨 어때요?",
    "점심 메뉴 추천해 주세요",
]


def top_distance(question: str) -> float:
    """가장 가까운 청크 하나의 거리 (작을수록 가깝다)"""
    doc, score = vectorstore.similarity_search_with_score(question, k=1)[0]
    return score


def answer_with_precheck(question: str) -> str:
    if top_distance(question) > THRESHOLD:
        return ABSTAIN + "  ← LLM 호출 없음"
    return answer_policy_question(question).strip()


print("1위 거리")
for q in questions:
    print(f"  {top_distance(q):.3f} | {q}")

print(f"\n기준 {THRESHOLD} 로 미리 거르기")
for q in questions[3:]:
    print(f"고객: {q}")
    print(f"하루: {answer_with_precheck(q)}\n")
$ python ex14_precheck.py

실행 결과 (거리는 누가 실행해도 같습니다)

1위 거리
  0.440 | 단순 변심인데 반품 배송비 얼마예요?
  0.565 | 환불 신청하면 카드 취소는 며칠 걸려요?
  0.450 | 하루클럽 골드 등급은 어떻게 되는 거예요?
  0.533 | 해외 배송도 반품 되나요?
  0.768 | 오늘 날씨 어때요?
  0.734 | 점심 메뉴 추천해 주세요

기준 0.7 로 미리 거르기
고객: 해외 배송도 반품 되나요?
  [검색] 4개 청크:
    - [제8조 (배송 지연 보상), 제9조 (분쟁 처리), 자주 묻는 질문 (FAQ)] 서 제외된다. 제9조 (분쟁 처리) 반품·교환·환불에 관하여 회사와 고객 간 분쟁이 발생한 경우, ...
    - [제7조 (반품·교환 불가 사유), 제8조 (배송 지연 보상), 제9조 (분쟁 처리)] 제7조 (반품·교환 불가 사유) 다음 각 호에 해당하면 반품·교환이 제한된다. ① 고객의 사용·착용...
    - [제1조 (용어의 정의), 제2조 (반품 사유별 기준), 제3조 (반품 신청 절차), 제4조 (교환)] "환불 신청"은 법률상 청약철회에 해당한다. 제2조 (반품 사유별 기준) [표 1] 사유별 신청 기...
    - [제3조 (반품 신청 절차), 제4조 (교환), 제5조 (환불 처리 기간)] 행된다. 제4조 (교환) ① 동일 상품의 다른 색상·사이즈 교환은 수령일로부터 7일 이내 신청 가능...
하루: 해당 내용은 정책 문서에서 확인되지 않아 상담원 확인이 필요합니다.

고객: 오늘 날씨 어때요?
하루: 해당 내용은 정책 문서에서 확인되지 않아 상담원 확인이 필요합니다.  ← LLM 호출 없음

고객: 점심 메뉴 추천해 주세요
하루: 해당 내용은 정책 문서에서 확인되지 않아 상담원 확인이 필요합니다.  ← LLM 호출 없음

해설 — 세 가지를 봅니다.

첫째, 정책과 상관없는 두 질문(날씨 0.768, 점심 0.734)은 1위 거리가 0.7을 넘습니다. 정책 질문들은 0.44~0.57입니다. 둘 사이에 기준을 두면 상관없는 질문을 LLM을 부르지 않고 돌려보낼 수 있습니다. 그만큼 비용과 시간이 줄어듭니다.

둘째, "해외 배송도 반품 되나요?"는 0.533입니다. 문서에 답이 없는 질문인데도 정책 질문들과 같은 범위에 있습니다. "반품"이라는 말이 문서의 말과 가깝기 때문입니다. 이 질문은 기준을 통과해 LLM까지 갔고, 모델이 발췌를 읽고 기권했습니다. 거리로 거르는 일과 발췌를 읽고 판단하는 일이 차례로 일했습니다.

셋째, 그래서 두 장치의 역할이 다릅니다.

장치 보는 것 걸러내는 것
거리 기준 (우리 코드) 질문의 말이 문서의 말과 얼마나 닮았는가 정책과 무관한 질문
규칙 2 (LLM) 발췌에 답이 실제로 있는가 정책 이야기이지만 문서에 없는 질문

기준 0.7은 사람이 이 여섯 질문의 거리를 보고 정한 값입니다. 문서나 임베딩 모델이 바뀌면 다시 재서 정합니다.


문제 3 — 두 문서에 걸친 질문

표준 질문 Q5("단순 변심으로 반품하면 배송비 내야 해요?")는 고객의 등급에 따라 답이 달라지고, 정책 문서 두 개를 함께 봐야 답할 수 있습니다. 여기서는 고객이 등급을 직접 밝힌 것으로 바꿔 묻습니다.

"저 VIP인데요, 단순 변심 반품해도 배송비 내야 해요?"

반품 배송비의 기본 규정은 반품·교환·환불 정책에, VIP의 면제 혜택은 멤버십 정책에 있습니다. 새 파일 ex14_cross.py에서 이 질문을 돌려, 검색된 청크 네 개가 어느 문서에서 왔는지와 답변을 출력하세요. 실행하기 전에 네 개가 두 문서에 어떻게 나뉠지 짐작해 보세요.

해답 보기
from lesson14_rag_retriever import answer_policy_question, retriever

question = "저 VIP인데요, 단순 변심 반품해도 배송비 내야 해요?"

print("검색된 청크의 출처 문서")
for d in retriever.invoke(question):
    print(f"  - {d.metadata['doc_title']} · {d.metadata.get('article') or '조항 미상'}")

print(f"\n고객: {question}")
print(f"하루: {answer_policy_question(question).strip()}")
$ python ex14_cross.py

이전 출처 표기 방식의 실행 기록 (현재 코드는 선택한 청크의 doc_title · article 전체를 표시합니다. 아래는 수정 후 재실행한 기록이 아닙니다.)

검색된 청크의 출처 문서
  - 멤버십정책 · 제5조 (하루포인트 유효기간과 소멸), 제6조 (등급 조정과 제외), 자주 묻는 질문 (FAQ)
  - 멤버십정책 · 제1조 (등급 체계와 산정 기준), 제2조 (등급별 혜택), 제3조 (하루포인트 적립)
  - 반품교환환불정책 · 제8조 (배송 지연 보상), 제9조 (분쟁 처리), 자주 묻는 질문 (FAQ)
  - 반품교환환불정책 · 제5조 (환불 처리 기간), 제6조 (환불 금액 산정), 제7조 (반품·교환 불가 사유)

고객: 저 VIP인데요, 단순 변심 반품해도 배송비 내야 해요?
  [검색] 4개 청크:
    - [제5조 (하루포인트 유효기간과 소멸), 제6조 (등급 조정과 제외), 자주 묻는 질문 (FAQ)] 내한다. ③ 유효기간이 지나 소멸된 포인트는 복구되지 않는다. 단, 회사 귀책 사유로 소멸된 경우는...
    - [제1조 (등급 체계와 산정 기준), 제2조 (등급별 혜택), 제3조 (하루포인트 적립)] [표 1] 등급 산정 기준 (최근 6개월 실결제액) 등급 산정 기준 산정 주기 일반 가입 시 기본 ...
    - [제8조 (배송 지연 보상), 제9조 (분쟁 처리), 자주 묻는 질문 (FAQ)] 서 제외된다. 제9조 (분쟁 처리) 반품·교환·환불에 관하여 회사와 고객 간 분쟁이 발생한 경우, ...
    - [제5조 (환불 처리 기간), 제6조 (환불 금액 산정), 제7조 (반품·교환 불가 사유)] 계좌이체·무통장 계좌 환급 검수 후 3일 이내 하루포인트·쿠폰 즉시 복원 검수 후 1일 이내 간편결...
하루: 안녕하세요 고객님, 하루입니다. VIP 등급의 단순 변심 반품 배송비 면제는 월 1회에 한하여 자동 적용됩니다. 다만, 이번 달에 이미 면제 혜택을 사용하셨다면 편도 3,000원이 부과됩니다. (근거: 멤버십정책 · 자주 묻는 질문 (FAQ))

해설 — 세 가지를 봅니다.

첫째, 검색된 네 청크가 두 문서에서 두 개씩 왔습니다. 질문에 "VIP"와 "반품 배송비"가 함께 들어 있어, 두 문서의 청크가 고르게 가까웠습니다. 문서를 나눠 검색하라고 시킨 적은 없습니다.

둘째, 같은 "제5조"와 "제6조"가 두 문서에 모두 나옵니다. 멤버십정책의 제5조는 포인트 유효기간이고 반품교환환불정책의 제5조는 환불 처리 기간입니다. 꼬리표에 문서 이름이 없었다면 둘을 구별할 수 없습니다. 문서가 둘 이상이면 조항 번호만으로는 출처가 되지 않습니다.

셋째, 답은 "월 1회 면제, 이미 썼다면 편도 3,000원"으로 정책과 같고, 출처는 멤버십정책으로 적혔습니다. 이 교재를 준비하며 세 번 실행했을 때 세 번 모두 같은 내용이었습니다. 한 가지를 짚어 둡니다. 이 답은 고객이 정말 VIP인지, 이번 달에 면제를 이미 썼는지 확인한 것이 아닙니다. 그 정보는 정책 문서가 아니라 고객 데이터와 주문 데이터에 있습니다. 문서 검색으로는 규정까지 답하고, 이 고객에게 그 규정이 어떻게 적용되는지는 6~7장의 조회 도구가 맡습니다. 두 가지를 함께 쓰는 것은 21장 이후에서 합니다.

← 이전 절정리와 체크리스트15장 →왜 검색 품질을 따져야 하는가 — 검색이 못 찾으면 답도 틀린다
오명운 · macro@prag-ai.com