在业务系统里接入 Claude API 做 JSON 输出时,真正麻烦的地方通常不是“Claude 会不会返回 JSON”,而是它返回的内容能不能做到:稳定、可解析、能校验,出错之后还能恢复

先把结论放前面:

生产环境里,千万不要只写一句“请返回 JSON”就完事。更稳的做法是优先使用 JSON Schema 级别的结构化输出,或者 strict tool use;如果当前平台不支持,再考虑普通 tool use;实在只能拿文本输出时,才用 Prompt + Prefill + JSON.parse + Schema 校验 + 重试这一套兜底方案。


在这里插入图片描述

先说推荐方案:Claude API 稳定返回 JSON 应该怎么选

使用场景 推荐方案 稳定性 适合场景
平台支持结构化输出 JSON Schema / Structured Output 最高 生产系统、数据抽取、批处理
需要工具调用或参数抽取 Tool Use / Strict Tool Use Agent、函数调用、分类抽取
只能返回文本 Prompt + Prefill + Validate + Retry 兼容旧接口、临时迁移
Demo 或低风险脚本 只提示“返回 JSON” 内部测试、一次性任务

一个相对靠谱的 Claude API JSON 输出链路,通常不只是“调用一下模型”这么简单,而是要包含这些环节:

  • 先定义清楚 Schema;
  • 调用 Claude;
  • 检查 stop_reason
  • 从响应中提取 texttool_use.input
  • 执行 JSON.parse
  • 再用 Zod、Pydantic、Ajv 这类工具做结构校验;
  • 如果失败,把错误信息带回去重试;
  • 记录原始响应和解析错误;
  • 最后还要监控 JSON 解析失败率。

也就是说,稳定性不是靠一句提示词保证的,而是靠一整套工程链路兜住的。


为什么明明要求 Claude 返回 JSON,它还是会出错?

原因其实很简单:Claude 默认是自然语言生成模型,不是严格意义上的 JSON 序列化器。即使你在 Prompt 里写了“只返回 JSON”,它仍然可能出现各种小问题,比如:

  • 返回 Markdown 代码块,例如 ```json;
  • JSON 前后夹了一段解释文字;
  • 字符串里的换行、引号、反斜杠没有正确转义;
  • 字段名变了,比如 sentiment 被写成了 情绪
  • 数字被输出成字符串;
  • 长数组或者长摘要被 max_tokens 截断,最后少了 }
  • 模型不确定时自己编了字段,而不是按要求返回 null
  • 还有一种情况是请求体本身就不是合法 JSON,直接导致 400 或 deserialize 错误。

这里尤其要分清楚一点:请求体 JSON 错误模型输出 JSON 错误不是同一个问题。


先分清三件事:请求体 JSON、返回文本 JSON、结构化输出

1. 请求体 JSON

这是你发给 Claude API 的 HTTP Body。比如你用 cURL、Node.js、Python 调接口时,传过去的参数必须本身就是合法 JSON。

常见坑包括中文引号、未转义换行、手写字符串拼接、尾逗号等。这些问题还没轮到模型发挥,请求就已经失败了。

2. 返回文本 JSON

这是 Claude 在 content.text 里生成的一段字符串。它可能“看起来像 JSON”,但不代表一定能被 JSON.parsejson.loads 成功解析。

比如下面这段本身是合法 JSON:

{
  "category": "投诉",
  "priority": "高"
}

但如果模型在外面包了一层 Markdown 代码块,或者前面加一句“以下是结果:”,它就不再是纯 JSON 字符串了。

3. Schema 保证的结构化输出

结构化输出指的是 API 或平台根据 JSON Schema 来约束模型输出。相比单纯写 Prompt,它明显更稳定。

不过也别误会,Schema 并不代表万无一失。你仍然要考虑模型版本、平台参数、输出被截断、安全拒答、SDK 兼容性等问题。


方案一:用结构化输出 / JSON Schema,让 Claude 按结构返回 JSON

如果你使用的平台支持 Claude 结构化输出,那么优先用 JSON Schema。它的核心价值在于,把“请返回这些字段”从一句自然语言要求,提升成了明确的结构约束。

比如做中文客服工单分类,可以设计成下面这种 Schema:

{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "category": {
      "type": "string",
      "enum": ["售后", "物流", "退款", "投诉", "其他"],
      "description": "工单分类"
    },
    "priority": {
      "type": "string",
      "enum": ["低", "中", "高"],
      "description": "处理优先级"
    },
    "summary": {
      "type": "string",
      "description": "不超过50字的中文摘要"
    },
    "confidence": {
      "type": "number",
      "description": "0到1之间的置信度"
    },
    "reason": {
      "type": ["string", "null"],
      "description": "无法判断时返回null"
    }
  },
  "required": ["category", "priority", "summary", "confidence", "reason"]
}

这里有几个细节很重要:

  • additionalProperties: false 可以减少模型随手加字段的情况;
  • 分类、优先级这类固定范围字段,尽量用 enum
  • 不确定的字段,最好允许返回 null
  • 如果有数组,要把 items 的结构也写清楚;
  • 尽量不要让模型生成动态 key;
  • Schema 不要设计得太深,否则失败概率和成本都会上升;
  • 即使用了 Schema,业务层最好还是再校验一次。

另外,不同平台对结构化输出的参数支持并不完全一致。Anthropic 官方 API、AWS Bedrock、Google Cloud、Deno 示例、第三方兼容服务,写法都有可能不一样。实际接入前,一定要确认当前模型、SDK、endpoint 和文档版本。


方案二:用 Tool Use / Strict Tool Use 获取结构化参数

如果你的任务本质上是在“抽取参数”或者“调用函数”,那 Tool Use 往往比直接让 Claude 输出一段文本 JSON 更稳。

两者最大的区别是:

  • 普通文本 JSON:你要从 content.text 里解析字符串;
  • Tool Use:你直接从 tool_use block 的 input 里拿结构化参数。

示例大致可以这样写:

const response = await client.messages.create({
  model: "claude-xxx",
  max_tokens: 1000,
  tools: [
    {
      name: "classify_ticket",
      description: "分类中文客服工单",
      input_schema: {
        type: "object",
        additionalProperties: false,
        properties: {
          category: { type: "string", enum: ["售后", "物流", "退款", "投诉", "其他"] },
          priority: { type: "string", enum: ["低", "中", "高"] },
          summary: { type: "string" }
        },
        required: ["category", "priority", "summary"]
      }
    }
  ],
  tool_choice: { type: "tool", name: "classify_ticket" },
  messages: [
    {
      role: "user",
      content: "用户反馈:快递显示签收但我没收到,客服一直没人处理。"
    }
  ]
});

拿结果时,不要再去普通文本里找 JSON。正确做法是遍历 response.content,找到 type === "tool_use" 的 block,然后读取里面的 input

Strict Tool Use 通常会进一步限制工具参数必须符合 Schema。不过具体能严格到什么程度,要看模型、平台和 API 版本。不要把某个平台里的参数原样复制到另一个环境里,否则很容易遇到参数不识别的问题。


方案三:不支持结构化输出时,用 Prompt + Prefill + 校验 + 重试兜底

如果当前环境只能拿到文本输出,那就只能用强 Prompt 来尽量约束模型。但要注意,Prompt 只是第一步,后面必须配合解析、校验和重试。

基础版 Prompt 可以这样写:

请只返回合法 JSON,不要使用 Markdown,不要添加解释。

如果想约束得更强一点,可以写成:

你必须只返回一个可被 JSON.parse 解析的 JSON 对象。
不要使用 Markdown 代码块。
不要输出任何解释文字。
如果某个字段无法判断,使用 null。
字段必须严格使用以下结构:
{
  "category": "售后|物流|退款|投诉|其他",
  "priority": "低|中|高",
  "summary": "中文摘要",
  "confidence": 0.0
}

如果解析失败,可以把错误带回去,让模型修复:

上一次输出不是合法 JSON,解析错误是:
{error}

请在不改变业务含义的前提下,重新输出一个合法 JSON。
只输出 JSON,不要解释。
原始输出如下:
{bad_json}

还可以配合一些参数和技巧:

  • 使用 temperature: 0,减少随机性;
  • 适当提高 max_tokens,避免输出被截断;
  • 使用 assistant prefill,比如让 assistant 从 { 开始;
  • 用 stop sequences 控制多余输出。

但有一点要说清楚:temperature=0 并不能保证 JSON 一定合法,更不能替代 Schema 校验。


JSON Schema 怎么写会更稳?

一个好的 Schema,应该尽量减少模型自由发挥的空间。

比如下面这种就不太推荐:

{
  "type": "object",
  "properties": {
    "result": {
      "type": "object"
    }
  }
}

这个 Schema 太宽泛了,result 里面到底有什么字段,模型可以随便生成。

更好的写法是把字段、类型、枚举和必填项都说清楚:

{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": { "type": "string", "description": "实体名称" },
          "type": { "type": "string", "enum": ["人物", "公司", "地点", "产品"] },
          "confidence": { "type": "number", "description": "0到1之间" }
        },
        "required": ["name", "type", "confidence"]
      }
    }
  },
  "required": ["items"]
}

实践中可以遵循这些原则:

  • 字段名要固定,不要让模型自己发明;
  • 类型要明确,比如 string、number、array;
  • 固定选项尽量用 enum
  • 无法判断时,约定返回 null 或空数组;
  • 尽量避免动态字段名;
  • 金额、日期、评分这类字段,要写清格式;
  • 如果需要解释原因,单独放到 reason 字段;
  • 置信度也单独放到 confidence 字段,方便后续处理。

Node.js / TypeScript:生产级 JSON 解析封装思路

下面这个例子展示的是文本 JSON 的兜底方案。实际项目里,如果你能使用结构化输出或者 tool use,还是应该优先用它们。

import { z } from "zod";

const TicketSchema = z.object({
  category: z.enum(["售后", "物流", "退款", "投诉", "其他"]),
  priority: z.enum(["低", "中", "高"]),
  summary: z.string(),
  confidence: z.number().min(0).max(1)
});

async function callClaudeJson(client: any, userText: string, maxRetry = 2) {
  let lastError = "";
  let lastOutput = "";

  for (let i = 0; i <= maxRetry; i++) {
    const prompt = i === 0
      ? `请只返回合法 JSON,不要 Markdown。分析工单:${userText}`
      : `上一次 JSON 解析失败:${lastError}
原始输出:${lastOutput}
请重新输出合法 JSON,不要解释。`;

    const res = await client.messages.create({
      model: "claude-xxx",
      max_tokens: 800,
      temperature: 0,
      messages: [{ role: "user", content: prompt }]
    });

    if (res.stop_reason === "max_tokens") {
      lastError = "输出被 max_tokens 截断";
      continue;
    }

    const textBlock = res.content.find((b: any) => b.type === "text");
    lastOutput = textBlock?.text?.trim() || "";

    try {
      const data = JSON.parse(lastOutput);
      return TicketSchema.parse(data);
    } catch (err: any) {
      lastError = err.message;
    }
  }

  throw new Error(`Claude JSON 输出解析失败:${lastError}`);
}

在生产环境里,建议额外记录这些信息:

  • 原始请求;
  • 原始响应;
  • stop_reason
  • JSON parse 错误;
  • Schema validation 错误;
  • 重试次数;
  • 模型版本和 SDK 版本。

这些日志后面排查问题会非常有用,尤其是当失败率突然升高时。


Python:用 Pydantic 校验 Claude API 返回的 JSON

import json
from pydantic import BaseModel, Field, ValidationError
from typing import Literal

class Ticket(BaseModel):
    category: Literal["售后", "物流", "退款", "投诉", "其他"]
    priority: Literal["低", "中", "高"]
    summary: str
    confidence: float = Field(ge=0, le=1)

async def call_claude_json(client, user_text: str, max_retry: int = 2) -> Ticket:
    last_error = ""
    last_output = ""

    for i in range(max_retry + 1):
        if i == 0:
            prompt = f"请只返回合法 JSON,不要 Markdown。分析工单:{user_text}"
        else:
            prompt = f"""上一次输出不是合法 JSON:
错误:{last_error}
原始输出:{last_output}
请重新输出合法 JSON,不要解释。"""

        res = await client.messages.create(
            model="claude-xxx",
            max_tokens=800,
            temperature=0,
            messages=[{"role": "user", "content": prompt}]
        )

        if getattr(res, "stop_reason", None) == "max_tokens":
            last_error = "输出被 max_tokens 截断"
            continue

        text = ""
        for block in res.content:
            if block.type == "text":
                text = block.text.strip()
                break

        last_output = text

        try:
            data = json.loads(text)
            return Ticket.model_validate(data)
        except (json.JSONDecodeError, ValidationError) as e:
            last_error = str(e)

    raise RuntimeError(f"Claude JSON 解析失败:{last_error}")

这类封装的重点,其实不是“想办法清洗字符串”。更重要的是把失败变成可观察、可重试、可报警的工程事件。这样一来,线上问题才不会悄悄发生、悄悄吞掉。


常见错误排查表

错误现象 常见原因 修复建议
返回 ```json 代码块 模型习惯使用 Markdown Prompt 明确禁止 Markdown,最好使用 Schema
JSON.parse 报错 多余解释、尾逗号、转义错误 parse 失败后带错误信息重试
400 deserialize 请求体不是合法 JSON 检查 HTTP Body、中文引号、换行转义
末尾少 } max_tokens 不够 增加 max_tokens,或者缩小输出结构
字段缺失 Schema 不够严格 使用 required,并做校验
数字变成字符串 类型约束太弱 用 Schema / Zod / Pydantic 校验
tool_use 没出现 没有强制调用工具 设置 tool_choice
Bedrock 参数被拒绝 endpoint 不支持该参数 核对 Converse / InvokeModel 的支持范围
SDK 示例跑不通 SDK 或 API 版本不匹配 升级 SDK,并查看当前官方文档
中文内容转义异常 手写 JSON 字符串 使用语言内置 JSON 序列化

Anthropic API、AWS Bedrock、Google Cloud 的参数差异

不同调用环境对 Claude API JSON 输出的支持并不完全一样,这一点很容易被忽略。

调用环境 常见方式 注意事项
Anthropic 官方 SDK messagestoolstool_choice、结构化输出相关能力 以当前官方 API 文档为准
AWS Bedrock Converse 可能使用 outputConfig.textFormat 等平台参数 参数名和 Anthropic 原生 API 不一样
AWS Bedrock InvokeModel 可能使用 output_config.format 不同 endpoint 支持范围不同
Google Cloud / Vertex / Agent Platform 可能使用 output_config.format.schema 可能受到组织策略和平台限制
Bubble / no-code 通常通过 Tool Use 配置 不等同于通用代码实现
ClaudeAPI 等第三方兼容接入服务 可能提供兼容接入、多线路选择、中文支持、企业充值、开票、基础技术协助 不是 Anthropic 官方,具体能力以服务商最新说明为准

所以,不要把某篇文档里的 output_config、beta header、strict 参数直接复制到所有环境里。遇到参数不识别时,优先检查下面几个地方:

  • 当前 endpoint 是否支持;
  • 当前模型是否支持;
  • SDK 版本是否匹配;
  • 是否需要特定 header;
  • 是否使用了平台自己的封装参数。

很多时候问题并不是 Claude 不能返回 JSON,而是调用环境和参数写法对不上。


最终建议:生产环境到底该怎么做?

如果你希望在生产环境里让 Claude API 稳定返回 JSON,推荐按这个顺序来选:

第一,优先使用 Claude 结构化输出 / JSON Schema。能在 Schema 层面约束输出,就不要只靠 Prompt。

第二,其次考虑 strict tool use / tool use。这类方式特别适合参数抽取、函数调用、Agent 流程。

第三,如果当前平台暂时不支持这些能力,再用 Prompt + Prefill + Validate + Retry 兜底

另外还有几个底线建议:

  • 不要只依赖“请返回 JSON”;
  • 不要以为 temperature=0 就一定安全;
  • 不要把正则提取 JSON 当成长期方案;
  • 一定要检查 stop_reason,尤其要防止 max_tokens 截断;
  • 必须做 JSON parse、Schema 校验、日志记录和失败率监控。

一句话总结:

Claude API 返回 JSON 的稳定性,不能只靠提示词。真正靠谱的方案,是把 Schema 约束、Tool Use、解析校验、失败重试和可观测性一起用起来。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐