返回博客

API 与 SDK指南

从 LLM API 获取有效 JSON:response_format 与重试

一个可复制的 Python 请求,返回可解析的 JSON;一个处理空答案或被截断答案的 json.loads 包装器;以及一份 JSON 出错的排查清单。

作者:CheapTokens Team发布于 5 分钟阅读

一个终端,显示带有 response_format json_object 的聊天补全请求
本页内容
  1. 任务:输入一封邮件,输出一行记录
  2. 为什么示例比标志更重要
  3. 安全解析:为空、被截断,或者只是损坏
  4. 检查结构,而不仅仅是语法
  5. 一批邮件的成本是多少
  6. 快速检查

要从聊天模型中获得 JSON,需要做三件事:传入 response_format={"type": "json_object"},在提示词中放入单词 "json" 和一个结构示例,并将 max_tokens 设置得足够高以容纳整个对象。然后永远不要盲目相信答案:检查它是否为空,检查 finish_reason,只有在这之后才调用 json.loads。

这是简短版本。本指南的其余部分是一个完整的脚本,用于一个非常常见的任务:从电子邮件中提取字段。示例使用基础 URL https://api.cheaptokens.dev/v1 和模型 ID deepseek-ai/DeepSeek-V4-Flash-0731(文档)。

任务:输入一封邮件,输出一行记录#

假设你的脚本读取支持邮件,并且必须将每封邮件转换为一条记录:发件人、日期、金额以及发件人的诉求。以这封为例:

你好,我是 Lumen Ltd. 的 Anna Petrova。10 月 14 日我支付了发票 #3381,金额为 1,250 欧元,但收据仍显示未付款。请在周五之前修复。

你希望得到以下内容,且仅此而已:

json
{"sender": "Anna Petrova", "date": "2026-10-14", "amount": 1250, "currency": "EUR", "action": "fix receipt status"}

一个简单的 "提取字段" 提示词可能会给你一句友好的话、一个代码围栏,或两者兼有。你的 json.loads 会在第一个反引号处出错。这就是 response_format 和一个好的提示词共同解决的问题。

Python 和 curl 中的同一请求

Python

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

bash
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
  }'

DeepSeek API 关于 JSON Output 的参考文档(检查于 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。 对象前面有一句话,或者对象周围有围栏。
对空答案调用 json.loads 会引发 JSONDecodeError
Python 中空的 JSON 模式答案是什么样子

第一种和第三种情况的修复方法是使用更严格的提示词重试。对于第二种情况,使用相同限制重试不会有任何改变,因此包装器会抛出异常并告诉你要提高 max_tokens:

python
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。它不能证明它包含你要求的字段。在解析后立即添加一个低成本的检查:

python
REQUIRED = {"sender", "date", "amount", "currency", "action"}

data = ask_json(email_text)
missing = REQUIRED - data.keys()
if missing:
    raise ValueError(f"missing fields: {sorted(missing)}")

从这里开始,字典会直接进入数据库行或电子表格。如果你需要模型调用你自己的函数,而不是填充固定记录,那是另一种工具:参见 Python 中的函数调用。

从邮件到已检查记录的路径:带示例的提示词、json_object、解析、字段检查
五步走完整个路径

一批邮件的成本是多少#

短提取是便宜的那类请求:输入几百个 token,输出几十个。假设每天 1,000 封邮件,每封约 400 个输入 token(提示词、示例和邮件)和约 80 个输出 token(JSON)。在 DeepSeek-V4-Flash 上,每天是 $0.014,表格显示了其他模型上的相同负载:

模型 每次请求 1,000 次请求 OpenRouter,1,000 次请求
GLM-5.3-Flash $0.000011 $0.0112 $0.0226
DeepSeek-V4-Flash $0.000014 $0.014 $0.0315
MiniMax-M2.7 $0.000032 $0.032 $0.216

每次请求 400 个输入 token 和 80 个输出 token。每 100 万 token 的价格为当前价格,会随我们的资费一同变动;OpenRouter 列显示相同工作负载的参考价格。

总的来说,将输入和输出一起计算,对于这个负载,DeepSeek-V4-Flash 比 OpenRouter 的参考价格更便宜。它还允许每次回答最多 32,768 个输出 token,因此一长串行项目不会像小限制那样被截断,而且价格在任何时间都相同。GLM-5.3-Flash(zai-org/GLM-5.3-Flash)是同一 API 地址上的另一个选择:最多 8,192 个输出 token,对于每封邮件一条记录来说绰绰有余。当前价格在定价页面上;DeepSeek 模型页面有此处所用模型的详细信息。

快速检查#

快速检查

你的 JSON 在字符串中间被截断了。你首先看什么?

显示答案

答案: finish_reason 和 max_tokens

被截断的对象以 finish_reason 设置为 length 结束。达到了限制,所以提高 max_tokens;更改温度或地址没有帮助。

如果你还没有发送过请求,请从你的第一个 API 请求开始,然后回来替换上面的代码。