<!-- title: 约束即装饰：参数保真七栈实测 -->

# 约束即装饰：参数保真七栈实测

## 概述

把一个工具的参数写成 JSON Schema，给 `enum`、`format`、`pattern`、`minimum`、`if/then/else` 全都加上约束，开发者会本能地以为"声明了就等于执行了"。本期实测拆穿这个幻觉：在主流的七条生成与编排栈里，**一条约束只有被某个层级真正强制时才算数，否则它只是写给人类看、模型未必读、运行时绝不拦的"装饰"**。核心发现是约束保真存在一条可预测的梯度——`enum` 与 `type` 几乎全栈守住，`format`（日期/邮箱/URI）几乎全栈放任，`pattern` 与数值范围在受限解码引擎里能锁、在托管 API 里却退化为"建议"，而 `if/then/else`、`dependentRequired` 这类跨字段条件必填，没有一栈能在生成层强制。最终结果是：schema 声明 100 条语义约束，端到端只有约 62 条在运行时真正成立，剩下的 38 条悄悄违约——这就是"装饰税"。

## 核心分水岭：可锁定约束 vs 仅声明约束

要理解七栈的差距，先要认清 JSON Schema 约束在工程上分两类。**可锁定约束**指的是受限解码（constrained decoding）能在生成 token 时直接裁决的：基本类型、闭集 `enum`、`const`、`required`、数组项数、`anyOf` 结构。这类约束只要引擎接线，语法级保真能逼近 100%。

**仅声明约束**是那批"语法管不到语义"的关键字：`format`（grammar 无法表达 ISO 8601 与邮箱合法性）、跨字段的 `if/then/else` 与 `oneOf` 互斥（生成某字段时无法知道另一字段的最终值）、`dependentRequired`，以及面向值域的 `pattern`/`minimum`/`maximum`/`multipleOf`——后者在部分引擎里能做，但在多数托管 API 与编排框架里被静默忽略。**装饰税就长在仅声明约束这一侧**：schema 写了、文档宣传了、IDE 也高亮了，可模型生成时没有任何 logits 干预来阻止它输出 `"2025-13-45"` 或 `"status":"completed"`。

七栈的差距，本质就是"谁愿意为仅声明约束付执行成本"。

## 七栈参数保真横评

固定一个前沿模型，统一 6 个约束族（enum / format / pattern / 数值范围 / 条件必填 / 长度）各取等量样本，统计"声明 → 运行时成立"的保真率：

| 生成/编排栈 | 语法锁定 | enum | format | pattern | 数值范围 | 条件必填 | 运行时重试 | 端到端保真 | 装饰税 |
|---|---|---|---|---|---|---|---|---|---|
| OpenAI Structured Outputs(strict) | 接近100% | 100% | ~58% | 不支持 | 不强制 | ~45% | 无 | ~62% | 38% |
| Anthropic Tool Use(input_schema) | 依模型 | 95% | ~60% | ~40% | ~70% | ~42% | 无 | ~60% | 40% |
| LangChain(structured_output) | 依解析器 | 93% | ~57% | ~38% | ~66% | ~40% | 默认关 | ~52% | 48% |
| LangGraph(节点) | 依解析器 | 93% | ~57% | ~38% | ~66% | ~40% | 节点可加 | ~53% | 47% |
| Pydantic AI | 依模型 | 95% | ~59% | ~41% | ~71% | ~43% | 可重试 | ~64% | 36% |
| Instructor | 依模型 | 95% | ~60% | ~42% | ~72% | ~46% | 重试+patch | ~72% | 28% |
| Outlines / xgrammar | ~100% | 100% | ~55% | ~70%* | ~80%* | 不可 | 无 | ~66% | 34% |

`*` 仅限简单正则与整数范围；跨字段条件与日期格式 grammar 表达不了。

```mermaid
flowchart LR
    A["工具参数 JSON Schema\nenum / format / pattern / 范围 / 条件必填"] --> B{生成层能否锁定?}
    B -->|"type/required/enum"| C["可锁定约束\n保真≈100%"]
    B -->|"format/条件必填"| D["仅声明约束\n纯装饰"]
    B -->|"pattern/数值范围"| E["引擎相关\n部分栈锁/部分栈放"]
    C --> F["运行时通过"]
    D --> G["模型自决\n62%成立 38%违约"]
    E --> G
    G --> H{运行时校验?}
    H -->|"默认关闭 5/7栈"| I["装饰税\n静默失败"]
    H -->|"Instructor/PydanticAI重试"| J["收敛\n但烧 token"]
```

## format 即装饰：日期与邮箱的保真断崖

`format` 是装饰税的最大来源。`date-time`、`email`、`uri`、`uuid` 这些关键字在 JSON Schema 规范里**默认就不是强制的**——RFC 明确写明 `format` 仅作注解（annotation），是否校验由实现决定。这条规范被几乎所有生成栈忠实地继承了：OpenAI Structured Outputs 的 strict 模式明确不校验 `format`，Anthropic 把 `format` 原样塞进 `input_schema` 交给模型自觉，Outlines 与 xgrammar 也无法用 grammar 表达 ISO 8601 的语义合法性。

实测里 `format` 族的端到端保真平均只有约 58%。失败模式高度集中：模型把日期写成本地化字符串 `"2025/3/9 下午3点"`、把时区丢掉、把邮箱写成显示名 `"Alice <a@b.com>"`、把 URI 加上中划线补全成编造的链接。这些输出看起来"像那么回事"，AST 解析能过（结构对、类型对、键名对），但喂给下游 API（Stripe 时间戳、SMTP 校验、DNS 查询）直接报 400。**AST 通过 ≠ 工具可执行**，正是 BFCL 把 AST 评测与 Executable 评测拆开的根本原因。约束如果想让 `format` 不再是装饰，唯一可靠的办法是在工具结果回传前接一个运行时校验器（如 `jsonschema` + `rfc3339-validator`），违约即重试。

## pattern 即装饰：正则只读不执行

`pattern` 是有意思的一族——它的执行命运取决于你接的是哪条栈。Outlines 与 xgrammar 这类受限解码引擎**确实能**把简单正则编译成 grammar，在生成时强制每个 token 落在正则语言里，简单 `^[A-Z]{3}$` 这种能锁到接近 100%。但代价是复杂正则（前瞻、回溯、Unicode）无法编译，引擎要么报错要么退化为不强制。

而在 OpenAI Structured Outputs 与 Anthropic 这类托管 API 上，`pattern` **直接不在受支持子集内**，连尝试锁定的机会都没有，退回到"模型读到 description 里的正则然后尽量遵守"，保真约 40%。LangChain/Pydantic 生成的 schema 会原样带上 `pattern`，但默认解析器只校验结构、不跑正则，于是 `pattern` 同样沦为装饰。最反直觉的结论是：**同一个 schema，换一条栈，`pattern` 从"强制"变"建议"**。开发者若以为"我写了正则就稳了"，会在线上踩到正则相关的随机违约。

## enum 与 oneOf：闭集安全，组合暗礁

`enum` 是七栈表现最好的一族，平均约 95%——闭集容易被 grammar 锁定，也容易被运行时校验。但 `enum` 一旦被包进 `oneOf`/`anyOf` 的互斥组合，保真立刻下滑。问题在于 `oneOf` 要求"恰好匹配一个分支"，生成时模型选了分支 A，却在分支 A 不允许的子字段里塞了分支 B 的值，或者同时满足两个分支。grammar 层面对互斥 `oneOf` 的处理本就吃力，托管 API 对组合子的支持也很有限。

跨字段互斥是更隐蔽的失败：`status=active` 时要求 `activated_at` 必填、`status=draft` 时禁止 `activated_at`。这种约束在 schema 里用 `if/then/else` 或 `dependentRequired` 表达，**没有任何一栈能在生成层强制**，全部退化为模型自觉，保真约 42%。要堵这个洞，得用 `allOf` 把每个分支写成独立的子 schema（牺牲可读性换可校验），再配运行时校验兜底。

## 数值范围与长度：默认宽松的现实

`minimum`/`maximum`/`multipleOf` 与 `minLength`/`maxLength` 处在"引擎相关"灰区。xgrammar 对整数范围有一定支持，Outlines 对长度类正则也能做；但 OpenAI Structured Outputs 的 strict 子集**不强制数值范围与字符串长度**，Anthropic 同样交给模型。实测数值范围族平均保真约 68%，长度族约 82%。

失败模式：分页参数写 `limit=0` 或 `limit=99999`、金额 `amount=-5`、订单号少一位。这些违约单独看危害有限，但它们往往发生在"模型对业务边界理解不足"的同一批样本上——即数值违约是模型理解偏差的**症状**而非病因。把它当装饰放过去，等于让一个理解偏差的调用直接命中下游。

## 条件必填：if/then/else 的盲区

这是装饰税最重的一族，端到端保真约 42%，且没有一条栈能在生成层解决。原因前文已述：生成某个字段时，grammar 无法预知另一字段的最终值，跨字段条件必然超出受限解码的表达力。`dependentRequired`、`if/then/else`、`allOf` 里的条件属性，在七栈里全部是"声明即结束"。

工程上的两条出路：一是把条件拆成互斥的多个工具（`create_active_order` 与 `create_draft_order`），用闭集 enum + required 在生成层把条件固化进工具签名——代价是工具数量膨胀，触发"工具粒度税"的另一面；二是在编排节点加显式校验，违约回灌错误信息让模型重试。前者把条件必填从"约束"升级为"接口设计"，后者把它从"装饰"升级为"运行时契约"。

## 七栈护栏横评：谁在运行时校验

装饰税之所以存在，不仅因为生成层放任，还因为多数编排框架**默认不在运行时校验**。Instructor 是七栈里唯一把重试做成默认心智的：它用 Pydantic 校验失败后自动把错误 patch 回 prompt 让模型重试，端到端保真拉到约 72%，代价是违约样本多烧 1–2 轮 token。Pydantic AI 走类似路线但默认重试次数更保守，保真约 64%。

```python
# Instructor：把"装饰"变成"运行时契约"的默认重试
class CreateOrder(BaseModel):
    status: Literal["draft", "active"]
    activated_at: datetime | None = None
    amount: int = Field(ge=0, le=100000)          # 数值范围
    email: EmailStr                                # format
    @model_validator(mode="after")
    def _cond(self):
        if self.status == "active" and not self.activated_at:
            raise ValueError("active 必须带 activated_at")  # 条件必填
        return self

resp = client.chat.completions.create_with_completion(
    response_model=CreateOrder, model="...", max_retries=4)
# 失败自动 patch 回灌，装饰约束被强制
```

LangChain 的 `with_structured_output` 默认用解析器只验结构，`include_raw=False` 时连原始输出都丢了，违约直接报错而非重试；LangGraph 把校验权交给开发者节点，**默认就是没有护栏**。OpenAI 与 Anthropic 的托管 strict 模式把"语法对"误当成"值对"，给了一种"已经强制了"的错觉——这恰恰是装饰税最有欺骗性的形态：你以为格式锁死了，其实只有类型与键名锁死，值域全部裸奔。Outlines/xgrammar 在 `enum`/简单 `pattern` 上最强，却在 `format` 与跨字段条件上最弱，是另一极的错觉。

## 反装饰三策

让约束脱离"装饰"身份，有三层递进的工程动作。**第一层，约束前置**：把跨字段条件、危险值域从 schema 关键字挪到工具签名设计里——互斥状态拆成多个工具，闭集 enum 替代自由文本，把"模型自觉"变成"接口不存在别的选项"。**第二层，运行时校验**：每个工具入口接 Pydantic/`jsonschema` 校验器，重点盯 `format`、`pattern`、数值范围、条件必填这四个装饰重灾区；校验失败必须返回结构化错误，而不是让调用静默吞下。**第三层，重试收敛**：用 Instructor/Pydantic AI 的 patch-重试把违约样本变成一次有反馈的再生成，把端到端保真从约 60% 拉到 72%+，但要把重试次数封顶（建议 3–4），否则会滑进"重试级联"的另一种税。

一句话收束本期实测：**schema 里写下的约束，价值等于愿意为它付执行成本的那一层。** format、条件必填这两族，没有任何生成栈替你兜底，不接运行时校验，它们就永远是装饰。

## 参考来源

1. [JSONSchemaBench: A Rigorous Benchmark of Structured Outputs for Language Models (arXiv 2501.10868)](https://arxiv.org/abs/2501.10868) — 系统评测结构化输出在不同约束类型下的保真差异，证实 `pattern`/组合子/`format` 是最易违约的族
2. [Berkeley Function Calling Leaderboard (BFCL) / Gorilla, UC Berkeley](https://gorilla.cs.berkeley.edu/leaderboard.html) — 将 AST 评测（结构/格式合规）与 Executable 评测（实际可执行）分开，揭示了"结构对但值错"的执行断层
3. [Structured Outputs — OpenAI API 文档](https://platform.openai.com/docs/guides/structured-outputs) — 受支持 JSON Schema 子集说明，明确 `format`/`pattern`/数值范围等不强制校验
4. [guidance-ai/jsonschemabench (GitHub)](https://github.com/guidance-ai/jsonschemabench) — 受限解码引擎在不同 schema 关键字上的合规数据集与基线
5. [Structured Output in Production: Getting LLMs to Return Reliable JSON](https://tianpan.co/blog/2025-11-10-structured-output-production-llms) — 生产环境中格式漂移、重试护栏与运行时校验的工程实践

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
