구조화된 출력은 미리 정해 둔 모양의 답입니다. 이름이 정해진 키가 있는 JSON 객체, 열이 고정된 표, 매번 같은 제목이 붙는 템플릿 같은 것입니다. 프로그램이 답을 읽을 때는 반드시 필요하고, 사람이 읽을 때도 도움이 됩니다. 모든 답이 같은 모습이 되어 훑어보거나 비교하기 쉬워지기 때문입니다.
요령은 모델이 고를 것이 하나도 남지 않을 만큼 모양을 정확하게 설명하는 것입니다. "JSON"이라는 단어 하나만으로는 설명이 되지 않습니다. 아래 블록은 버그 리포트에서 세부 정보를 뽑아냅니다. "핵심 정보"를 요청할 때와 스키마를 풀어 쓴 요청을 비교해 보세요.
버그 리포트의 핵심 정보는 다음과 같습니다.
- 문제: 프로젝트를 내보낼 때 앱이 강제 종료됨
- 발생 조건: 사진이 50장 넘는 프로젝트에서 내보내기를 누를 때
- 시작 시점: 가장 최근 업데이트 이후
- 플랫폼: 안드로이드 14
- 앱 버전: 3.2.0
- 영향: 높음, 고객 납품이 막혀 있음
작은 프로젝트는 문제없이 내보내지는 것으로 보입니다.
자유 형식 답변은 정확하고 읽기 쉽지만, 실행할 때마다 라벨이 달라질 것이고, 심각도가 값이 아니라 문장이며, 프로그램은 각 필드가 어디서 시작하는지 추측해야 합니다. JSON 답변은 곧바로 버그 트래커에 넣을 수 있습니다. 답변이 여전히 코드 블록 안에 들어 있다는 점도 눈여겨보세요. 채팅 앱은 보통 JSON을 이렇게 감싸는데, 코드에서 파싱할 때 이 점이 중요합니다.
JSON 요청하는 법
좋은 JSON 요청은 모델이 대신 답해 버릴 모든 질문에 미리 답합니다.
- 모든 키를 정확한 철자로. 키 이름을 나와야 할 그대로 따옴표 안에 쓰세요. "정확히 다음 키"라고 말해서 모델이 키를 더하지 않게 하세요.
- 모든 값의 타입. 문자열, 숫자, 불리언, 문자열 배열, 중첩 객체.
- 허용되는 값. 심각도나 카테고리처럼 값이 정해진 필드에 필요합니다. 목록이 없으면 네 번 실행에 "High", "high", "severe", "P1"이 나옵니다.
- 입력에 값이 없을 때 할 일. "명시되지 않았으면 null"이라고 말하지 않으면 모델은 그럴듯한 추측으로 빈칸을 채우는 경향이 있고, 추측한 앱 버전은 진짜 버전과 똑같아 보입니다.
- 앞뒤에 아무것도 없게. "앞뒤에 다른 텍스트 없이 JSON만 돌려줘"는 친절한 첫 문장을 없애 줍니다.
모양이 중첩되어 있거나 특이하다면, 설명하는 것보다 완전한 예시 객체 하나를 보여 주는 편이 낫습니다. 형식에 few-shot 프롬프팅을 적용하는 것입니다. 모델이 따라 하지 않도록 예시의 값은 실제 입력과 확실히 다르게 하세요.
재사용할 수 있는 추출 프롬프트
이 블록은 같은 요청을 여러 부분으로 나눈 것입니다. 형식 부분을 꺼도 제약 조건이 JSON을 요구하므로 모델은 여전히 JSON을 돌려주지만, 키 이름을 제멋대로 고릅니다. job_title 대신 title을 쓰는 식이고, 여러분의 키를 기대하는 코드는 깨집니다. 제약 조건 부분은 모델이 반쯤 쓴 전화번호를 완성하거나 이메일 도메인으로 회사를 추론하지 못하게 막아 줍니다. 실제 서명을 입력칸에 붙여 넣어 테스트해 보세요.
{
"name": "문자열",
"job_title": "문자열 또는 null",
"company": "문자열 또는 null",
"email": "문자열 또는 null",
"phone": "문자열 또는 null"
}{
"name": "김지현",
"job_title": "데이터 총괄",
"company": "노스윈드 랩스",
"email": "jihyun.kim@northwind.example",
"phone": null
}
표와 고정 템플릿
구조화된 출력은 프로그램만을 위한 것이 아닙니다. 답을 직접 읽을 때도 마크다운 표나 고정 템플릿은 같은 이점을 줍니다. 보기 전에 이미 각 정보가 어디에 있을지 알 수 있습니다.
| 타입 | 순서 유지 | 변경 가능 | 중복 허용 | 주요 용도 |
|---|---|---|---|---|
| list | 예 | 예 | 예 | 항목을 추가, 삭제, 정렬하는 시퀀스 |
| tuple | 예 | 아니요 | 예 | 좌표처럼 고정된 값의 묶음 |
| set | 아니요 | 예 | 아니요 | 중복 제거와 빠른 포함 여부 확인 |
더 긴 글에는 템플릿이 같은 방식으로 통합니다. 제목을 순서대로 주고 각 제목 아래에 무엇이 들어갈지 말하세요. "원인, 해결책, 확인 방법이라는 굵은 라벨 세 개로 답해 줘"라고 하면 매번 같은 세 라벨이 나오므로, 여러 답변을 비교하기 쉬워집니다.
API의 JSON 모드
여러 모델 API에는 문법적으로 올바른 JSON을 강제하는 설정이 있습니다. OpenAI Python SDK에서는 response_format입니다. JSON 모드를 쓰려면 메시지 어딘가에 "JSON"이라는 단어가 있어야 하므로, 아래 시스템 프롬프트는 이 단어를 쓰고 키를 나열합니다.
import json
from openai import OpenAI
client = OpenAI()
MODEL = "your-model-id" # e.g. from your provider's model list
report_text = "Since yesterday's update the app crashes when I tap Export..."
response = client.chat.completions.create(
model=MODEL,
response_format={"type": "json_object"},
messages=[
{
"role": "system",
"content": (
"Extract the bug report into JSON with the keys "
"summary (string), severity (one of low, medium, high, critical) "
"and steps_to_reproduce (array of strings)."
),
},
{"role": "user", "content": report_text},
],
)
data = json.loads(response.choices[0].message.content)
JSON 모드는 (답변이 토큰 한도에서 잘리지 않는 한) 텍스트가 파싱된다는 것을 보장할 뿐, 스키마와 일치한다는 것은 보장하지 않습니다. 키가 빠질 수도 있고 심각도가 "urgent"로 나올 수도 있습니다. 여러 제공사는 출력 형식으로, 또는 도구(함수) 정의의 입력 스키마로 전체 JSON Schema도 받으며, 이 중 일부 모드는 답을 스키마에 맞게 제한합니다. 이런 기능은 API마다 다르므로 정확한 매개변수는 제공사 문서를 확인하세요.
코드에서 결과 검증하기
모델의 JSON은 프로그램 밖에서 들어오는 다른 입력과 똑같이 다루세요. 파싱한 다음 확인하는 것입니다. 파싱은 깨진 문법을 잡아내고, 확인은 문법은 맞지만 내용이 틀린 객체를 잡아냅니다.
ALLOWED_SEVERITIES = {"low", "medium", "high", "critical"}
def problems(data):
if not isinstance(data, dict):
return ["the answer must be a JSON object"]
found = []
if not isinstance(data.get("summary"), str):
found.append("summary must be a string")
if data.get("severity") not in ALLOWED_SEVERITIES:
found.append("severity must be low, medium, high or critical")
steps = data.get("steps_to_reproduce")
if not isinstance(steps, list) or not all(isinstance(s, str) for s in steps):
found.append("steps_to_reproduce must be an array of strings")
return found
확인에 실패하면 한 번 다시 시도하는 것으로 해결되는 경우가 많습니다. 모델이 낸 출력과 문제 목록을 함께 보내고 수정된 JSON을 요청하세요. 재시도 횟수에는 상한을 두고, 실패를 기록하세요. 자주 실패하는 필드는 프롬프트가 불분명하다는 신호입니다. 더 큰 프로젝트에서는 Pydantic 같은 검증 라이브러리나 JSON Schema 검증기가 직접 쓴 함수를 대신합니다.
조용한 오류를 막는 습관이 두 가지 더 있습니다. 잘린 객체는 절대 파싱되지 않으므로, 출력 토큰 한도를 예상되는 가장 큰 답보다 넉넉하게 설정하세요. 그리고 입력이 길거나 사용자에게서 온다면 구분자나 XML 태그로 지시와 구분해서, 그 안의 텍스트가 지시로 읽힐 가능성을 줄이세요. 프롬프트 하나가 추론도 하고 JSON도 만들어야 한다면 프롬프트 체이닝을 고려하세요. 한 단계는 자유 형식으로 생각하게 하고, 두 번째 단계가 그 결과를 구조로 바꾸게 하는 것입니다.
자주 묻는 질문
ChatGPT가 JSON으로 출력하게 하려면 어떻게 하나요?
답이 반드시 JSON이어야 한다고 말하고, 모든 키와 그 타입을 나열하고, 값이 정해진 필드에는 허용되는 값을 주세요. "앞뒤에 다른 텍스트 없이 JSON만 돌려줘"를 더하세요. API에서는 response_format={"type": "json_object"}로 JSON 모드도 켜세요. JSON 모드를 쓰려면 메시지 어딘가에 JSON이라는 단어가 있어야 합니다.
모델은 왜 JSON 앞뒤에 텍스트를 붙이나요?
채팅 모델은 대화하듯 답하도록 학습되어 있어서 "JSON은 다음과 같습니다" 같은 문장으로 시작하거나 객체를 마크다운 코드 블록으로 감싸는 경우가 많습니다. JSON만 요청하고, 코드에서는 API의 JSON 모드를 쓰거나 파싱하기 전에 감싸는 코드 블록을 벗겨 내세요.
JSON 모드란 무엇인가요?
JSON 모드는 출력 토큰 한도에서 답변이 잘리지 않는 한 모델이 문법적으로 올바른 JSON을 내도록 하는 API 설정입니다. 모델이 여러분의 스키마를 따르게 하지는 않습니다. 키가 빠지거나, 철자가 틀리거나, 타입이 틀릴 수 있습니다. 일부 제공사는 전체 JSON Schema를 받아 출력을 거기에 맞추는 더 엄격한 모드도 제공합니다.
LLM이 항상 올바른 JSON을 돌려줄 수 있나요?
프롬프트만으로는 안 됩니다. 명확한 지시도 가끔 실패하고, 출력 토큰 한도에서 잘린 답은 항상 올바르지 않습니다. 모든 응답을 실제 JSON 파서로 파싱하고, 필요한 필드를 확인하고, 확인에 실패하면 다시 시도하거나 오류를 분명히 드러내세요.