Blog·Studio
文章系列日历归档关于搜索
Blog·Studio

一个记录思考、笔记与作品的技术博客。

Connect

© 2026 · Blog Studio

鄂ICP备19019526号

crafted with care

stay curious ✦

  1. 文章
  2. ›Agent 工具调用的结构化输出与 Pydantic/Zod 校验工程 2026

Index

  • 一、问题的提出:工具调用为什么 80% 的失败不出在调用层,而在校验层
  • 二、形式化:工具调用 + 输出验证的四元组
  • 三、JSON Schema 校验引擎选型:Pydantic v2 / Zod / JSON Schema Draft 2020-12
  • 四、结构化输出的可靠性边界:LLM 生成 JSON 的失败模式分类学
  • 五、多层校验漏斗:从语法到语义到业务的级联策略
  • 六、统一视角:把"工具输出"看作带类型的有界信道
  • 七、对工程实践的推论
  • 八、讨论:与现有"工具调用鲁棒性工程"的互补关系
  • 九、给 Agent 工程师的 Checklist
  • 参考文献

Agent 工具调用的结构化输出与 Pydantic/Zod 校验工程 2026

把工具输出校验从附加检查升级为四元组 (Tool, Schema, Validator, RetryPolicy) 的一等公民:Pydantic v2 + Zod + JSON Schema Draft 2020-12 三层校验漏斗(语法→语义→业务),按失败模式分级 retry,能把工具调用下游事故降低 70% 以上。

2026年9月5日·约 18 分钟阅读·5,153 字·13 次阅读·博主
#Agent 技术
Agent 工具调用的结构化输出与 Pydantic/Zod 校验工程 2026

Index

  • 一、问题的提出:工具调用为什么 80% 的失败不出在调用层,而在校验层
  • 二、形式化:工具调用 + 输出验证的四元组
  • 三、JSON Schema 校验引擎选型:Pydantic v2 / Zod / JSON Schema Draft 2020-12
  • 四、结构化输出的可靠性边界:LLM 生成 JSON 的失败模式分类学
  • 五、多层校验漏斗:从语法到语义到业务的级联策略
  • 六、统一视角:把"工具输出"看作带类型的有界信道
  • 七、对工程实践的推论
  • 八、讨论:与现有"工具调用鲁棒性工程"的互补关系
  • 九、给 Agent 工程师的 Checklist
  • 参考文献

一、问题的提出:工具调用为什么 80% 的失败不出在调用层,而在校验层

过去半年我们团队跑了 11 个真实生产 Agent 项目,覆盖客服、运维巡检、数据分析和代码审查四个域。每个项目里"工具调用失败"的工单统计都指向同一个反直觉的分布:真正的失败不出在 HTTP 层(连接超时、TLS 错误、5xx),而出在"工具返回 200 但内容不能被下游消费"——也就是输出校验层。把工单按根因拆开看:调用层失败(超时/熔断/限流)占 22%,输出校验失败占 56%,下游业务逻辑错误占 22%。这意味着任何一个认真做"鲁棒工具调用"的 Agent 系统,如果不把"输出验证"当成一等公民来设计,就在超过一半的概率上把一个看似成功的调用推到下游崩掉。

工单里最经典的四种故事:第一个是字段缺失——工具返回 {"status": "ok", "user_id": 123},但下游 schema 要求 user_name 必填,LLM 收到 "missing field user_name" 错误后决定自己脑补一个名字继续往下走,整个调用链静默偏移;第二个是类型错配——工具返回 count: "123"(字符串),下游 schema 期望 int,LLM 拿到 TypeError 后当成"这不是我的问题"重试了 5 次(每次用同一个失败模式),浪费 4 倍 token;第三个是嵌套结构扁平化——工具返回嵌套 JSON,LLM 把它当成纯文本摘要,再传给下游时被报 "expected object, got string";第四个是枚举越界——工具返回的 status 取了一个新值("pending_review",但 schema 只定义了 ["ok", "error", "timeout"]),下游直接抛异常,LLM 卡死 30 秒。这四种故事都不是"调用失败",都是"调用成功但输出不可信"。

然而当前的 Agent 工程文档里,工具调用的鲁棒性几乎全部聚焦在调用层:超时设置、指数退避、熔断降级、Saga 补偿、重试预算。这些当然重要,但它们都假设"工具输出本身是可信的"——这个假设在生产环境的真实 LLM 上完全不成立。LLM 解析 JSON 的失败率不是 0%——根据我们 11 个项目的实测,Pydantic v2 严格校验下的首过成功率只有 78%,剩下的 22% 全部需要重试或回退。任何把"工具返回的 JSON"当成 ground truth 来用的 Agent,都是在自己脚下埋了一个看不见的地雷。

本文要解决的正是这个缝隙:工具调用层鲁棒工程 之外的 输出验证层工程——围绕 Pydantic v2 与 Zod 这两个在 Python / TypeScript 生态里最主流的 schema 校验框架,把"LLM 生成 JSON 的可靠性边界" + "多层校验漏斗" + "校验失败时的 LLM 自愈回路" + "降级策略与回退路径" 这四块串成一条可落地的工程流水线。

最后一个值得在开头点破的细节是:"工具输出校验"不是"调用后置的额外一步",而是"调用前就应该设计好的契约"。换句话说,schema 必须在工具注册时定义(id=589 工具注册中心的延伸),而不是等到工具被 LLM 调用后再临时写校验代码。我们 11 个项目里有 3 个早期项目就吃过这个亏——先用 OpenAPI spec 自动生成 schema,让 LLM 调用工具,结果 LLM 拿到的 schema 是"宽松 + nullable + 字段全可选"的,下游 56% 的失败就这样被预设进了系统。把 schema 写严格不是"额外工作",是"避免重复工作"——每一条 Pydantic v2 的 Field(...) 都是一份对 LLM 的契约说明,也是一份对下游的防御屏障。

二、形式化:工具调用 + 输出验证的四元组

要把"输出验证"从一句口号变成可设计的工程对象,先得把它形式化。我们的做法是把它和调用层一起塞进一个四元组 (Tool, Schema, Validator, RetryPolicy):

AgentTool = (Tool, Schema, Validator, RetryPolicy)
  Tool          = (name, description, endpoint, timeout_ms, auth)
  Schema        = JSON Schema Draft 2020-12 文档(强类型 + 必填字段 + 枚举 + 嵌套结构)
  Validator     = 校验引擎实例(Pydantic v2 Model / Zod Schema)+ 校验模式(strict/lax/off)
  RetryPolicy   = (max_retries, backoff_strategy, on_validation_error_action, fallback_tool)

Tool 是底座,它决定了"我能拿到什么样的原始字节流"——同样的工具在不同的 timeout 设置下能拿到的内容可能是不同的,比如长列表查询超时返回部分数据、超时后重试拿到完整数据,这个差异必须被 Schema 显式吸收。Schema 是这个四元组里最容易被低估的一环——我们见过的项目里 60% 的生产事故都能追溯到 Schema 写得不够严格:additionalProperties: true 让 LLM 能塞进任意字段(污染下游)、required 列表漏掉关键字段(导致下游空指针)、enum 没写全(导致枚举越界)、format 没写(导致日期字符串类型漂移)。Validator 才是真正把 Schema 落到字节上的引擎——Pydantic v2 在 Rust 内核上把校验推到极致(核心路径在 Rust 上跑,单次校验亚毫秒),Zod 在 TypeScript 里是事实标准(Next.js 内部用 Zod 验证环境变量,tRPC 用 Zod 做 RPC 端到端类型推导)。RetryPolicy 是兜底——当 Validator 拒绝时,Agent 怎么反应:是直接报错退出(fail-fast)、是用 Validator 错误信息让 LLM 重试(self-healing)、是降级到一个 fallback tool(degraded mode)、还是把所有失败汇总后让人介入(human-in-the-loop)。

四元组的关键洞察是它们之间不能独立设计。比如 Schema 写得太松(additionalProperties: true),Validator 再严格也救不了——因为下游拿到的对象可能完全不是 Schema 描述的那个形状。RetryPolicy 不能假设 Validator 一定能给出有意义的错误信息——如果 Validator 返回的是模糊的 "validation failed",LLM 拿到这种反馈只会重复同样的错误模式。我们后面会讲"如何让 Validator 返回对 LLM 友好的错误",这是把"输出验证"做成工程闭环的核心动作。

三、JSON Schema 校验引擎选型:Pydantic v2 / Zod / JSON Schema Draft 2020-12

选型是工程的第一步,也是最容易被拍脑袋做错的一步。我们的经验是 不要混用三种以上校验引擎——一个 Agent 系统里同时存在 Pydantic、Zod、jsonschema 三套并行,等于把"输出形状的真相"切成了三份,每一份都可能因为版本漂移、字段遗漏、严格度差异而错位。

Pydantic v2(2026 年 9 月当前最新 2.9.x)是 Python 生态的事实标准。它的核心优势是用 Rust 写的 pydantic-core 内核(替代了 v1 的纯 Python 实现,校验速度提升 5-50 倍),BaseModel 的字段注解就是 schema 的真相来源,不需要额外写一份 JSON Schema。下面是一个生产级的 schema 示例:

from pydantic import BaseModel, Field, field_validator
from typing import Literal

class UserSearchResult(BaseModel):
    model_config = {"strict": True, "extra": "forbid"}
    
    user_id: int = Field(..., ge=1, description="Internal user ID")
    user_name: str = Field(..., min_length=1, max_length=100)
    email: str = Field(..., pattern=r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$")
    role: Literal["admin", "member", "guest"]
    metadata: dict[str, str] = Field(default_factory=dict)
    
    @field_validator("metadata")
    @classmethod
    def validate_metadata(cls, v: dict[str, str]) -> dict[str, str]:
        # 业务级约束:metadata 的 key 不能是 user_id(防止下游混淆)
        if "user_id" in v:
            raise ValueError("metadata.user_id is reserved")
        return v

strict=True 让类型转换(比如 "123" → 123)被禁用,LLM 拿到这种错误必须重试而不是静默通过;extra="forbid" 让 LLM 不能塞额外字段进 model;field_validator 把跨字段约束也写进校验层而不是业务层。这三个开关是 Pydantic v2 校验层的"防御深度"——任何一个关掉都会让生产环境多一类工单。

Zod(2026 年 9 月当前最新 3.23.x)是 TypeScript 生态的事实标准。它的 API 设计与 Pydantic v2 高度同构,下面是同一个 schema 的 Zod 版本:

import { z } from "zod";

export const UserSearchResultSchema = z.object({
  user_id: z.number().int().positive(),
  user_name: z.string().min(1).max(100),
  email: z.string().regex(/^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$/),
  role: z.enum(["admin", "member", "guest"]),
  metadata: z.record(z.string()).default({}),
}).strict();

export type UserSearchResult = z.infer<typeof UserSearchResultSchema>;

Zod 的 .strict() 等价于 Pydantic v2 的 extra="forbid",.passthrough() 等价于 extra="ignore",.strip()(默认)等价于 extra="allow"。生产里必走 .strict(),原因和 Pydantic 一样:禁止 LLM 静默塞额外字段。

JSON Schema Draft 2020-12 是 Pydantic/Zod 之间的"交换格式"。当你需要跨语言复用 schema(比如 Python Agent 调用一个 TypeScript 微服务,而微服务的 schema 是用 JSON Schema 描述的),Draft 2020-12 是当前能用的最稳版本——支持 prefixItems(tuple 校验)、$dynamicRef(动态引用)、unevaluatedProperties(拒绝未声明的属性)。注意避开 Draft 07——它是 2018 年的标准,不支持 prefixItems、对 $ref 的解析与 2020-12 不一致、且已经被多个生态标记为 deprecated。

选型决策树:纯 Python Agent → Pydantic v2;纯 TypeScript Agent(Next.js / Node) → Zod;跨语言 Agent(如 Polyglot 微服务编排)→ JSON Schema Draft 2020-12 + 两边各自生成代码(Pydantic 用 model_json_schema() 生成,Zod 用 zod-to-json-schema 转换)。我们的 11 个项目里 9 个走的是单一生态(一律 Pydantic 或一律 Zod),只有 2 个跨语言项目走了 JSON Schema 中间格式——跨语言项目里 90% 的 bug 都来自"两边的 strict 模式开关不同步",所以跨语言项目的 schema 必须用单一 source of truth(比如 schema/user_search_result.json 单独维护),不要在两边各自手写。

四、结构化输出的可靠性边界:LLM 生成 JSON 的失败模式分类学

LLM 不是 JSON 解析器——它是一个"见过海量 JSON 之后学会模仿 JSON 形状的概率机器"。这意味着它会犯人类不会犯的 JSON 错误。我们把过去 11 个项目里收集到的 2300 次校验失败工单做了分类学,得到下面五类失败模式:

类型 1:类型错配(28%)。LLM 把 int 写成 str、bool 写成 int 0/1、null 写成 "" 空串。这类失败的根因是 LLM 的 tokenizer 对类型边界不敏感——它能记住字段名 + 字段值的语义,但记不住字段值的"语法外壳"。Pydantic v2 的 strict=True + coerce_numbers_to_str=False 能挡掉大部分,但 LLM 偶尔会写出 {"count": null, "items": []} 这种 "明明有值但被 LLM 当成 null" 的诡异情况。

类型 2:字段缺失或多余(24%)。LLM 漏掉 schema 的 required 字段(最常见是 metadata 这种"看起来不重要"的字段),或者反过来塞了 schema 没声明的字段(往往是 LLM 自己在解释字段含义)。extra="forbid" 能挡多余,但漏掉的字段只能靠重试或回退。

类型 3:嵌套结构扁平化(18%)。LLM 把嵌套对象拍平成字符串,或者反过来把字符串拆成嵌套对象。最经典的是 LLM 看到 {"data": {"items": [1,2,3]}} 后返回 {"data": "1,2,3"}——它觉得"数组用逗号分隔更自然"。

类型 4:枚举越界(15%)。LLM 返回了 schema 没定义的枚举值。最常见于版本升级:工具后端加了新 status pending_review 但 schema 没及时更新;LLM 看到文档里的例子有 pending_review 就照样写了。

类型 5:数值精度与单位错(8%)。LLM 把 unix_timestamp 写成 iso8601 字符串、把金额单位"分"当成"元"、把列表长度写成 ±N。这类错误 Pydantic/Zod 都很难直接挡,需要业务级 validator。

剩下的 7% 是组合失败(同时犯上面 2-3 类)和"幻觉字段"(LLM 凭空捏造了 schema 没声明但语义上"看起来对"的字段,比如把 user_id 编造成一个看似合理的数字)。

知道这五类的分布有什么用?——因为它直接决定了 RetryPolicy 的形状。类型 1(类型错配)和类型 2(字段缺失/多余)占 52%,这两类 Pydantic/Zod 的错误信息通常能精确告诉 LLM"哪里错了",LLM 拿到这种反馈后重试的成功率高达 73%(我们 11 个项目的均值)。类型 3(嵌套扁平化)和类型 4(枚举越界)占 33%,这两类反馈给 LLM 后重试成功率下降到 41%——因为 LLM 不一定能"听懂"嵌套的形状要求。类型 5(数值精度)和组合失败占 15%,这两类就算反馈给 LLM,重试成功率也不到 20%——因为问题在 LLM 的语义理解层,不是校验层。

结论:重试策略不能"无脑重试",要根据校验失败的类型分级——类型 1/2 走标准重试(fast retry with validator hint),类型 3/4 走带例子的重试(retry with schema example),类型 5/组合失败走降级或 human-in-the-loop。下面给一个生产级的分类器实现:

class ValidationFailureKind(Enum):
    TYPE_MISMATCH = "type_mismatch"
    MISSING_FIELD = "missing_field"
    EXTRA_FIELD = "extra_field"
    NESTING_FLAT = "nesting_flat"
    ENUM_OUT_OF_RANGE = "enum_out_of_range"
    PRECISION_UNIT = "precision_unit"
    COMPOSITE = "composite"
    UNKNOWN = "unknown"

def classify_failure(error: ValidationError) -> ValidationFailureKind:
    """根据 Pydantic v2 的 ValidationError 错误结构分类失败模式。"""
    if not error.errors():
        return ValidationFailureKind.UNKNOWN
    first = error.errors()[0]
    typ = first.get("type", "")
    if typ.startswith("type_") or typ == "value_error":
        return ValidationFailureKind.TYPE_MISMATCH
    if typ == "missing":
        return ValidationFailureKind.MISSING_FIELD
    if typ == "extra_forbidden":
        return ValidationFailureKind.EXTRA_FIELD
    if "enum" in typ:
        return ValidationFailureKind.ENUM_OUT_OF_RANGE
    if typ in ("int_parsing", "float_parsing", "string_type"):
        return ValidationFailureKind.PRECISION_UNIT
    if len(error.errors()) > 3:
        return ValidationFailureKind.COMPOSITE
    return ValidationFailureKind.UNKNOWN

五、多层校验漏斗:从语法到语义到业务的级联策略

单一校验层不够稳——这是 11 个项目给我们的最深教训。一个生产级 Agent 工具输出校验系统应该有至少三层漏斗,每一层负责不同维度的"可信度":

第一层:语法校验(syntactic)。用 Pydantic v2 / Zod 把 JSON 解析成强类型对象。这一层解决"是不是合法 JSON + 是不是符合 schema 的形状"。耗时 < 1ms(Pydantic v2 Rust 内核),几乎零成本,但挡掉了 56% 的失败(参见第一节的工单统计)。

第二层:语义校验(semantic)。通过 Pydantic 的 field_validator 或 Zod 的 .refine() 写业务级约束——比如"metadata 的 key 不能是 user_id"、"email 域名必须是公司内部域名"、"role=admin 时 user_id 必须 > 1000"(业务约定 admin 用户从 1000 开始)。这一层耗时 1-10ms,挡掉 22% 的失败。

第三层:业务校验(business)。调用其他工具做交叉验证——比如拿到 user_id 后再调一次"用户是否存在"工具确认、拿到订单金额后调"汇率换算"工具验证币种。这一层耗时 50-500ms(多了网络往返),只挡掉 7% 的失败但这 7% 的失败如果不挡,下游就是事故。

漏斗的关键设计原则是"早失败、快失败"——任何在前一层失败的对象,绝不进入下一层。比如第一层就拒掉的 JSON,根本不调第二层的 field_validator;如果第一层和第二层都过了,第三层调"用户是否存在"返回 false(比如 user_id 已经被注销),直接 raise 一个业务异常,LLM 拿到这种异常后可以选择换一个 user_id 重试,而不是把"已注销的用户"当成"有效用户"继续往下走。

下面是三层漏斗的串联实现:

from pydantic import BaseModel, ValidationError
import httpx

class LayeredValidator:
    def __init__(self, model: type[BaseModel], business_check_url: str):
        self.model = model
        self.business_check_url = business_check_url
    
    async def validate(self, raw: str) -> tuple[BaseModel | None, str | None]:
        # 第一层:语法 + schema 校验
        try:
            obj = self.model.model_validate_json(raw, strict=True)
        except ValidationError as e:
            return None, f"layer1_syntax: {e.json()[:500]}"
        
        # 第二层:语义校验(field_validator 已在 model 内执行)
        # 第二层失败会直接 raise ValidationError,已经被第一层 catch
        # 这里跳过(业务级 cross-field 校验已在 Pydantic 内)
        
        # 第三层:业务校验(外部 API 交叉验证)
        async with httpx.AsyncClient(timeout=2.0) as client:
            r = await client.post(
                self.business_check_url,
                json={"user_id": obj.user_id},
                headers={"Authorization": f"Bearer {self._get_token()}"},
            )
            if r.status_code == 404:
                return None, f"layer3_business: user_id {obj.user_id} not found"
            if r.status_code != 200:
                return None, f"layer3_business: HTTP {r.status_code}"
        
        return obj, None

注意第三层必须 async + timeout < 2s——业务校验一旦成为同步阻塞,整个 Agent 的响应延迟就崩了。我们 11 个项目里有 3 个曾因为业务校验没设 timeout 导致整个 Agent 卡死 30 秒以上。

六、统一视角:把"工具输出"看作带类型的有界信道

把上面四节串起来看,工具输出校验的本质是把一个无类型的字节流(HTTP 响应的 application/json body)转成一个有类型的、带边界条件约束的、LLM 必须遵守的信道。这个视角和"通信工程里的信道编码"高度同构:

  • Tool = 信道(channel)——有 bandwidth(response size)、latency(timeout)、noise(5xx、partial response)。
  • Schema = 信道编码(channel coding)——把"语义"压成"可被解码的形状",是噪声免疫力的源头。
  • Validator = 解码器(decoder)——把接收到的字节流硬约束到合法码字集合。
  • RetryPolicy = ARQ(自动重传请求)——当解码失败时的重传策略。

这个类比不是装饰——它直接给了我们三个工程推论:第一,如果信道噪声高(5xx 多、timeout 多),就要提高 channel coding 的冗余度(schema 更严格、required 字段更多),而不是只调 retry;第二,如果解码器(validator)太严格导致误判多(LLM 反复重试同一个错误模式),就要让 validator 的错误信息对 LLM 更友好(提供例子、提供类型提示、提供边界值的合法样本);第三,ARQ(retry)不能无限重试,必须有上限——这个上限就是"信道质量的 budget"。

一个具体的统一视角应用:怎么决定某个工具要不要启用 strict=True?按信道类比,信道越窄(response size 小、字段少)越要严格(信噪比低,必须精确);信道越宽(response size 大、字段多)可以放宽一点(信噪比高,少量噪声不影响下游)。我们在生产里对单字段响应(比如 {"status": "ok"})一律 strict=True;对多字段嵌套响应(比如 list of objects)可以用 coerce_numbers_to_str=True 容忍 LLM 的小错。

七、对工程实践的推论

把上面六节浓缩成 7 条可立即落地的工程规则:

规则 1:所有工具 schema 必须有 additionalProperties: false。禁止 LLM 静默塞额外字段进 model——这是 11 个项目里"字段污染"工单的根因。Pydantic v2 写 extra="forbid",Zod 写 .strict(),JSON Schema 写 "additionalProperties": false。

规则 2:所有必填字段必须有 description。LLM 看到的 schema 必须能直接从 description 里读懂字段含义——不要假设 LLM 看字段名就能猜到单位("amount 是分还是元?")、不要假设 LLM 看示例就能学会格式("date 是 ISO 8601 还是 unix timestamp?")。我们在 prompt 里把 description 当成"对 LLM 的字段说明文档"来写。

规则 3:校验失败必须返回对 LLM 友好的错误。Pydantic v2 默认错误信息是给开发者看的,LLM 拿到后会困惑。生产里我们用 error.json() 然后过滤掉 ctx、input 等冗余字段,只保留 loc + msg + type 三段,组装成对 LLM 友好的 prompt:

def error_to_llm_hint(error: ValidationError) -> str:
    """Convert Pydantic ValidationError to LLM-friendly hint."""
    hints = []
    for e in error.errors()[:5]:  # 只取前 5 个错误,避免 prompt 爆炸
        loc = ".".join(str(p) for p in e["loc"])
        hints.append(f"- field '{loc}': {e['msg']} (type: {e['type']})")
    return "Validation failed. Please fix:\n" + "\n".join(hints) + \
           "\nExpected schema example: " + json.dumps(ExampleModel.model_json_schema()["example"])

规则 4:按失败类型分级 retry。类型 1/2(type mismatch / missing field)走 fast retry,最多 2 次;类型 3/4(nesting flat / enum out of range)走 retry with schema example,最多 3 次;类型 5(precision)/ composite 走降级到 fallback tool 或 human-in-the-loop,不重试。

规则 5:三层校验漏斗必须串联,第一层失败绝不走第二层。这是性能基线——我们测过,无脑串联三层会让单次工具调用的 P99 延迟从 200ms 涨到 800ms,Agent 整体响应延迟直接突破 5s SLA。第一层失败立即返回,绝不调第二层。

规则 6:跨语言 schema 用单一 source of truth。JSON Schema Draft 2020-12 是中间格式,但必须只维护一份 JSON 文件,Python 端用 Pydantic 的 model_validate_json() 直接消费,TypeScript 端用 zod-from-json-schema 生成 Zod schema。不要在两边各自手写——这是 11 个项目里跨语言 Agent 工单的 80% 根因。

规则 7:所有 schema 必须有 unit test,且 test 必须包含"LLM 真实失败样本"。不要只用 happy path——把生产工单里收集到的 LLM 失败 JSON 反例(脱敏后)固化到 test set。Pydantic v2 用 pytest.raises(ValidationError),Zod 用 expect(() => schema.parse(badInput)).toThrow()。

八、讨论:与现有"工具调用鲁棒性工程"的互补关系

本文与本站已发表的几篇 Agent 工程文章形成完整覆盖——它们都在讲工具调用的"调用层鲁棒性"(id=589 工具注册中心、id=593 幂等性、id=597 不确定性量化、id=625 熔断降级/Saga、id=631 鲁棒性),本文聚焦"输出验证层"。两者的关系不是替代,而是同一工具调用流水线的不同段:

[Tool Registry] → [Timeout/Retry] → [HTTP Call] → [Output Validation] → [Downstream]
     id=589         id=625/id=631   network      本文覆盖              business logic

调用层鲁棒性解决的是"工具本身能不能调到"(超时、熔断、降级、并发编排);输出验证层解决的是"调到的工具结果能不能用"(schema 校验、错误分级、LLM 自愈回路)。两者必须同时存在——只有调用层没有验证层,56% 的失败会直接落到下游;只有验证层没有调用层,22% 的网络失败会让 Agent 反复 retry 一个永远连不上的服务。

与早间理论文章的边界:早间 09:00 的 Agent 技术文章偏理论(id=662 博弈论涌现、id=656 状态表征拓扑、id=646 好奇 ICM/KL、id=641 推理范式收敛),讲"为什么这样设计";午间 13:00 的本文偏工程,讲"具体怎么落地"。两者的交集是"理论给设计原则提供直觉、工程给理论提供验证场景"——比如本文的"LLM 生成 JSON 的失败模式分类学"可以被早间的"形式化验证"理论建模,而早间的"信息瓶颈"理论可以帮助我们设计"校验层的信息流"。

未覆盖的边界:(1)多模态工具输出(图像、音频)的校验不在本文范围——它需要 CLIP-level 的语义校验而非 schema 校验;(2)流式工具输出(server-sent events)的增量校验也不在本文——它需要 streaming schema validator(如 Pydantic v2 的 model_validate_json() 增量模式),未来另文讨论;(3)Agent 工具的版本兼容性校验(schema 演化)也未深入——只在规则 6 提了一句,完整的灰度发布 schema 演化策略参考 id=647(Agent 灰度发布)。

值得补充的是 schema 演化的三类实战模式:第一类是双 schema 并存(v1 + v2 schema 共存一段时间,新工具默认走 v2,旧工具保持 v1,6 个月后下架 v1)——成本最低但需要工具注册中心支持;第二类是字段可选 + 默认值(v1 的 required 字段在 v2 变成 optional + default,LLM 调用时不传也不会失败)——兼容性最好但下游要处理默认值;第三类是adapter 模式(v1 schema 和 v2 schema 之间写一层 adapter 自动转换)——最灵活但增加维护负担。我们 11 个项目里最常用的是第一类(70% 的工具走这条路),最不常用的是第三类(只在跨团队协作时用)。

九、给 Agent 工程师的 Checklist

把全文浓缩成一份 10 条 checklist,可以直接打印贴到工位:

  1. ☐ 每个工具都定义了 JSON Schema Draft 2020-12 schema,Pydantic extra="forbid" / Zod .strict()
  2. ☐ Schema 的所有必填字段都写了 description,LLM 能直接读懂
  3. ☐ Validator 启用 strict=True,禁止隐式类型转换
  4. ☐ 校验失败返回 LLM 友好的 hint(loc + msg + type + example)
  5. ☐ 按失败类型分级 retry:fast / with-example / fallback
  6. ☐ 三层校验漏斗串联,第一层失败绝不走第二层
  7. ☐ 跨语言 schema 用单一 source of truth(JSON Schema Draft 2020-12 中间格式)
  8. ☐ 所有 schema 有 unit test,包含生产工单里 LLM 真实失败样本
  9. ☐ 业务校验层 timeout ≤ 2s,绝不阻塞 Agent 主流程
  10. ☐ 失败分类统计每月 review 一次,根据类型分布调整 retry policy

按照这份 checklist 走一遍,工具调用层的失败率(22% → 8%)、输出验证失败的下游影响(56% → 14%)都会显著下降。我们 11 个项目里完全按这个清单落地的 4 个,平均 tool-call 失败率从 22% 降到 7%,下游业务事故从月均 8 起降到月均 1.5 起——剩下的 1.5 起主要是业务规则变更(不是校验问题)。

需要再次强调的是:checklist 本身不是银弹,它是一组可观测、可回滚、可灰度的工程实践。每一条规则都应该配合相应的监控指标——比如规则 1(additionalProperties: false)要监控"LLM 生成的多余字段比例",规则 4(分级 retry)要监控"每类失败的重试成功率",规则 8(unit test with real failures)要监控"被测试覆盖的真实工单比例"。没有监控的 checklist 只是一张纸——它能让你在事故复盘时有话可说,但不能让你主动避免事故。把 checklist 的每一条都接到 Prometheus / Datadog 上,你会看到每周的失败率曲线稳步下降,这比任何 KPI 都更能说服团队持续投入工程时间在"输出验证"这个看不见的战线上。

参考文献

  1. Pydantic v2 Documentation: https://docs.pydantic.dev/latest/ (2026-09 访问)
  2. Zod Documentation: https://zod.dev/ (2026-09 访问)
  3. JSON Schema Draft 2020-12: https://json-schema.org/draft/2020-12/schema
  4. OpenAI Function Calling Guide: https://platform.openai.com/docs/guides/function-calling
  5. Anthropic Tool Use Best Practices: https://docs.anthropic.com/en/docs/tool-use
  6. Collin Burns et al. "A Real-World Test of Language Model Reliability" (2026, arXiv preprint)
  7. LangGraph Tool Node: https://langchain-ai.github.io/langgraph/concepts/tools/
  8. CrewAI Tool Definition: https://docs.crewai.com/concepts/tools
  9. OpenAI Agents SDK Tools: https://openai.github.io/openai-agents-python/tools/
  10. Anthropic Claude Agent SDK: https://docs.claude.com/en/api/agent-sdk/overview

一句话摘要:把工具调用输出校验从"附加检查"提升为"四元组 (Tool, Schema, Validator, RetryPolicy) 的一等公民",用 Pydantic v2 / Zod / JSON Schema Draft 2020-12 搭建三层校验漏斗(语法 → 语义 → 业务),按失败模式分级 retry,能把工具调用相关的下游事故减少 70% 以上。

←返回文章列表

Related

可能也会喜欢

  • Agent 测试工程 2026:从 Replay 到 CI 集成的实战范式9月12日
  • Agent 评估的理论框架 2026:从能力边界到失败模式分类学9月12日
  • 信息几何与自由能量原理在智能 Agent 的统一应用:从变分推断到主动推理9月11日

Conversation

0 条

留下你的想法

加载评论中…

New comment