Tool Use Patterns — Agent 防御性工具集成
为 AI Agent 设计健壮的工具调用方案,防止生产环境中最常见的 Agent 失败模式。
核心洞察: 工具误用是生产中最常见的 Agent 特定失败模式——也是最阴险的:第 2 步的一个畸形参数静默地腐化了后续每一步。
Goal
为 AI Agent 设计健壮的工具集成方案。覆盖防御性 Schema 设计、重试/降级策略、输出解析验证、工具组合编排
Trigger
- 用户说"tool use"、"工具调用"、"function calling"、"工具集成"
- 构建需要调用外部工具/API 的 Agent
- 调试 Agent 中的工具调用失败
- 设计 Agent 系统的工具 Schema
工作流程
审计工具面 → 设计 Schema → 实现防御包装 → 添加监控 → 测试失败模式
Step 1: 工具面审计
盘点 Agent 可调用的所有工具,按风险分级:
| 风险等级 | 定义 | 示例 | 处理方式 |
|---|---|---|---|
| 只读 | 不修改任何状态 | 搜索、读文件、查询数据库 | 自动执行 |
| 可逆写入 | 修改状态但可回滚 | 创建分支、写临时文件 | 确认后执行 |
| 不可逆写入 | 修改状态且无法回滚 | 发送邮件、删除数据、发布内容 | 显式审批 |
| 危险 | 可能造成安全问题 | 执行代码、修改权限、访问密钥 | 默认拒绝 |
对每个工具记录:
- 工具名称和功能描述
- 输入参数列表(类型、必填/可选、约束条件)
- 返回值格式
- 失败模式(超时、限流、参数错误、服务不可用)
- 副作用(是否修改状态、是否可逆)
详细失败模式目录见 references/failure-modes.md
Step 2: 防御性 Schema 设计
工具参数 Schema 是 Agent 和工具之间的契约。设计不良的 Schema 是工具误用的根本原因。
Schema 设计原则
- 严格类型 — 用 enum 约束可选值,用 format 约束格式,不留模糊空间
- 必填/可选分离 — 必填参数必须在
required数组中声明 - 默认值 — 所有可选参数提供合理默认值
- 描述即文档 — description 字段要告诉 LLM 何时用、怎么用、什么格式
Schema 模板
{
"name": "tool_name",
"description": "做什么 + 返回什么 + 什么时候用。不要模糊描述如'获取数据'。",
"parameters": {
"type": "object",
"properties": {
"param_name": {
"type": "string",
"enum": ["option_a", "option_b"],
"description": "参数含义 + 格式约束 + 示例值"
}
},
"required": ["param_name"],
"additionalProperties": false
}
}
详细 Schema 模板见 references/schema-templates.md
Step 3: 重试与降级策略
工具调用失败不是异常,是常态。需要在代码层面(非提示层面)实现防御。
错误分类与响应
| 错误类型 | 特征 | 响应策略 |
|---|---|---|
| 瞬时故障 | 超时、5xx、限流 | 指数退避重试(最多 3 次) |
| 参数错误 | 4xx、验证失败 | 将错误信息返回 LLM,让它修正参数重试 |
| 工具不可用 | 连接拒绝、服务下线 | 切换备用工具或优雅降级 |
| 输出异常 | 格式不符、数据缺失 | 解析错误 + 请求澄清或换方案 |
断路器模式
在代码层面实现,不要依赖提示中的"不要循环超过 N 次":
连续失败 3 次 → 打开断路器 → 暂停调用 30 秒 → 半开状态试探 → 成功则关闭
关键规则:
- 断路器在工具调度层实现,不在提示层
- 工具失败时返回结构化错误给 LLM(
{ success: false, error: "..." }),永远不要静默吞掉错误 - 永远不要返回空结果让 LLM 以为成功了
- 幂等性键:对有副作用的工具,每次调用携带唯一 ID,防止重试导致重复执行
详细组合模式见 references/composition-patterns.md
Step 4: 输出解析与验证
工具返回值是 LLM 后续推理的基础。畸形输出会级联腐化所有后续步骤。
防御性解析
- Schema 验证 — 用 Pydantic/Zod 验证输出结构,不信任自由格式
- 部分响应处理 — 工具返回部分数据时,标记缺失字段而非猜测
- 异常检测 — 工具返回空数组、null、意外格式时,报告而非忽略
- 工具幻觉检测 — Agent 可能伪造工具输出(声称调用了工具但实际没有),通过验证调用记录来检测
错误返回格式
工具执行失败时,返回结构化错误而非崩溃:
# 正确:结构化错误,LLM 可以据此决策
return {"success": False, "error": "参数格式错误:report_id 应为 RPT-XXXX 格式", "retryable": False}
# 正确:可重试错误
return {"success": False, "error": "服务暂时不可用 (HTTP 503)", "retryable": True}
# 错误:静默返回空结果
return None # LLM 以为成功了,继续在错误基础上推理
# 错误:抛出异常让框架捕获
raise ValueError("bad input") # 错误信息不够具体,LLM 无法恢复
Step 5: 工具组合模式
单工具调用很少能满足复杂任务。多工具编排有四种基本模式:
顺序流水线
A → B → C(串行,每步依赖前一步的输出)
适用:数据获取 → 处理 → 存储
并行扇出
A + B → 合并结果(同时执行,无依赖)
适用:多数据源聚合、并行搜索
条件分支
A 成功 → B;A 失败 → C(根据结果选择路径)
适用:有降级方案的工具调用
补偿回滚(Saga 模式)
A → B → C 失败 → 补偿 B → 补偿 A
适用:多步写入操作,需要在失败时恢复状态
编排规则:
- 每个编排步骤记录执行结果到 Ledger(复用 task-loom 模式)
- 总步骤数设置硬上限(代码级,非提示级)
- 每步注册补偿动作后再执行(Saga 模式)
Step 6: 测试工具集成
工具集成测试需要覆盖正常路径和失败路径。
测试矩阵
| 测试类型 | 方法 | 覆盖点 |
|---|---|---|
| Mock 测试 | 模拟工具响应 | 参数验证、输出解析、错误处理 |
| 故障注入 | 模拟超时、5xx、空响应 | 重试逻辑、断路器、降级策略 |
| 集成测试 | 真实工具 + 沙箱环境 | 端到端流程、实际延迟、真实错误 |
| 属性测试 | 随机输入生成 | 边界条件、Schema 鲁棒性 |
必测场景
- 工具正常返回 → 参数正确传递、输出正确解析
- 工具超时 → 重试后成功
- 工具持续失败 → 断路器触发、优雅降级
- 参数格式错误 → LLM 收到错误信息并修正
- 工具返回异常格式 → 解析不崩溃、标记异常
- 并发工具调用 → 无竞态条件
- 幂等性验证 → 重试不产生重复副作用
完整测试清单见 references/testing-checklist.md
快速使用
用户:我正在构建一个能调用 GitHub API 的 Agent,但工具调用经常失败
助手:使用 /tool-use-patterns 审计工具面、设计防御性 Schema、实现断路器
边界情况
- 工具数量超过 25 个 — Anthropic 工程博客指出,超过 25 个工具后 Agent 可靠性显著下降。使用检索或分层策略筛选当前步骤的可用工具
- 工具 Schema 漂移 — 第三方 API 更新响应格式时,Agent 无法感知。需要在工具包装层做版本校验
- 工具返回超大响应 — 截断到合理长度,避免撑爆上下文窗口
- 多 Agent 共享工具 — 每个 Agent 应有独立的工具权限范围,避免权限堆叠
安全检查
- 所有工具输入在执行前做 Schema 验证(LLM 生成的参数是不可信输入)
- 有副作用的工具必须接受幂等性键
- 文件操作类工具做路径沙箱检查(
Path.resolve()验证) - 不要让工具异常冒泡到 Agent 层(捕获后返回结构化错误)