跳到主要内容

结构化输出不等于结构化思维:大多数团队跳过的语义验证层

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个医疗排班系统从其 LLM 提取层收到了一个合法的 JSON 对象。Schema 验证通过,类型检查通过,必填字段齐全。然而,当下游任务尝试预约时,却发现 end_timestart_time 早了三个小时。两个字段都是格式正确的 ISO 时间戳,没有任何一个违反 schema。预约悄悄失败,患者没有收到任何通知——没有错误,没有告警。

这就是当 schema 验证被误以为是正确性验证时的样子。模型遵循了格式,却没有遵循逻辑。

约束生成实际上保证了什么

现代结构化输出 API——JSON 模式、工具调用 schema、语法约束生成——做出了一个具体的承诺:输出将符合你指定的结构。Token 掩码或有限状态机约束确保模型只能生成产生符合你 schema 的合法 JSON 的 token。这是一个语法保证,而不是语义保证。

当你指定一个包含 start_dateend_date 的 schema(均为 ISO 8601 字符串)时,约束系统保证你获得两个该格式的字符串,但无法保证 end_datestart_date 之后。当你将 confidence 字段定义为浮点数时,约束确保你得到一个数字,但无法保证该数字落在你期望的 0 到 1 范围内。当你的提取 schema 包含枚举值为 ["pending", "active", "completed"]status 字段时,约束确保你得到这三个值之一,但无法保证该值在语义上适合被提取的记录。

约束系统在 token 层面运行,执行语法规则。你的应用在意义层面运行,执行领域规则。这是需要不同解决方案的不同问题。

schema 合法失败的分类

一旦团队开始认真审视,生产系统中就会出现一组可预测的语义失败。

时间矛盾是最常见的。Agent 提取事件记录,其中 start_timeend_time 都是合法时间戳但顺序颠倒。合同解析器返回生效日期在 2025 年、过期日期在 2024 年。排班系统产生的 duration_minutes: 90 与时间戳所表示的 45 分钟不符。每个字段单独看都是合法的,组合起来却毫无意义。

超出范围的数值输出在提取和评分管道中频繁出现。置信度分数返回 1.7,百分比超过 100,本应在 1–5 范围内的优先级排名返回 8 或 0,金额在只允许正值的上下文中变成负数。schema 类型满足了——这些都是数字——但当下游代码假设有效范围时应用就会崩溃。

互斥字段组合出现在分类任务中。一个文档被同时标记为 requires_human_review: trueauto_approved: true。订单状态为 "completed"fulfillment_date 却是 null。用户账户被标记为 verified: trueverification_token 仍然存在——该字段本应在验证时被清除。每个值单独看都没问题,合在一起却代表一个不可能的状态。

静默类型强制转换失败在工具边界处影响最大。LLM 返回 customer_id: 12345 为数字,而下游工具签名期望字符串。JSON 解析器通常会静默接受,工具收到错误的类型、行为异常,而调用系统毫无察觉。Schema 说 customer_id 应该存在,它确实存在了,但 schema 对工具需要的精确类型契约保持了沉默。

通过结构验证的幻觉字段值造成最难察觉的失败。名称提取在未找到名称时返回 "N/A"——技术上是字符串,schema 合法,但被下游当作字面名称处理。地址字段返回空字符串而非 null,schema 允许但应用拒绝。日期字段返回 "9999-12-31" 作为"无日期"的哨兵——这是一个有效的 ISO 时间戳,下游日期计算会将其转换为无意义的结果。

为什么团队不构建语义层

大多数团队在 schema 验证处停下来了。原因可以理解:一旦结构化输出 API 运行起来并且 JSON 不再解析失败,感觉问题已经解决了。Schema 捕获了类型错误,LLM 在"遵循指令"。还有什么能出错?

答案是:schema 验证和语义验证解决的是不同的问题,而 schema 问题更容易被看到。Schema 失败会产生立即的硬错误——JSON 解析异常、类型强制转换失败、缺少必填字段异常。这些是嘈杂的,发生在开发阶段。语义失败更安静:没有被预约的诊疗、进入不可能状态的订单、几天内逐渐出错的仪表板指标。这些会在生产中出现,通常被归因于其他原因。

还有一个思维模型问题。使用结构化输出 API 构建的团队倾向于将 schema 视为与模型的契约——如果模型遵循契约,输出就是正确的。但 schema 只是部分契约,它描述结构,而不是意义。完整的契约包括业务规则、领域约束、跨字段不变量和 schema 无法表达的应用层语义。

构建语义验证层

验证层位于模型输出和应用逻辑之间,其工作是捕获所有 schema 验证无法捕获的内容。

第 1 层:Schema 验证。 这是你已经拥有的。它在原始模型输出上立即运行,捕获结构性失败,且成本低廉——纯内存计算,无 API 调用。对于结构性问题,它应该快速且强硬地失败,在其他任何内容运行之前。

第 2 层:领域约束验证。 这是对字段值和跨字段关系的基于规则的验证。将这些写成针对输出模型的显式验证器:日期范围检查、数值范围断言、互斥规则、状态转换合法性。这些在输出对象上同步运行,失败时带有精确识别违反了哪个约束及原因的错误消息。

Pydantic 验证器在 Python 应用中是这种验证的自然归宿。输出模型上的验证器可以同时检查所有字段,而不仅仅是正在被输入的字段。当你定义一个在 end_date 字段上触发的 check_date_range 验证器时,它可以从同一模型引用 start_date,并在约束失败时抛出消息为"end_date must be after start_date"的 ValueError。该错误消息是关键——它成为启用恢复的反馈。

第 3 层:业务规则验证。 某些约束需要外部上下文:数据库状态、当前系统配置、实时库存数量、用户权限。这些更昂贵,只应在第 1、2 层通过后运行。把昂贵的检查放在廉价的检查之后。一个日期范围检查失败的记录不需要数据库查询来确定它无效。

第 4 层:语义合理性检查。 某些失败需要判断而非规则——结构正确、领域有效但对输入上下文语义上不合理的内容。情感提取将明显负面的评论标记为高度正面。类别分类器将技术文档归入明显无关的领域。基于规则的检查无法捕获这些。LLM 作为评判者可以:一个轻量级评分调用,接收输入、输出和你的评估标准,并标记不合理的结果以供重试或人工审查。

验证重试循环

结构化验证失败应触发恢复循环,而不是立即报错。模型产生了约束违反,告诉它确切出了什么问题并要求修复。

这就是具体错误消息的价值所在。"验证失败"不是有用的反馈。"end_date (2024-01-15) 早于 start_date (2024-03-20)。请生成新日期,使 end_date 在 start_date 之后",为模型提供了纠正输出所需的确切信息。当反馈如此具体时,一次重试就能解决大多数语义验证失败。

循环结构是:生成 → 验证 → 如果失败,带错误上下文重试 → 再次验证 → 如果 N 次重试后仍然失败,升级处理。Instructor 等框架自动实现这个循环,将验证错误作为下一次生成尝试的上下文反馈回去。重要的是重试不仅包含原始任务提示,还包含具体的违反和需要的纠正。

将重试次数限制在两到三次。如果模型在多次尝试后持续违反同一约束,问题很可能在于你的提示或 schema 设计,而不是短暂的生成问题。针对结构性提示问题的无限重试只会产生无限失败。

Schema 设计作为语义辅助

Schema 本身可以做更多工作来在语义失败发生之前预防它们。以下几个模式始终有效:

有意使用可选字段。如果一个字段在数据缺失时应该为 null,将其设为 Optional[str] 并明确说明。给模型一个没有有效值填充的必填字符串字段,它会幻觉一个。给它 null 路径。

为你的 schema 属性添加描述字段。当你使用 schema 驱动的工具调用时,这些描述成为提示上下文的一部分。一个描述为"置信度分数从 0.0 到 1.0,其中 1.0 是最大置信度"的字段比仅描述为"置信度分数"的字段更不容易产生超出范围的值。

对分类输出优先使用枚举而非字符串。["active", "inactive", "pending"] 的枚举消除了整类语义失败——模型无法生成不在集合中的值。当业务逻辑进一步约束哪些枚举值在哪些上下文中有效时,在验证器中编码该逻辑,而不是依赖模型推断它。

添加显式哨兵处理。如果你的应用需要区分"未找到"和空值,添加一个布尔 not_found 字段,而不是期望模型在正确情况下将字符串字段留空或为 null。在每一层,显式胜过隐式。

跳过这一层的失败模式

跳过语义验证的系统性失败模式不是单一的灾难性错误,而是缓慢的污染。schema 合法但语义无效的记录在数据库中积累。基于该数据运行的报告产生错误数字。假设有效范围的下游逻辑遇到没有人预料到的边缘情况。用户发现错误输出并失去对系统的信任。等到找出原因时,已经有了一个回填问题。

LLM 这部分管道因不可靠而受到指责,而实际原因是你的应用接受了无效输出,因为它们在语法上是有效的。模型的错误在于内容;你的系统的错误在于接受了本应拒绝的内容。

验证层不是什么

语义验证不能替代评估(Evals)。评估衡量你的系统是否在有代表性的分布上产生正确输出。验证在运行时捕获特定的约束违反。两者都是必要的,它们在问题的不同层面运行。

语义验证也不能替代好的提示工程。如果你的模型持续产生日期范围违反,第一个问题是你的提示是否明确了这个约束。验证捕获提示工程遗漏的内容,但它不应该是你对抗可预测失败的第一道防线。

验证层是一种生产可靠性机制。其工作是确保应用层只接收语义有效的输入,无效输入被纠正或升级而非静默接受,以及当出现问题时,错误足够具体以推动修复。

最小可行版本

如果你今天没有语义验证层,从这里开始:列举出你的应用在结构化输出中出现时会失败的前五个约束违反,并为这五个编写验证器。对任何有时间范围的系统进行日期排序检查,对任何有分数或百分比的系统进行数值范围检查,对任何有状态机的系统进行状态字段组合检查。这五个验证器将捕获你的系统目前正在静默接受的大多数语义无效输出。

Schema 合规是前提条件,不是终点线。终点线是你的应用可以信任其含义的输出——不仅仅是 schema 描述的格式,而是你的业务逻辑所需的领域意义。

References:Let's stay in touch and Follow me for more thoughts and updates