3.3 自定义工具的设计与实现
🎯 本节目标:学会如何设计高质量的工具——不是"怎么写代码",而是"怎么思考设计"。
核心观点:工具设计质量 > 模型能力
很多 Agent 表现不佳的原因,不是模型不够聪明,而是工具没设计好:
- 工具描述模糊 → LLM 不知道什么时候该用 → 要么不用,要么乱用
- 参数没有验证 → LLM 传入非法值 → 工具崩溃
- 错误信息不清晰 → LLM 无法理解发生了什么 → 不能自我纠错
一个设计精良的工具 + GPT-4.1-mini 的效果,往往优于设计糟糕的工具 + GPT-4.1。工具就是 Agent 的“感官器官”——眼睛(搜索)、耳朵(监听)、手(操作)。感官越灵敏、信号越清晰,大脑的决策就越准确。
设计原则一:单一职责
一个工具只做一件事,做好这一件事。
这不是软件工程教条——它对 Agent 有直接的量化影响。
一个“瑞士军刀式工具”通常会把搜索、分析、摘要、格式转换等能力塞进同一个入口里。结果是模型每次调用时都要同时判断:要不要分析、要不要摘要、返回什么格式、取多少结果、参数之间是否互斥。决策空间越大,模型越容易漏填参数或传错值。
更好的方式是拆成多个职责清晰的小工具:
| 工具 | 只负责什么 | 为什么更适合 Agent |
|---|---|---|
search_web | 搜索网页并返回候选结果 | 模型只需要判断“是否需要搜索” |
analyze_data | 对已有数据做分析 | 输入输出边界清晰 |
summarize_text | 对文本做摘要 | 不会和搜索、分析逻辑混在一起 |
为什么这很重要? 因为 LLM 选择工具时是在做多分类问题。每增加一个可选参数或功能分支,决策空间就指数级膨胀。3 个单职责工具的组合空间是 种用法;而 1 个多功能工具的参数组合可能达到几十种,模型更容易出错。
设计原则二:描述写给 LLM 看
工具的 description 不是给人看的文档——它是给另一个 AI 读的"使用说明书"。这意味着写法要完全不同于传统 API 文档。
好的 vs. 不好的描述对比
| 维度 | 不好的 | 好的 |
|---|---|---|
| 功能说明 | "处理邮件" | "向指定邮箱发送邮件,支持 HTML 格式" |
| 使用时机 | (缺失) | "仅在用户明确要求发送时调用" |
| 排除时机 | (缺失) | "不适用于读取邮件或查询收件箱" |
| 参数格式 | {"email": string} | "邮箱地址,如 boss@company.com" |
描述的黄金公式
一句话功能说明(动词 + 对象 + 核心特征)→ 适用场景(何时用)→ 不适用场景(何时不用)→ 返回值格式(让 LLM 知道拿到结果后该怎么解读)
设计原则三:输入必须验证
LLM 生成的参数不总是合法的。它可能传入:
| 异常输入 | 典型问题 | 工具应该怎么处理 |
|---|---|---|
| 空字符串 | 用户没有提供必要信息 | 返回“缺少必填字段”,提示需要补充什么 |
不完整邮箱,如 user@example | 格式不符合预期 | 返回期望格式,如 user@example.com |
| 超长文本 | 可能导致超时、内存问题或费用暴涨 | 设置长度上限,并说明如何缩短 |
| 缺失必需参数 | 模型漏填字段 | 返回字段名和补充建议 |
| 类型错误 | 期望字符串却传了对象 | 返回期望类型和实际类型 |
最小可行的输入验证模式
不需要一上来写复杂框架,先建立这个原则:工具入口必须先验证,再执行。 一个可靠的工具至少要完成四件事:
- 类型检查:参数是不是期望的类型。
- 格式检查:邮箱、日期、URL、股票代码等是否符合格式。
- 范围检查:数量、时间跨度、文本长度是否超过安全范围。
- 错误转译:把底层异常转成人和模型都能理解的提示。
如果使用 Python,后续实战中可以用 Pydantic、dataclass 或框架自带 Schema 来完成这些校验;如果使用 TypeScript,也可以用 Zod 等工具。这里先记住目标:不要相信模型传来的参数一定正确。
为什么推荐 Schema 校验而不是到处手写判断?
- 类型声明即验证规则,文档和校验更容易保持一致
- 错误信息可以自动包含字段名和原因
- 与 Structured Outputs、Function Calling 等机制天然配合
设计原则四:错误信息是给 LLM 看的
这是最容易被忽略的原则:
| 错误信息 | LLM 能否自我修正 | 原因 |
|---|---|---|
Invalid input | 很难 | 不知道哪个字段错了,也不知道应该怎么改 |
“邮箱地址格式不正确,期望如 user@example.com,收到 user@example” | 容易 | 模型知道错误字段、期望格式和当前值 |
| “搜索超时,请缩短关键词或稍后重试” | 容易 | 模型可以换关键词、减少范围或告知用户 |
好的错误信息能让 LLM 自行纠错:
- 参数格式错 → LLM 修正格式重试
- API 超时 → LLM 换更简单的查询
- 权限不足 → LLM 告知用户需要授权
设计原则五:考虑缓存
Agent 在一次推理中可能多次调用同一个工具:
用户:“北京天气怎么样?适合户外活动吗?湿度大吗?” → LLM 可能在推理过程中分别查询天气 2-3 次 → 如果每次都调付费 API → 浪费錢和时间
解决方案:对相同参数的结果做短期缓存(TTL 通常设为几分钟到几小时)。
💡 缓存不需要自己实现。大多数 Agent 框架(LangChain、CrewAI)都有内置的工具缓存机制。了解这个概念就够,具体实现交给框架。
一个完整的工具设计清单
在把任何新工具接入 Agent 之前,用这张清单自检:
□ 单一职责:这个工具是否只做一件事?
□ 名称清晰:函数名能否一眼看出功能?(get_weather ✅ / process ✅)
□ 描述完整:是否包含 适用/不适用/返回格式?
□ 参数有类型注解 + 描述?
□ 输入验证:Pydantic 或等价的校验逻辑?
□ 错误处理:所有异常都被捕获并转为可读字符串?
□ 安全评估:最坏情况下这个工具能造成什么破坏?
□ 必要时加了缓存?
📝 动手练习
练习:为以下需求设计一个工具 Schema(只写 JSON 定义,不写函数体):
需求:一个股票查询工具,可以查股票的当前价格、涨跌幅、市值。支持 A 股(带 .SS/.SZ 后缀)和美股(纯字母代码)。
参考方案
一个合格的股票查询工具 Schema 应该包含这些要素:
| 字段 | 建议设计 | 作用 |
|---|---|---|
| 工具名 | get_stock_info | 名称明确,模型容易选择 |
| 功能说明 | 查询股票实时行情数据 | 告诉模型这个工具能做什么 |
| 适用场景 | 当前价格、涨跌幅、市值等实时指标 | 帮助模型判断何时调用 |
| 不适用场景 | 历史 K 线、技术指标计算、投资建议 | 避免模型把所有股票问题都丢给它 |
symbol 参数 | 字符串,支持 AAPL、600036.SS、000001.SZ、0700.HK | 明确不同市场代码格式 |
metrics 参数 | 可选列表,如价格、涨跌幅、市值、市盈率 | 限制可查询指标,减少幻觉 |
| 返回格式 | symbol、price、change_percent、market_cap、currency | 让模型知道如何解读结果 |
关键点:
- description 中明确区分了适用和不适用场景
- symbol 的描述包含了不同市场的代码格式示例
- metrics 用固定枚举限制可选值范围,减少模型幻觉
小结
| 原则 | 核心要点 |
|---|---|
| 单一职责 | 一个工具一件事,降低 LLM 决策复杂度 |
| 描述质量 | 写给 LLM 看,不是给人看——包含适用/不适用场景 |
| 输入验证 | 用 Pydantic 做类型安全 + 格式校验 |
| 错误信息 | 返回给 LLM 的结构化错误,而非抛异常 |
| 缓存策略 | 对重复调用的耗时/收费工具加缓存 |
| 安全自检 | 每个工具上线前评估最坏情况的影响 |
下一节:3.4 工具描述的编写技巧