출력 스키마 고정 — 파싱이 간헐적으로 깨질 때
시리즈 프롬프트를 코드처럼 다루기의 세 번째 글.
터진 문제
리포트 생성 결과를 JSON으로 받아 뒷단에서 가공했다. 로컬 테스트에서는 잘 됐다. 운영에서 깨졌다.
```json
{ "recommendations": [...] }
```
이런 식으로 코드펜스가 붙어서 오거나, 앞에 “네, 요청하신 결과입니다”가 붙거나, 배열 마지막에 trailing comma가 들어가거나, 한글 필드명이 섞여 왔다.
핵심은 빈도였다. 대부분 성공하니까 로컬에서는 안 보인다. 500명이 쓰는 서비스에서는 그 소수가 매일 발생한다.
Before — 프롬프트로 형식을 부탁하기
결과를 아래 JSON 형식으로만 출력해줘. 다른 설명은 붙이지 마.
{
"recommendations": [
{ "major": "학과명", "reason": "근거" }
]
}
이 방식의 문제는 보장이 없다는 것이다. “다른 설명은 붙이지 마”는 요청이고, 요청은 지켜지지 않을 수 있다. 지켜지지 않았을 때 터지는 건 내 파서다.
After — 스키마로 강제하고, 그래도 검증하기
1단계: 모델이 형식을 벗어날 수 없게 한다
Tool use(function calling)나 structured output을 쓰면 형식이 프롬프트가 아니라 API 레벨 제약이 된다.
TOOL = {
"name": "submit_recommendations",
"description": "학과 추천 결과를 제출한다.",
"input_schema": {
"type": "object",
"properties": {
"recommendations": {
"type": "array",
"minItems": 3,
"maxItems": 3,
"items": {
"type": "object",
"properties": {
"major": {"type": "string"},
"reason": {"type": "string"},
"evidence": {
"type": "string",
"description": "생활기록부 원문 인용. 요약하지 않는다.",
},
},
"required": ["major", "reason", "evidence"],
},
}
},
"required": ["recommendations"],
},
}
여기서 중요한 건 minItems/maxItems다.
“3개만 추천해줘”라는 프롬프트 지시가 스키마 제약으로 옮겨갔다.
프롬프트에서 지킬 규칙이 하나 줄었다.
description도 프롬프트의 일부다. evidence 필드에 “요약하지 않는다”를 붙이는 게
본문 프롬프트에 같은 말을 쓰는 것보다 잘 작동했다.
규칙은 그 규칙이 적용되는 필드 옆에 둘 때 가장 잘 지켜진다.
2단계: 그래도 검증한다
class Recommendation(BaseModel):
major: str
reason: str
evidence: str
class Report(BaseModel):
recommendations: list[Recommendation] = Field(min_length=3, max_length=3)
스키마를 걸어도 검증은 남겨둔다. 이유는 두 가지다.
- 모델 버전이 올라가면 동작이 미묘하게 바뀐다. 검증은 그걸 조용한 실패가 아니라 에러로 만든다.
- 스키마는 타입을 보장하지만 내용은 보장하지 않는다.
evidence가 빈 문자열이어도 스키마는 통과한다.
3단계: 내용 검증은 따로 한다
가장 중요한 검증은 스키마가 못 잡는 쪽이었다.
def validate_evidence(report: Report, source_text: str) -> list[str]:
"""인용했다는 문구가 실제로 원문에 있는지 확인한다."""
return [
r.major for r in report.recommendations
if r.evidence not in source_text
]
(이 검증에서 걸러진 비율과, 그걸 보고 프롬프트를 어떻게 고쳤는지 적는다.)
무엇이 좋아졌는가
(파싱 실패율 변화와 측정 방법을 적는다.)
이 기법이 안 통하는 경우
- 출력이 긴 산문일 때. 스키마는 구조화된 출력에 맞다. 리포트 본문 같은 자유 서술을 억지로 필드에 쪼개면 글이 부자연스러워진다. 이때는 산문은 산문으로 받고, 메타데이터만 스키마로 분리하는 편이 나았다.
- 스키마가 깊게 중첩될 때. 3단 이상 중첩되면 모델의 필드 누락이 늘었다. 중첩을 줄이거나 호출을 나누는 게 빨랐다.
- 탐색적인 작업. 어떤 출력이 필요한지 아직 모르는 단계에서 스키마를 먼저 박으면, 모델이 줄 수 있었던 더 나은 형태를 못 보게 된다.
정리
“JSON으로만 답해줘”는 프롬프트 엔지니어링이 아니라 희망이다. 형식은 프롬프트에서 빼서 스키마와 검증으로 내리는 것이 맞았다. 그러면 프롬프트에는 형식이 아니라 판단 기준만 남는다.
다음 글: Few-shot 예시 설계 — 그 판단 기준을 예시로 가르치는 방법.