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

13차시. 프롬프트 최적화

4. 실습 ① — 프롬프트 파일 분리

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

프롬프트를 코드에 박아두면 실험과 롤백이 불가능합니다. 파일로 분리합니다.

📝 src/prompts.py

Python
from langchain_core.prompts import ChatPromptTemplate

# ── V1: 최소 버전 (비교 대조군) ──
RAG_PROMPT_V1 = ChatPromptTemplate.from_template(
    "아래 자료를 참고해서 질문에 답해주세요.\n\n"
    "[자료]\n{context}\n\n"
    "[질문] {question}"
)

# ── V2: 규칙 강화 버전 (권장) ──
RAG_PROMPT_V2 = ChatPromptTemplate.from_template(
    "당신은 사내 문서를 안내하는 담당자입니다.\n\n"
    "[규칙]\n"
    "1. 아래 [자료]에 있는 내용만 근거로 답하십시오.\n"
    "2. 자료에 없는 내용은 '자료에서 확인할 수 없습니다'라고 "
    "명시하십시오.\n"
    "3. 추측하거나 일반 상식으로 보충하지 마십시오.\n"
    "4. 각 문장 끝에 근거 번호를 [1] 형식으로 표기하십시오.\n"
    "5. 3~5문장으로 간결하게 답하십시오.\n\n"
    "[자료]\n{context}\n\n"
    "[질문]\n{question}"
)

# ── V3: 부분 답변 허용 버전 (과잉 거부 방지) ──
RAG_PROMPT_V3 = ChatPromptTemplate.from_template(
    "당신은 사내 문서를 안내하는 담당자입니다.\n\n"
    "[규칙]\n"
    "1. 아래 [자료]에 있는 내용만 근거로 답하십시오.\n"
    "2. 질문의 일부만 자료에 있다면, 있는 부분은 답하고 "
    "없는 부분만 '자료에서 확인할 수 없습니다'라고 하십시오.\n"
    "3. 추측하거나 일반 상식으로 보충하지 마십시오.\n"
    "4. 각 문장 끝에 근거 번호를 [1] 형식으로 표기하십시오.\n"
    "5. 3~5문장으로 간결하게 답하십시오.\n\n"
    "[자료]\n{context}\n\n"
    "[질문]\n{question}"
)

PROMPTS = {"v1": RAG_PROMPT_V1,
           "v2": RAG_PROMPT_V2,
           "v3": RAG_PROMPT_V3}

💡 V2와 V3의 차이 (중요)
V2 규칙 2 — "자료에 없으면 확인할 수 없다고 하라"
→ 질문에 일부만 없어도 전체를 거부할 수 있습니다
V3 규칙 2 — "있는 부분은 답하고 없는 부분만 밝혀라"
→ 부분적으로라도 도움이 됩니다
실무에서는 대개 V3가 사용자 만족도가 높습니다.

실습 코드 · 13/prompts.py

실습 코드: 실습/src/13/prompts.py · 위의 원본 PDF 13차시 9~10쪽 「4️⃣ 실습 ① — 프롬프트 파일 분리」과 이어집니다.

Python
# ============================================================
# 13차시 | PPT: 13차시_프롬프트최적화.pptx
# 역할: 답변 규칙을 조금씩 바꾼 prompt 틀을 모아 둠. 실행해도 출력은 없습니다.
# 입력: 없음 (다른 파일이 이 틀을 import)
# 출력: 없음
# 흐름:
#   문자열 틀 -> ChatPromptTemplate -> PROMPTS 딕셔너리
#   (25차시 이후 틀은 같은 파일에 정의만 되어 있고 PROMPTS에는 넣지 않음)
#   generator2.py는 RETRY_PROMPT를 직접 import 합니다.
# ============================================================
# prompt: 모델에게 주는 지시문 틀. {context}는 검색된 자료, {question}은 질문입니다.
# citation: 문장 끝에 붙이는 [1] 같은 근거 번호. V2·V3 규칙 4번이 이것을 요구합니다.

# ChatPromptTemplate는 {이름} 칸이 있는 문자열을 프롬프트 틀로 만들어 줍니다.
from langchain_core.prompts import ChatPromptTemplate

# ── V1: 최소 버전 (비교 대조군) ──
# 자료와 질문만 넣고, 추측 금지·근거 번호 규칙은 없습니다.
# 괄호 안에서 줄을 나눠 쓴 따옴표 문자열은 하나로 이어 붙습니다.
RAG_PROMPT_V1 = ChatPromptTemplate.from_template(     
    "아래 자료를 참고해서 질문에 답해주세요.\n\n"     
    "[자료]\n{context}\n\n"
    "[질문] {question}"
)
# ── V2: 규칙 강화 버전 (권장) ──
# 자료 밖이면 전부 '자료에서 확인할 수 없습니다'라고 하게 합니다.
# 규칙 1, 2, 4가 수업 자료의 '무조건 넣어야 할 세 줄'입니다.
RAG_PROMPT_V2 = ChatPromptTemplate.from_template(     
    "당신은 사내 문서를 안내하는 담당자입니다.\n\n"
    "[규칙]\n"
    "1. 아래 [자료]에 있는 내용만 근거로 답하십시오.\n"     
    "2. 자료에 없는 내용은 '자료에서 확인할 수 없습니다'라고 "     
    "명시하십시오.\n"
    "3. 추측하거나 일반 상식으로 보충하지 마십시오.\n"     
    "4. 각 문장 끝에 근거 번호를 [1] 형식으로 표기하십시오.\n"     
    "5. 3~5문장으로 간결하게 답하십시오.\n\n"     
    "[자료]\n{context}\n\n"
    "[질문]\n{question}"
)
# ── V3: 부분 답변 허용 버전 (과잉 거부 방지) ──
# 질문의 일부만 자료에 있으면 그 부분만 답하게 합니다.
# V2와 다른 곳은 규칙 2번 한 줄뿐입니다.
RAG_PROMPT_V3 = ChatPromptTemplate.from_template(     
    "당신은 사내 문서를 안내하는 담당자입니다.\n\n"
    "[규칙]\n"
    "1. 아래 [자료]에 있는 내용만 근거로 답하십시오.\n"     
    "2. 질문의 일부만 자료에 있다면, 있는 부분은 답하고 "     
    "없는 부분만 '자료에서 확인할 수 없습니다'라고 하십시오.\n"     
    "3. 추측하거나 일반 상식으로 보충하지 마십시오.\n"     
    "4. 각 문장 끝에 근거 번호를 [1] 형식으로 표기하십시오.\n"     
    "5. 3~5문장으로 간결하게 답하십시오.\n\n"     
    "[자료]\n{context}\n\n"
    "[질문]\n{question}"
)

# 이름표로 세 틀을 고릅니다. compare_prompt.py가 이 딕셔너리를 순회합니다.
PROMPTS = {"v1": RAG_PROMPT_V1,            
           "v2": RAG_PROMPT_V2,            
           "v3": RAG_PROMPT_V3}

# 25차시에 추가
# 직전 답이 반려된 이유를 넣고 다시 쓰게 하는 틀입니다.
# generator2.py의 create_retry_chain()이 RETRY_PROMPT를 직접 가져옵니다.
# PROMPTS에 넣지 않습니다. 13차시 비교가 {reason} 없이 전체를 돌리기 때문입니다.
RETRY_PROMPT = ChatPromptTemplate.from_template(
    "당신은 사내 문서를 안내하는 담당자입니다.\n\n"
    "[이전 시도 결과]\n"
    "직전 답변이 아래 이유로 반려되었습니다.\n"
    "{reason}\n"
    "이번에는 반드시 이 문제를 해결해서 작성하십시오.\n\n"
    "[규칙]\n"
    "1. 아래 [자료]에 있는 내용만 근거로 답하십시오.\n"
    "2. 질문의 일부만 자료에 있다면 있는 부분만 답하십시오.\n"
    "3. 각 문장 끝에 근거 번호를 [1] 형식으로 반드시 표기하십시오.\n"
    "4. 반드시 한 문장으로 간결하게 답하십시오.\n\n"
    "[자료]\n"
    "{context}\n\n"
    "[질문]\n"
    "{question}"
)

# 등록하지 않습니다. generator2.py가 RETRY_PROMPT를 직접 가져옵니다.
# PROMPTS["retry"] = RETRY_PROMPT

# 26차시에 추가
# 답을 새로 만들지 않고, 근거에 있는지·질문에 맞는지 판정만 하게 합니다.
# 중괄호를 두 번 쓴 부분은 format 빈칸이 아니라 출력에 남을 JSON 예시입니다.
JUDGE_PROMPT = ChatPromptTemplate.from_template(
    "당신은 RAG 답변을 검증하는 엄격한 심판입니다.\n"
    "답변을 생성하지 말고, 오직 평가만 하십시오.\n\n"
    "[근거 자료]\n{context}\n\n"
    "[사용자 질문]\n{question}\n\n"
    "[검증할 답변]\n{answer}\n\n"
    "[평가 기준]\n"
    "grounded : 답변의 모든 주장이 근거 자료 안에 있으면 true.\n"     
    "           근거에 없는 숫자·조건·설명이 하나라도 있으면 false.\n"     
    "relevant : 답변이 질문에 실제로 대답하면 true.\n"
    "           질문의 핵심을 비껴갔으면 false.\n"
    "reason   : 판정 이유를 한 문장으로. false인 항목이 있으면\n"     
    "           어느 부분이 문제인지 구체적으로 쓸 것.\n\n"
    "[출력 형식 - 다른 말은 절대 쓰지 마십시오]\n"
    '{{"grounded": true, "relevant": true, "reason": "..."}}' )

# 29차시 추가
# 인사, 계산, 문서와 무관한 질문, 문서 질문을 한 단어로만 고르게 합니다.
# 마지막 줄을 '[유형]'으로 끝내 모델이 그 뒤에 단어 하나만 잇게 합니다.
CLASSIFY_PROMPT = ChatPromptTemplate.from_template(     
    "사용자 질문의 유형을 하나만 골라 그 단어만 출력하십시오.\n"
    "다른 설명은 절대 하지 마십시오.\n\n"
    "[유형]\n"
    "greeting : 인사, 감사, 잡담 (정보 요청이 아님)\n"
    "calc     : 순수한 수식 계산\n"
    "scope    : 우리 문서와 무관한 주제 "
    "(날씨, 뉴스, 일반 상식 등)\n"
    "document : 사내 문서에서 찾아야 할 질문\n\n"
    "[판단 원칙]\n"
    "애매하면 document를 선택하십시오.\n\n"     
    "[질문]\n{question}\n\n"
    "[유형]" 
    )

# 30차시 추가
# 검색이 실패했을 때 같은 뜻의 짧은 검색어로 질문을 다시 쓰게 합니다.
# {tried}에는 이미 써 본 검색어, {reason}에는 실패 이유가 들어갑니다.
REWRITE_PROMPT = ChatPromptTemplate.from_template(
    "당신은 문서 검색을 돕는 질의 재작성 전문가입니다.\n"
    "아래 질문으로 문서를 검색했지만 관련 자료를 찾지 못했습니다.\n"
    "검색이 더 잘 되도록 질의를 다시 작성하십시오.\n\n"
    "[원본 질문]\n{question}\n\n"
    "[이미 시도한 질의 - 반복하지 마십시오]\n{tried}\n\n"
    "[실패 이유]\n{reason}\n\n"
    "[재작성 규칙]\n"
    "1. 일상적 표현을 문서에서 쓸 법한 공식 용어로 바꾸십시오.\n"
    "   예) 반품 -> 환불 / 택배비 -> 배송비 / 비번 -> 비밀번호\n"
    "2. 구어체를 문어체로 바꾸십시오.\n"
    "3. 군더더기를 빼고 핵심 명사 위주로 3~8단어로 압축하십시오.\n"     
    "4. 원본의 의미를 바꾸지 마십시오.\n"
    "5. 이미 시도한 질의와 다르게 작성하십시오.\n\n"     
    "[출력 - 재작성된 질의만 한 줄로. 다른 말 금지]" 
    )

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

출력
===== 실행 결과 샘플 =====
실행: cd 실습/src/13
      python prompts.py
(표준 출력 없음. 틀만 만들고 print하지 않습니다.)

코드 해설

답변 규칙을 조금씩 바꾼 prompt 틀을 모아 둠. 실행해도 출력은 없습니다. 입력은 없음 (다른 파일이 이 틀을 import)이고, 출력은 없음입니다.

주석에 적힌 흐름입니다.

출력
문자열 틀 -> ChatPromptTemplate -> PROMPTS 딕셔너리

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

  • 25차시 이후 틀은 같은 파일에 정의만 되어 있고 PROMPTS에는 넣지 않습니다. generator2.py는 RETRY_PROMPT를 직접 가져옵니다.
  • prompt: 모델에게 주는 지시문 틀. {context}는 검색된 자료, {question}은 질문입니다.
  • citation: 문장 끝에 붙이는 [1] 같은 근거 번호. V2·V3 규칙 4번이 이것을 요구합니다.

함수로 나누지 않고 위에서 아래로 실행됩니다.

  • RAG_PROMPT_V1 = ChatPromptTemplate.from_template( — ── V1: 최소 버전 (비교 대조군) ── 자료와 질문만 넣고, 추측 금지·근거 번호 규칙은 없습니다.
  • RAG_PROMPT_V2 = ChatPromptTemplate.from_template( — ── V2: 규칙 강화 버전 (권장) ── 자료 밖이면 전부 '자료에서 확인할 수 없습니다'라고 하게 합니다.
  • RAG_PROMPT_V3 = ChatPromptTemplate.from_template( — ── V3: 부분 답변 허용 버전 (과잉 거부 방지) ── 질문의 일부만 자료에 있으면 그 부분만 답하게 합니다.
  • PROMPTS = {"v1": RAG_PROMPT_V1, — 이름표로 세 틀을 고릅니다. compare_prompt.py가 이 딕셔너리를 순회합니다.
  • RETRY_PROMPT = ChatPromptTemplate.from_template( — 25차시에 추가. 직전 답이 반려된 이유를 넣고 다시 쓰게 하는 틀입니다. generator2.py가 이 변수를 직접 가져오고, 등록 줄은 주석인 채로 둡니다.
  • JUDGE_PROMPT = ChatPromptTemplate.from_template( — 26차시에 추가. 답을 새로 만들지 않고, 근거에 있는지·질문에 맞는지 판정만 하게 합니다. 중괄호를 두 번 쓴 부분은 format 빈칸이 아니라 출력에 남을 JSON 예시입니다.
  • CLASSIFY_PROMPT = ChatPromptTemplate.from_template( — 29차시 추가 인사, 계산, 문서와 무관한 질문, 문서 질문을 한 단어로만 고르게 합니다.
  • REWRITE_PROMPT = ChatPromptTemplate.from_template( — 30차시 추가 검색이 실패했을 때 같은 뜻의 짧은 검색어로 질문을 다시 쓰게 합니다.

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

← 이전 절13차시 · 3. 규칙 설계 — 무엇을 넣을까?다음 절 →13차시 · 5. 실습 ② — 프롬프트 A/B 비교