
1. 从“薛定谔的JSON”到结构化输出的刚需如果你也经常调用大模型的API尤其是那些非OpenAI系的模型那你一定经历过这种“薛定谔的JSON”时刻你满怀期待地发送了一个精心设计的Prompt要求模型返回一个结构化的JSON对象结果它要么给你一段夹杂着解释的文本要么JSON格式残缺不全要么干脆给你编造几个不存在的字段。更让人头疼的是这种输出不稳定是随机的同一个Prompt这次可能成功下次就失败调试起来简直让人抓狂。这背后的核心矛盾在于大语言模型LLM本质上是“下一个词预测器”它们被训练来生成符合统计规律的自然语言序列而不是严格遵守语法规则的编程语言输出。让模型稳定输出JSON相当于要求一个习惯了自由发挥的诗人每次都必须按照严格的十四行诗格律来创作。随着我们将大模型集成到更复杂的应用流水线中——比如自动生成数据、构建知识图谱、驱动工作流引擎——这种对结构化输出的需求已经从“锦上添花”变成了“雪中送炭”。一个不稳定的JSON输出足以让下游的数据解析模块崩溃整个自动化流程戛然而止。因此“Structured Outputs”结构化输出成为了Prompt工程和LLM应用开发中的一个关键课题。它不再是简单的“在Prompt里写‘请输出JSON’”而是一套系统工程旨在通过约束、引导和后期处理让模型的输出变得可靠、可预测、可直接被程序消费。今天我们就来深入对比三种主流的、经过实战检验的方案看看它们各自的原理、适用场景和那些官方文档里不会写的“坑”。2. 方案一Prompt工程与上下文约束——最原始也最考验技巧这是最直接、门槛最低的方法完全依赖于你在Prompt中提供的指令和上下文示例。它的核心思想是通过清晰的指令和少样本Few-shot学习在模型的上下文窗口中建立一个强大的“输出格式”约束。2.1 基础指令清晰、具体、无歧义首先指令必须足够强硬和具体。模糊的“请用JSON格式回复”是远远不够的。你需要像给一个粗心的实习生写工作说明一样事无巨细。一个糟糕的指令可能是请分析以下用户评论的情感并输出结果。模型可能回复“该评论表达了积极的情绪。”这完全不是我们想要的。一个合格的指令应该是你是一个情感分析助手。请严格遵循以下要求 1. 输出必须是**一个且仅一个**合法的JSON对象。 2. JSON对象必须包含且仅包含两个字段 - sentiment字符串类型值只能是“positive”、“negative”或“neutral”中的一个。 - confidence浮点数类型范围在0到1之间代表判断的置信度。 3. 不要输出任何额外的文本、解释、Markdown代码块标记或换行符。直接以 { 开始以 } 结束。 用户评论{{user_comment}}这里的关键点在于唯一性强调“一个且仅一个”JSON对象防止模型输出多个或嵌套在其他文本中。字段规范明确定义每个字段的名称、类型、甚至枚举值或取值范围。这极大地减少了模型“瞎编”的可能性。格式纯净明确禁止任何“画蛇添足”的内容要求输出是“纯净”的JSON字符串。2.2 少样本示例用例子“教”会模型对于复杂的嵌套结构仅靠文字指令可能力不从心。这时就需要引入少样本示例。这是Few-shot Learning在Prompt中的直接应用。你需要在Prompt中提供一到多个完整的输入-输出对让模型通过类比来学习。假设我们需要一个更复杂的输出包含实体列表{ instruction: 提取以下句子中的人名和公司名。, input: 苹果公司的蒂姆·库克与特斯拉的埃隆·马斯克进行了会面。, output: { entities: [ {name: 蒂姆·库克, type: PERSON}, {name: 埃隆·马斯克, type: PERSON}, {name: 苹果公司, type: ORGANIZATION}, {name: 特斯拉, type: ORGANIZATION} ] } }在你的系统指令或用户消息中先展示这个例子然后再给出你的实际查询。模型会倾向于模仿示例中的输入-输出映射关系和严格的JSON格式。注意少样本示例会消耗宝贵的上下文窗口Token。你需要权衡示例的复杂性和数量。通常1-3个高质量、覆盖边界的示例比5-6个普通示例更有效。2.3 实战心得与常见“坑”这个方法看似简单但在实战中陷阱不少。心得1位置很重要。指令放在哪里对于大多数模型系统指令System Prompt的权重高于用户指令。因此将核心的结构化输出要求放在System Prompt中效果通常比放在用户消息里更稳定。对于不支持System Prompt的模型则需要在第一条用户消息中明确提出。心得2利用“角色扮演”强化约束。给模型定义一个强约束性的角色比如“你是一个严格的JSON格式化器你的唯一功能是将输入信息转换为指定格式的JSON不做任何额外处理。”这能在心理层面虽然模型没有心理加强其遵循格式的倾向。心得3后处理是必须的而非可选的。无论你的Prompt写得多完美永远不要100%信任模型的原始输出。在你的代码中必须用try...except包裹JSON解析逻辑。import json def parse_model_output(raw_output): # 首先尝试直接解析 try: return json.loads(raw_output) except json.JSONDecodeError: # 如果失败尝试一些启发式清理 # 1. 去除可能存在的markdown代码块标记 cleaned raw_output.strip().strip().strip() # 2. 查找第一个{和最后一个} start cleaned.find({) end cleaned.rfind(}) 1 if start ! -1 and end ! 0: json_str cleaned[start:end] try: return json.loads(json_str) except json.JSONDecodeError: pass # 所有尝试都失败返回错误或默认值 return {error: Failed to parse JSON, raw_output: raw_output[:100]}这个后处理函数能应对模型在JSON外包裹了json或\n等常见情况。最常见的“坑”尾随逗号模型可能在JSON对象的最后一个字段后加上逗号这在标准JSON中是非法的。后处理时需要处理或使用json5这类更宽松的解析库。字符串转义如果模型输出的字符串值内包含未转义的双引号或换行符会导致解析失败。这在处理模型生成的文本内容时尤其常见。数值类型混淆模型可能将数字输出为字符串如score: 0.95如果你的下游代码期待的是浮点数就会出错。需要在后处理中进行类型转换或验证。方案一总结优点是零依赖、最灵活适用于所有模型。缺点是稳定性最差严重依赖Prompt技巧和模型本身的能力并且需要健壮的后处理逻辑。它适合对稳定性要求不高、或无法使用其他方案的简单场景。3. 方案二函数调用与工具范式——主流API的“原生”支持当OpenAI在2023年推出Function Calling功能后它迅速成为了解决结构化输出问题的“官方答案”。随后Anthropic的Claude、Google的Gemini等主流模型也提供了类似的功能可能叫Tools、Tool Use等。这不再是“请求模型输出JSON”而是“请求模型调用一个工具函数”而工具的返回值结构是预先定义好的。3.1 核心机制将输出结构定义为“函数”其核心思想是“反转”。你不是要求模型“输出一个描述用户的JSON”而是告诉模型“这里有一个get_user_info函数可以调用这是它的参数结构Schema”。模型的工作变成了理解用户请求判断是否需要调用这个函数如果需要则生成一个符合该参数结构的调用参数。例如定义这样一个函数tools [ { type: function, function: { name: extract_contact_info, description: 从文本中提取联系信息, parameters: { type: object, properties: { name: {type: string, description: 联系人姓名}, phone: {type: string, description: 电话号码}, email: {type: string, description: 电子邮箱地址}, address: { type: object, properties: { city: {type: string}, street: {type: string} }, required: [city] } }, required: [name, phone] } } } ]当你向模型发送消息“帮我记一下张三的电话是13800138000他在北京工作”时模型不会直接输出一段文本而是会返回一个意图调用extract_contact_info函数的响应并附上它“认为”正确的参数{ role: assistant, content: null, tool_calls: [{ id: call_abc123, type: function, function: { name: extract_contact_info, arguments: {\name\: \张三\, \phone\: \13800138000\, \address\: {\city\: \北京\}} } }] }这个arguments字符串就是一个完美的、符合我们预定义Schema的JSON。下游程序只需要解析这个JSON就能获得结构化数据。3.2 为什么它更稳定系统级的约束这种方案稳定性高的根本原因在于约束发生在API的系统层面而不是模型的“自由发挥”层面。模型的任务从“生成一段文本”变成了“填充一个已知结构的表单”。虽然底层依然是下一个词预测但目标空间被极大地缩小了——它只需要为每个预定义的字段生成合适的值而不需要同时发明字段名和结构。关键优势极高的可靠性对于符合函数描述的任务输出格式几乎100%正确。字段缺失、类型错误、格式非法等问题大幅减少。清晰的意图分离模型可以同时返回自然语言内容content和结构化数据tool_calls实现“答案”和“数据”的分离。触发机制模型可以自主决定何时调用函数为实现智能体Agent工作流奠定了基础。3.3 实战配置与边界情况处理在实际使用中有几个配置参数和边界情况需要特别注意。tool_choice参数这个参数控制是否强制模型调用函数。默认是auto由模型决定。如果你明确需要结构化数据可以设置为{type: function, function: {name: your_function_name}}来强制调用特定函数。这在构建确定性流程时非常有用。并行工具调用最新版本的API支持模型在一次响应中调用多个工具。这适用于需要从一段文本中提取多种独立信息的场景。你需要处理好可能返回的多个tool_calls对象。描述Description的质量至关重要函数的description和每个参数的description不是注释而是模型理解如何填充这些字段的核心依据。模糊的描述会导致模型填充错误的数据。差的描述“name”: {type: string}好的描述“name”: {type: string, description: 完整的个人姓名不包括头衔、职称。如果是中文名请保留完整姓氏和名字。}处理“不适用”的情况如果用户输入的信息不足以填充必填required字段模型可能会拒绝调用函数或者生成一个包含null或默认值的调用。你需要定义好业务逻辑是接受部分数据还是要求模型必须通过content字段询问用户这需要在函数描述和系统指令中说明。一个常见的“坑”是数组类型的定义如果你需要模型提取一个列表比如多个产品名称在定义parameters时需要明确指定type: “array”以及items的类型。否则模型可能会返回一个拼接的字符串。properties: { product_names: { type: array, items: {type: string}, description: 从文本中识别出的所有产品名称以列表形式返回。 } }方案二总结这是目前平衡稳定性、易用性和功能性的最佳方案尤其适用于基于主流云APIOpenAI, Anthropic, Google等的开发。其缺点是厂商锁定并且对于输出结构极其复杂、动态或递归的场景静态的函数Schema定义可能不够灵活。4. 方案三JSON Schema与格式模式——专为结构而生的“语法枷锁”如果说函数调用是“用工具范式引导模型”那么JSON Schema模式就是“给模型的输出直接戴上语法枷锁”。这是一些模型开始提供的更直接、更底层的结构化输出功能。例如Anthropic Claude在其消息API中提供了response_format参数可以指定为{“type”: “json_schema”, “json_schema”: {...}}。其核心理念是直接向模型指定一个完整的JSON Schema要求其输出必须严格符合该模式。4.1 JSON Schema比函数参数更强大的约束JSON Schema本身就是一个用于描述和验证JSON数据结构的强大标准。用它来约束LLM输出可以提供比函数参数定义更细致入微的控制。一个简单的函数参数定义可能只规定了字段类型。而一个JSON Schema可以规定字符串格式必须是邮箱、日期、URI等。数值范围最小值、最大值、倍数等。数组约束最小/最大项目数、唯一性等。条件依赖如果字段A为某值则字段B必须存在。枚举与常量字段值必须是指定列表中的一个。例如约束一个用户档案{ type: object, properties: { username: { type: string, pattern: ^[a-zA-Z0-9_]{3,20}$ }, age: { type: integer, minimum: 0, maximum: 150 }, email: { type: string, format: email }, tags: { type: array, items: {type: string}, minItems: 1, maxItems: 5, uniqueItems: true } }, required: [username, email], additionalProperties: false // 禁止出现未定义的字段 }将这个Schema传给模型就等于下达了死命令“你的输出必须是一个对象包含username和emailusername必须是3-20位字母数字下划线email必须是合法格式可以有age和tags但tags不能超过5个且不能重复除此之外一个多余的字段都不准有”4.2 实现原理与模型负担这种模式的实现推测是在模型生成Token时加入了基于Schema的约束解码Constrained Decoding或指导性生成Guided Generation。模型在每一步预测下一个Token时不仅要考虑语言概率还要考虑当前已生成的部分是否符合Schema的语法规则。例如当它生成到tags: [时下一个Token被约束为]如果数组为空或一个字符串如果数组有内容而不能生成一个数字或{。这无疑给模型推理增加了额外的计算负担和复杂度。因此一个非常关键的经验是Schema并非越复杂越好。过于复杂、嵌套过深的Schema可能会降低输出的质量甚至导致模型因无法满足约束而输出无意义内容或报错。设计Schema时应在“必要的严格性”和“模型的实现能力”之间取得平衡。4.3 与函数调用模式的异同与选型相同点两者都提供了强类型的结构约束都能极大提升输出稳定性都需要预先定义模式。不同点抽象层级函数调用是更高层的抽象它融合了“意图判断”是否调用函数和“参数填充”。JSON Schema模式是更底层的抽象只关心“输出格式”不涉及“动作”或“意图”。灵活性JSON Schema在描述复杂数据结构、自定义格式和约束条件方面更强大、更灵活。函数调用的参数Schema通常是其子集。适用场景函数调用更适合交互式、任务导向的场景尤其是构建智能体Agent模型需要自主决定何时、调用哪个工具来完成任务。JSON Schema模式更适合数据提取、格式化转换等纯输出任务你明确知道你需要什么结构的数据并且这个结构可能非常复杂。如何选择如果你的应用流程是“用户提问 - 模型思考并可能采取行动”选函数调用。如果你的应用流程是“给我这段文本按这个模板抽出数据”选JSON Schema模式。如果你的模型提供商如Claude同时支持两者对于纯数据提取任务可以优先尝试JSON Schema模式因为它可能提供更精确的字段控制。方案三总结这是结构化输出的“终极形态”提供了最强大、最精确的控制能力。但它对模型本身的支持度要求高目前并非所有模型都支持且设计不当的复杂Schema可能带来副作用。它是追求极致输出稳定性和数据质量的场景下的利器。5. 方案对比与混合策略实践为了更直观地对比我们可以从几个维度来审视这三种方案特性维度Prompt工程与上下文约束函数调用/工具范式JSON Schema模式稳定性低严重依赖模型能力和Prompt技巧高系统级约束极高语法级强制约束灵活性极高可随时修改Prompt中需预定义函数Schema中需预定义JSON Schema开发复杂度低无需特殊API支持中需理解工具调用流程中需理解JSON Schema模型支持度所有模型主流闭源/开源模型API部分先进模型API适用场景简单结构、快速原型、模型不支持高级功能时智能体、交互式任务、需要意图识别复杂数据提取、严格数据格式化输出“纯度”需后处理清洗高参数即JSON最高直接是合规JSON额外开销上下文Token消耗轻微的系统延迟可能的推理负担增加在实际项目中我们往往不会死守一种方案而是采用混合策略。策略一分层降级。优先使用方案三JSON Schema或方案二函数调用。如果当前调用的模型不支持则降级到方案一强化Prompt并在代码层面配备更健壮的后处理逻辑。这样能保证应用在不同后端模型下的兼容性。策略二预处理与后处理结合。即使用方案二或三也不要完全放弃后处理。你应该始终对API返回的“结构化”数据进行验证。使用jsonschema库Python或类似工具根据你定义的Schema对输出进行校验。from jsonschema import validate, ValidationError response_schema { ... } # 你的JSON Schema model_output client.chat.completions.create(...) # 获取模型输出 parsed_data json.loads(model_output.tool_calls[0].function.arguments) try: validate(instanceparsed_data, schemaresponse_schema) print(数据验证通过) except ValidationError as e: print(f数据验证失败{e.message}) # 触发重试、降级或人工处理流程这种“不信任验证”是构建生产级鲁棒性应用的关键。策略三Prompt作为Schema的补充。即使在使用JSON Schema时清晰的指令描述依然重要。你可以在系统指令中说明“你将始终以JSON格式回复并且必须严格遵守用户提供的JSON Schema。不要添加任何Schema中未定义的字段。”这为模型理解任务提供了额外的上下文有时能提高输出内容而不仅仅是格式的质量。6. 超越格式结构化输出的本质与未来当我们深入使用这些方案后会发现“稳定输出JSON”只是一个表面目标。其本质是实现人机与机机交互中信息的无损、高效、自动化传递。对人开发者而言结构化输出意味着可维护性。一个明确定义的接口Schema远比在自由文本中“淘金”要可靠得多。它让调试、测试和迭代变得可行。对机器下游系统而言结构化输出意味着互操作性。一个标准的JSON可以直接被数据库写入、被API转发、被前端图表库消费无需复杂且脆弱的文本解析逻辑。未来的趋势可能会朝着两个方向发展一是标准化。就像OpenAI的Function Calling催生了一批兼容工具一样可能会出现更通用的结构化输出协议或中间件让开发者用同一套定义对接不同模型。二是更智能的约束。目前的约束是静态的、预先定义的。未来可能会有动态的、基于上下文的约束生成。例如模型可以根据对话历史自动提议或协商一个适合当前数据提取的Schema。回到我们开头的痛点解决“薛定谔的JSON”没有银弹。Prompt工程是必备的基础技能函数调用是当前主流应用的首选平衡点而JSON Schema模式则代表了追求确定性的前沿。理解它们的原理、优劣和组合拳才能在你的大模型应用里真正把“概率输出”变成“可靠交付”。