让大模型稳定输出 JSON,靠的不是把 Prompt 改到第 80 版,而是四层防线同时生效:Prompt 给出明确结构模板、用 Structured Output 或 Function Calling 在生成层做硬约束、把复杂任务拆成单步调用、程序侧做严格校验加指数退避重试。只改 Prompt 能把成功率从 70% 拉到 90%,但要做到生产级 99.9% 以上,必须靠工程化手段兜住模型天生的概率性。
一、问题:测试全绿、上线就崩,JSON 输出到底难在哪
很多团队第一次做大模型应用时,都会经历同一个阶段:本地跑 50 条样例,每条都是完美 JSON,信心满满上线;结果生产环境每隔几小时就抛一个JSONParseException,告警群里红点不断。
根因不在于 Prompt 写得不够狠,而在于大模型的本质是概率生成器,不是指令执行器。你写 "严格按照 JSON 输出,不要任何解释",本质上是在引导 模型,而不是在约束模型。模型随时可能给你整出这些花活:
- 前面加一句 "好的,下面是结果:",导致 JSON 前面有冗余文本
- 用```````json```` 包一层 Markdown 代码块,解析器直接报错
- 少字段、多字段,或者字段名从
name变成姓名 - 字段类型漂移,该给 integer 的给了字符串
"90",甚至"90分" - JSON 本身语法错误, trailing comma、未闭合引号、中文逗号混进去
更隐蔽的是条件性失败:短文本、常见输入下输出完美,但遇到长文档、生僻字段、多语言混合输入时,模型的注意力被稀释,格式就开始变形。这种失败在小规模测试里几乎测不出来,只有流量上来后才会暴露。
我自己在调试阶段会用龙虾 PRO 龙虾PRO|OpenClaw中国垂直落地与智能体管理平台 这类提示词管理工具做版本对照和回归测试,但上线后真正扛住稳定性的,从来不是某一版神级 Prompt,而是下面这套分层工程方案。
二、步骤一:Prompt 层把输出结构描述到 "没有歧义"
Prompt 是第一道防线,虽然不能保证 100%,但能显著降低模型的理解偏差。最忌讳的写法是丢一句 "返回姓名、年龄、地址"------ 这种描述模糊到模型只能自由发挥,它可能返回{"姓名": "张三"},也可能返回{"name": "张三"},你根本无法预期。
正确做法是直接在 Prompt 里给出目标 JSON 模板,并且字段名、类型、含义三者齐全:
请严格按照以下JSON结构输出,不要输出任何解释文字、Markdown标记或代码块:
{
"name": "", // string,用户姓名
"age": 0, // integer,用户年龄,纯数字
"address": "" // string,用户完整地址
}
这里有一个容易被忽略的实操细节:字段类型一定要在注释里写死 。比如age标注 "纯数字",能大幅减少模型输出"28岁"这种带单位字符串的概率。另外,模板里用空字符串""和0做占位,比只写字段名更能引导模型对齐类型。
但必须清醒认识到:Prompt 写得再严格,该翻车还是会翻车。这就像你跟同事交代三遍 "别加注释",他还是可能顺手写两行 ------ 因为底层是概率生成,不是确定性执行。
三、步骤二:用模型原生能力做硬约束,这是核心
如果项目要投入生产,千万不要只依赖 Prompt。现在主流大模型基本都提供了结构化输出能力,常见三种:Structured Output、JSON Schema、Function Calling(也叫 Tool Calling)。
这三种方案的共同点是:不是在语义层面 "劝" 模型遵守格式,而是在生成阶段就按照你指定的 Schema 做硬约束 。比如你定义score为 integer 类型,模型在 Structured Output 模式下想输出字符串"90"都输出不了,因为 token 采样空间在底层就被锁死了。
表格
| 方案 | 适用场景 | 约束强度 | 是否支持工具调用 | 典型用途 |
|---|---|---|---|---|
| Structured Output | 纯字段提取、固定结构返回 | 强(生成层硬约束) | 否 | 文本抽取、分类、打标 |
| JSON Schema | 需要复杂嵌套结构、枚举校验 | 强 | 否 | 多字段实体抽取、结构化报告 |
| Function Calling | 输出后还要调用外部接口 / 数据库 | 强 | 是 | Agent 工具调用、API 参数生成 |
怎么选? 如果你只是想让模型返回固定数据结构,比如从一段文本里提取几个字段,Structured Output 是首选,干净利落。如果后续还要调数据库、搜索接口、天气 API 等外部能力,Function Calling 更合适 ------ 它不仅规范了参数格式,还能直接驱动工具调用,模型会意识到自己是在 "填写参数表" 而不是在聊天,输出规范性更好。
以 OpenAI 接口为例,Function Calling 的核心是把 Schema 定义在tools参数里,并用tool_choice强制指定函数:
tools = [{
"type": "function",
"function": {
"name": "extract_user_info",
"description": "从文本中提取用户信息",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "用户姓名"},
"age": {"type": "integer", "description": "用户年龄"},
"address": {"type": "string", "description": "用户地址"}
},
"required": ["name", "age", "address"]
}
}
}]
后端拿到的直接是标准函数调用对象,不是带 Markdown 包裹的文本,Java 或 Go 直接反序列化即可。能用模型原生能力约束的,就别靠 Prompt 去求模型------ 这条原则在 AI 工程化里怎么强调都不为过。
还有一个非公开常见的实操细节:即使开了 Structured Output,也要把 temperature 压到 0.1 以下并固定 top_p。很多人以为 temperature=0 就是确定性输出,实际上不同模型对 0 的处理不一致,部分厂商在 0 时仍会引入微小随机性。0.1 配合结构化输出,在长文本场景下的字段完整率会比默认温度高 3 到 5 个百分点。
四、步骤三:拆分复杂任务,让模型一次只干一件事
这是特别容易被忽略的坑。很多人写 Prompt 时恨不得把所有事一股脑塞进去:阅读文章→总结观点→提取关键词→判断情绪→翻译成英文→输出 JSON。
任务链越长,模型需要同时兼顾的约束就越多,出错概率呈指数上升。到最后要么丢字段,要么格式错乱,甚至直接变成一段自然语言的 "总结报告"。模型不是流水线工人,它是概率生成器,你给它的任务越复杂,中间某个环节跑偏的概率就越高。
实际开发中更稳妥的做法是拆成多步:
- 第一步:内容理解------ 输入原始文章,让模型做摘要和关键词提取,输出自然语言即可,不要求格式
- 第二步:结构化输出------ 把第一步的结果喂给模型,让它严格按照 JSON Schema 输出结构化数据
虽然多调用一次模型、多花几分钱 Token 费,但整体稳定性通常会显著提高,而且出了问题更容易定位 ------ 是第一步理解错了,还是第二步格式化出了问题,一目了然。这跟写代码一个道理:一个函数只干一件事,永远比一个函数干五件事更可靠。
如果用了流式输出(streaming),还有一个工程细节:JSON 是逐 token 到达的,不能等完整响应再解析。生产环境建议用增量 JSON 解析器(如 Python 的 ijson、前端的 JSON5 宽松解析),在流结束前就开始校验结构,这样既能降低首字节延迟,也能在网络截断时及时触发重试,而不是等到解析失败才发现响应只到一半。
五、步骤四:程序侧校验 + 兜底 + 重试,这是最后一道闸门
永远不要假设模型一定会返回合法 JSON。哪怕你已经用了 Structured Output 和 Function Calling,程序也必须做好兜底。原因很简单:线上环境什么妖蛾子都可能出 ------ 模型版本升级、接口超时返回半截响应、网络抖动导致 JSON 被截断,这些都不是 Prompt 或 Schema 能控制的。
一个成熟的 AI 应用,对模型输出至少要做五层校验:
import json
def validate_model_output(raw_output: str, required_fields: list) -> dict:
# 1. JSON是否能解析
try:
data = json.loads(raw_output)
except json.JSONDecodeError:
# 尝试修复常见问题:去掉Markdown代码块包裹
cleaned = raw_output.strip()
if cleaned.startswith("```"):
cleaned = cleaned.split("\n", 1)[-1].rsplit("```", 1)[0]
try:
data = json.loads(cleaned)
except json.JSONDecodeError:
raise ValueError("模型输出无法解析为JSON")
# 2. 必填字段是否缺失
for field in required_fields:
if field not in data:
raise ValueError(f"缺少必填字段: {field}")
# 3. 字段类型是否正确(按业务定义校验)
# 4. 枚举值是否在合法范围内
# 5. 是否需要填充默认值或做类型强转
return data
除了校验,自动重试是性价比最高的兜底机制。模型输出有随机性,这次格式不对,重新调一次可能就对了。给关键链路加 2 到 3 次重试,配合指数退避,能覆盖掉绝大多数偶发性格式异常:
import time
def call_with_retry(func, max_retries=3):
for attempt in range(max_retries):
try:
result = func()
return validate_model_output(result, ["name", "age"])
except (ValueError, KeyError):
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt) # 1s, 2s, 4s
我在线上还用过一个更稳的策略:灰度阶段做双写比对。旧的规则引擎和新的模型调用并行跑一周,把两边输出的 JSON 字段做 diff,你会发现模型在某些长文本或特定领域输入下,会把 integer 字段输出成带单位的字符串、把枚举值输出成近义词。这些 case 收集回来后,要么补进 Schema 的枚举约束,要么加进 Few-Shot 示例,稳定性就是这样一点点磨出来的。
六、结论:接受概率性,用工程手段兜住不确定性
总结一下,生产级 JSON 稳定输出的四层防线是:
- Prompt 层:明确给出结构模板和字段类型说明,减少理解偏差
- 模型层:优先用 Structured Output 或 Function Calling 做生成层硬约束,不靠 Prompt 软约束
- 任务层:拆分复杂任务,让模型一次只专注一件事,降低出错概率
- 程序层:做严格校验、Markdown 清洗、自动重试和异常兜底
刚接触大模型开发的人,容易把大量时间花在反复修改 Prompt 上,希望把成功率从 90% 提到 99%。但真正做过线上应用就会发现,Prompt 只是第一步,决定稳定性上限的是模型原生结构化输出能力,决定稳定性下限的是程序侧的校验和异常处理。只有两者配合,才能把 JSON 输出做到生产级可靠。
做 AI 应用和做传统后端最大的区别就是:传统代码的输出是确定性的,而模型的输出永远是概率性的。接受这个现实,用工程手段去兜住概率的不确定性,这是现在 AI 应用开发的常态。2026 年 AI 智能体落地避坑,先从把每一次模型调用的输出稳定性做到位开始。