API & SDK가이드
LLM API에서 유효한 JSON 얻기: response_format과 재시도
파싱 가능한 JSON을 반환하는 복사 가능한 Python 요청, 비었거나 잘린 답변을 처리하는 json.loads 래퍼, 그리고 JSON이 깨지는 이유 체크리스트.

이 페이지의 내용
챗 모델에서 JSON을 얻으려면 세 가지를 하세요: response_를 전달하고, 프롬프트에 "json"이라는 단어와 구조 예시를 넣고, 전체 객체에 충분하도록 max_tokens를 높게 설정하세요. 그런 다음 답변을 맹신하지 마세요: 비어 있지 않은지 확인하고, finish_reason를 확인한 다음에야 json.loads를 호출하세요.
이게 짧은 버전입니다. 이 가이드의 나머지는 아주 흔한 작업을 위한 하나의 완전한 스크립트입니다: 이메일에서 필드를 추출하는 것. 예제는 base URL https:/와 모델 id deepseek-를 사용합니다 (docs).
작업: 이메일 하나 들어가고, 행 하나 나오기#
스크립트가 지원 이메일을 읽고 각각을 레코드로 바꿔야 한다고 합시다: 발신자, 날짜, 금액, 그리고 발신자가 원하는 것. 이 이메일을 보세요:
안녕하세요, Lumen Ltd의 Anna Petrova입니다. 10월 14일에 인보이스 #3381로 1,250 EUR를 지불했는데, 영수증에는 여전히 미납으로 표시됩니다. 금요일 전에 고쳐주세요.
이것만 돌려받고 싶습니다, 다른 건 없이:
{"sender": "Anna Petrova", "date": "2026-10-14", "amount": 1250, "currency": "EUR", "action": "fix receipt status"}단순한 "필드를 추출하라"는 프롬프트는 친절한 문장, 코드 펜스, 또는 둘 다를 줄 수 있습니다. 여러분의 json.loads는 첫 백틱에서 막힙니다. 이것이 response_format와 좋은 프롬프트가 함께 해결하는 문제입니다.
Python과 curl로 같은 요청
Python
import json
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.cheaptokens.dev/v1",
api_key=os.environ["CHEAPTOKENS_API_KEY"],
)
MODEL = "deepseek-ai/DeepSeek-V4-Flash-0731"
SYSTEM = """Extract fields from the email and answer with json only.
Example of the json format:
{"sender": "Jane Roe", "date": "2026-01-31", "amount": 99.5, "currency": "USD", "action": "short phrase"}
Use null for a field that is not in the email."""
resp = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": email_text},
],
response_format={"type": "json_object"},
max_tokens=1000,
temperature=0,
)
print(resp.choices[0].message.content)curl
curl https://api.cheaptokens.dev/v1/chat/completions \
-H "Authorization: Bearer $CHEAPTOKENS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-ai/DeepSeek-V4-Flash-0731",
"messages": [
{"role": "system", "content": "Extract fields from the email and answer with json only. Example: {\"sender\": \"Jane Roe\", \"amount\": 99.5}"},
{"role": "user", "content": "Hi, this is Anna Petrova from Lumen Ltd. I paid invoice #3381 for 1,250 EUR..."}
],
"response_format": {"type": "json_object"},
"max_tokens": 1000
}'JSON Output에 대한 DeepSeek API 레퍼런스(2026년 10월 27일 확인; 페이지에 게시 날짜 없음)는 이 요청이 따르는 규칙을 명시합니다: response_format를 json_object로 설정, 시스템 또는 사용자 프롬프트 어딘가에 "json"이라는 단어, 그리고 대상 구조의 예시. 예시가 없으면 모델이 여러분의 필드 이름을 추측해야 하고, 다음 이메일에서는 다르게 추측할 것입니다.
왜 플래그보다 예시가 더 중요한가#
response_format는 API에 JSON 객체를 반환하라고 요청합니다. 모델에게 어떤 객체인지는 말하지 않습니다. 어떤 열인지 말하지 않고 동료에게 "스프레드시트 보내줘"라고 하는 것과 같다고 생각하세요. 스프레드시트는 받지만, 여러분의 것은 아닙니다.
프롬프트의 예시는 열 헤더 행입니다. 두 가지 습관이 더 잘 작동하게 만듭니다:
- 타입을 포함한 실제 형태를 보여주세요:
1250는 숫자,"2026-10-14"는 고정된 날짜 형식의 문자열입니다. - 필드가 없을 때 어떻게 할지 말하세요 (위의
null). 그렇지 않으면 모델이 빈틈을 채우기 위해 값을 지어냅니다.
response_format를 보장이 아닌 강한 힌트로 취급하세요. 모델은 때때로 여전히 빈 문자열을 반환하거나 객체를 산문으로 감쌉니다. 그래서 다음 부분이 존재합니다.
안전하게 파싱하기: 비었거나, 잘렸거나, 그냥 깨졌거나#
실무에서 세 가지 실패가 나타나고, 각각 다른 흔적을 남깁니다:
- 빈 콘텐츠. DeepSeek 문서는 JSON 모드가 때때로 빈 콘텐츠를 반환할 수 있으며 프롬프트를 다시 쓰라고 제안한다고 말합니다. 여러분 쪽에서는
message.content가""또는None이고,json.loads("")가Expecting value: line 1 column 1 (char 0)를 발생시키는 것으로 보입니다. - 잘림.
max_tokens가 객체 중간에 소진되었습니다.finish_reason는length이고, 텍스트는 문자열 중간에서 끝납니다. 이에 대한 자세한 내용은 AI 답변이 문장 중간에 멈추는 이유를 참조하세요. - JSON이 아님. 객체 앞의 문장, 또는 객체를 감싼 펜스.

첫 번째와 세 번째에 대한 해결책은 더 강한 프롬프트로 재시도하는 것입니다. 두 번째의 경우, 같은 한도로 재시도해도 아무것도 바뀌지 않으므로, 래퍼가 예외를 발생시키고 max_tokens를 높이라고 알려줍니다:
def ask_json(email_text: str, retries: int = 1) -> dict:
user = email_text
for attempt in range(retries + 1):
resp = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": user},
],
response_format={"type": "json_object"},
max_tokens=1000,
temperature=0,
)
choice = resp.choices[0]
if choice.finish_reason == "length":
raise ValueError("JSON cut off by max_tokens: raise the limit")
text = (choice.message.content or "").strip()
if text:
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# empty or not JSON: ask again, more firmly
user = email_text + "\n\nReply with one json object only, in the format of the example."
raise ValueError("no valid JSON after retry")스크립트에는 한 번의 재시도로 충분합니다. 두 번 실패하면 이메일 id를 기록하고 넘어가세요; 세 번째 동일한 요청은 거의 도움이 되지 않습니다.
짝 맞추기
어떤 증상이 어떤 원인을 가리키나요?
왼쪽 항목을 탭한 다음 짝을 탭하세요
- finish_reason이 "length"max_tokens가 객체에 비해 너무 작음
- content가 빈 문자열JSON 모드가 아무것도 반환하지 않음; 프롬프트를 다시 쓰고 재시도
- 백틱 문자에서 JSONDecodeError모델이 JSON을 코드 펜스로 감쌌음
- 이메일마다 필드 이름이 다름프롬프트에 구조 예시가 없음
구문뿐 아니라 형태도 확인하세요#
json.loads는 텍스트가 유효한 JSON임을 증명할 뿐입니다. 요청한 필드가 있는지는 증명하지 않습니다. 파싱 직후에 저렴한 검사를 추가하세요:
REQUIRED = {"sender", "date", "amount", "currency", "action"}
data = ask_json(email_text)
missing = REQUIRED - data.keys()
if missing:
raise ValueError(f"missing fields: {sorted(missing)}")여기서부터 dict는 데이터베이스 행이나 스프레드시트로 바로 들어갑니다. 고정된 레코드를 채우는 대신 모델이 여러분의 함수를 호출하게 하려면, 그것은 다른 도구입니다: Python에서의 함수 호출을 참조하세요.

이메일 배치의 비용#
짧은 추출은 저렴한 종류의 요청입니다: 입력 몇백 토큰, 출력 몇십 토큰. 하루 1,000개의 이메일이 약 400 입력 토큰(프롬프트, 예시, 이메일)과 약 80 출력 토큰(JSON)이라고 가정합시다. DeepSeek-V4-Flash에서는 하루 $0.014이고, 표는 다른 모델에서의 같은 부하를 보여줍니다:
| 모델 | 입력 $/1M | 출력 $/1M | 요청당 | 1,000회 요청 | OpenRouter, 1,000회 요청 |
|---|---|---|---|---|---|
| GLM-5.3-Flash | $0.02 | $0.04 | $0.000011 | $0.0112 | $0.0226 |
| DeepSeek-V4-Flash | $0.015 | $0.10 | $0.000014 | $0.014 | $0.0315 |
| MiniMax-M2.7 | $0.05 | $0.15 | $0.000032 | $0.032 | $0.216 |
요청당 입력 400개 및 출력 80개 토큰입니다. 100만 토큰당 가격은 현재 기준이며 당사 요금제에 따라 함께 변경됩니다. OpenRouter 열은 동일한 작업량에 대한 참고 가격을 보여줍니다.
입력과 출력을 합쳐서 총계로 보면, DeepSeek-V4-Flash가 이 부하에 대해 OpenRouter의 참조 가격보다 저렴합니다. 또한 답변당 최대 32,768 출력 토큰을 허용하므로, 긴 항목 목록이 작은 한도에서처럼 잘리지 않으며, 가격은 어느 시간에나 동일합니다. GLM-5.3-Flash (zai-)는 같은 API 주소에서 또 다른 옵션입니다: 최대 8,192 출력 토큰으로, 이메일당 레코드 하나에는 충분합니다. 현재 가격은 가격 페이지에 있습니다; DeepSeek 모델 페이지에 여기서 사용된 모델의 세부 정보가 있습니다.
빠른 확인#
빠른 확인
JSON이 문자열 중간에서 잘렸습니다. 무엇을 먼저 보나요?
정답 보기
정답: finish_reason과 max_tokens
잘린 객체는 finish_reason이 length로 설정되어 끝납니다. 한도에 도달했으므로 max_tokens를 높이세요; temperature나 주소를 바꾸는 것은 도움이 되지 않습니다.
아직 요청을 보내지 않았다면, 첫 API 요청으로 시작한 다음, 돌아와서 위의 코드로 바꾸세요.



