LLM 엔지니어링10장 · MCP 와 Prompt·Context Engineering17 / 22← 이전목차다음 →MetaContents

10장. MCP 와 Prompt·Context Engineering

실습 4 — 프롬프트 다듬기

고객 문의 40개(inquiries.csv)를 다섯 분류(배송, 교환반품, 결제, 회원, 기타)로 나누는 일에서, 프롬프트를 네 판으로 다듬으며 정확도를 잽니다. 앞 20개는 퓨샷 예시로만 쓰고, 뒤 20개로 채점합니다. 문의는 연습용으로 지어낸 것입니다.

1단계. 데이터와 채점 함수

Python
import json, urllib.request
# 이 PC 의 Ollama 는 오래 쓰다 보면 가끔 깨진 글자(@@@@)만 내는 일이 있었다. 실습을 시작할 때 모델을 내려 깨끗하게 다시 올린다
urllib.request.urlopen(urllib.request.Request("http://localhost:11434/api/generate", headers={"Content-Type": "application/json"},
                                              data=json.dumps({"model": "qwen2.5:3b", "keep_alive": 0}).encode())).read()
import time
import pandas as pd
from openai import OpenAI

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
MODEL = "qwen2.5:3b"
inq = pd.read_csv("inquiries.csv")
shots, test = inq.iloc[:20], inq.iloc[20:]       # 예시용 20개와 시험용 20개를 나눈다(겹치지 않게)
LABELS = ["배송", "교환반품", "결제", "회원", "기타"]
print("시험용 분류:", test["분류"].value_counts().to_dict())

def to_label(text):
    """답에서 분류 이름을 찾는다. 없으면 '?'"""
    t = (text or "").replace(" ", "").replace("/", "").replace("·", "")
    return next((l for l in LABELS if l in t), "?")

def score(make_messages, name):
    start = time.time()
    preds = [to_label(client.chat.completions.create(model=MODEL, messages=make_messages(q), temperature=0,
                                                     seed=42, max_tokens=40).choices[0].message.content)
             for q in test["문의"]]
    right = sum(p == y for p, y in zip(preds, test["분류"]))
    wrong = [(q[:14], p, y) for q, p, y in zip(test["문의"], preds, test["분류"]) if p != y]
    print(f"{name}: {right}/20 · {time.time() - start:.0f}초 · 틀린 예 {wrong[:3]}")
    return right
출력
시험용 분류: {'결제': 5, '배송': 4, '교환반품': 4, '회원': 4, '기타': 3}

2단계. 짧은 지시와 분류 목록

Python
results = {}
results["1 짧은 지시"] = score(lambda q: [{"role": "user", "content": f"이 고객 문의를 분류하세요: {q}"}], "1 짧은 지시")
RULE = "고객 문의를 배송, 교환반품, 결제, 회원, 기타 가운데 하나로 분류하세요. 분류 이름 하나만 쓰세요."
results["2 분류 목록"] = score(lambda q: [{"role": "system", "content": RULE}, {"role": "user", "content": q}], "2 분류 목록")
출력
1 짧은 지시: 7/20 · 24초 · 틀린 예 [('문 앞에 두고 가 달라고 ', '?', '배송'), ('포장을 뜯었는데 반품 되나', '?', '교환반품'), ('다른 상품이 왔어요.', '?', '교환반품')]
2 분류 목록: 9/20 · 6초 · 틀린 예 [('해외로도 보내 주시나요?', '회원', '배송'), ('교환 신청한 지 일주일인데', '?', '교환반품'), ('불량이라 새 것으로 받고 ', '?', '교환반품')]

3단계. 분류 설명과 퓨샷

Python
DESC = RULE + """
- 배송: 도착, 송장, 배송지, 해외 배송, 배송 기사
- 교환반품: 불량, 파손, 다른 상품, 교환과 반품 신청
- 결제: 카드, 할부, 쿠폰 할인, 환불 금액, 영수증, 세금계산서
- 회원: 비밀번호, 아이디, 등급, 적립금, 생일 쿠폰, 탈퇴, 수신 설정
- 기타: 위에 없는 것(매장, 입고, 포장, 리뷰)"""
results["3 분류 설명"] = score(lambda q: [{"role": "system", "content": DESC}, {"role": "user", "content": q}], "3 분류 설명")

def few_shot(q):
    msgs = [{"role": "system", "content": DESC}]
    for _, r in shots.iterrows():                 # 예시 20개를 user/assistant 쌍으로 넣는다
        msgs += [{"role": "user", "content": r["문의"]}, {"role": "assistant", "content": r["분류"]}]
    return msgs + [{"role": "user", "content": q}]
results["4 설명+예시 20개"] = score(few_shot, "4 설명+예시 20개")
print(results)
출력
3 분류 설명: 11/20 · 8초 · 틀린 예 [('문 앞에 두고 가 달라고 ', '기타', '배송'), ('포장을 뜯었는데 반품 되나', '?', '교환반품'), ('다른 상품이 왔어요.', '?', '교환반품')]
4 설명+예시 20개: 18/20 · 6초 · 틀린 예 [('문 앞에 두고 가 달라고 ', '기타', '배송'), ('쿠폰을 썼는데 할인이 안 ', '기타', '결제')]
{'1 짧은 지시': 7, '2 분류 목록': 9, '3 분류 설명': 11, '4 설명+예시 20개': 18}

이 실습이 뜻하는 것

이 실습은 신입 사원에게 우편물 분류를 맡기며 설명을 점점 자세히 해 주는 일과 같습니다. 시험 볼 우편물 20통과 채점표를 준비하고(1단계), 처음에는 "분류해 주세요"라고만 하고, 다음에는 분류함 다섯 개의 이름을 알려 줍니다(2단계). 그다음 분류함마다 무엇이 들어가는지 설명하고, 마지막에는 앞서 분류해 둔 우편물 20통을 본보기로 보여 줍니다(3단계). 여기서 신입 사원이 qwen2.5:3b, 설명이 프롬프트, 본보기가 퓨샷 예시, 시험 우편물이 시험용 문의 20개입니다.

단계출력뜻
1. 시험용 분류{'결제': 5, '배송': 4, '교환반품': 4, '회원': 4, '기타': 3}채점에 쓸 20개가 다섯 분류에 고루 나뉘어 있습니다. 예시 20개와는 겹치지 않습니다
2. 짧은 지시7/20 · 24초분류 이름을 모르니 제 말로 답해 20개 가운데 7개만 맞았습니다. 틀린 예의 '?' 는 답에 분류 이름이 없었다는 뜻입니다
2. 분류 목록9/20 · 6초분류 다섯 개의 이름을 알려 주자 2개 더 맞혔습니다
3. 분류 설명11/20 · 8초분류마다 무엇이 들어가는지 적어 주자 11개가 되었습니다
3. 설명+예시 20개18/20 · 6초본보기 20개를 보여 주자 18개로 크게 늘었습니다. 남은 둘은 "문 앞에 두고 가 달라"(배송)와 "쿠폰 할인이 안 됐다"(결제)를 기타로 보낸 것입니다

한 줄로 줄이면, 같은 모델과 같은 문의에서 프롬프트만 다듬어 정답이 7개에서 18개로 늘었고, 가장 큰 차이는 예시를 보여 준 판에서 났다는 실습입니다. 다만 시험 문의가 20개뿐이라 한두 개 차이는 우연일 수 있습니다. 코드 설명에 적은 것처럼 앞선 실행에서는 설명만 붙인 판이 3/20 까지 떨어진 일도 있었으니, 판을 바꿀 때마다 다시 재 봐야 합니다.

코드 설명

  • 예시(shots)와 시험(test)을 나눠, 시험 문의가 예시에 들어가지 않게 했습니다. 본문 "프롬프트 엔지니어링"의 주의할 점입니다.
  • 짧은 지시만 주면 20개 가운데 7개를 맞혔습니다. 모델이 분류 목록을 모르니 제 말로 답해, 다섯 분류 이름이 하나도 들어 있지 않은 답이 많았기 때문입니다(채점은 '?'). 분류 다섯 개를 알려 주자 9개로 늘었습니다.
  • 분류마다 설명을 붙이면 11개, 설명에 예시 20개를 더하면 18개입니다. 같은 모델, 같은 문의인데 프롬프트만 바꿔 7개에서 18개가 되었습니다. 예시를 보여 준 판이 가장 큰 차이를 냈는데, 모델이 "분류 이름 하나만"이라는 출력 꼴을 예시에서 그대로 따라 했기 때문입니다.
  • 설명만 붙인 판에서는 모델이 분류 이름 대신 설명의 낱말("교환과 반품 신청")을 옮겨 쓰는 일이 있었습니다. 교안을 만들며 먼저 돌렸을 때는 이 판이 3/20 까지 떨어지기도 했습니다. 지시를 늘리면 늘 좋아지는 것이 아니라서, 판마다 재 봐야 합니다. 마지막 판에서도 "문 앞에 두고 가 달라"(배송)를 기타로, "쿠폰 할인이 안 됐다"(결제)를 기타로 보냈습니다.
← 이전 절10장 · 실습 3 — MCP 도구를 LLM 에 잇기다음 절 →10장 · 실습 5 — 컨텍스트 고르기