让 Claude 稳定吐 JSON 的 5 个套路

Claude 中文知识站 Lv4

两年前我刚开始在生产环境跑 Claude,第一个痛点就是 JSON 不稳定。

你在 playground 里跑 10 次都好好的,上线之后一天跑一万次,总有几十次它给你塞一句”Here is the JSON you requested:”开头,或者最后多个 markdown 代码块标记,或者引号没闭合,或者塞了个注释(JSON 是不允许注释的)。

那段时间我下游 parser 的 try/except 里能喂饱一个告警群。

后来慢慢摸出来一套组合拳,现在我项目里 JSON 出错率能压到 0.5% 以下,而且剩下那 0.5% 也能被兜底解析救回来。这篇把所有套路说清楚。

先来组真实数字

我拿一个抽取 10 个字段的任务跑 10000 条,不同方案的合规率:

  • **只说 “Return JSON”**:87.3%
  • + XML 标签 + 明确 schema:94.1%
  • + Prefill { 作为起始:97.6%
  • + stop_sequences 卡住结尾:98.2%
  • 上 Tool Use(structured output):99.8%

从 87% 到 99.8%,差的不是一点半点。但是 tool use 也不是每个场景都能用,所以后面的套路都要备着。

套路一:Tool Use 是王道

这是 Anthropic 官方也在推的方案。本质上你定义一个 tool(带 JSON schema),让 Claude”调用”这个 tool 来输出——Claude 在调用 tool 时的参数就是严格遵循 schema 的 JSON。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
tools = [{
"name": "extract_info",
"description": "抽取合同关键信息",
"input_schema": {
"type": "object",
"properties": {
"party_a": {"type": "string"},
"party_b": {"type": "string"},
"amount": {"type": "number"},
"signed_date": {"type": "string", "format": "date"},
},
"required": ["party_a", "party_b"]
}
}]

response = client.messages.create(
model="claude-sonnet-4",
tools=tools,
tool_choice={"type": "tool", "name": "extract_info"},
messages=[{"role": "user", "content": "..."}]
)

tool_choice 强制指定调用哪个 tool——这样模型不会犹豫要不要调,直接调。返回的 tool_use block 里的 input 就是合规 JSON。

这条路的好处:

  • 结构严格,类型会被校验
  • 不会混入自然语言
  • Anthropic 后端做了额外保障

坏处:

  • 有些场景你不想引入 tool 的概念(比如一个单次批处理脚本)
  • 嵌套非常深的 schema 偶尔还是会翻车
  • 想让模型先”解释一下”再输出 JSON 的场景不适合

关于 tool use 和 MCP 的关系、什么时候用哪个,这篇我专门讨论过。

套路二:严格 schema 描述 + XML 封装

不用 tool use 的时候,先把你要的结构写清楚。光说”return JSON”不够,得告诉它是什么样的 JSON。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<output_format>
返回严格的 JSON,结构如下:
{
"party_a": "string, 甲方公司全称",
"party_b": "string, 乙方公司全称",
"amount": "number, 合同金额,单位元,不含逗号",
"signed_date": "string, 签约日期, 格式 YYYY-MM-DD",
"risks": [
{"level": "high|medium|low", "description": "string"}
]
}

不要用 markdown 代码块包裹。不要加任何解释性文字。
直接以 { 开头,以 } 结尾。
</output_format>

几个要点:

  • 每个字段后面跟一句中文说明,比起纯 JSON schema 对 Claude 更友好
  • 枚举值用 high|medium|low 这种竖线语法,模型理解很好
  • 明确说”不要 markdown 代码块”、”不要解释文字”——这两条漏掉的话翻车率立刻上去
  • <output_format> 这种 XML 标签包起来比 markdown 标题稳。Claude 对 XML 的偏好我在这篇里系统讲过

套路三:Prefill 预填起始

这是个非常好使的小 trick。在 API 调用里,你可以在 messages 的最后塞一个 assistant 消息,内容是 {——Claude 会从这个 { 继续往下写,自然就不会再加什么”Here is the JSON:”这种废话了。

1
2
3
4
messages = [
{"role": "user", "content": "..."},
{"role": "assistant", "content": "{"}
]

返回结果会缺少最前面的 {,你自己拼回去就行。这个技巧光加这一条,成功率从 94% 干到 97% 以上。

注意:

  • prefill 只在 API 可用,Claude.ai 界面不行
  • prefill 内容要和你期望的输出”无缝衔接”,有换行有空格都可能引起模型迷惑
  • 如果你用 XML 输出格式,prefill 也可以用,比如填 <result>

套路四:Stop Sequences 卡住结尾

前面解决了开头,还有结尾问题。有时候模型会在 JSON 结束后多输出几行解释文字,虽然 parser 能切掉,但不优雅。

1
2
3
4
response = client.messages.create(
...,
stop_sequences=["\n\n", "```", "}\n\n"]
)

这个要根据你的 JSON 结构定。如果你的 JSON 只有一个顶层对象,} 后面跟两个换行基本能卡住收尾。如果是嵌套多层的复杂对象,你得想清楚停在哪儿。

Stop sequences 不会出现在返回结果里,是个干净的截断。

套路五:兜底解析

就算你前面四招都用上,总还有那么 0.5% 会翻车。真实线上不能让这 0.5% 打穿 pipeline。我现在的兜底层是这么写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def robust_json_parse(text: str):
# 1. 直接解析
try:
return json.loads(text)
except json.JSONDecodeError:
pass

# 2. 剥离 markdown 代码块
if "```" in text:
text = re.sub(r"```(?:json)?\s*", "", text)
text = text.replace("```", "")

# 3. 找到第一个 { 和最后一个 }
start = text.find("{")
end = text.rfind("}")
if start >= 0 and end > start:
text = text[start:end+1]

# 4. 再试
try:
return json.loads(text)
except json.JSONDecodeError:
pass

# 5. 修常见错误:尾逗号、单引号
text = re.sub(r",\s*}", "}", text)
text = re.sub(r",\s*\]", "]", text)
text = text.replace("'", '"')

# 6. 还是不行,重试调用模型
return None # 触发上层重试

这套逻辑能救回大约 90% 的”小翻车”。剩下实在救不回来的,触发重试机制,重跑一次基本就好了。

几个容易踩的细节

转义字符的坑。中文内容里夹英文引号、代码片段里的反斜杠,这些在 JSON 里必须转义。Claude 大多数时候会处理对,但偶尔会漏。我现在的做法是:需要输出代码或者含引号内容时,尽量用 base64 或单独字段存储,避免它在 string 里硬转义。

日期和数字。你在 schema 里说”amount: number”,它有 3% 概率给你一个 "12000" 字符串。解决办法:要么 parser 里做类型归一,要么在 schema 描述里加一句”金额必须是纯数字,不能加引号”。

可选字段。Claude 对”可选”理解有时候偏差。你让它”字段 X 如果没有就不要这个 key”,它有时候会给你填 null 或空字符串。我现在干脆规定:”所有可选字段,如果无值,填 null”——统一了就好处理。

多层嵌套深不要过四层。我跑过一个五层深的 schema,错误率直接飙到 15%。复杂结构建议拍扁成多轮调用,每次处理一层。

我现在的生产标准模板

组合起来长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
SYSTEM = """你是一个结构化数据抽取助手。严格按照指定 JSON schema 输出,
不要添加任何解释文字,不要用 markdown 代码块包裹。"""

USER = f"""
<input>
{content}
</input>

<output_format>
{json_schema_description}
</output_format>

直接输出 JSON,以 {{ 开头。
"""

response = client.messages.create(
model="claude-sonnet-4",
system=SYSTEM,
messages=[
{"role": "user", "content": USER},
{"role": "assistant", "content": "{"}
],
stop_sequences=["\n\n\n"],
max_tokens=2048,
)

raw = "{" + response.content[0].text
result = robust_json_parse(raw)
if result is None:
# 重试一次
...

能用 tool use 的场景我直接上 tool use。不能用的场景这个模板顶得住 99.5%。

最后说一句

JSON 稳定性这事,单个技巧提升都是边际的,组合起来才是降维打击。我见过太多人在一个技巧上死磕——“我加了 prefill 还是不稳啊”。那当然,你没加 schema 描述、没加 stop、没加兜底,单靠 prefill 救不了命。

把这 5 招全部铺上,再留一层重试和兜底,基本能把 JSON 这块的工程风险降到可以不再每天看告警的程度。

对了,如果你项目还没开 prompt caching,赶紧开。JSON schema 这种长期固定的内容进了缓存,省下的钱够你一个月的咖啡钱。这篇里有详细的配置方法。刚开始接触 Claude API 的同学可以先去看看API 快速入门打个底。

延伸阅读

Tool use 和 MCP 怎么选,看 MCP vs Function Calling。让 schema 进缓存省钱,去 Prompt Caching 深度指南。API 入门和基本用法,看 Claude API 快速入门。配合 XML 标签让整套 prompt 更稳,别错过 XML vs Markdown 对比

  • 标题: 让 Claude 稳定吐 JSON 的 5 个套路
  • 作者: Claude 中文知识站
  • 创建于 : 2026-04-18 22:00:00
  • 更新于 : 2026-04-19 09:30:00
  • 链接: https://claude.cocoloop.cn/posts/prompt-output-format-json-schema/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论