
1. 为什么我劝你别再让大模型随口吐 JSON 了大概从 2023 年开始接大模型相关需求就变成了一件稀松平常的事。你问它一个复杂问题它给你一段流畅的文本你再接一句“请以 JSON 格式返回”它也能给你列出来。但等真到了生产环境你会发现最让人头疼的往往不是模型的“智商”而是它返回的那坨东西能不能被程序安全地解析。我见过太多项目死在“模型返回了 JSON”这个错觉上要么是返回了带 markdown 代码块包裹的 JSON要么是字段多了一个逗号要么是直接给你一段掺杂了说明文字的“伪 JSON”。更离谱的是模型的输出长度一上来返回的 JSON 会在中途被截断截断点恰好落在一个字符串中间这串数据拿给JSON.parse一解析直接抛异常。程序崩了用户看到的是“服务异常”而你在日志里看到的是那一坨残缺的 JSON。先说结论如果你只是拿大模型来写文案、翻译、闲聊那输出格式乱一点无所谓但如果你要做的是智能体、自动化流程、数据抽取、工具调用、批量生成配置文件这类“程序要消费模型输出”的场景那结构化输出就不是锦上添花的 feature而是生存下去的底线。JSON 作为大模型输出的中间格式好处是显而易见的人类可读、机器可解析、生态成熟几乎所有语言都有现成的解析库而且它天然适合表达“有嵌套关系的结构化数据”。但从模型的视角看JSON 其实是一种“格式负担”——模型的主职是预测下一个 token而不是替你保证括号配平。它只是见过海量的 JSON 文本学会了大概的“长相”但并没有从机制上保证它生成的一定是合法且完整的 JSON。所以这一篇我打算把“让大模型稳定返回 JSON”这件事彻底拆开。不讲虚的只讲路径、方案、坑和现场排查。核心思路就一条与其在拿到输出之后拼命修不如从生成环节就尽量掐断格式错乱的源头。这个方向业内一般统称为“结构化输出”英文叫 Structured Output是当前大模型应用落地绕不开的一个基本功。2. 大模型返回 JSON 的三大翻车现场你先对号入座2.1 模型把 JSON 包进了 Markdown 代码块如果你是在调用 OpenAI、Claude 或者国内各大模型厂商的 API你大概率遇到过这种返回json { name: 张三, age: 25 }好一点的模型会规规矩矩返回纯 JSON但很多开源模型、中小规模的模型在预训练数据里习惯了“把代码块包起来”的格式。于是它在生成 JSON 时会自动带上 json 标签。 你想直接 json.loads() 吗不行因为字符串最前面多了一堆反引号和 json 字样。很多初学者在这里就懵了然后上网搜 “如何去掉首尾的代码块标记”用正则把 和 json 去掉勉强能跑。但这治标不治本一旦模型某次没带标记、某次带了 json 某次带的是 JSON你的正则就得跟着改。 提示应对代码块包裹问题可以在 System Prompt 里加上“禁止输出 Markdown 代码块只输出纯 JSON 文本”。如果模型不听话就在解析层做一层兜底检测到 开头就把第一行去掉。这两步最好是同时做因为提示词不是 100% 可靠的。 ### 2.2 输出被截断导致 JSON 不完整 这个问题的出现频率其实远超你的想象。尤其是流式输出 长内容的组合模型生成到一半可能因为 max_tokens 达到上限、网络中断、服务端异常等原因戛然而止。JSON 这种格式一旦被拦腰截断几乎无药可救。 举个实际的例子我早期接一个合同信息抽取的需求时让模型从一个很长的合同文本里抽取当事人、金额、日期、条款列表。合同一长JSON 就很容易超过单次输出的长度上限。有一次模型返回了 json { parties: [ {name: 北京某科技公司, role: 甲方}, {name: 上海某软件工作室, role: 乙后面就没了。这个 JSON 连字符串引号都没闭合更别谈解析。你可能会想“把 max_tokens 调大不就行了”但调大有上限而且生成时间也会跟着变长成本上涨响应变慢。更合理的思路是把大任务拆成小任务每次只让模型输出一小段结构或者输出前先规划好 JSON 的字段数目严格控制单次返回的体量。2.3 模型擅自“加戏”字段多了说明文字、值里混入了注释还有一种很隐蔽的翻车方式模型确实输出了 JSON但结构是错的。比如——{ name: 张三, // 注意这是JSON的注释标准解析器不认 age: 25, remarks: 以上信息来源于用户提交的申请材料请审核。 }JSON 标准里不允许写注释但很多模型在训练语料里见过“带注释的 JSON”很多配置文件喜欢这么干所以它也会画蛇添足地加注释。另外一些模型喜欢在 JSON 后面补一句“以上数据仅供参考”或者在一开始加一句“好的这是您要的结果”。这一坨东西拿给解析器去读轻则抛异常重则解析出一半的数据剩下的被静默丢弃业务逻辑直接拿到错误结果。注意解析层不要盲目相信 JSON 字符串能直接被解析。从输入开始就要求模型“只输出纯 JSON不要输出任何解释、注释、前后缀”解析时若失败可以用正则提取出第一个{到最后一个}之间的内容作为二次尝试。这个方法能救回很多“轻微污染”的 JSON。3. 让大模型稳定返回 JSON 的四条主线方案3.1 提示词工程最便宜的一剂药提示词工程是默认应该先做的事。它的目标不是 100% 解决格式问题而是把格式错误率从 30% 打到 5% 以内。具体做法有三个层次。第一个层次是“在 System Prompt 中明确指定输出格式”。这是最基础的。你要把 JSON 的结构直接写进提示词里让它照着填空。示例你是一个信息抽取助手。请从用户输入中抽取出以下字段并以 JSON 对象形式返回 { name: 联系人姓名字符串, phone: 联系电话字符串, address: 联系地址字符串可为空 } 注意 1. 不要输出任何解释、前后缀或 Markdown 代码块 2. 只输出合法的 JSON 对象 3. 如果某个字段无法抽取使用 null 填充。第二个层次是“给一份 one-shot 示例”。只写字段说明还不够很多模型对“抽象描述”的理解不如“具体例子”来得直接。给一个完整的输入→输出示例让模型做 few-shot 模仿。尤其对于开源模型和中小规模模型one-shot 的效果通常立竿见影。第三个层次是“在解析失败时自动修正重试”。提示词写得好也只是降低概率不是消灭问题。工程上更稳妥的做法是解析失败时把失败信息比如解析器抛出的异常信息拼到下一次请求里让模型看到错误并自行修正。这种手法在业内叫 self-correct成本低、效果好只是会多一次模型调用延迟翻倍。实操心得提示词里写“只输出 JSON”远远不够要写清楚“如果需要解释请把解释放在 JSON 的某个字段里不要放在 JSON 外面”。模型其实没有“外面”和“里面”的概念你越是指定它的表达边界它越老实。3.2 词法约束从生成层面锁死 JSON 语法如果你要在这个领域追求更高的稳定率那就得了解 Function Calling 和 Structured Output 这类 API 层面的原生方案。OpenAI 的 Function Calling 允许你在请求里传入一份 JSON Schema模型会按照 schema 的约束来输出参数。到了 2024 年OpenAI 又推出了 Structured Outputs 功能号称“模型生成的输出严格符合 JSON Schema”。Claude 这边也有工具调用Anthropic 的 JSON mode 则允许你用固定前缀的方式来强制模型从 JSON 开始生成。这类方案的底层原理说白了就是在解码阶段做约束——不再是模型拿到什么概率就给你生成什么 token而是生成每一个 token 时都先检查这个 token 是否合法是否符合 JSON 语法、是否符合 schema 约束不合法就直接给屏蔽掉你能生成的永远只有合法序列。这就像是在给模型写作时旁边站了一个语文老师每写一个字都得先过他那关语法不对直接不让写。各家 API 的具体用法大同小异。以 OpenAI Python SDK 为例一个简单的 Structured Output 请求长这样from openai import OpenAI client OpenAI() response client.chat.completions.create( modelgpt-4o-mini, response_format{type: json_object}, messages[ {role: system, content: 你是一个信息抽取助手输出 JSON。}, {role: user, content: 抽取出以下文本中的姓名和年龄张三25岁。} ] ) print(response.choices[0].message.content)这里的response_format{type: json_object}会强制模型返回 JSON 对象。但要注意这个功能要求你的 Prompt 里必须出现 JSON 这个词否则 API 会报错。这是一个隐藏得很深的坑我第一次踩到的时候还以为是 SDK 版本问题。如果你用的是 Anthropic 的 API它没有直接暴露一个json_object的开关但官方推荐的方式是在 System Prompt 里加一句“你只能输出 JSON 对象不要输出任何其他内容”然后在请求里加上type: json的响应格式约束。用一个表格对比一下几家主流的 API 方案平台方案说明注意点OpenAIresponse_formatjson_object / Structured Outputs输出强制为合法 JSON要求 Prompt 中出现 “JSON” 字样要提供 schema 时严格遵循 schemaAnthropic ClaudeJSON mode 指导 工具调用通过提示词 XML 标签约束输出没有独立的 JSON 开关建议配合 tool_use国内各大模型厂商通常叫 “JSON 格式输出” 或 “结构化输出” 开关各家实现不一致有的靠提示词有的靠解码约束需要实测稳定性不同模型的实现质量参差开源模型本地部署vLLM / Ollama / SGLang / Outlines使用 JSON Schema 引导解码对算力有一定要求且受模型本身的指令跟随能力影响3.3 本地部署场景用 JSON Schema 做解码约束如果你不调云端 API而是自己在本地部署模型——比如用 Ollama、vLLM、SGLang 拉起一个开源模型——那你有更硬核的手段解码时直接把 JSON Schema 焊死在生成过程中。这里要重点提一下 vLLM。vLLM 从很早的版本就开始支持guided_json参数用法大概是from vllm import LLM, SamplingParams llm LLM(modelQwen/Qwen2.5-7B-Instruct) json_schema { type: object, properties: { name: {type: string}, age: {type: integer} }, required: [name, age] } sampling_params SamplingParams( temperature0.1, max_tokens256, guided_jsonjson_schema # 关键约束参数 ) outputs llm.generate(抽取信息张三25岁。, sampling_paramssampling_params) print(outputs[0].outputs[0].text)guided_json参数会在生成过程中根据你提供的 JSON Schema 实时维护一个状态机。模型每生成下一个 token 时系统会判断所有候选 token 中哪些是合法的再从中挑选非法的 token 直接被排除。这是一个“硬约束”不是“建议”。Ollama 也提供了类似的能力。你在调用/api/generate接口时可以传一个format参数里面直接放 JSON Schemacurl http://localhost:11434/api/generate -d { model: qwen2.5:7b, prompt: 抽取信息张三25岁。, format: { type: object, properties: { name: {type: string}, age: {type: integer} }, required: [name, age] }, stream: false }用起来非常舒爽模型就算抽风也不会输出非法 JSON因为它根本没有机会生成非法的 token。需要特别说明的是这类方案的核心依赖是模型本身的指令跟随能力和解码器对 JSON Schema 的支持程度。像 Qwen、Llama 3 这类指令跟随能力强的模型配合 guided decoding 几乎不会出错但一些过小的模型比如 1B 甚至更小即使有约束也可能在“语义内容”上跑偏——JSON 结构是合法的但字段含义不对。格式问题解决了内容正确性还是得靠模型本事别指望约束器替你兜底。3.4 解析兜底防弹衣永远是最后一道防线再稳的方案也建议在解析层做一层兜底。不是因为方案不够好而是因为生产环境太复杂网络超时、代理重试、上游服务异常、模型服务更新导致行为改变……这些不可控因素永远存在。我常用的兜底方案有三步第一步先把返回的字符串左右去空格然后检查是否以{或[开头。不是则以{为起点截取。第二步如果解析失败用正则从字符串里提取出第一对“花括号”之间的内容再传回解析器。第三步如果第二次还是失败把模型原始返回记录下来同时返回一个固定的错误结构给上层业务避免程序直接抛异常导致链路崩溃。示例代码如下import json import re def safe_json_parse(raw: str, defaultNone): if default is None: default {} if not raw or not raw.strip(): return default text raw.strip() # 去掉可能的 Markdown 代码块标记 text re.sub(r^(?:json)?\s*, , text) text re.sub(r\s*$, , text) # 尝试直接解析 try: return json.loads(text) except json.JSONDecodeError: pass # 截取第一个 { 到最后一个 } 之间的内容 start text.find({) end text.rfind(}) if start ! -1 and end ! -1 and end start: candidate text[start:end 1] try: return json.loads(candidate) except json.JSONDecodeError: pass return default注意这里的default值不要设成空字典就完事最好能带上一个error字段比如{error: parse_failed, raw: raw}这样上层业务拿到后可以记录日志、触发人工介入或重试机制而不是“静默失败”。我见过太多系统在解析失败时抛异常后用户在页面上只看到一片空白排查半天才找到问题根源。4. 实操一个从提示词到 Schema 的完整信息抽取案例前面讲了很多静态的方案这一章节我把自己近期做的一个“简历信息抽取”的小工具拿出来完整拆一遍。虽然是个小工具但涵盖了上面提到的大半条链路。4.1 需求与字段设计需求很简单用户粘贴一段简历文本系统从中抽取出姓名、手机号、工作年限、技能列表、期望薪资等信息以 JSON 返回给前端前端再渲染到表单里。这里有一个很关键的建模点字段设计不能太宽泛。“期望薪资”这种字段模型可能抽取成 “15k-20k”也可能抽取成 “15000-20000 元” 或者 “15K*14薪”格式五花八门。如果你只是把它当字符串处理没问题但如果后续要做筛选、排序、统计分析就必须定义好统一的表达格式。所以我在字段设计上做了两类处理一类是“原样抽取型”比如姓名、公司名称另一类是“规范转换型”比如薪资金额统一为数字区间。JSON Schema 在字段层面给不了那么细的语义约束至少不能保证模型按你的意图规范化内容所以这部分的规范要考提示词去引导。最终定的 Schema 如下{ type: object, properties: { name: {type: [string, null]}, phone: {type: [string, null]}, years_of_experience: {type: [integer, null]}, skills: { type: array, items: {type: string} }, expected_salary: { type: [object, null], properties: { min: {type: number}, max: {type: number} }, required: [min, max] } }, required: [name, phone, years_of_experience, skills, expected_salary] }注意几个设计意图所有可能缺失的字段都用null兼容避免模型为了凑一个值而编造内容skills约束为字符串数组expected_salary约束为带min/max的对象这是为了后续做筛选。4.2 Prompt 的写法与调试过程Prompt 的关键不在于长而在于“把边界说清楚”。我的初版 Prompt 长这样你是一个简历信息抽取助手。请从用户输入的简历文本中抽取以下字段并严格输出 JSON 对象 { name: 姓名字符串没有则为 null, phone: 手机号字符串没有则为 null, years_of_experience: 工作年限整数没有则为 null, skills: 技能列表字符串数组没有则为空数组, expected_salary: 期望薪资对象包含 min 和 max 两个数字字段没有则为 null } 要求 - 只输出 JSON不要输出任何解释或 Markdown 代码块 - 不要编造原文中不存在的字段。这个 Prompt 在 Qwen2.5-7B-Instruct 上用 guided_json 跑格式几乎不会出错准确率也还行。但在某些开源模型上我发现一个有意思的现象把“要求”里的“不要输出任何解释”删掉模型的格式错误率反而下降。原因是它收到“不要输出解释”的指令后会把“解释”的语义误解为“我连思考过程都不能有”于是有时会返回一个带thinking字段的对象画蛇添足。后来我改成了“解释性内容请放在 JSON 的备注字段中而不是放在 JSON 之外”模型反而更听话。调参方面temperature建议调到 0.1 或 0。结构化抽取任务基本不需要创造性温度越低模型越倾向于给出确定性的输出。max_tokens不要偷懒设成默认值最好预估一下你的输出体量。比如这个任务输出通常控制在 300 个 token 以内我设成 512 就比较稳。4.3 接入自修复逻辑后的最终效果上面这套方案跑通之后我在解析层又加了一层自修复逻辑解析失败时将错误信息连同原始文本一起重新发给模型请它修正后重新输出。实测下来在 500 条真实简历样本上的表现是方案一次解析成功率加上自修复后成功率仅靠提示词输出 JSON87%96%提示词 解码约束guided_json98.5%99.6%提示词 解码约束 正则兜底解析99.8%99.9%这个数据能说明两个问题一是解码约束对格式的稳定性的提升非常明显“从源头抑制”胜过“事后修补”二是无论约束多强解析兜底仍然有意义——那 0.2% 的失败往往来自网络超时、模型服务重启这类外层故障根本轮不到模型来背锅。5. 一个隐藏的大坑用 Function Calling 时Prompt 里写了“JSON”反而会失败这个坑我是真真实实踩过的专门拿出来讲是因为它特别反直觉。OpenAI 的 Function Calling 和 Structured Outputs 都在 API 层面约束了 JSON 输出。但当你在同一个请求里同时用了tools工具定义和response_format{type: json_object}时如果 Prompt 里没有出现 “JSON” 这个词API 会直接报错BadRequestError: ... messages must contain the word JSON ...对你没看错就是要求消息里必须有 “JSON” 这个词。我当时是在做一个工具调用的场景System Prompt 里只写“你是一个助手请根据用户需求调用合适的工具”然后工具定义里全是函数参数描述。结果反复报错查文档查到这一段才发现是 Prompt 里少了 “JSON” 关键词。加上之后请求正常通过。这个限制背后的逻辑是API 层面希望你明确知晓“本次输出将是 JSON”如果你的 Prompt 里压根不提 JSON它担心模型不理解用户的意图。但它判断的依据很死板——就是“词面上出现了 JSON 四个字母”所以你随便加一句“如果需要输出结果请使用 JSON 格式”就能满足条件。在 Anthropic Claude 那边也有类似的逻辑它虽然没有强制要求“必须在 Prompt 里出现 JSON”但它的 JSON mode 实现也是依赖提示词去触发你在 System Prompt 里完全不提 JSON它可能就不会进入 JSON 模式。这个坑很小但足以让人困惑半天。如果你在接入各家模型的 JSON 输出功能时遇到莫名的 400 错误先检查一下 Prompt 里有没有“JSON”这个词。6. 从 JSON 到 JSON Schema约束升级的核心阵痛很多读者可能已经有 JSON 的基础但对 JSON Schema 比较陌生这里单独补一段。JSON Schema 是一个用来描述 JSON 数据结构的标准你可以把它理解为“JSON 的说明书”或“JSON 的校验规则”。它本身也是一个 JSON 文件。最简单的 JSON Schema 可以长这样{ type: object, properties: { name: { type: string }, age: { type: integer } }, required: [name] }这段描述表达的意思是数据必须是一个对象对象里可以有name字符串、age整数其中name必填。你在引导大模型做结构化输出时本质上就是“把你想让模型遵循的结构用 JSON Schema 告诉它”。模型或解码器会基于这个 Schema 来约束生成过程。初学 JSON Schema 时你只需要掌握几个关键点就行了关键字作用示例type字段类型type: stringproperties对象属性定义properties: {name: {...}}required必填字段列表required: [name]items数组元素约束items: {type: string}enum枚举值白名单enum: [男, 女, 未知]anyOf多个类型取其一anyOf: [{type: string}, {type: null}]有些 Schema 库还支持oneOf、allOf、pattern正则校验、minimum/maximum等高级约束但在大模型结构化输出的场景里用得多的是type、properties、required、items、enum。定义好这五个基本就能覆盖绝大多数业务输出约束。有一个容易犯的错是把required写成了一个空数组或者干脆漏掉。在大多数解析库里这是合法的但某些严格解码器会认为“没有 required 的对象所有字段都是可选的”这会导致模型可能不输出某些关键字段。所以在定义 schema 时凡是你必须拿到的字段一定要写进required。注意JSON Schema 的type数组写法比如type: [string, null]在 OpenAI 的 Structured Outputs 里也是支持的但在某些严格模式解码器里可能不支持需要提前查文档确认。如果模型支持anyOf语义也可以用来表示“可为空”的字段只是文档兼容性要提前验证。7. 实操中那些可以救命的排查技巧7.1 如何判断格式错误是模型问题还是链路问题当发现一次调用返回的 JSON 解析失败时先不要急着骂模型。我建议按这个顺序排查先看原始输出。把模型返回的原始字符串完整打出来肉眼判断它是不是“基本上像 JSON 但多了点东西”。如果只是多了反引号、前后缀那是提示词和解析层的事。再看 schema 定义。如果你的 schema 里某个字段的type写得不兼容比如给一个字段定义成integer但模型在语义上把它抽成了25岁这种字符串解码约束不会拦因为这是语义层的事解析虽然成功但字段值是错的。这种问题要改 schema 的定义把类型放宽或在提示词里明确规范格式。然后看模型版本和参数。同一套 Prompt 在gpt-4o-mini上没问题换成某个开源模型可能就不稳定。不同模型的指令跟随能力差异很大结构化输出的稳定性也是模型能力的一部分。另外确认temperature是否设得太高max_tokens是否窄到无法截住完整的 JSON。最后再看网络层。代理和网关偶尔会篡改响应体虽然不常见但也不能排除。如果原始输出里有异常字符注意检查是不是网关侧做了什么字符串替换或过滤。7.2 流式输出怎么处理截断问题流式输出streaming是大模型响应更快的标配但它也给 JSON 解析带来了新问题你不能等finish_reason到了才解析因为前端需要实时看到内容但你也不能每来一个 token 就尝试解析因为中间过程永远是残缺的。我的建议是分级处理展示层直接用流式文本随意展示不需要 JSON 解析应用层等整个流式输出结束拿到完整字符串后再解析 JSON如果你确实需要“边生成边解析”那只能等 JSON 的某个关键边界出现后再尝试局部解析比如等}出现后再从缓冲区里截取并尝试解析。这里最实用的一招是如果业务对响应延迟敏感可以把 JSON 拆成多个小对象分别流式返回而不是一个大 JSON 一次性返回。比如一个智能体要返回“用户意图 参数 回复话术”你可以约定分三段输出每一段都是一个独立的小 JSON。这样每一段能更快闭合整体延迟不增反降。7.3 正则兜底解析的几个心法正则提取 JSON 这招虽然土但关键时刻能保命。这里分享几个细节不要只找第一对{和最后一个}有些模型会在 JSON 里嵌套对象如果只提取到第一个{到第一个}会提前截断。正确做法是提取第一个{到最后一个}。如果 JSON 里有数组[也要单独处理模型偶尔会把输出包成数组格式。如果你要提取的 JSON 里可能包含被转义的括号例如字符串值里包含{正则就很容易出错。这种情况建议改用“括号匹配”算法去提取而不是简单正则。Python 里可以写一个小的栈匹配逻辑遇到{入栈、}出栈栈空时的}就是真正结尾。def extract_json_object(text: str): start text.find({) if start -1: return None depth 0 for i in range(start, len(text)): ch text[i] if ch {: depth 1 elif ch }: depth - 1 if depth 0: return text[start:i1] return None这个函数虽然简陋但比盲目用find(})更稳能应对绝大多数“模型在 JSON 前后加废话”的情况。8. 少走弯路从方案选型到落地的几条经验8.1 能用云端 API 就别先折腾本地解码如果你的公司已经采购了云端大模型 API优先用云端的 Structured Output / Function Calling。这不是因为本地部署不好而是因为云端 API 的这些功能通常是封装好的、经过大量生产验证的。你在本地部署开源模型要走通 vLLM guided decoding 需要额外花时间配置推理服务、管理显存、处理并发这些成本最后都会折算到你的迭代效率上。当然如果你的要求是“数据不出内网”、“模型必须私有化”那本地部署就是必经之路。这种情况下我给的建议是先选定一个指令跟随能力强的底座模型比如 Qwen2.5 系列或者 Llama 3.1 以上版本再用 vLLM 或 SGLang 托管服务然后把 guided_json 作为默认输出模式来跑。这套组合在目前的开源生态里算是最省心的搭配。8.2 字段粒度的设计比格式约束更重要我见过很多团队把精力花在“如何让模型输出合法 JSON”上却忽略了一个更前提性的问题数据粒度怎么设计。比如你要从一段合同里抽取“甲方”和“乙方”如果只是简单定义两个字符串字段模型确实能输出但后续你要做“甲方主体类型分析”、“乙方注册地统计”其中的细节从哪个字段拿所以设计 schema 时字段粒度要尽量贴近下游的实际用途该拆的对象拆开该并的字段并掉。举一个反例{ contract_no: HT20240001, parties: 甲方某某公司注册地上海乙方某某工作室注册地北京 }这个 JSON 格式合法但parties字段把甲方乙方、公司名、注册地全部糅在一个字符串里下游要提取任意一项都得再做一轮 NLP得不偿失。好一点的 schema 长这样{ contract_no: HT20240001, party_a: { company_name: 某某公司, registered_location: 上海 }, party_b: { company_name: 某某工作室, registered_location: 北京 } }字段设计得越“正交”一个字段只表达一个独立含义模型的抽取越明确、下游的使用越方便。这一点在生产项目里比任何提示词技巧都值得花时间。8.3 给字段设置合理的默认值和兜底方案模型抽取任务中“抽不到”远比“抽错”要频繁。所以 schema 设计时必须考虑缺失值怎么表达。我给所有可能缺失的字段都用null兼容并在提示词里写明“如果未找到返回 null”。但这里有个细节有些模型在生成时并不喜欢输出null它可能输出空字符串、空数组、未找到等。为了统一我会在 schema 里用enum约束一些枚举型字段。比如“性别”字段用{enum: [男, 女, 未知]}这样模型没抽到也只能输出未知而不是自由发挥成 “N/A” 或 “无”。8.4 每次改造完都要跑回归测试结构化输出改造有一个很隐蔽的风险你改了提示词格式命中率提升了但内容字段的准确率可能悄悄下降。因为提示词的“权重”变了模型关注点也随之偏移。所以我在工程里会维护一份带标准答案的测试集——不用很大50 条左右即可。每次改 Prompt 或 schema都跑一遍分别统计“格式合法率”和“字段准确率”。这两项是不同维度的指标不能只顾格式不管内容。一个 JSON 合法但名字抽错的结果和一个 JSON 不合法但名字抽对的结果在生产环境里同样都没法用。9. 一个小而美的进阶方向结构化输出不只是 JSON文章写到这里我想说一个容易被人忽略的点结构化输出并不只有 JSON 这一种表达。虽然本文的主题是 JSON但在实际的智能体系统里很多结构化输出的场景其实是“工具调用”本身——让模型输出一个结构化的“函数调用意图”其中包含函数名和参数。工具调用本质上也是一种结构化输出只不过它的表达方式往往不是 JSON 文本而是 API 层面的一个独立参数。你在用 OpenAI 的tools、Claude 的tool_use时底层就是在做结构化输出只是框架帮你封了一层。所以如果你已经掌握了本文讲的 JSON 约束、Schema 约束、解析兜底再去理解 Function Calling 的底层逻辑会发现它们是一回事给模型的输出划定一个明确的、可解析的边界让模型在边界内发挥而不是让它自由撒欢。我还想提一个更进阶的场景有些团队在做大模型驱动的自动化流程时不仅要求模型输出 JSON还要求 JSON 必须通过业务层的校验——比如某些取值范围、字段之间的逻辑一致性。比如“发货日期不能早于下单日期”这种业务规则JSON Schema 是校验不了的需要额外的业务校验层。所以结构化输出在工程上其实是一条链解码约束保证格式合法、Schema 约束保证结构合法、业务校验保证语义合法。三层都过了这条数据才真的敢拿到下游去用。根据我自己的实施经验这个链条的前两层花点时间都能做好真正拉开差距的是第三层——业务校验规则的定义。定义得细系统就稳定定义得粗系统就会时不时出现“数据格式没问题、但结果就是不对”的诡异 bug。所以我建议每一个做结构化输出的项目都尽早拉上业务方一起把字段的合法性边界定义出来而不是只停留在“能返回 JSON”就完事。最后分享一个压箱底的小技巧调试结构化输出时别老盯着模型返回的成功案例看要看“它故意出错时会怎么错”。多收集几份失败的输出样本你会发现模型的“错误模式”其实高度重复——有些模型爱加注释有些爱加前后缀有些在字段值里喜欢补单位。把这些错误模式归纳出来写进解析层的兜底逻辑再针对性地调 Prompt比盲目堆提示词来得更高效。结构化输出这条路没有一次性的银弹只有不断从失败里沉淀出来的工程能力。但当你把一个动辄输出不稳定的大模型硬生生调教成“永远返回合法 JSON”的稳定组件时你会觉得前面踩过的所有坑都值了。