RAG2026-5일13차시 · 프롬프트 최적화8 / 18← 이전목차다음 →MetaContents

13차시. 프롬프트 최적화

5. 실습 ② — 프롬프트 A/B 비교

출처: 원본 PDF 13차시_프롬프트최적화.pdf 11~12쪽 · 쪽 그림 11쪽 12쪽

검색은 한 번만 하고 프롬프트만 바꿔 비교합니다. 이러면 비용도 아끼고 조건도 같아집니다.

📝 src/compare_prompt.py

Python
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from retriever import search, build_context
from prompts import PROMPTS
from dotenv import load_dotenv
load_dotenv()

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

def compare(question):
    docs = search(question)            # 검색은 한 번만!
    ctx  = build_context(docs)

    print("=" * 60)
    print("Q:", question)
    print(f"근거 {len(docs)}개")
    print("-" * 60)

    for name, tmpl in PROMPTS.items():
        chain = tmpl | llm | StrOutputParser()
        ans = chain.invoke({"context": ctx, "question": question})
        print(f"\n[{name}]")
        print(ans)
    print("=" * 60)

compare("환불 신청 방법과 수수료를 알려주세요")

실행 결과 예시 (코드를 읽고 만든 예상 화면입니다. 실제 실행 결과, 특히 모델의 답은 다를 수 있습니다)

출력
→ 저장된 인덱스를 불러옵니다 (비용 0)
============================================================
Q: 환불 신청 방법과 수수료를 알려주세요
근거 2개
------------------------------------------------------------

[v1]
환불은 상품 수령 후 7일 이내에 신청하실 수 있으며, 수수료는 보통 상품 가격의 10% 정도입니다. 자세한 사항은 고객센터로 문의하세요.

[v2]
자료에서 확인할 수 없습니다.

[v3]
환불은 상품 수령 후 7일 이내에 신청하실 수 있습니다[1]. 단순 변심의 경우 왕복 배송비는 고객이 부담합니다[1]. 다만 신청 방법과 별도 수수료에 관한 내용은 자료에서 확인할 수 없습니다.
============================================================

실습 코드 · 13/compare_prompt.py

실습 코드: 실습/src/13/compare_prompt.py · 위의 원본 PDF 13차시 11쪽 「5️⃣ 실습 ② — 프롬프트 A/B 비교」과 이어집니다.

Python
# ============================================================
# 13차시 | PPT: 13차시_프롬프트최적화.pptx
# 역할: 같은 검색 자료로 v1, v2, v3 prompt의 답을 나란히 출력
# 입력: 질문 "환불 신청 방법과 수수료를 알려주세요"
# 출력: 질문, 근거 개수, 버전별 답변
# 흐름:
#   search(질문) -> chunk 목록 -> build_context
#     -> PROMPTS의 각 prompt | llm -> print
# ============================================================
# prompt: 모델에게 주는 지시문 틀. v1은 규칙이 적고 v2·v3는 근거 번호(citation)를 요구합니다.
# citation: 답변 문장 끝의 [1] 같은 근거 번호
# retriever: search()가 쓰는 검색. 질문과 가까운 chunk만 남깁니다.
# chunk: 문서 조각. context는 그 조각들을 이어 붙인 자료 문자열입니다.

import os
import sys

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

# 11차시 폴더를 모듈 검색 경로에 넣어 retriever.py의 search와 build_context를 불러옵니다.
CURRENT_DIR = os.path.dirname(__file__)
RETRIEVER_DIR = os.path.join(CURRENT_DIR, "..", "11")

sys.path.append(RETRIEVER_DIR)

from retriever import search, build_context
# prompts.py는 같은 13차시 폴더에 있어 경로를 더하지 않아도 찾습니다.
from prompts import PROMPTS

# .env의 API 키를 읽습니다.
load_dotenv()

# temperature=0으로 두어야 버전 사이의 차이를 프롬프트 탓이라고 말할 수 있습니다.
llm = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0
)

def compare(question):
    # search는 점수 기준을 통과한 조각만 돌려줍니다. 네트워크 검색입니다.
    documents = search(question)
    # 조각에 [1], [2] 번호와 출처를 붙인 자료 문자열입니다.
    context = build_context(documents)

    # 질문과 근거 개수를 먼저 보여 줍니다. 근거가 0개면 세 버전 모두 빈 자료로 답하게 됩니다.
    print("=" * 60)
    print("Q:", question)
    print("근거:", len(documents), "개")
    print("-" * 60)

    # 검색은 위에서 한 번만 했습니다. 여기서는 프롬프트만 바꿔 같은 자료로 답을 받습니다.
    for name, prompt in PROMPTS.items():
        # prompt 틀, 채팅 모델, 문자열 변환기를 한 줄로 잇습니다.
        chain = prompt | llm | StrOutputParser()
        # 딕셔너리의 키 이름이 틀 안의 {context}, {question} 칸 이름과 같아야 합니다.
        answer = chain.invoke({
            "context": context,
            "question": question
        })
        
        print()
        print(f"[{name}]")
        print(answer)
    

# 직접 실행할 때만 비교를 돌립니다.
if __name__ == "__main__":
    compare("환불 신청 방법과 수수료를 알려주세요")

실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)

출력
===== 실행 결과 샘플 =====
실행: cd 실습/src/13
      python compare_prompt.py
예시 출력 (API 키 필요, 실행하지 않음)
3쪽 로딩 완료 (총 1529 자)
============================================================
Q: 환불 신청 방법과 수수료를 알려주세요
근거: (검색 통과 개수, 유사도 미기록) 개
------------------------------------------------------------

[v1]
상품 수령 후 7일 이내에 환불을 신청할 수 있습니다. 단순 변심의 왕복 배송비는 고객 부담입니다.

[v2]
상품 수령 후 7일 이내에 환불을 신청할 수 있습니다. [1]
단순 변심의 왕복 배송비는 고객 부담입니다. [1]
별도의 수수료 금액은 자료에서 확인할 수 없습니다. [1]

[v3]
환불은 상품 수령 후 7일 이내에 신청할 수 있습니다. [1]
단순 변심이면 왕복 배송비는 고객 부담입니다. [1]
수수료라는 항목의 금액은 자료에서 확인할 수 없습니다. [1]
(본문은 manual.pdf의 환불 문장입니다. 수수료 금액은 문서에 없습니다.
 유사도 점수가 없어 근거 개수는 숫자로 적지 않았습니다.)

코드 해설

같은 검색 자료로 v1, v2, v3 prompt의 답을 나란히 출력입니다. 입력은 질문 "환불 신청 방법과 수수료를 알려주세요"이고, 출력은 질문, 근거 개수, 버전별 답변입니다.

주석에 적힌 흐름입니다.

출력
search(질문) -> chunk 목록 -> build_context
-> PROMPTS의 각 prompt | llm -> print

코드 앞에서 고정해 두는 말입니다.

  • prompt: 모델에게 주는 지시문 틀. v1은 규칙이 적고 v2·v3는 근거 번호(citation)를 요구합니다.
  • citation: 답변 문장 끝의 [1] 같은 근거 번호
  • retriever: search()가 쓰는 검색. 질문과 가까운 chunk만 남깁니다.
  • chunk: 문서 조각. context는 그 조각들을 이어 붙인 자료 문자열입니다.

함수와 클래스로 나누어 읽습니다.

  • compare: 이름 그대로 한 가지 일만 합니다. 본문 주석을 같이 읽습니다.

파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.

실습 코드 · 13/validators.py

실습 코드: 실습/src/13/validators.py · 위의 원본 PDF 13차시 11쪽 「5️⃣ 실습 ② — 프롬프트 A/B 비교」과 이어집니다.

Python
# ============================================================
# 13차시 | PPT: 13차시_프롬프트최적화.pptx
# 역할: v1~v3 답에 근거 번호(citation)가 올바른지 검사
# 입력: 환불·배송 관련 질문 3개
# 출력: 버전별 답과 인용 검증 표시
# 흐름:
#   search -> build_context -> prompt별 답
#     -> check_citation(답, 문서 개수) -> ✓ 또는 ✗
# ============================================================
# citation: 답변 안의 [1], [2] 같은 근거 번호
# prompt: PROMPTS의 v1, v2, v3 지시문 틀
# chunk: search가 돌려준 문서 조각. 번호는 1부터 조각 개수까지가 유효합니다.

import os
import re
import sys

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

# 11차시 폴더를 모듈 검색 경로에 넣어 search와 build_context를 불러옵니다.
CURRENT_DIR = os.path.dirname(__file__)
RETRIEVER_DIR = os.path.join(CURRENT_DIR, "..", "11")

sys.path.append(RETRIEVER_DIR)

from retriever import search, build_context
from prompts import PROMPTS

# .env의 API 키를 읽고, 아래에서 답을 만들 모델을 준비합니다.
load_dotenv()

llm = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0
)

# 답 문자열과 근거 조각 개수를 받아 (통과 여부, 설명) 두 값을 돌려줍니다.
def check_citation(answer, document_count):

    # 답변에서 [숫자] 형태의 인용 번호를 모두 찾습니다.
    # re.findall은 괄호로 묶은 숫자 부분만 문자열 목록으로 돌려줍니다. 예: '[1] [3]' -> ['1', '3']
    found_numbers = re.findall(r"\[(\d+)\]", answer)

    # 인용 번호가 하나도 없으면 실패입니다.
    if not found_numbers:
        return False, "인용 번호가 전혀 없음"

    # 찾은 문자열 번호를 정수로 바꿉니다.
    citation_numbers = []

    for number in found_numbers:
        citation_numbers.append(int(number))

    # 존재하지 않는 인용 번호가 있는지 검사합니다.
    invalid_numbers = []

    for number in citation_numbers:

        # 인용 번호는 1부터 검색 문서 개수까지만 가능합니다.
        if number < 1 or number > document_count:
            invalid_numbers.append(number)

    # 잘못된 인용 번호가 있으면 실패입니다.
    if invalid_numbers:
        return False, f"존재하지 않는 근거 번호: {invalid_numbers}"

    # 모든 인용 번호가 정상입니다.
    return True, f"인용 {len(citation_numbers)}건 정상"

# compare_prompt.py의 compare에 인용 검사 한 단계를 더한 함수입니다.
def compare(question):

    # 검색은 한 번만 하고 세 버전이 같은 자료를 씁니다.
    documents = search(question)
    context = build_context(documents)

    print("=" * 60)
    print("Q:", question)
    print("근거:", len(documents), "개")
    print("-" * 60)

    # 버전마다 답을 받고 바로 인용 번호를 검사합니다.
    for name, prompt in PROMPTS.items():
        chain = prompt | llm | StrOutputParser()
        answer = chain.invoke({
            "context": context,
            "question": question
        })
        
        print()
        print(f"[{name}]")
        print(answer)

        # 근거 개수(len(documents))보다 큰 번호가 있으면 없는 근거를 인용한 것입니다.
        citation_ok, citation_message = check_citation(
            answer,
            len(documents)
        )

        # 번호가 모두 유효하면 체크, 아니면 엑스입니다.
        if citation_ok:
            mark = "✓"
        else:
            mark = "✗"

        print("🔍 인용 검증:", mark, citation_message)

if __name__ == "__main__":
     
    # 자료에 있는 질문과 일부만 자료에 있는 질문을 섞었습니다.
    test_questions = [
        "환불 신청 방법과 수수료를 알려주세요",
        "환불은 언제까지 가능한가요?",
        "배송 비용은 얼마인가요?"
    ]

    # 질문마다 세 버전의 답과 인용 검증 결과를 출력합니다.
    for question in test_questions:
        compare(question)
        print()

실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)

출력
===== 실행 결과 샘플 =====
실행: cd 실습/src/13
      python validators.py
예시 출력 (API 키 필요, 실행하지 않음)
3쪽 로딩 완료 (총 1529 자)
============================================================
Q: 환불 신청 방법과 수수료를 알려주세요
근거: (검색 통과 개수, 유사도 미기록) 개
------------------------------------------------------------

[v1]
상품 수령 후 7일 이내에 환불을 신청할 수 있습니다. 단순 변심의 왕복 배송비는 고객 부담입니다.
🔍 인용 검증: ✗ 인용 번호가 전혀 없음

[v2]
상품 수령 후 7일 이내에 환불을 신청할 수 있습니다. [1]
수수료 금액은 자료에서 확인할 수 없습니다. [1]
🔍 인용 검증: ✓ 인용 2건 정상

[v3]
환불은 수령 후 7일 이내에 신청할 수 있습니다. [1]
수수료 금액은 자료에서 확인할 수 없습니다. [1]
🔍 인용 검증: ✓ 인용 2건 정상

============================================================
Q: 환불은 언제까지 가능한가요?
근거: (검색 통과 개수, 유사도 미기록) 개
------------------------------------------------------------

[v1]
상품 수령 후 7일 이내입니다.
🔍 인용 검증: ✗ 인용 번호가 전혀 없음

[v2]
상품 수령 후 7일 이내에 환불을 신청할 수 있습니다. [1]
🔍 인용 검증: ✓ 인용 1건 정상

[v3]
상품 수령 후 7일 이내에 환불을 신청할 수 있습니다. [1]
🔍 인용 검증: ✓ 인용 1건 정상

============================================================
Q: 배송 비용은 얼마인가요?
근거: (검색 통과 개수, 유사도 미기록) 개
------------------------------------------------------------

[v1]
단순 변심의 왕복 배송비는 고객 부담이고, 불량 교환의 배송비는 당사가 부담합니다.
🔍 인용 검증: ✗ 인용 번호가 전혀 없음

[v2]
단순 변심의 왕복 배송비는 고객 부담입니다. [1]
상품 불량으로 교환하면 배송비는 당사가 부담합니다. [1]
금액은 자료에서 확인할 수 없습니다. [1]
🔍 인용 검증: ✓ 인용 3건 정상

[v3]
단순 변심이면 왕복 배송비는 고객 부담입니다. [1]
불량 교환의 배송비는 당사가 부담합니다. [1]
얼마인지는 자료에서 확인할 수 없습니다. [1]
🔍 인용 검증: ✓ 인용 3건 정상

(인용 정상 문장은 [1]이 문서 개수 안에 있을 때의 형식입니다.
 검색 개수를 모르므로 근거 줄의 숫자는 적지 않았습니다.
 배송비 금액은 문서에 없고, 누가 부담하는지만 있습니다.)

코드 해설

v1~v3 답에 근거 번호(citation)가 올바른지 검사입니다. 입력은 환불·배송 관련 질문 3개이고, 출력은 버전별 답과 인용 검증 표시입니다.

주석에 적힌 흐름입니다.

출력
search -> build_context -> prompt별 답
-> check_citation(답, 문서 개수) -> ✓ 또는 ✗

코드 앞에서 고정해 두는 말입니다.

  • citation: 답변 안의 [1], [2] 같은 근거 번호
  • prompt: PROMPTS의 v1, v2, v3 지시문 틀
  • chunk: search가 돌려준 문서 조각. 번호는 1부터 조각 개수까지가 유효합니다.

함수와 클래스로 나누어 읽습니다.

  • check_citation: 이름 그대로 한 가지 일만 합니다. 본문 주석을 같이 읽습니다.
  • compare: 이름 그대로 한 가지 일만 합니다. 본문 주석을 같이 읽습니다.

파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.

5-1) 비교 결과 예시

출력
Q: 환불 신청 방법과 수수료를 알려주세요
근거 2개
────────────────────────────────────────
[v1]
환불은 수령 후 7일 이내에 신청 가능하며, 수수료는 보통
상품가의 10% 내외입니다. 고객센터로 문의하세요.
   ⚠ 수수료를 지어냄 (환각)

[v2]
자료에서 확인할 수 없습니다.
   ⚠ 답할 수 있는 부분까지 거부 (과잉 거부)
[v3]
환불은 상품 수령 후 7일 이내에 신청하실 수 있습니다[1].
신청은 고객센터를 통해 접수하실 수 있습니다[2].
수수료에 관한 내용은 자료에서 확인할 수 없습니다.

✅ 아는 것은 답하고 모르는 것은 밝힘

🎯 이 실험이 보여주는 것
V1 — 규칙이 없으면 지어냅니다
V2 — 규칙이 과하면 쓸모없어집니다
V3 — 균형이 맞으면 실용적입니다
프롬프트 최적화는 "제약을 세게 거는 것"이 아니라
"적절한 지점을 찾는 것"입니다.

← 이전 절13차시 · 4. 실습 ① — 프롬프트 파일 분리다음 절 →13차시 · 6. 실습 ③ — 출처 인용 검증하기