10장. MCP 와 Prompt·Context Engineering
퓨샷과 출력 꼴 고정
신입 사원에게 보고서 쓰는 법을 말로만 설명하는 것보다, 잘 쓴 보고서 두세 장을 보여 주는 편이 빠릅니다. 퓨샷은 모델에게 문제와 정답의 본보기를 보여 주는 것이고, 출력 꼴 고정은 "이 양식 칸에 맞춰 써 주세요"입니다.
왼쪽 카드는 예시를 메시지로 넣는 퓨샷입니다. user 에 문제, assistant 에 정답을 번갈아 넣으면 모델은 같은 꼴로 답합니다. 오른쪽 카드는 출력을 JSON 으로 고정하는 방법입니다. response_format={"type": "json_object"} 를 주면 모델이 JSON 만 쓰고, 우리 코드는 json.loads 로 읽습니다. 여기서 본보기 보고서가 퓨샷 예시, 양식 칸이 JSON 의 키입니다.
퓨샷을 넣는 두 방법
| 방법 | 생김새 | 좋은 점 |
|---|---|---|
| 글에 넣기 | 시스템 지시에 "예: 문의 → 분류"를 줄줄이 | 간단하다 |
| 메시지로 넣기 | user, assistant 메시지를 번갈아 | 모델이 학습 때 본 대화 꼴과 같아 잘 따른다 |
출력 검증하기
JSON 으로 받았어도 키가 빠지거나 값이 목록 밖일 수 있습니다. 받은 뒤 코드로 확인합니다.
import json
def check(text, labels=("배송", "교환반품", "결제", "회원", "기타")):
"""JSON 글을 읽어 '분류' 키가 목록 안의 값인지 본다. 틀리면 까닭을 돌려준다"""
try:
obj = json.loads(text)
except json.JSONDecodeError:
return None, "JSON 이 아닙니다"
if obj.get("분류") not in labels:
return None, f"분류 '{obj.get('분류')}' 는 목록에 없습니다"
return obj, "통과"
for t in ['{"분류": "결제", "이유": "쿠폰 할인"}', '{"분류": "쿠폰"}', '분류: 결제']:
print(check(t))첫째는 통과, 둘째는 "분류 '쿠폰' 는 목록에 없습니다", 셋째는 "JSON 이 아닙니다"가 나옵니다. 통과하지 못하면 그 까닭을 붙여 다시 묻습니다("실패 복구 설계" 절).
코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
json.loads(text) | JSON 글을 파이썬 사전으로 읽는다. JSON 꼴이 아니면 오류를 낸다 |
try: … except json.JSONDecodeError: | 읽다가 JSON 해석 오류(decode error)가 나면 멈추지 말고 아래 줄로 가라는 뜻이다 |
labels=("배송", …) | 허용하는 분류 목록의 기본값이다. 괄호로 묶은 튜플이라 함수 안에서 바뀌지 않는다 |
obj.get("분류") | "분류" 키의 값을 꺼낸다. 키가 없으면 오류 대신 None 을 준다 |
not in labels | 값이 허용 목록에 없으면 True 다 |
return None, "…" | 값 두 개(결과, 까닭)를 한꺼번에 돌려준다. 실패하면 결과 자리에 None 을 둔다 |
이 코드가 뜻하는 것
관공서 민원 창구로 읽어 봅니다. 창구 직원은 서류를 받으면 먼저 정해진 양식인지 보고, 다음에 칸의 값이 허용된 것인지 봅니다. 둘 중 하나라도 어긋나면 까닭을 적어 돌려줍니다. 여기서 정해진 양식이 JSON, 칸이 "분류" 키, 허용된 값이 labels, 까닭을 적은 반려 쪽지가 함수가 돌려주는 둘째 값입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
obj = json.loads(text) | 글을 JSON 으로 읽는다 | 양식에 맞는 서류인지 본다 |
except json.JSONDecodeError: | 읽지 못하면 "JSON 이 아닙니다"를 돌려준다 | "양식이 다릅니다" 반려 |
if obj.get("분류") not in labels: | 분류 값이 목록 밖이면 돌려보낸다 | "이 칸에 쓸 수 없는 값입니다" 반려 |
return obj, "통과" | 두 검사를 지나면 읽은 사전을 돌려준다 | 접수 완료 |
결과의 둘째 경우 {"분류": "쿠폰"} 은 JSON 꼴은 맞지만 값이 목록 밖이라 걸렸습니다. "JSON 으로 받았다"와 "쓸 수 있는 답을 받았다"는 다른 일이라는 뜻입니다. 이 함수는 JSON 이 사전이라고 가정합니다. 모델이 [1, 2] 같은 목록 JSON 을 주면 obj.get 에서 오류가 나므로, 실제 서비스에서는 사전인지도 먼저 확인합니다.
작게 줄여서 눈으로 보기
json.dumps 의 ensure_ascii 옵션과, 모델이 자주 하는 실수인 작은따옴표 JSON 을 봅니다.
import json
print(json.dumps({"분류": "결제"})) # 기본값: 한글을 \u 꼴로 바꾼다
print(json.dumps({"분류": "결제"}, ensure_ascii=False)) # 한글 그대로
for text in ['{"분류": "결제"}', "{'분류': '결제'}"]:
try:
print(json.loads(text))
except json.JSONDecodeError as e:
print("읽기 실패:", e.msg){"분류": "결제"}
{"분류": "결제"}
{'분류': '결제'}
읽기 실패: Expecting property name enclosed in double quotes| 결과 | 읽는 법 |
|---|---|
{"분류": …} | 기본값에서는 한글이 글자 번호 꼴로 바뀐다. 뜻은 같지만 사람이 읽기 어렵다 |
{"분류": "결제"} | ensure_ascii=False 를 주면 한글이 그대로 적힌다 |
{'분류': '결제'} | 큰따옴표 JSON 은 잘 읽혀 사전이 되었다 |
읽기 실패: Expecting property name enclosed in double quotes | 작은따옴표로 쓴 글은 JSON 이 아니다. "키 이름은 큰따옴표로 감싸야 한다"는 오류다 |
e.msg 는 오류 내용 가운데 사람이 읽는 설명 부분입니다. 파이썬 사전을 그대로 찍은 꼴(작은따옴표)은 JSON 처럼 보여도 json.loads 로 읽히지 않습니다. 모델의 답이 이렇게 오면 검증에서 걸러 다시 묻습니다.
주의할 점
- 예시가 한쪽 분류에 몰리면 모델도 그 분류로 쏠립니다. 분류마다 고르게 넣습니다.
- 예시가 많을수록 프롬프트가 길어집니다. 예시 20개는 질문 하나보다 수십 배 깁니다. 컨텍스트 예산 안에서 정합니다.