API & SDKHướng dẫn
Lấy JSON hợp lệ từ một API LLM: response_format và thử lại
Một yêu cầu Python có thể sao chép trả về JSON có thể phân tích, một wrapper json.loads xử lý câu trả lời rỗng hoặc bị cắt, và một danh sách kiểm tra lý do JSON bị hỏng.

Trên trang này
Để lấy JSON ra từ một mô hình chat, hãy làm ba việc: truyền response_, đặt từ "json" và một ví dụ về cấu trúc trong prompt, và đặt max_tokens đủ cao cho toàn bộ đối tượng. Sau đó đừng bao giờ tin câu trả lời một cách mù quáng: kiểm tra xem nó không rỗng, kiểm tra finish_reason, và chỉ sau đó mới gọi json.loads.
Đó là phiên bản ngắn. Phần còn lại của hướng dẫn này là một script hoàn chỉnh cho một công việc rất phổ biến: trích xuất các trường từ một email. Các ví dụ sử dụng base URL https:/ và model id deepseek- (docs).
Công việc: một email vào, một hàng ra#
Giả sử script của bạn đọc email hỗ trợ và phải chuyển mỗi email thành một bản ghi: người gửi, ngày, số tiền, và người gửi muốn gì. Lấy ví dụ này:
Xin chào, tôi là Anna Petrova từ Lumen Ltd. Vào ngày 14 tháng 10 tôi đã thanh toán hóa đơn #3381 với 1.250 EUR, nhưng biên lai vẫn hiển thị là chưa thanh toán. Vui lòng sửa nó trước thứ Sáu.
Bạn muốn nhận lại điều này, và không gì khác:
{"sender": "Anna Petrova", "date": "2026-10-14", "amount": 1250, "currency": "EUR", "action": "fix receipt status"}Một prompt "trích xuất các trường" đơn giản có thể cho bạn một câu thân thiện, một code fence, hoặc cả hai. json.loads của bạn bị nghẽn ở dấu backtick đầu tiên. Đó là vấn đề mà response_format và một prompt tốt cùng giải quyết.
Cùng một yêu cầu trong Python và 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
}'Tài liệu tham khảo API DeepSeek cho JSON Output (đã kiểm tra ngày 27 tháng 10 năm 2026; trang không có ngày xuất bản) nêu các quy tắc mà yêu cầu này tuân theo: response_format được đặt thành json_object, từ "json" ở đâu đó trong system hoặc user prompt, và một ví dụ về cấu trúc mục tiêu. Nếu không có ví dụ, mô hình phải đoán tên trường của bạn, và nó sẽ đoán khác nhau ở email tiếp theo.
Tại sao ví dụ quan trọng hơn cờ#
response_format yêu cầu API trả về một đối tượng JSON. Nó không cho mô hình biết đối tượng nào. Hãy nghĩ về nó như bảo một đồng nghiệp "gửi cho tôi một bảng tính" mà không nói cột nào. Bạn nhận được một bảng tính, chỉ không phải của bạn.
Ví dụ trong prompt là hàng tiêu đề cột. Hai thói quen làm nó hoạt động tốt hơn:
- Hiển thị hình dạng thực, bao gồm cả kiểu:
1250là một số,"2026-10-14"là một chuỗi ở định dạng ngày cố định. - Nói phải làm gì khi một trường bị thiếu (
nullở trên). Nếu không, mô hình sẽ bịa ra một giá trị để lấp đầy khoảng trống.
Coi response_format như một gợi ý mạnh, không phải một bảo đảm. Các mô hình đôi khi vẫn trả về một chuỗi rỗng, hoặc bọc đối tượng trong văn xuôi. Đó là lý do phần tiếp theo tồn tại.
Phân tích an toàn: rỗng, bị cắt, hoặc chỉ hỏng#
Ba lỗi xuất hiện trong thực tế, và mỗi lỗi để lại một dấu vết khác nhau:
- Nội dung rỗng. Tài liệu DeepSeek nói chế độ JSON đôi khi có thể trả về nội dung rỗng và đề nghị diễn đạt lại prompt. Về phía bạn, nó trông như
message.contentlà""hoặcNone, vàjson.loads("")ném raExpecting value: line 1 column 1 (char 0). - Bị cắt.
max_tokenshết ở giữa đối tượng.finish_reasonlàlength, và văn bản kết thúc giữa chuỗi. Thêm về điều này trong tại sao câu trả lời AI dừng giữa câu. - Không phải JSON. Một câu trước đối tượng, hoặc một fence bao quanh nó.

Cách khắc phục cho lỗi thứ nhất và thứ ba là thử lại với một prompt chắc chắn hơn. Đối với lỗi thứ hai, thử lại với cùng giới hạn không thay đổi gì, vì vậy wrapper ném ra và bảo bạn tăng 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")Một lần thử lại là đủ cho một script. Nếu nó thất bại hai lần, ghi log id email và tiếp tục; một yêu cầu giống hệt lần thứ ba hiếm khi giúp ích.
Ghép cặp
Triệu chứng nào chỉ ra nguyên nhân nào?
Nhấn vào một mục bên trái, rồi đến cặp của nó
- finish_reason là "length"max_tokens quá nhỏ cho đối tượng
- content là một chuỗi rỗngchế độ JSON không trả về gì; diễn đạt lại prompt và thử lại
- JSONDecodeError trên một ký tự là dấu backtickmô hình bọc JSON trong một code fence
- Tên trường khác nhau giữa các emailprompt không có ví dụ về cấu trúc
Kiểm tra hình dạng, không chỉ cú pháp#
json.loads chỉ chứng minh văn bản là JSON hợp lệ. Nó không chứng minh nó có các trường bạn yêu cầu. Thêm một kiểm tra rẻ ngay sau khi phân tích:
REQUIRED = {"sender", "date", "amount", "currency", "action"}
data = ask_json(email_text)
missing = REQUIRED - data.keys()
if missing:
raise ValueError(f"missing fields: {sorted(missing)}")Từ đây dict đi thẳng vào một hàng cơ sở dữ liệu hoặc một bảng tính. Nếu bạn cần mô hình gọi các hàm của riêng bạn thay vì điền vào một bản ghi cố định, đó là một công cụ khác: xem function calling trong Python.

Một lô email tốn bao nhiêu#
Một trích xuất ngắn là loại yêu cầu rẻ: vài trăm token vào, vài chục ra. Giả sử 1.000 email mỗi ngày với khoảng 400 token đầu vào (prompt, ví dụ và email) và khoảng 80 token đầu ra (JSON). Trên DeepSeek-V4-Flash đó là $0.014 mỗi ngày, và bảng cho thấy cùng tải trên các mô hình khác:
| Mô hình | Đầu vào $/1M | Đầu ra $/1M | Mỗi yêu cầu | 1.000 yêu cầu | OpenRouter, 1.000 yêu cầu |
|---|---|---|---|---|---|
| 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 token đầu vào và 80 token đầu ra mỗi yêu cầu. Giá trên 1 triệu token là giá hiện hành và thay đổi cùng với biểu phí của chúng tôi; cột OpenRouter hiển thị giá tham chiếu cho cùng khối lượng công việc.
Tổng cộng, với đầu vào và đầu ra được tính cùng nhau, DeepSeek-V4-Flash rẻ hơn giá tham chiếu của OpenRouter cho tải này. Nó cũng cho phép tối đa 32.768 token đầu ra mỗi câu trả lời, vì vậy một danh sách dài các mục hàng sẽ không bị cắt như khi dùng giới hạn nhỏ, và giá là như nhau vào bất kỳ giờ nào. GLM-5.3-Flash (zai-) là một lựa chọn khác tại cùng địa chỉ API: tối đa 8.192 token đầu ra, đủ cho một bản ghi mỗi email. Giá hiện tại có trên trang giá; trang mô hình DeepSeek có chi tiết cho mô hình được sử dụng ở đây.
Kiểm tra nhanh#
Kiểm tra nhanh
JSON của bạn bị cắt ở giữa một chuỗi. Bạn xem gì đầu tiên?
Hiện đáp án
Trả lời: finish_reason và max_tokens
Một đối tượng bị cắt kết thúc với finish_reason được đặt thành length. Giới hạn đã bị chạm, vì vậy hãy tăng max_tokens; thay đổi nhiệt độ hoặc địa chỉ không giúp ích.
Nếu bạn chưa gửi yêu cầu nào, hãy bắt đầu với yêu cầu API đầu tiên của bạn, sau đó quay lại và thay thế bằng mã ở trên.



