3장. LangChain 기본 구조와 LCEL
실습 4 — 다양한 프롬프트 견주기
같은 질문 "비행기 탈 때 짐 싸는 요령 알려 주세요"를 세 가지 프롬프트(기본, 역할·형식·제약, 퓨샷)로 물어 답의 꼴을 견줍니다. 1단계의 프롬프트 만들기와 토큰 세기는 모델을 부르지 않지만, 2단계부터 진짜 모델을 부르므로 API 키가 있어야 합니다. 퓨샷의 예시 답은 연습용으로 지어낸 것입니다. 이 절의 출력은 API 키를 받기 전에 실행하지 않고 유추한 예상 출력입니다.
1단계. 세 가지 프롬프트 만들기
import os
import pandas as pd
import tiktoken
from dotenv import find_dotenv, load_dotenv
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
from langchain_openai import ChatOpenAI
QUESTION = "비행기 탈 때 짐 싸는 요령 알려 주세요"
# (가) 기본: 질문만 보낸다(제로샷)
basic = ChatPromptTemplate.from_messages([("human", "{question}")])
# (나) 역할·형식·제약을 시스템 메시지에 적는다
styled = ChatPromptTemplate.from_messages([
("system", "너는 구름다리여행의 친절한 상담원이다.\n"
"답은 글머리표(-) 세 줄로 쓰고, 한 줄은 30자 안팎으로 한다.\n"
"항공사마다 다른 규정(무게, 개수)은 숫자를 지어내지 말고 '항공사 규정을 확인하세요'라고 쓴다."),
("human", "{question}"),
])
# (다) 퓨샷: "핵심 한 줄 + 팁 두 개" 꼴의 예시 두 쌍을 보여 준다
examples = [
{"question": "여권 챙길 때 주의할 점 알려 주세요",
"answer": "핵심: 남은 기간부터 확인하세요.\n팁 1. 만료일이 6개월 넘게 남았는지 봅니다.\n팁 2. 사진 면을 찍어 휴대폰에 따로 둡니다."},
{"question": "환전은 어떻게 준비하면 좋아요?",
"answer": "핵심: 쓸 만큼만 나눠 준비하세요.\n팁 1. 큰 금액은 카드로, 잔돈만 현금으로 챙깁니다.\n팁 2. 남은 외화는 다음 여행을 위해 따로 모아 둡니다."},
]
fewshot = ChatPromptTemplate.from_messages([
("system", "너는 구름다리여행의 친절한 상담원이다. 예시와 같은 꼴로 답한다."),
FewShotChatMessagePromptTemplate(
examples=examples,
example_prompt=ChatPromptTemplate.from_messages([("human", "{question}"), ("ai", "{answer}")]),
),
("human", "{question}"),
])
PROMPTS = {"기본": basic, "역할·형식·제약": styled, "퓨샷": fewshot}
# 프롬프트마다 보내는 메시지 수와 토큰 수를 센다(여기까지는 모델을 부르지 않는다)
enc = tiktoken.get_encoding("o200k_base")
for name, p in PROMPTS.items():
msgs = p.invoke({"question": QUESTION}).to_messages()
print(f"{name}: 메시지 {len(msgs)}개, 토큰 {sum(len(enc.encode(m.content)) for m in msgs)}")기본: 메시지 1개, 토큰 13
역할·형식·제약: 메시지 2개, 토큰 91
퓨샷: 메시지 6개, 토큰 173코드가 의미하는 것 같은 QUESTION 하나에 프롬프트 세 장(basic, styled, fewshot)을 만들어 PROMPTS 사전에 담습니다. 이 단계는 모델을 부르지 않고 enc.encode 로 보내는 토큰만 세므로 실제로 돌린 숫자이고, 지시와 예시가 요청 한 번의 무게를 얼마나 늘리는지 미리 보여 줍니다.
2단계. 같은 질문을 세 프롬프트로 묻기
# 2차시에서 한 방식대로 키와 모델 이름을 읽는다
load_dotenv(find_dotenv(usecwd=True))
MODEL = os.getenv("OPENAI_MODEL", "gpt-4.1-mini")
# 셋을 공정하게 견주려고 온도 0 과 같은 최대 토큰을 쓴다
model = ChatOpenAI(model=MODEL, temperature=0, max_tokens=400, timeout=30)
answers = {}
for name, p in PROMPTS.items():
# 프롬프트만 바꿔 끼우고 모델과 파서는 그대로다
chain = p | model | StrOutputParser()
answers[name] = chain.invoke({"question": QUESTION})
print(f"===== {name}")
print(answers[name])===== 기본
비행기 탈 때 짐 싸는 요령을 알려 드릴게요.
1. **액체류는 100ml 이하 용기에 담아** 투명한 지퍼백에 넣으세요.
2. **보조 배터리는 기내에 들고 타야** 합니다. 부치는 짐에 넣으면 안 됩니다.
3. **무거운 물건은 캐리어 바퀴 쪽에** 두면 끌기 편합니다.
4. **옷은 돌돌 말아** 넣으면 자리를 아끼고 주름도 덜 갑니다.
5. **귀중품과 상비약은 기내 가방에** 넣으세요.
항공사마다 무게와 개수 규정이 다르니 출발 전에 꼭 확인하세요.
===== 역할·형식·제약
- 보조 배터리와 귀중품은 기내 가방에 넣으세요.
- 옷은 돌돌 말아 넣으면 자리를 아낄 수 있어요.
- 무게와 개수는 항공사 규정을 확인하세요.
===== 퓨샷
핵심: 기내에 들고 갈 것부터 나누세요.
팁 1. 보조 배터리와 상비약은 기내 가방에 넣습니다.
팁 2. 무거운 물건은 바퀴 쪽에 두면 끌기 편합니다.코드가 의미하는 것 p | model | StrOutputParser() 에서 프롬프트 부품만 바꿔 끼우고 모델과 파서는 그대로 둡니다. 온도 0 과 같은 max_tokens 로 맞춘 것은 답의 차이가 프롬프트에서만 오게 하려는 것입니다. 예상 출력의 세 답은 실제로 돌리면 글이 달라질 수 있습니다.
3단계. 답의 꼴을 숫자로 견주기
rows = []
for name, text in answers.items():
lines = [line for line in text.splitlines() if line.strip()] # 빈 줄은 빼고 센다
rows.append({
"프롬프트": name,
"글자 수": len(text),
"줄 수": len(lines),
"답 토큰": len(enc.encode(text)),
"숫자를 적었나": any(ch.isdigit() for ch in text), # 글자 가운데 숫자가 하나라도 있으면 True
})
compare = pd.DataFrame(rows)
print(compare.to_string(index=False)) 프롬프트 글자 수 줄 수 답 토큰 숫자를 적었나
기본 260 7 170 True
역할·형식·제약 80 3 50 False
퓨샷 80 3 60 True코드가 의미하는 것 len(text), 줄 수, enc.encode(text), isdigit 은 답의 꼴을 사람이 읽기 전에 숫자로 재는 작은 채점기입니다. 챗봇 화면에 넣을 답은 길이와 꼴이 한결같아야 하므로, 프롬프트를 고를 때 이런 숫자를 근거로 삼습니다. 이 표의 숫자는 예상 출력의 답에서 센 것입니다.
4단계. 저장하기
# 답 글도 함께 남겨 엑셀에서 나란히 읽어 볼 수 있게 한다
compare["답"] = [answers[name] for name in compare["프롬프트"]]
compare.to_csv("out_ch03_prompt_compare.csv", index=False, encoding="utf-8-sig")
print("저장: out_ch03_prompt_compare.csv")저장: out_ch03_prompt_compare.csv코드가 의미하는 것 to_csv 로 숫자 옆에 답 글까지 남기는 것은, 숫자만으로는 놓치는 말투와 내용을 사람이 나란히 읽어 보려는 것입니다. 키를 받아 실제로 돌린 뒤 이 파일을 열어 예상 출력과 견줍니다.
이 실습이 뜻하는 것
이 실습은 같은 꽃집에 같은 꽃다발을 세 가지 방식으로 주문해 보는 일과 같습니다. 주문서 세 장을 쓰고(1단계), 주문을 넣고(2단계), 받은 꽃다발의 크기를 재고(3단계), 기록해 둡니다(4단계). 여기서 "꽃다발 하나요"가 기본 프롬프트, 받을 사람·크기·뺄 꽃을 적은 주문이 역할·형식·제약 프롬프트, 본보기 사진 두 장을 보여 준 주문이 퓨샷입니다. 비유와 다른 점은, 사진을 보여 주면 꽃집 주인은 분위기만 참고하지만 모델은 "핵심:", "팁 1." 같은 머리말까지 그대로 따라 한다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 프롬프트 셋 | 토큰 13, 91, 173 | 실제로 센 숫자입니다. 지시를 적고 예시를 보여 줄수록 보내는 토큰이 늘어납니다 |
| 2. 기본 | 번호 목록 다섯 줄과 앞뒤 문장 | 지시가 없으니 모델이 스스로 고른 길고 꾸민 꼴(굵은 글씨 표시 **)로 답합니다 |
| 2. 역할·형식·제약 | 글머리표 세 줄 | 형식 지시대로 세 줄이고, 무게 규정은 숫자 없이 "항공사 규정을 확인하세요"라고 썼습니다 |
| 2. 퓨샷 | 핵심 한 줄과 팁 두 개 | 예시와 같은 머리말과 줄 수를 따라 했습니다 |
| 3. 숫자로 견주기 | 글자 260, 80, 80 | 지시나 예시를 준 두 답은 기본 답의 3분의 1 쯤입니다. 퓨샷의 "숫자를 적었나 True" 는 "팁 1." 의 숫자 때문입니다 |
한 줄로 줄이면, 모델과 질문은 그대로 두고 프롬프트만 바꿔 끼워도 답의 길이와 꼴이 크게 달라진다는 것을 견준 실습입니다. 보내는 토큰은 13, 91, 173 으로 늘지만, 예상대로라면 답의 토큰은 170 에서 50~60 으로 줄어듭니다. 1장 실습 3의 가정한 단가(100만 토큰당 입력 1달러, 출력 4달러)로 셈하면 기본은 13 + 170 × 4 = 693, 역할·형식·제약은 91 + 50 × 4 = 291 로, 지시를 적어 답을 짧게 한 쪽이 오히려 쌉니다. 모델이 쓰는 답의 글은 실행할 때마다, 모델마다 조금씩 달라지므로 2·3단계의 숫자는 어림으로 보고 실제로 돌린 뒤 다시 읽습니다. 화면에 보여 줄 챗봇에는 꼴이 한결같은 역할·형식·제약이나 퓨샷 쪽이 다루기 쉽습니다.
코드 설명
PROMPTS사전에 프롬프트 셋을 담고p | model | StrOutputParser()로 하나씩 끼웁니다. 모델과 파서는 그대로 두고 프롬프트만 바꿔 끼우는 것이 LCEL 의 좋은 점입니다.styled의 시스템 글은 파이썬이 따옴표로 감싼 글 여러 개를 이어 붙이는 성질을 써서 줄마다 나눠 적었습니다.\n은 줄바꿈입니다.- 퓨샷의 예시 답 안에는
{가 없어서 그대로 넣어도 됩니다. 예시 답에 중괄호가 있으면{{,}}로 겹쳐 적어야 합니다. any(ch.isdigit() for ch in text)는 답에 숫자 글자가 하나라도 있는지 봅니다. 제약 지시("숫자를 지어내지 말라")를 지켰는지 대충 가늠하는 검사입니다. "팁 1." 의 1 도 숫자로 세므로 사람이 다시 읽어 확인합니다.- 온도를 0 으로 두어도 세 답은 프롬프트에 따라 꼴이 크게 다릅니다. 같은 모델이라도 프롬프트가 답의 길이와 모양을 정합니다.