← 返回首页

Agent 工具设计最佳实践

30秒结论

  • 解决什么问题:工具定义得差,模型就不断调错工具、传错参数、陷入死循环。
  • 核心方法:8 条经生产验证的规则:触发条件描述、参数自文档化、合理粒度、结构化输出、可操作错误消息、分层暴露、幂等性设计、真实模型测试。
  • 关键结论:Schema 正确 ≠ 模型兼容。必须用生产实际使用的模型测试工具调用。
  • 读完能做什么:用 8 条规则审视和改进每一个工具定义。

工具是 Agent 的手和脚。工具定义得好,模型如虎添翼;定义得差,模型不断调错工具、传错参数、陷入死循环。

本文总结了来自生产环境 Agent 项目的 8 条规则。每条都附带前后对比示例,可直接套用。

规则 1:工具描述必须包含触发条件

不要只描述工具做什么——要说清楚什么时候用它。模型需要回答:「在我当前的情况下,我该调这个工具吗?」

好的描述包含三部分:

  1. 触发条件——什么情况下需要这个工具
  2. 做什么——工具执行的操作
  3. 返回什么——输出的结构和含义
# ❌ 太模糊——模型不知道什么时候该用
"description": "Search the web"

# ✅ 明确的触发 + 行为 + 返回值
"description": "搜索互联网获取最新信息。当答案需要训练截止日期之后的实时数据时使用,或被明确要求查询当前事实时使用。返回前 10 条结果,包含标题、URL 和摘要。"
💡 真实案例:我们曾有一个叫 fetch_documentation 的工具,模型几乎从不调用。加上「当你遇到不熟悉的库、API 或框架时使用此工具」后,调用率翻了三倍——模型需要你把触发条件明确写出来。

规则 2:参数名就是给模型看的自然语言

记住:参数名是模型读取的 prompt 的一部分。它们直接影响工具选择和参数填充的准确率。

❌ 差的名字✅ 好的名字原因
qsearch_query自文档化;模型能理解意图
fpfile_path避免缩写让模型产生歧义
iduser_id限定作用域,防止与其他 ID 混淆
datacsv_content描述预期的格式和内容

每个参数也需要自己的 description 字段——永远不要省略。一个名叫 limit 却没有 description 的参数,对模型来说就是猜谜游戏。

# ❌ 缺少 description
{"name": "limit", "type": "integer"}

# ✅ 自解释
{"name": "max_results", "type": "integer",
 "description": "返回结果的最大数量。默认 10,最大 50。"}

规则 3:工具粒度——一个工具完成一个完整操作

这是最难把握的规则。极端情况容易识别,但找到最佳粒度需要实践。

模式示例问题
太细open_file()read_byte()close_file()一个任务需要连续 10 次调用;模型丢失上下文
太粗do_everything(action, target, format, filter, sort, ...)参数爆炸;模型不知道该传什么
刚好read_file(path), write_file(path, content), search_files(pattern)各自完成一个完整操作;可组合但独立

黄金法则:如果你能用一句话描述工具的全部用途且不用「和」字连接,那可能就是正确的粒度。「读取文件并返回其内容」——好。「读取文件、解析内容、过滤行、写入输出」——太粗。

规则 4:返回结构化、可解析的输出

工具的返回值是模型的下一个输入。返回的是垃圾 → 模型的推理就是垃圾。

# ❌ 非结构化——模型需要解析自然语言
"找到 3 个文件:report.csv (2.3MB, 最后修改 2024-01-15),
data.json (156KB, 最后修改 2024-01-14), notes.txt (4KB, 最后修改 2024-01-10)"

# ✅ 结构化 JSON——模型准确提取字段
{
  "files": [
    {"name": "report.csv", "size_bytes": 2411725, "modified": "2024-01-15T14:30:00Z"},
    {"name": "data.json", "size_bytes": 159744, "modified": "2024-01-14T09:15:00Z"},
    {"name": "notes.txt", "size_bytes": 4096, "modified": "2024-01-10T18:00:00Z"}
  ],
  "count": 3
}

JSON 不是必须——但一致性是必须的。如果返回文本,使用可预测的格式。如果返回 JSON,所有工具遵循相同的 schema。

规则 5:错误信息必须建议下一步动作

工具最糟糕的返回值是空字符串或模糊的「出错了」。模型此时没有任何信息来恢复,要么重复同样错误的调用,要么幻觉出一个结果。

# ❌ 无用的错误
{"error": "Failed"}

# ❌ 稍好但仍无助益
{"error": "File not found"}

# ✅ 可操作的错误——模型能自我纠正
{
  "success": false,
  "error": "文件未找到: /data/reports/2024/summary.csv",
  "suggestion": "尝试列出 /data/reports/2024/ 目录查看可用文件,或检查路径拼写。",
  "available_directories": ["/data/reports/2023", "/data/reports/2025"]
}
💡 错误分类法:我们将工具错误分为三类以指导恢复策略——可重试(超时、频率限制)、可修复(参数错误、路径错误)、致命(权限拒绝、服务宕机)。在错误响应中包含此分类,Agent 就能决定:重试、调整、还是上报。

规则 6:用分层暴露限制工具数量

单个 prompt 中超过约 20 个工具时,模型的选择准确率会显著下降。在我们的测试中,从 10 个工具增加到 30 个,选错工具的比例上升了 40%。

分层暴露策略:

  1. 第一层(始终可见):5-8 个核心工具——读取、写入、搜索、执行、询问
  2. 第二层(上下文触发):仅在任务涉及相关关键词时暴露高级工具
  3. 第三层(按需发现):通过 list_advanced_tools() 元工具让 Agent 自行发现专用工具
# 分层工具注册模式
TOOL_TIERS = {
    "tier1": ["read_file", "write_file", "search_web", "execute_code", "ask_user"],
    "tier2": ["query_database", "send_email", "create_chart", "run_test_suite"],
    "tier3": ["deploy_service", "manage_permissions", "generate_report"]
}

规则 7:为幂等性设计

Agent 会重试。很多次。如果用相同参数调用同一个工具两次会产生不同的副作用(重复扣款、重复发送邮件、创建重复记录),那你有严重问题。

操作非幂等(危险)幂等(安全)
创建用户create_user(email) — 会创建重复get_or_create_user(email) — 存在则返回已有
发送消息send(to, body) — 每次调用都发送send(to, body, idempotency_key) — 去重
收款charge(amount) — 每次调用都扣款charge(order_id, amount) — 已扣款则跳过

idempotency_key 模式是最简单的修复:为每个逻辑操作生成唯一 key,传给工具,工具如果已处理过该 key 则跳过执行。

规则 8:用真实模型调用测试工具

单元测试工具实现是不够的。你需要测试模型是否真的能正确使用这个工具。Schema 正确 ≠ 模型兼容。

def test_tool_usage(tool_def, test_scenarios):
    """测试模型在各种场景下是否正确调用工具。"""
    for scenario in test_scenarios:
        response = model.chat(
            messages=[{"role": "user", "content": scenario["prompt"]}],
            tools=[tool_def]
        )
        tool_call = response.tool_calls[0]

        # 验证:模型是否调用了正确的工具?
        assert tool_call.name == scenario["expected_tool"], \
            f"期望 {scenario['expected_tool']},实际 {tool_call.name}"

        # 验证:参数是否合理?
        for param, validator in scenario["param_checks"].items():
            assert validator(tool_call.arguments.get(param)), \
                f"参数 '{param}' 校验失败"
⚠️ 常见陷阱:通过 JSON Schema 校验的工具定义仍然可能让模型困惑。例如,类型为 "string" 但没有示例的参数,模型可能传入 Markdown 格式,而工具期望的是纯文本。务必用生产环境实际使用的模型进行测试。

快速参考清单

检查项对应规则
☐ 描述包含触发条件?规则 1
☐ 每个参数都有 description?规则 2
☐ 每个工具完成一个完整操作?规则 3
☐ 返回格式一致且可解析?规则 4
☐ 错误信息包含建议的恢复动作?规则 5
☐ 同时可见的工具不超过 20 个?规则 6
☐ 有副作用的工具有幂等 key?规则 7
☐ 用真实模型调用测试过?规则 8

下一步阅读

常见问题

Q: 工具描述怎么写模型才能真正理解?

A: 好的工具描述必须包含:① 触发条件(什么时候用)、② 具体行为(做什么)、③ 返回值(输出什么)。模型需要知道"什么时候该用"。

Q: 一个 Agent 同时暴露多少个工具比较合适?

A: 超过约 20 个工具时,模型选择准确率显著下降。建议核心层 5-8 个常用工具始终可见,高级工具按需发现。

Q: 为什么工具要设计成幂等的?

A: Agent 会重试——很多次。如果重复调用产生副作用(重复扣款、重复发邮件),自动恢复时就可能造成灾难。

Q: 如何测试工具定义是否被模型正确理解?

A: 必须用真实的模型调用来测试。设计多个典型场景,调用模型看它是否选择正确的工具、传入合理的参数。