云计算百科
云计算领域专业知识百科平台

结构化输出与 Function Calling 工程化:让模型输出可被机器可靠消费

结构化输出与 Function Calling 工程化:让模型输出可被机器可靠消费

本文把「让模型输出能被代码可靠消费」拆成两条正交的原语——tool/function calling(中间动作)与 structured outputs/JSON mode(最终答案),讲清为什么生产环境里一句「请返回 JSON」远远不够。核心章节给出 2026-09 三大厂商(OpenAI / Anthropic / Gemini)的精确参数、约束解码(采样期 mask 非法 token)与事后校验(解析后 reject)两种机制的取舍,以及一份从 Pydantic/Zod 定义到业务规则二次校验、再到单次重试与人工降级的完整工程实现。最后用 6 条踩坑手册,告诉你 additionalProperties 漏写、strict 与 best-effort 混用、依赖 schema 却不做业务校验这些会让你凌晨被报警叫醒的坑。读完你应能搭出一个「即使 provider 保证也留客户端校验」的稳健结构化输出管线。

关键词:structured outputs、function calling、JSON mode、约束解码、Pydantic、Zod、OpenAI Responses API、Anthropic output_config、Gemini、GBNF、XGrammar、Outlines、BFCL

一、写在前面:为什么现在必须聊这件事

2026 年,大模型不再只是「聊天框里吐一段字」。它要填工单、下订单、调接口、写报表——这些下游系统只认结构化的、类型正确的数据,不认「看起来像 JSON 的自由文本」。一句话:模型输出能不能被机器可靠消费,直接决定它能不能进生产。

但很多团队在这件事上摔得最惨。他们以为 prompt 里写一句「请严格以 JSON 格式返回,包含字段 a、b、c」就万事大吉,上线后发现:模型偶尔在 JSON 前面加一句「好的,这是结果:」;数字被当成字符串;必填字段偶尔漏掉;转义引号写错导致解析崩溃;字段名被悄悄换成同义词。每一个都是一次生产事故。

这篇文章的目标有四个。第一,厘清两条正交的原语:tool calling 是「中间动作」,structured output 是「最终答案」,别再把它们混为一谈。第二,讲清为什么「请返回 JSON」在生产不够,以及约束解码和事后校验为什么要结合。第三,给出 2026-09 三大厂商的精确参数和约束解码机制,照着抄就能用。第四,给你一套完整工程实现和 6 条踩坑手册,把「模型输出可被机器可靠消费」从愿望变成 SOP。

一句话结论先放这:结构化输出不是 prompt 工程,是系统工程。provider 给的 strict 保证要接,但客户端校验一道都不能省;温度没降、additionalProperties 漏写、strict 和 best-effort 混用,任何一个都能让凌晨的报警响起来。

二、技术背景与现状梳理

2.1 两条正交的原语

这是全文的地基,先把边界划干净。很多人把 function calling 和 JSON mode 当成同一件事,结果在架构上犯糊涂。它们是正交的,可以单独要求,也可以组合要求。

原语一:tool / function calling(工具调用)。模型在推理过程中输出一个「中间动作」:调用一个具名工具,并给出带类型的参数。它不是最终答案,而是「我要干这件事」。关键性质有三:第一,它是中间输出,模型通常还会继续推理、调用多个工具、最后才给最终回复;第二,参数有类型(来自工具 schema),但工具 schema 不等于最终响应 schema;第三,可以并行——一个回合里模型可能同时决定调用多个独立工具。

原语二:structured outputs / JSON mode(结构化输出)。模型的最终答案被约束成某个 JSON schema,且不产生任何工具调用。它回答的是「这件事的结论是什么」,而不是「我要调什么工具」。它的保证通常比 tool calling 更强——比如 OpenAI 的 strict 模式能保证输出必定符合 schema,不会出现字段缺失或类型错误(在不违反约束限制的前提下)。

两者怎么组合?一个典型 Agent 流程:先用 tool calling 决定「调搜索、调数据库」,最后用 structured outputs 把综合结论约束成固定 schema 返回给上游系统。也可以只要求其一:纯 RAG 问答只想要结构化答案,就只开 JSON mode;纯工具编排只想要动作,就只开 tool calling。

2.2 为什么 BFCL 这类基准重要

tool calling 的可靠性不是拍脑袋的,有公开基准在量。BFCL(Berkeley Function Calling Leaderboard)是 2026 年最被广泛引用的工具调用评测,它把「模型能否正确选工具、填对参数、处理并行与嵌套」拆成可量化指标。一个反直觉的事实是:很多在通用 benchmark 上分数很高的模型,在 BFCL 上会翻车——因为它们擅长聊天,不擅长「严格按 schema 填参数」。所以选型时不要只看 MMLU,要看 BFCL 这类「结构化消费」相关的分数。

2.3 2026 年的工程现状

几条已经形成共识的事实:

第一,「请返回 JSON」式的 prompt 约束,本质上是个 best-effort(尽力而为)。模型学会了「通常」会返回 JSON,但没有任何机制保证它这次一定返回。生产环境要的是 guarantee,不是 usually。

第二,主流厂商都在把结构化输出做成「协议级保证」。OpenAI 的 strict、Anthropic 的 output_config、Gemini 的 response_json_schema,都是在采样阶段就介入,而不是事后祈祷。

第三,自托管模型也能拿到保证。llama.cpp 的 GBNF、XGrammar、Outlines 这类工具,把 schema 编译成 token 级的约束,压到采样阶段做 mask,等价于厂商的 strict。这意味着结构化输出不是闭源特权。

三、核心原理拆解

这一章是全文最重的部分,我们按「为什么 prompt 不够 -> 约束解码 vs 事后校验 -> 逐厂商机制」拆开。

3.1 为什么「请返回 JSON」在生产不够

prompt 式约束的失效模式有五种,每一种都对应一类真实生产事故:

  • 解释前缀:模型在 JSON 前加「好的,以下是结果:」或者后面加「希望对你有帮助」。解析器遇到非 JSON 开头直接抛异常。
  • 漏必填:某个 required 字段偶尔没填,下游 obj["x"] 抛 KeyError。
  • 类型漂移:数字写成字符串(“123” 而不是 123),下游做数值运算时静默出错或类型错误。
  • 转义错误:字段内容里有引号、换行、反斜杠,模型没正确转义,JSON 解析失败。
  • 字段名漂移:模型把 user_id 写成 userId 或 userid,下游取不到值。
  • 这五种里,类型漂移和字段名漂移最阴险——它们不报错,只是让你的业务逻辑拿到 None 或错误类型,然后悄无声息地写出错误数据。这就是为什么纯靠 prompt 不够:它没有「硬约束」,只有「软默契」。

    3.2 两条防线:约束解码 vs 事后校验

    工程上对付上述问题有两条路线,它们不是二选一,而要叠加。

    约束解码(constrained decoding):在 token 采样阶段就介入。每当模型要输出下一个 token,解码器根据当前已生成的 JSON 结构和 schema,算出「哪些 token 是合法的」,把非法 token 的采样概率 mask 成 0。于是模型根本不可能输出一个不符合 schema 的 token 序列。这是最强保证——输出在构造时就合法。OpenAI strict、Anthropic strict tool schema、Gemini 的受限模式、自托管的 GBNF/XGrammar/Outlines 都属于这一类。代价是解码器变复杂、吞吐略有下降(要实时算合法 token 集合)。

    事后校验(post-hoc validation):模型自由生成,生成完再 parse 和校验。如果不符合(格式错、缺字段、类型错),就 reject 并重试或降级。代价是偶尔要重试,且如果模型反复生成非法输出会浪费 token。但它能兜住约束解码覆盖不到的「业务语义」问题(比如 quantity 是数字但 <= 0,schema 允许但业务不允许)。

    正确做法是两者结合:约束解码保证「语法合法、类型对、字段齐」,事后校验(尤其是业务规则校验)保证「语义正确」。技术约束管格式,业务约束管含义,各有各的边界。

    3.3 逐厂商约束解码机制(2026-09)

    把三大厂商的机制和参数摆在一张表里:

    厂商约束机制关键参数(2026-09)保证强度
    OpenAI strict 约束解码 Responses API: text.format={type:json_schema, strict:true};Chat Completions 用 response_format 强,输出必符合 schema
    Anthropic strict tool schema / output_config output_config={format:{type:json_schema}},已 GA 无 beta 强
    Gemini ANY 受限模式 response_mime_type + response_json_schema,propertyOrdering 控制顺序 强
    自托管 GBNF / XGrammar / Outlines 把 schema 编译成 token 级语法 强(取决于实现)

    注意:机制名字各家不同,但本质都是「采样期 mask 非法 token」。选择哪家的核心差异不在机制,而在参数细节和约束限制——下一节讲。

    3.4 OpenAI 的 strict 参数纪律

    OpenAI 的 strict 模式(2026-09 仍有效)对 schema 有一组硬性要求,违反任何一条都会报错或不给保证:

    • additionalProperties 必须显式设为 false。不写等于允许额外字段,strict 不保证字段封闭。
    • 所有字段都必须在 required 里。strict 下没有「可选字段」的概念,除非你用 union 把类型写成 ["x", "null"] 来表达「可空」。
    • 可选字段的写法:把类型声明为 ["string", "null"] 这类两元素数组,而不是用 required 省略。
    • 禁止的 schema 关键字:minimum / maximum、maxLength、pattern、递归($ref 自引用 / 递归结构)。这些在 strict 下不被支持,写了会报错。所以严格的范围 / 正则约束只能放到事后业务校验里做。

    这套纪律很反直觉的地方是「可选字段要用 [“x”,“null”]」。很多人习惯把可选字段从 required 里去掉,但在 strict 下这会破坏保证。记住:strict 的世界里,要么 required,要么 [“type”,“null”],没有第三种「可选」。

    3.5 Anthropic 的 output_config(已 GA)

    Anthropic 在 2026 年把结构化输出从 beta 的 output_format 推进到了 GA 的 output_config:

    output_config = { "format": { "type": "json_schema", "schema": {…} } }

    两个要点:第一,不要再使用旧的 output_format(beta 接口),新代码统一走 output_config,GA 意味着稳定性承诺更强、不会因为 beta 调整而变动。第二,Anthropic 的 strict tool schema 同样在采样期做约束,保证工具参数符合 schema;而 output_config 是把「最终响应」约束成 schema,二者机制一致、作用对象不同(一个是工具参数,一个是最终答案)。

    3.6 Gemini 的 response_json_schema 与 propertyOrdering

    Gemini 的写法是在请求里同时给 response_mime_type 和 response_json_schema:

    response_mime_type = "application/json"
    response_json_schema = { "type": "object", "properties": {…}, "propertyOrdering": ["a","b","c"] }

    propertyOrdering 是 Gemini 特有的一个细节:JSON 对象本身是无序的,但有些下游系统(特别是强顺序依赖的解析或人眼比对)希望字段按固定顺序排列。propertyOrdering 显式指定字段输出顺序,避免每次顺序漂移带来的 diff 噪声。如果你 downstream 不在乎顺序,可以不用;但如果你的管线对字段顺序敏感(比如做签名、做二进制比对),这个参数很关键。

    3.7 自托管的约束解码:GBNF / XGrammar / Outlines

    如果你跑的是开源模型(llama.cpp、vLLM 自托管、TGI 等),也能拿到等价保证:

    • GBNF:llama.cpp 的语法格式,手写或自动从 schema 生成。它把 JSON schema 编译成一个上下文无关文法,解码时只允许走合法的产生式。
    • XGrammar:字节跳动开源的高性能约束解码引擎,把语法编译成高效的位掩码,吞吐损失很小,被多个推理框架集成。
    • Outlines:Python 侧把 Pydantic / JSON schema 转成约束,配合 transformers 系模型使用,适合研究和中小规模部署。

    这类方案的价值在于:你不需要依赖闭源厂商的 strict 实现,也能让本地模型输出「语法合法、类型对、字段齐」的 JSON。但同样的,业务语义校验仍要自己做。

    四、环境与依赖准备

    4.1 依赖清单

    按本文的工程实现(Python 为主,附 Node/Zod 说明),版本组合(2026-10 验证):

    # Python 侧:schema 定义 + 三大厂商 SDK + 校验
    pip install -U "pydantic>=2.7"
    pip install -U "openai>=1.50" # Responses API / Chat Completions
    pip install -U "anthropic>=0.40" # output_config
    # Gemini 通过 google-generativeai 或 openai 兼容端点
    pip install -U "google-generativeai>=0.8"

    # Node 侧(如需 Zod):npm install zod
    # 自托管约束解码(可选)
    pip install -U "outlines" # 或 xgrammar

    两个坑提前避:第一,Pydantic 一定要 2.x,1.x 的 schema 导出 API 不同,本文代码基于 v2;第二,OpenAI SDK 的 Responses API 是较新的接口,旧版本没有 text.format,装完确认版本号。

    4.2 最小可用验证

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    """结构化输出环境自检:确认 pydantic / SDK 可用。"""
    import importlib
    import sys

    def have(mod):
    try:
    importlib.import_module(mod)
    return True
    except Exception:
    return False

    def main():
    for mod, label in (("pydantic", "Pydantic"),
    ("openai", "OpenAI SDK"),
    ("anthropic", "Anthropic SDK"),
    ("google.generativeai", "Gemini SDK")):
    print("%-22s %s" % (label, "OK" if have(mod) else "未安装"))
    from pydantic import BaseModel, Field
    print("Pydantic 版本能力: v2 导出 JSON schema 可用")

    if __name__ == "__main__":
    main()

    五、完整实操

    5.1 第一步:用 Pydantic 定义 schema(单一事实源)

    所有可靠性都从「一份权威 schema」开始。不要在前端、prompt、校验里各写一遍字段,用 Pydantic 当单一事实源,导出 JSON schema 给模型,解析时也用它。

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    """订单结构化输出:Pydantic 单一事实源 + 业务规则校验。"""
    from enum import Enum
    from typing import Optional
    from pydantic import BaseModel, Field, field_validator

    class Priority(str, Enum):
    LOW = "low"
    NORMAL = "normal"
    HIGH = "high"

    class OrderItem(BaseModel):
    sku: str = Field(..., description="商品编码,形如 SKU-XXXX")
    quantity: int = Field(..., ge=1, description="购买数量,必须为正整数")
    price: float = Field(..., description="单价,纯数字,不含货币符号")

    class Order(BaseModel):
    order_id: str = Field(..., description="订单号,形如 ORD-YYYYMMDD-XXXX")
    items: list[OrderItem] = Field(..., min_length=1)
    priority: Priority
    requires_review: bool = Field(..., description="是否需人工复核")
    note: Optional[str] = None # strict 下需用 ["string","null"] 表达

    @field_validator("order_id")
    @classmethod
    def check_order_id(cls, v):
    if not v.startswith("ORD-"):
    raise ValueError("order_id 必须以 ORD- 开头")
    return v

    注意 price 是 float 不是 str——这正是「显式类型」纪律的体现:货币金额是数字,不要在 schema 里写成带货币符号的字符串,否则下游做金额运算时会类型崩溃。

    5.2 第二步:向 OpenAI 请求 strict 结构化输出

    把 Pydantic 导出的 schema 喂给 OpenAI 的 Responses API,并遵守 strict 纪律(additionalProperties:false、全部 required、可选字段用 [“string”,“null”]):

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    """OpenAI Responses API strict 结构化输出请求骨架。"""
    import json
    from openai import OpenAI
    from order_schema import Order # 见 5.1

    def build_strict_schema(model: type) –> dict:
    """把 Pydantic schema 改成 strict 友好:补 additionalProperties:false。"""
    schema = model.model_json_schema()
    schema["additionalProperties"] = False
    # strict 要求所有属性进 required
    schema["required"] = list(schema.get("properties", {}).keys())
    return schema

    def ask_openai(client: OpenAI, user_text: str) –> dict:
    resp = client.responses.create(
    model="gpt-4o-mini",
    input=user_text,
    text={
    "format": {
    "type": "json_schema",
    "name": "order",
    "strict": True,
    "schema": build_strict_schema(Order),
    }
    },
    temperature=0.0, # 结构化输出必须降温,见第七章
    )
    return json.loads(resp.output_text)

    if __name__ == "__main__":
    c = OpenAI()
    out = ask_openai(c, "用户想买 2 件 SKU-A100,单价 9.9,加急,需复核")
    print(json.dumps(out, ensure_ascii=False, indent=2))

    temperature=0.0 不是可选项——第七章第五坑会讲,温度没降偶发格式错。

    5.3 第三步:向 Anthropic 请求 output_config

    Anthropic 走 GA 的 output_config,不再用 beta 的 output_format:

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    """Anthropic output_config 结构化输出请求骨架。"""
    import json
    from anthropic import Anthropic
    from order_schema import Order

    def ask_anthropic(client: Anthropic, user_text: str) –> dict:
    resp = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    temperature=0.0,
    messages=[{"role": "user", "content": user_text}],
    output_config={
    "format": {
    "type": "json_schema",
    "schema": Order.model_json_schema(),
    }
    },
    )
    # output_config 保证 text 即合法 JSON
    return json.loads(resp.content[0].text)

    if __name__ == "__main__":
    c = Anthropic()
    out = ask_anthropic(c, "下单:3 件 SKU-B200,单价 19.9,普通优先级")
    print(json.dumps(out, ensure_ascii=False, indent=2))

    5.4 第四步:Gemini 的 response_json_schema 请求体

    Gemini 用 response_mime_type + response_json_schema,并用 propertyOrdering 固定字段顺序:

    {
    "contents": [
    { "role": "user", "parts": [{ "text": "下单:1 件 SKU-C300,单价 5.0,高优先级,需复核" }] }
    ],
    "generationConfig": {
    "responseMimeType": "application/json",
    "responseSchema": {
    "type": "OBJECT",
    "properties": {
    "order_id": { "type": "STRING" },
    "items": {
    "type": "ARRAY",
    "items": {
    "type": "OBJECT",
    "properties": {
    "sku": { "type": "STRING" },
    "quantity": { "type": "INTEGER" },
    "price": { "type": "NUMBER" }
    }
    }
    },
    "priority": { "type": "STRING", "enum": ["low", "normal", "high"] },
    "requires_review": { "type": "BOOLEAN" }
    },
    "propertyOrdering": ["order_id", "items", "priority", "requires_review"]
    }
    }
    }

    5.5 第五步:解析 + 业务规则二次校验 + 单次重试 + 人工降级

    这是全文最该被抄走的工程骨架。约束解码保证「格式对」,但「业务对」要靠这里:

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    """结构化输出完整管线:解析 -> 业务校验 -> 单次紧化重试 -> 人工降级。"""
    import json
    import sys
    from order_schema import Order, Priority

    def parse_and_validate(raw: str, user_text: str) –> Order:
    """解析 JSON 并做 Pydantic + 业务规则校验,失败抛异常。"""
    data = json.loads(raw) # 格式错在此抛出
    order = Order(**data) # 类型/必填/字段名在此保证
    # 业务规则二次校验(schema 管不了语义)
    total = sum(i.price * i.quantity for i in order.items)
    if total <= 0:
    raise ValueError("订单总金额必须为正")
    if order.requires_review and order.priority == Priority.LOW:
    raise ValueError("需复核的订单不应是 low 优先级")
    return order

    def pipeline(ask_fn, user_text: str, max_retry: int = 1) –> Order:
    last_err = None
    for attempt in range(max_retry + 1):
    try:
    raw = ask_fn(user_text, feedback=str(last_err) if last_err else None)
    return parse_and_validate(raw, user_text)
    except Exception as exc: # noqa: BLE001
    last_err = exc
    print("[retry %d] 校验失败: %s" % (attempt, exc))
    # 兜底:降级人工队列,绝不直接把脏数据写库
    print("降级人工队列: %s" % user_text)
    raise RuntimeError("structured output 多次失败,已转人工")

    if __name__ == "__main__":
    # 示例 ask_fn:真实环境里替换为 5.2/5.3/5.4 的调用
    def fake_ask(text, feedback=None):
    return ('{"order_id":"ORD-20261003-0001",'
    '"items":[{"sku":"SKU-A100","quantity":2,"price":9.9}],'
    '"priority":"high","requires_review":true}')
    order = pipeline(fake_ask, "用户下单")
    print("解析成功:", order.order_id, "总额",
    sum(i.price * i.quantity for i in order.items))

    关键设计:provider 给了 strict 保证,我仍然在客户端用 Pydantic 再验一遍。因为 strict 不覆盖业务语义(quantity>0、order_id 格式、优先级与复核的一致性),而这些都是脏数据进库的高频来源。即使有一天 provider 升级绕过了保证(第七章第四坑),客户端这层仍能兜底。

    5.6 第六步:tool calling 与 structured output 组合

    一个 Agent 既要用工具、最后又要结构化答案时,组合写法(以 OpenAI 为例):

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    """组合示例:先用 tool calling 查库存,再用 structured output 返回结论。"""
    import json
    from openai import OpenAI

    def ask_with_tools(client: OpenAI, user_text: str) –> dict:
    # 第一步:开 tool calling,让模型决定查库存
    resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": user_text}],
    tools=[{
    "type": "function",
    "function": {
    "name": "check_stock",
    "parameters": {
    "type": "object",
    "properties": {"sku": {"type": "string"}},
    "required": ["sku"],
    },
    },
    }],
    )
    # 第二步:拿到工具结果后,再要求 structured output 返回最终结论
    # (此处省略第二次调用的 json_schema 约束,复用 5.2 的 build_strict_schema)
    return resp.choices[0].message

    if __name__ == "__main__":
    c = OpenAI()
    msg = ask_with_tools(c, "SKU-A100 有货吗?给我一个结构化结论")
    print(json.dumps(msg.model_dump(), ensure_ascii=False, indent=2))

    六、结果对比与数据分析

    6.1 三种保证强度的对照

    把「纯 prompt」「约束解码」「约束解码 + 业务校验」三种方案放在同一维度比:

    方案格式合法类型正确字段齐业务语义正确额外成本
    纯 prompt「请返回 JSON」 偶尔失败 偶尔漂移 偶尔漏 不保证 几乎无
    约束解码(strict/GBNF) 保证 保证 保证 不保证 解码略慢
    约束解码 + 业务校验 保证 保证 保证 保证 解码略慢 + 校验代码

    结论很清楚:只有第三行能进生产。第一行是 demo 级别,第二行能挡住语法/类型事故但挡不住「quantity=-1」这种语义事故。

    6.2 约束解码 vs 事后校验的取舍

    维度约束解码事后校验
    失败时机 构造期就合法 生成完才发现
    能否 100% 保证格式 能(在约束限制内) 不能,依赖 retry
    能否覆盖业务语义 不能(schema 不管语义) 能(写业务规则)
    吞吐影响 略降(算合法 token) 重试时浪费 token
    适用 所有结构化输出 作为兜底叠加

    所以二者是互补不是替代:约束解码做「第一道语法闸」,事后校验做「第二道语义闸」。

    6.3 逐厂商参数速查表

    厂商接口位置strict/约束开关必写纪律禁止关键字
    OpenAI Responses: text.format;CC: response_format strict:true additionalProperties:false、全 required、可选用 [“t”,“null”] minimum/maxLength/pattern/递归
    Anthropic output_config.format 默认约束 走 json_schema 同 OpenAI 类约束
    Gemini response_mime_type+response_schema ANY 受限 用 propertyOrdering 控顺序 视实现

    6.4 一个真实事故的量级

    community 里反复出现的案例:某订单系统用纯 prompt 约束,上线首周约 3% 的请求产出非法 JSON 或字段漂移,其中约一半是「类型漂移」(价格变字符串),导致下游对账系统把一批订单金额算成 0。换成 strict + 业务校验后,格式事故归零,业务语义事故(quantity<=0、order_id 格式错)由校验层拦截,转人工处理。3% 听起来不大,但在日均百万订单的体量下是每天三万次脏数据——足以让财务对账崩溃。

    七、踩坑记录与排查手册

    7.1 踩坑 1:漏写 additionalProperties:false,冒出多余字段

    现象:strict 模式下,模型偶尔多返回一个文档里没定义的字段(比如多塞一个 confidence),下游系统虽然大多忽略,但有几个严格校验的消费者直接报错。

    定位:看发出的 schema,发现 additionalProperties 没显式写 false。OpenAI strict 要求显式声明,不写等于允许额外字段,保证被破坏。

    原因:很多人以为 strict 默认就封闭字段,实际上必须显式 additionalProperties:false。漏写后,模型可以在 schema 之外自由加字段,破坏「字段封闭」的承诺。

    解决:schema 生成函数里强制补 schema["additionalProperties"] = False,并在单测里断言。这是一行代码,但漏了就是一类事故。

    7.2 踩坑 2:嵌套层级 / 属性数超限

    现象:把一个很深的嵌套 schema(比如 6 层嵌套、单对象 60 个属性)交给 strict,返回报错或模型吐出截断/畸形的 JSON。

    定位:查厂商的 schema 限制。strict 对嵌套深度、单对象属性数量、枚举数量都有上限(不同厂商数值不同,常见是嵌套不超过若干层、属性总数有上限)。

    原因:约束解码的实现要在采样期维护「当前合法 token」的状态机,schema 太复杂会让状态空间爆炸,要么报错要么退化成 best-effort。

    解决:把超复杂 schema 拆成「先抽骨架、再补细节」的多步调用——第一步让模型返回 id 列表,第二步逐个补全详情。或者在平面化 schema 与业务可读性之间做权衡,能平铺就别深层嵌套。

    7.3 踩坑 3:把 strict 与 best-effort JSON mode 混用

    现象:代码里一部分走 strict(保证),一部分走 response_format={"type":"json_object"} 的 best-effort JSON mode(不保证),两边行为不一致,排查时一头雾水。

    定位:grep 全局的 response_format / text.format,发现混用了两种模式:一种带 strict,一种只要求 json_object。

    原因:best-effort JSON mode 只是「鼓励」模型输出 JSON,不保证字段和类型;strict 才保证。混用意味着你的系统一部分有硬保证、一部分没有,故障只在「没有」的那部分冒出来,定位成本高。

    解决:统一走 strict(或等价的强约束),不要在同一管线里混用。如果某个场景确实不需要强约束(比如内部 debug 工具),也要显式标注,别让它和强约束路径混在同一个工具函数里。

    7.4 踩坑 4:provider 升级绕过保证

    现象:某次 SDK 或模型版本升级后,原本稳定的结构化输出开始偶发字段缺失或类型漂移,但代码没改。

    定位:比对升级前后的实际返回。发现新版本的 strict 实现在某些 schema 形态下退化(或默认行为变化),保证被悄悄削弱。

    原因:厂商的 strict 实现本身在迭代,个别版本会引入回归;或模型默认版本切换(比如 gpt-4o 指向的底层模型变了),而新模型的约束遵循度略有差异。provider 的「保证」是版本相关的,不是永恒真理。

    解决:客户端永远再校验一遍。即使 provider 承诺 strict,本地 Pydantic 校验这道闸不能省——它是对 provider 回归的兜底。同时锁模型版本(用具体 snapshot / 日期版本),不要把 model 写成会漂移的别名。

    7.5 踩坑 5:温度没降,偶发格式错

    现象:strict 模式下仍然偶发(比如千分之一)输出非法 JSON,重试就好,但高峰期堆积。

    定位:看请求参数,发现 temperature 设成了 0.7(沿用聊天配置的默认值)。

    原因:约束解码通常要求 temperature 为 0 才能稳定走合法路径;温度偏高时,采样会在边界 token 上「试探」非法分支,虽然后端大多能纠正,但偶发会漏。这是最容易忘、却最高频的一坑。

    解决:所有结构化输出请求强制 temperature=0.0,在 SDK 封装层写死,不允许业务侧覆盖。把它当基础设施约束,而不是可选项。

    7.6 踩坑 6:依赖 schema 却不做业务校验,order_id 格式错照样过

    现象:系统用 strict 保证字段齐全、类型正确,但运营发现库里出现一批 order_id 为 12345、abc 这类明显非法的值,模型「严格地」填了错格式的字符串。

    定位:schema 里 order_id 只声明了 type:string,没有格式约束(strict 禁止 pattern),于是任何字符串都合法通过。

    原因:这是「约束解码管语法、不管语义」的典型后果。strict 禁止 pattern / minLength 这类约束,意味着「字符串长什么样」只能靠业务校验。只依赖 schema 的团队,天然放过了所有「类型对但语义错」的数据。

    解决:在客户端加业务规则校验层(见 5.5):order_id 必须匹配 ^ORD-\\d{8}-\\d{4}$、quantity>0、priority 与 requires_review 的一致性等。schema 是格式闸,业务校验是语义闸,两道都要。

    八、优化方案与进阶打法

    8.1 Schema 设计纪律

    把 3.4 和第七章的教训收成几条可执行的纪律:

    • required 越少越好:每一个 required 都是潜在失败点。能默认、能推断的就不要塞进 required。strict 下 required 是强制的,所以「可选」要用 ["type","null"] 表达,而不是省略。
    • 显式类型:price 是 number 不是「含货币符的字符串」;quantity 是 integer 不是 string。类型错了下游必崩。
    • 封闭选择用 enum:优先级、状态这类有限集合用 enum,别用自由 string 让模型自己编词。
    • 避免宽 string 字段:一个 description: string 装所有信息的设计,等于把结构化输出退化成非结构化。该拆字段就拆。
    • description 不替代约束:description 是给模型看的提示,不是给解析器看的保证。能用类型/enum/required 表达的,别依赖 description 让模型「自觉」。

    8.2 重试与缓存的工程取舍

    重试:单次「紧化重试」即可——把上一次的校验错误作为 feedback 喂回模型(5.5 的 feedback 参数),让它知道哪里错了。不要无限重试:超过一次就降级人工队列,否则高峰期会卡死在反复生成上。

    语义缓存:结构化输出常配合语义缓存省 token。但缓存的相似度阈值不能调太高——阈值过高会把「语义不同但表面相似」的请求当成同一题,返回答非所问的结构化结果。阈值设定要基于你的业务容忍度做 A/B,别拍脑袋。

    8.3 多厂商 schema 的翻译层

    如果你的系统要同时接 OpenAI / Anthropic / Gemini,不要为每家写一份 schema。用一个中立表示(Pydantic 是天然选择),导出 JSON schema 后,按各厂商的纪律做轻量翻译(补 additionalProperties、处理 optional 写法、加 propertyOrdering)。翻译层集中维护,避免三家行为漂移。

    九、适用场景与选型建议

    9.1 什么情况必须上结构化输出

    • 模型输出要直接进数据库、进下游 API、进报表——任何「下游是机器」的场景。
    • 输出要被代码稳定解析,不能容忍偶发格式错。
    • 多步骤 Agent 之间要用结构化消息传递状态。

    9.2 什么情况不该这么上

    不该只用 prompt 约束:生产环境里「请返回 JSON」不够,必须上 strict 或等价约束解码。

    不该混用 strict 与 best-effort:同一管线行为不一致,故障只在弱约束那侧冒头。

    不该跳过业务校验:schema 管语法不管语义,依赖它放过了所有「类型对但语义错」的脏数据。

    不该无限重试:单次紧化重试后直接降级人工,避免高峰期卡死。

    不该把语义缓存阈值调太高:会答非所问,且错误是结构化的(更难被发现)。

    9.3 一句话决策

    下游是机器?
    +– 是 -> 需要工具动作?
    +– 是 -> 开 tool calling(strict tool schema)
    +– 否 -> 开 structured output(strict / output_config / response_json_schema)
    两者都要?先 tool calling 再 structured output 收尾
    +– 否 -> 纯聊天,随意
    铁律: temperature=0 + additionalProperties:false + 客户端 Pydantic 再验 + 业务规则校验

    十、总结与展望

    把全文的账重算一遍,结论是五条:

    第一,结构化输出是系统工程,不是 prompt 工程。一句「请返回 JSON」在 demo 里能跑,在生产里是 3% 的脏数据率,是财务对账崩溃。必须上协议级约束。

    第二,两条原语正交:tool calling 是中间动作,structured output 是最终答案。组合使用时先动作后结论,别混淆。

    第三,约束解码和事后校验必须叠加。前者管语法(格式、类型、字段),后者管语义(quantity>0、order_id 格式、业务一致性)。任何一道缺失都会放过错类事故。

    第四,provider 的 strict 保证是版本相关的,客户端校验不能省。温度没降、additionalProperties 漏写、strict 与 best-effort 混用、provider 升级绕过保证——这四类坑都靠「客户端再验一遍」兜底。

    第五,schema 设计本身就是可靠性的一部分。required 越少越好、类型显式、封闭选择用 enum、description 不替代约束。烂 schema 再强的约束也救不回来。

    往前看,两件事值得关注。一是 BFCL 这类「结构化消费」基准会越来越重要,模型在聊天榜上的分数会越来越多地被「能不能正确填参数」稀释。二是 约束解码会下沉到更多自托管框架,XGrammar 这类高性能实现让本地模型的严格结构化输出几乎零成本,闭源 strict 不再是护城河。但无论机制怎么演进,「客户端校验 + 业务规则 + temperature=0」这套铁律不会变。

    参考素材与延伸阅读

    • Structured Output: 各厂商能力对照(OpenAI / Anthropic / Gemini) — AI Wiki,2026-09
    • How to Get Structured JSON Output from LLMs (2026 Guide): 逐厂商参数 — Andrew 的指南,2026-09
    • Structured Outputs and Constrained Decoding: 约束解码 vs 事后校验,订单 schema 实例 — Technology on the Net,2026-09
    • Reliable Tool Calling: 两种原语正交、BFCL 基准、常见故障模式 — ChangeGamer AI,2026-09
    • OpenAI Structured Outputs 官方文档(strict / json_schema) — OpenAI,2026-10
    • Anthropic Messages API: output_config(GA) — Anthropic,2026-10
    • Gemini API: response_json_schema 与 propertyOrdering — Google,2026-10
    • Outlines: 约束解码 Python 库 — dottxt,2026
    • XGrammar: 高性能约束解码引擎 — xgrammar-project,2026
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 结构化输出与 Function Calling 工程化:让模型输出可被机器可靠消费
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!