Claude API 怎么稳定返回 JSON?结构化输出、Tool Use 和兜底方案讲清楚
在业务系统里接入 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; - 从响应中提取
text或tool_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.parse 或 json.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_useblock 的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 | messages、tools、tool_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、解析校验、失败重试和可观测性一起用起来。
更多推荐



所有评论(0)