13차시. 프롬프트 최적화
4. 실습 ① — 프롬프트 파일 분리
출처: 원본 PDF 13차시_프롬프트최적화.pdf 9~10쪽 · 쪽 그림 9쪽 10쪽
프롬프트를 코드에 박아두면 실험과 롤백이 불가능합니다. 파일로 분리합니다.
📝 src/prompts.py
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️⃣ 실습 ① — 프롬프트 파일 분리」과 이어집니다.
# ============================================================
# 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차시 추가 검색이 실패했을 때 같은 뜻의 짧은 검색어로 질문을 다시 쓰게 합니다.
파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.