# 3.3 自定义工具的设计与实现

> 🎯 **本节目标**：学会如何设计高质量的工具——不是"怎么写代码"，而是"怎么思考设计"。

---

## 核心观点：工具设计质量 > 模型能力

很多 Agent 表现不佳的原因，不是模型不够聪明，而是**工具没设计好**：

> - 工具描述模糊 → LLM 不知道什么时候该用 → 要么不用，要么乱用
> - 参数没有验证 → LLM 传入非法值 → 工具崩溃
> - 错误信息不清晰 → LLM 无法理解发生了什么 → 不能自我纠错

一个设计精良的工具 + GPT-4.1-mini 的效果，往往优于设计糟糕的工具 + GPT-4.1。**工具就是 Agent 的“感官器官”——眼睛（搜索）、耳朵（监听）、手（操作）。感官越灵敏、信号越清晰，大脑的决策就越准确。**

---

## 设计原则一：单一职责

> 一个工具只做一件事，做好这一件事。

这不是软件工程教条——它对 Agent 有直接的量化影响。

一个“瑞士军刀式工具”通常会把搜索、分析、摘要、格式转换等能力塞进同一个入口里。结果是模型每次调用时都要同时判断：要不要分析、要不要摘要、返回什么格式、取多少结果、参数之间是否互斥。**决策空间越大，模型越容易漏填参数或传错值。**

更好的方式是拆成多个职责清晰的小工具：

| 工具 | 只负责什么 | 为什么更适合 Agent |
|---|---|---|
| `search_web` | 搜索网页并返回候选结果 | 模型只需要判断“是否需要搜索” |
| `analyze_data` | 对已有数据做分析 | 输入输出边界清晰 |
| `summarize_text` | 对文本做摘要 | 不会和搜索、分析逻辑混在一起 |

**为什么这很重要？** 因为 LLM 选择工具时是在做多分类问题。每增加一个可选参数或功能分支，决策空间就指数级膨胀。3 个单职责工具的组合空间是 $2^3 = 8$ 种用法；而 1 个多功能工具的参数组合可能达到几十种，模型更容易出错。

---

## 设计原则二：描述写给 LLM 看

工具的 `description` 不是给人看的文档——**它是给另一个 AI 读的"使用说明书"**。这意味着写法要完全不同于传统 API 文档。

### 好的 vs. 不好的描述对比

| 维度 | 不好的 | 好的 |
|------|--------|------|
| 功能说明 | "处理邮件" | "向指定邮箱发送邮件，支持 HTML 格式" |
| 使用时机 | （缺失） | "仅在用户明确要求发送时调用" |
| 排除时机 | （缺失） | "不适用于读取邮件或查询收件箱" |
| 参数格式 | `{"email": string}` | `"邮箱地址，如 boss@company.com"` |

### 描述的黄金公式

> **一句话功能说明**（动词 + 对象 + 核心特征）→ **适用场景**（何时用）→ **不适用场景**（何时不用）→ **返回值格式**（让 LLM 知道拿到结果后该怎么解读）

---

## 设计原则三：输入必须验证

LLM 生成的参数不总是合法的。它可能传入：

| 异常输入 | 典型问题 | 工具应该怎么处理 |
|---|---|---|
| 空字符串 | 用户没有提供必要信息 | 返回“缺少必填字段”，提示需要补充什么 |
| 不完整邮箱，如 `user@example` | 格式不符合预期 | 返回期望格式，如 `user@example.com` |
| 超长文本 | 可能导致超时、内存问题或费用暴涨 | 设置长度上限，并说明如何缩短 |
| 缺失必需参数 | 模型漏填字段 | 返回字段名和补充建议 |
| 类型错误 | 期望字符串却传了对象 | 返回期望类型和实际类型 |

### 最小可行的输入验证模式

不需要一上来写复杂框架，先建立这个原则：**工具入口必须先验证，再执行。** 一个可靠的工具至少要完成四件事：

1. **类型检查**：参数是不是期望的类型。
2. **格式检查**：邮箱、日期、URL、股票代码等是否符合格式。
3. **范围检查**：数量、时间跨度、文本长度是否超过安全范围。
4. **错误转译**：把底层异常转成人和模型都能理解的提示。

如果使用 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 后缀）和美股（纯字母代码）。

<details>
<summary>参考方案</summary>

一个合格的股票查询工具 Schema 应该包含这些要素：

| 字段 | 建议设计 | 作用 |
|---|---|---|
| 工具名 | `get_stock_info` | 名称明确，模型容易选择 |
| 功能说明 | 查询股票实时行情数据 | 告诉模型这个工具能做什么 |
| 适用场景 | 当前价格、涨跌幅、市值等实时指标 | 帮助模型判断何时调用 |
| 不适用场景 | 历史 K 线、技术指标计算、投资建议 | 避免模型把所有股票问题都丢给它 |
| `symbol` 参数 | 字符串，支持 `AAPL`、`600036.SS`、`000001.SZ`、`0700.HK` | 明确不同市场代码格式 |
| `metrics` 参数 | 可选列表，如价格、涨跌幅、市值、市盈率 | 限制可查询指标，减少幻觉 |
| 返回格式 | `symbol`、`price`、`change_percent`、`market_cap`、`currency` | 让模型知道如何解读结果 |

关键点：
1. description 中明确区分了适用和不适用场景
2. symbol 的描述包含了不同市场的代码格式示例
3. metrics 用固定枚举限制可选值范围，减少模型幻觉

</details>

---

## 小结

| 原则 | 核心要点 |
|------|---------|
| 单一职责 | 一个工具一件事，降低 LLM 决策复杂度 |
| 描述质量 | 写给 LLM 看，不是给人看——包含适用/不适用场景 |
| 输入验证 | 用 Pydantic 做类型安全 + 格式校验 |
| 错误信息 | 返回给 LLM 的结构化错误，而非抛异常 |
| 缓存策略 | 对重复调用的耗时/收费工具加缓存 |
| 安全自检 | 每个工具上线前评估最坏情况的影响 |

---

*下一节：[3.4 工具描述的编写技巧](./04_tool_description.md)*