跳到正文
AI 知识地图 0.18 · 2026-07-30
关于与纠错文字目录 / Search
理解原理

结构化输出:把概率文本接入确定性软件契约

从最小 schema、约束生成和分层校验,到版本迁移、有限修复与安全执行。

核心命题 结构化输出把模型回答转换为可解析、可校验、可版本化的数据契约。可靠链路不是“看起来像 JSON”,而是生成时约束、完整性判断、schema 校验、业务/事实校验、执行时鉴权;任何前一层通过都不能替代后一层。
读完你应该能:区分语法/schema/语义/权限;设计最小可演进 schema;演算字段校验与修复;安全连接工具和数据库。
  1. 定义最小版本化 schema
  2. 生成时约束并等待完整接受态
  3. 服务端重新解析与 schema 校验
  4. 验证事实和跨字段业务不变量
  5. 以当前主体鉴权后幂等执行
  6. 按失败层记录并有限恢复/回滚

1自然语言和程序接口的容错方式不同直觉

人理解一段话,靠的是语境与意图。消息里多一句“好的”、少一个引号、把逗号写成中文全角,只要意思没变,人几乎不会察觉。程序恰好相反:它不揣测意图,只按事先声明的规则逐字比对。调用方期望的是一个对象,收到的是一个字符串,程序就在类型检查处崩溃;字段叫 userName 而约定写 username,后面的逻辑就拿到空值。自然语言的容错来自共同语境下的宽容,程序的容错来自边界的精确:只要边界声明得足够清楚,异常就可以被机械地识别和拒绝,而不需要人去猜测“模型这次大概是什么意思”。

结构化输出正是把前一种沟通方式改造成后一种。它的输入是两部分:任务语义,即“这次调用要回答什么问题、返回什么内容”;以及版本化的数据契约,即“回答必须长成什么形状”。它的输出不是自由文本,而是可解析、可校验、并且可以被程序在字段级别拒绝的对象。生成约束与解析器一起完成这个转换:把自然语言里靠语境消解的容错,转成明确的字段和明确的状态——字段有没有、类型对不对、值在不在允许范围内,每一项都有确定答案。

这个转换落地为一条逐级收紧的链条。在提示里写“返回 JSON”只提高输出为 JSON 的概率,模型仍可能加注释、加解释文字,或者根本不返回 JSON。JSON 模式通常能保住语法:输出被约束为合法的 JSON 文本。但合法 JSON 只保证机器读得动,不保证内容可用——字段可以少、类型可以错、值可以超范围。JSON Schema 在这一层之上进一步约束形状,声明字段、必填项、枚举值、长度限制和版本号,消费者据此机械地拒绝异常对象,而不是用脆弱的正则表达式去猜模型的意图。链条的末端是约束解码:在生成过程中就阻止非法路径,让不符合契约的 token 序列根本没有机会被采样出来。四层手段的收紧程度依次提高,但每层的支持范围依具体实现而异,因此无论上游宣称支持到哪一层,接收侧都必须做服务端复验。

这里需要把“格式有效”与“内容可信”分开。一个对象通过了所有语法和形状校验,只说明机器能读取它、能定位到约定的字段,不说明字段里的值是真的,更不说明允许执行。契约把校验问题的答案变成了布尔值,但模型对事实的判断仍然是概率性的。数据契约的价值正在于让失败显式、局部、可计数:哪次调用、哪个字段、违反哪条约束,都可以被记录和定位,而不是让错误像错别字一样融进一大段自然语言里事后无从追查。它约束的是输出形态,不是模型的知识可靠性;它让概率模型可以被确定性软件安全地消费,却不会把概率模型变成可信数据库。

2四道门必须逐层通过分层

能被 JSON.parse 解析,距离“执行退款”还很远。解析成功只证明这段文本是合法 JSON,它回答不了任何业务问题:字段在不在、值合不合理、请求者有没有资格。要把一个模型生成的字符串变成一次真正执行的退款,中间要过四道门,而且必须逐层通过——前一层通过不能替代后一层,任何一层失败,下游都不该“尽量猜着继续”。

分层验证的输入有四样:模型返回的对象、约定好的 schema、权威的业务数据(来自数据库或服务端状态)、以及当前请求的主体(谁在发起这次操作)。输出是五种判定之一:通过、字段错误、需要补充信息、拒绝、或升级。四道门各自检查一类不同性质的问题:

第一道门只做纯语法判断:括号配对、引号闭合、转义合法、文本在正确的位置结束。模型输出经常被截断或夹带解释文字,这一层把“不是完整 JSON”的输入挡在门外,失败就拒绝整条消息或触发重新生成。

第二道门对照 schema 检查形状:该有的字段是否齐全、类型是否匹配、枚举值是否在允许集合内、数值是否落在声明范围内。action 字段只允许 refund 和 ask 两个值,模型写了别的字符串,错误被精确定位到字段级别,而不是让下游代码拿着错误值继续跑。

第三道门处理业务事实。schema 正确不代表内容正确:金额字段可以合法地是数字,却超过订单实际可退额度。这一层检查跨字段规则(两个字段各自合法、组合起来矛盾)、查询数据库核对状态、要求证据支撑,例子里就是 amount ≤ 订单可退额。失败时系统该做的是向用户补充询问或转人工,而不是自行猜一个“看起来合理”的值。

第四道门处理权限与副作用:执行会改变现实状态,所以必须确认主体是谁、策略允不允许、请求是否幂等、是否需要审批。例子里要回答的问题是“当前客服能否批准这笔退款”。失败意味着禁止执行或升级到有权的人。

四道门各回答一个下游无法代答的问题,且必须按序通过:语法层无法替 schema 层判断字段合法性,schema 层无法替业务层核对事实,业务层也无法替权限层决定谁能执行。任何一层的失败都让流程在这一层停下,明确地拒绝或升级。值得特别强调的是,模型给出的 user_id、金额和 action 全都是不可信建议:它们只是被结构化了的文本,没有因此获得任何可信度。执行层必须从认证上下文重新取得主体与权限,而不能拿模型声称的身份去查库、去授权。

检查例子失败处理
完整语法括号、引号、转义、结束态不是半截 JSON拒绝/重生
Schema字段、类型、枚举、范围action∈refund/ask字段级错误
业务与事实跨字段规则、查库、证据amount≤订单可退额补信息/转人工
权限与副作用主体、策略、幂等、审批当前客服能否批准禁止或升级

3最小 schema 比大而全更可靠设计

把所有可能用到的字段都塞进一个嵌套对象,看起来一步到位,实际同时抬高了错误率和耦合:每多一个可选字段,就多一批“合法但无意义”的组合;每个字段的改动都可能牵动其他字段的校验、消费者的解析和将来的迁移。最小 schema 的思路反着来:输入是业务上真正需要模型判断的字段、服务器本来就知道的事实、以及兼容性要求,输出只包含模型必须判断的字段和显式分支,其余一概不交给模型。

切分的第一条原则是:只让模型产生需要语义判断的内容。时间戳、用户 ID、总价、派生状态这些值服务器在请求上下文里已经掌握,模型转述反而可能抄错,由服务器填充。模型负责的只是它独有的贡献——从用户语言里判断出的意图、参数和理由。身份由服务器填写,而不是相信模型声称的身份,与验证层的结论一致。

切分的第二条原则是用枚举和 discriminated union 缩小状态空间。动作名不要用自由字符串:enum 让消费者不用猜拼写,但也留下一个后续成本——新增枚举值需要兼容处理,旧消费者遇到新值必须能安全降级。互斥的任务用 discriminated union 表达:先用 kind 字段在 refund 和 ask 之间做选择,再套用对应的小 schema。两个小 schema 各自只声明自己需要的字段,比一个装着几十个可选字段的大对象更容易约束、校验和迁移,因为无效组合在一开始就不可能被表达出来。

字段声明本身也有一套收紧规则:区分 missing 与 null——字段缺失和显式为 null 是两种状态,混为一谈会丢失信息;限制字符串与数组的长度,挡住资源耗尽和荒谬的超长值;默认拒绝 additionalProperties,让未知字段直接暴露 schema 漂移,而不是被静默吞掉。每一条规则的收益都对应一个必须管理的风险:

必填字段保证状态完整,代价是面对未知值时不能图省事填一个假默认值——宁可显式报错或询问;长度与范围限制挡住极端输入,但边界值必须跟业务规则同步更新,否则校验器会拒绝真实的合法请求;禁止额外字段让新旧版本的差异立刻可见,代价是每次改动都要在提供方与消费者之间协商。

这些规则合起来指向同一个结论:schema 越小,模型要做的选择越少,每项声明都对应一个真实存在的业务决策;schema 越大,声明越像一纸空文,错误率随路径数量一起膨胀。同时每个字段都应附带业务含义说明和正反例子,让校验失败时的错误信息能告诉人“缺什么、为什么错”,而不是只报一行堆栈。

设计选择收益风险
枚举动作消费者无需猜拼写新增值需兼容
必填字段状态完整未知值不要用假默认
长度/范围限制资源与荒谬值边界需与业务同步
禁止额外字段发现 schema 漂移前后版本需协商

4运行示例:合法对象如何在后两层失败案例推演

模型返回 action=refund、amount=120。这段输出通过了语法门——它是完整合法的 JSON;也通过了 schema 门——action 在允许的枚举里,amount 落在 schema 声明的通用范围 0–500 之内。两个门都放行,却仍然不能执行。问题出在后面两道门上,而它们检查的对象已经不再是模型输出本身。

案例的输入有四样:结构合法的对象、订单的实付与退款记录、政策金额上限、以及当前操作者的角色。输出是业务错误码或安全动作。业务事实门要核对一条订单不变量:可退金额 R 等于实付金额 P 减去已退款金额 D,与政策上限 L 两者中取较小者,即

R = min(P − D, L)

其中 P 是订单实付金额,D 是此前已退款金额,L 是政策允许的金额上限,min 表示取两个候选上限中较小的一个。代入本订单的数据,算得最多可退 80。120 超过了 80,违反订单不变量。注意这条判断依赖权威的订单与政策数据,schema 的通用范围无法代替它:0–500 里的 120 在形状上毫无问题,问题的性质完全不同。权限门接着给出第二个拒绝理由:当前角色的审批权限覆盖不了这笔金额,没有批准权。

图示把这条路径画成退款结构化输出依次经过语法、schema、业务事实和权限四道验证门的流程。图传达的核心关系是:格式保证只让错误更容易被检测,数据库里的事实和当前主体的权限才最终决定能否执行。前两道门是模型输出对契约的符合性检查,后两道门把输出放回真实世界对照。

失败后的正确恢复是返回业务错误码 amount_exceeds_remaining,让模型据此改为解释说明或引导转人工,而不是“悄悄处理掉”:把 120 静默截成 80 再执行,等于系统擅自改写模型的意图——模型可能确实认为该退 120,截断掩盖了上游某处的问题,也把“系统发现了违规”变成“系统按自己的理解执行了另一笔退款”。宁可让流程以显式错误停在原地,也不能用猜测修补一个校验失败的对象。

模型对象action=refundamount=120语法 ✓完整 JSON无截断Schema ✓枚举合法0≤amount≤500业务/事实 ✕订单实付 ¥80,可退上限 ¥80权限 ✕当前角色只能建议,不能批准
图 1 格式保证只让错误更容易检测;数据库事实和当前主体权限仍决定能否执行。
R=min(PD,L)=min(800,100)=80

5约束生成与生成后校验是互补的机制

既然服务端最终都会校验,为什么还要在生成时约束?因为生成时约束和服务端校验守的是两类不同的错误,省的是两种不同的成本。这套双层机制的输入是模型分布、schema 和服务端业务规则,输出是格式受约束且经过后验验证的对象:约束解码减少语法与 schema 层面无效的输出,从而减少无效重试;生成后校验则防实现缺陷、版本错配和语义错误——这些是解码器承诺与实际情况不符时才会出现的问题,在生成时根本不可见。

约束解码的机制是受限采样:在每一步,把当前状态下非法的 token 概率置零,再对剩下的合法 token 重新归一化。写成公式,状态 s 下 token t 的受限概率 P′(t|s) 与原概率 P(t|s) 乘一个指示函数成正比:

P′(t|s) ∝ P(t|s) × 1[t ∈ M(s)]

其中 P(t|s) 是状态 s 下原始 token t 的概率,P′(t|s) 是应用约束后的概率,M(s) 是当前仍合法的 token 集,1[·] 是合法取 1、非法取 0 的指示函数,符号 ∝ 表示右侧还要除以全部合法候选的概率总和才能归一化为概率分布。

这个公式同时说明了约束的边界。指示函数只是把非法候选的概率砍掉、把剩下的按比例放大,它没有新增任何信息。如果模型原本把大部分概率压在非法候选上,归一化之后,它只能被迫选择概率很低的合法值——格式通过了,内容却可能变差。所以不能只看结构通过率:格式成功但业务质量下降,恰恰是约束把分布问题推到了台面上。正确的度量要同时看首次结构通过率、业务正确率和任务质量,三者缺一不可。

正因两层机制各管一摊,生成后校验不可省。底层 API 对 schema 特性的支持范围依实现而异,宣称支持不等于真的支持:某些嵌套结构、某些关键字可能在运行时被忽略。稳妥的做法是在启动时跑契约测试验证声明,并在每次生成后复验,而不是默认相信文档。与之配套的原则是不要用自动修复掩盖分布问题:一个“修 JSON”的模型很可能在补引号的同时改了字段含义。只有纯语法、且可证明与原意等价的修复——比如补齐被截断的收尾括号——才适合自动应用;任何涉及字段值、类型的“修复”都必须回到显式失败与重试的流程里。

P(ts)P(ts)1[tM(s)]

6失败恢复要局部、有限且无副作用可靠性

校验失败之后最差的处理方式,是把整个错误对象原样塞回模型无限重试。它同时抬高延迟和成本,把不可信文本反复送回上下文,扩大提示注入面,更危险的是每次重试都可能被当成一次新的动作重复执行。失败恢复要同时做到局部、有限、无副作用,三样缺一不可。

恢复流程的输入不是原始错误消息,而是机器生成的字段路径、错误代码、允许范围,以及请求的幂等键;输出是五种动作之一:局部修复、向用户澄清、重取事实、拒绝、或人工升级。其中“回显给模型什么”有明确规则:只回传机器生成的错误信息——哪个字段违反了哪条约束、允许值是什么——不回显模型自己产生的大段不可信文本。错误对象里可能藏着用户注入的内容,把它原样送回模型等于给注入一个直达的通道。

无副作用是时间上的硬约束:第一次校验通过之前绝不执行任何副作用。校验本质上是纯读操作,所以它可以免费重试;副作用一旦发生就不可免费撤销。重试必须复用请求 ID 和幂等键,保证同一逻辑请求无论重试多少次,业务系统只认一次。有限则是次数上的硬约束:重试次数必须封顶,同类连续失败不能再硬试,而是降级到更简单的 schema 或确定性的表单——让任务的难度随模型的持续失败而下降,而不是让失败无限循环。

按失败原因分派动作,恢复才谈得上局部。纯语法失败,比如输出被截断或收尾括号缺失,用约束重生或一次修复处理。缺必填字段且可以向用户询问的,返回 ask 把问题还给用户,而不是替用户编一个值。事实或业务冲突时重新获取权威数据核对,不让模型猜。权限不足则禁止执行,必要时升级人工。每一类失败都停在它自己的层,不扩散成对整个任务的重来。

最后,这套机制的运转要靠度量支撑:记录首次通过率、每个字段的错误、修复后通过率、最终任务失败率、重试次数和重试带来的额外延迟(p95)。没有这些数字,局部、有限、无副作用就只是口号——只有看到错误集中在哪个字段、重试集中在哪一层,才能知道是 schema 声明得太苛刻、还是模型在这个任务上本就不该被赋予直接生成的责任。

7版本演进是一项分布式契约版本

生产者升级了 schema,旧消费者为什么可能静默误读?因为契约不是双方坐下来同时更新的:生产者和消费者各自独立部署,中间必然存在版本交错。旧消费者解析新对象时,可能把新增字段直接丢弃、把新增枚举值塞进错误的默认分支——不报错,却执行了错误逻辑。契约迁移的输入是旧 schema、新 schema、生产者和消费者各自的版本,以及历史 trace;输出是一份兼容矩阵和 expand/contract 的发布计划。

判断兼容性的第一个陷阱,是认为“新增等于兼容”。新增可选字段通常向后兼容,前提是消费者不拒绝额外字段——一个默认拒绝 additionalProperties 的严格消费者遇到新字段会直接失败,这就不再兼容。新增枚举值同样危险:旧消费者的 switch 语句不认识新值,会落入 default 分支,而 default 分支里往往写着错误的兜底动作。真正的破坏性变化更直接:删除字段、改变字段类型都会破坏读写两端。范围收紧也是一种破坏:把金额上限从 0–500 收到 0–200,旧数据就不再合法。

因此 schema 必须带版本号和明确的兼容政策,发布采用 expand/contract 三步:先扩展——让消费者同时接受新旧两种格式;再切换——让生产者开始发送新格式;最后收缩——退役旧格式。每类变更的风险和迁移路径如下:

新增可选字段要先把所有消费者更新到能接受该字段,再让生产者输出。新增枚举值的同时,所有消费者要补上显式 unknown 分支,保证遇到不认识的值得不到“默认照常执行”的结果。字段改名或改类型需要双写、双读的过渡期,两套格式并存一段时间后再退役旧字段。收紧范围则要先把存量数据迁移到新范围内,并且校验逻辑按数据所属的版本分别执行,而不是用新规则去审判旧数据。

版本不止属于 schema 一个文件:提示词、schema、约束编译器和消费者版本要一起记录。出问题时,只有把这几样恢复到当时的组合、回放旧 trace,才能复现当时的规则与行为;只看今天的提示和今天的 schema,永远解释不了昨天的错误。

变更典型风险迁移
新增可选字段严格消费者拒绝先更新消费者
新增枚举值未知值误执行显式 unknown 分支
字段改名/类型破坏读写双写/双读后退役
收紧范围旧数据不再合法迁移数据并分版本校验

8安全执行不信任模型提供的身份与工具名安全

schema 已经把 action 限定在 refund 和 ask 之间,为什么提示注入仍能造成伤害?因为攻击者的目标从来不是让模型输出非法 JSON,而是诱导它选择语法完全合法、内容却有害的参数:refund 是合法动作,但对象可以是别人的订单,金额可以顶到上限,理由里可以藏一段给下游 agent 的指令。结构化约束把“格式非法”这道口子焊死了,剩下的是“格式合法但意图被劫持”的口子。

安全执行的输入是合法参数、认证主体、工具白名单、策略与审批状态,输出只有两种:以最小权限执行,或拒绝。第一原则是模型提供的三样东西全部不可信:身份、工具名、自由文本。服务器从会话认证取得 user/tenant,不接受模型自报的身份;工具注册表只暴露当前流程允许的能力,模型“指名调用”某个工具名只意味着它建议了那个名字,最终映射到哪个真实能力由注册表和策略决定;每个参数都要过对象级授权、速率、金额、目的与审批校验。高风险动作先由模型生成计划,经过确定性的策略门和人工确认后才提交——计划和建议可以来自模型,决定权必须在确定性代码或人手里。

自由文本字段是结构化对象里最容易被低估的通道。攻击载荷不需要破坏 schema 也能放进去:SQL 片段、HTML 片段、或者写给下一个 agent 的指令,放进一个字符串字段完全合法。结构化不等于净化,它只是把攻击载荷装进了一个合法字符串。任何消费者在使用这些字段前,必须按目标上下文转义并标记为不可信——渲染成 HTML 前要转义,拼进 SQL 前要参数化,传给下一个 agent 前要隔离。

这些规则合起来防的是 confused deputy 问题:模型代表用户建议动作,但它不能继承服务账号的全部权限。如果执行层拿模型的输出直接调用一个权限全开的内部接口,模型就成了被操纵的代理人,注入者通过它借到了远大于用户的权限。因此每次工具调用都要以最小权限和当前主体重新鉴权——不是“这一轮已经鉴权过了”,而是每个动作、每个对象、每次调用独立核定。模型输出的身份声明、工具选择和自由文本,全都按不可信数据对待,安全边界才不会随着一个合法的 refund 对象一起被绕过。

9评测应沿整条契约定位失败评测

“结构化输出成功率 99.9%”听起来接近完美,却回答不了上线前最关键的问题:那 0.1% 的失败发生在哪一层、造成了什么后果。这个数字很可能把“重试了一次的格式错误”和“一次越权退款”平均在同一个分母里,而后者的代价远不是前者的数量级。端到端评测的输入是模型输出、验证日志、工具结果和业务后果,输出是沿整条契约展开的指标:语法、schema、字段、事实、权限、幂等、任务质量、延迟与成本。

按层报告意味着每个门单独有数:完整语法率、schema 首次通过率、死路(重试耗尽的请求)比例、字段级错误分布、业务/事实通过率、权限拦截次数、工具成功次数、重复副作用次数、最终任务质量、额外延迟和成本。一个请求在语法门被拦住还是在权限门被拦住,是性质完全不同的两类失败,混在一起就失去了定位能力。回归集要专门覆盖边界和对抗场景:断流输出、Unicode 文本、超长数组、未知枚举值、旧消费者、新政策生效后的存量数据,以及恶意自由文本。

错误预算必须按最终后果分级。大量无害的格式重试和一次越权退款不能放在同一个平均失败率里被互相抵消:前者只是成本问题,后者是安全事件。安全的做法是对高危类别设独立的硬门槛——越权类错误的可接受数量是零或趋近于零,并且配即时回滚,而不是“总体 99.9% 达标”。

诊断顺序也由契约结构决定:先问输出是否完整,再问形状是否正确,再问事实与关系是否成立,最后问当前主体是否能安全执行。四个问题的顺序对应从便宜到昂贵、从格式到后果的排查路径。沿着这个顺序,任何一次失败都能被归类到它真正发生的层,而不是被笼统地记成“模型输出错了”;也只有这样,“成功率 99.9%”剩下的 0.1% 才能被分解成具体、可修复、可按后果排优先级的问题清单。

10把因果链连起来综合

结构化输出解决的原始问题是:自然语言的容错与程序的确定性要求之间存在落差。从这个落差出发,每一条设计决策都由上一条的缺陷引出,一路连接到可验证的实践,整条因果链由六步串成。

起点是最小版本化 schema。它把“模型要判断什么”缩减到只含真实业务决策的字段,身份、时间、派生值留给服务器;同时带上版本号,为将来 expand/contract 的演进留出坐标。schema 越小,生成与校验两端的状态空间越小。

第二步是生成时约束并等待完整接受态。约束解码在采样时屏蔽非法 token,减少无效重试;“等待完整接受态”意味着不把流式输出的半截文本当结果用,只有生成器给出完整结束信号,对象才进入下一步。

第三步是服务端重新解析并做 schema 校验。服务端不信任上游的自我声明,重新解析一遍并对照 schema 检查字段、类型、枚举与范围,把实现缺陷和版本错配挡在外面。格式在此被机器确认可读,但仍不意味着内容可信。

第四步是验证事实和跨字段业务不变量。这里引入数据库事实与政策数据,核对 R = min(P − D, L) 这类依赖权威数据的规则;失败返回业务错误码,而不是静默修正。

第五步是以当前主体鉴权后幂等执行。身份来自会话认证而非模型自报,工具调用以最小权限和当前主体重新鉴权,执行携带幂等键,保证重试不会重复产生副作用。

第六步是按失败层记录并有限恢复或回滚。每一层有自己的失败形态与恢复动作:语法失败用约束重生或一次修复,缺必填字段转向 ask_clarification,业务冲突重取权威数据,权限不足拒绝或升级,同类连续失败降级 schema。恢复局部、有限、无副作用,且每层的通过率、错误与延迟都被记录,成为沿整条契约定位失败的评测数据。

链条的因果方向是单向的:schema 约束生成,生成约束服务端解析,解析通往业务验证,业务验证通往鉴权执行,执行结果反过来成为下一轮评测与版本演进的输入。任何一环绕过前一环——跳过 schema 直接执行、跳过业务验证直接截断金额、跳过鉴权直接调用工具——链条就断在那一处。这个概念从“程序与自然语言容错方式不同”这一观察出发,最终落到的不是某个模型参数,而是一整套可以被机械验证、按层定位、有限恢复的工程结构。

资料来源与改编说明
访问日期:2026-07-22