序:工具是 Agent 的手和脚

2024 年以来,AI Agent 从一个概念热词变成了实际的生产力工具。Cursor、Claude Code、Copilot 等编程 Agent 已经深度嵌入开发流程,而在企业场景中,客服 Agent、数据分析 Agent、自动化运维 Agent 也在批量落地。

所有这些 Agent 有一个共同点:它们不只是聊天,它们要干活

干活需要什么?工具

一个没有工具的 LLM 就像一个只会说话不会动手的人——能给你建议,但无法执行。工具就是 Agent 的手和脚,它决定了 Agent 的能力边界。

但大多数人对”工具”的理解还停留在很浅的层面:给模型塞几个函数,让它调用就行了。事实远非如此。工具的数量、粒度、命名、参数设计、错误处理,直接决定了 Agent 的成功率、效率和用户体验

这篇文章,就是要把 Agent 工具系统的设计模式讲透。从最基础的 Function Calling 到 MCP(Model Context Protocol)再到 Plugin 架构,从参数设计到编排策略,从常见陷阱到最佳实践——读完你就能为任何项目定制约自己的工具集。


一、工具的本质:不是函数,是契约

1.1 LLM 眼中的”工具”是什么?

在模型层面,工具就是一段结构化的描述,通常包含:

  • 名称:模型用来判断”该不该用这个”的第一信号
  • 描述:详细告诉模型”什么时候用、做什么、有什么限制”
  • 参数 Schema:JSON Schema 格式,定义输入参数的类型、必填性和约束

以 OpenAI Function Calling 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"name": "search_documents",
"description": "在项目文档库中全文搜索。当用户询问任何技术概念、API 用法或项目规范时使用此工具。",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词,建议使用简洁的术语而非完整句子"
},
"limit": {
"type": "integer",
"description": "返回结果数上限,默认 5,最大 20",
"default": 5,
"minimum": 1,
"maximum": 20
}
},
"required": ["query"]
}
}

模型的任务是:理解用户意图 → 匹配合适的工具 → 生成正确的参数 → 调用工具 → 解读结果 → 决定下一步

这中间每一步都可能出问题。而工具设计的质量,决定了每一步的成功率。

1.2 工具的三层架构

在实际工程中,Agent 工具系统的完整架构有三层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────┐
│ 第一层:工具描述层(给 LLM 看的) │
│ ┌─────────────────────────────────┐ │
│ │ name / description / parameters │ │
│ │ JSON Schema / Tool Manifest │ │
│ └─────────────────────────────────┘ │
├─────────────────────────────────────────┤
│ 第二层:工具注册层(给框架看的) │
│ ┌─────────────────────────────────┐ │
│ │ 工具注册表 / 路由 / 权限 / 限流 │ │
│ │ MCP Server / Plugin Manager │ │
│ └─────────────────────────────────┘ │
├─────────────────────────────────────────┤
│ 第三层:工具实现层(真正干活的) │
│ ┌─────────────────────────────────┐ │
│ │ 实际 API 调用 / 数据库操作 │ │
│ │ 文件系统 / 网络请求 / 外部服务 │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘

新手常犯的错误:只关注第三层(实现),忽视第一层(描述)。结果是工具能干活,但模型不知道怎么用、什么时候用。

工具设计的核心原则第一条:描述即产品。你写给模型的工具描述,就是工具的”UI”。模型能不能选对这个工具、传对参数,八成取决于描述写得好不好。


二、工具的分类体系

不是所有工具都一个样。理解工具的类别,才能用正确的模式设计它。

2.1 按操作性质分类

类型 说明 示例 设计要点
只读工具 获取信息,无副作用 searchread_fileget_weather 幂等,可安全重试,可缓存
写入工具 修改状态,有副作用 create_filesend_emaildeploy 需确认机制,需幂等设计
查询工具 单次查询返回结果 sql_queryapi_call 需限流,需超时,需格式约定
流式工具 持续产生输出 stream_logssubscribe_events 需流控,需中断机制
交互工具 需要用户参与 ask_userconfirm_action 需阻塞等待,需超时回退

2.2 按依赖关系分类

类型 说明 设计要点
无依赖工具 独立执行,不依赖其他工具 最简单,可以并行
链式依赖 前一个的输出是后一个的输入 需在描述中说明前置条件
条件依赖 根据情况决定是否调用 需清晰的决策逻辑描述
编排工具 本身是多个子工具的调度器 适用于复杂工作流

2.3 按抽象层级分类

这是最重要也最容易被忽视的分类维度。

层级 粒度 示例 优点 缺点
原子工具 单个操作 read_filerun_sqlhttp_get 灵活,组合性强 调用次数多,上下文消耗大
复合工具 多个原子操作的组合 search_and_summarizedeploy_project 高效,减少调用轮次 灵活性降低
自适应工具 参数决定行为范围 query_data(source, query) 平衡灵活性和效率 描述复杂度高

黄金法则默认用原子工具,只在模型频繁犯同样错误时引入复合工具。

为什么?因为:

  1. 模型自己做组合通常会做得更好(它比你对上下文的理解更动态)
  2. 复合工具会让模型做出更多假设,假设错了就是”幻觉执行”
  3. 原子工具更容易测试、调试和版本管理

三、工具描述设计:模型眼中的”产品体验”

3.1 命名:一秒钟的决策

模型的工具选择决策发生在毫秒级。工具名是最快的信息通道。好的命名:

1
2
3
4
5
// ❌ 坏命名
"tool_1"、"fetcher"、"do_stuff"、"util"

// ✅ 好命名
"search_docs"、"send_email"、"deploy_static_site"、"query_database"

命名原则

  1. 动词在前,领域在后search_docs 而非 docs_search
  2. 具体而非通用create_github_issue 而非 create_issue
  3. 避免技术黑话:用 list_files 而非 ls
  4. 区分标志性前缀:读操作用 get_/search_/query_/list_/read_,写操作用 create_/update_/delete_/deploy_/send_

为什么读操作用多个前缀? 因为不同前缀暗示不同的语义:

前缀 语义 适用场景
get_ 按 ID/精确条件获取单个 get_user(id)
list_ 简单列举 list_users()
search_ 全文/模糊搜索 search_docs(query)
query_ 结构化查询 query_db(sql)
read_ 读取文件/内容 read_file(path)

3.2 描述写作的五个要素

工具描述是模型判断”该不该用这个工具”的核心依据。一个好的描述应包含五个要素:

1
2
3
4
5
[场景] 什么时候用这个工具
[行为] 这个工具做什么
[输入] 需要的参数和约束
[输出] 会返回什么
[限制] 有哪些边界条件和注意事项

反例 vs 正例

1
2
3
4
5
6
// ❌ 反例:模糊描述
{
"name": "search",
"description": "搜索东西",
"parameters": { "q": { "type": "string" } }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ✅ 正例:结构化描述
{
"name": "search_docs",
"description": "在项目文档库中全文检索技术文档。"
+ "当用户询问任何技术概念、API 用法、最佳实践或项目规范时,"
+ "优先使用此工具而非直接回答。"
+ "支持中英文搜索,返回相关文档的标题、摘要和链接。"
+ "注意:此工具仅搜索已有文档,不会生成新内容。",
"parameters": {
"query": {
"type": "string",
"description": "搜索查询词。建议使用 2-5 个关键词而非完整句子。"
+ "例如用 'JWT 认证' 而非 '如何在项目中使用JWT进行用户认证'"
},
"doc_type": {
"type": "string",
"enum": ["api", "guide", "tutorial", "all"],
"description": "限定文档类型。默认为'all'即搜索全部类型。"
}
}
}

看出差距了吗?反例的模型会”瞎猜”何时用、用错了也不知道。正例的模型能精确判断:用户问技术问题 → 先搜文档再回答 → 参数用简洁关键词 → 可限定文档类型。

3.3 参数设计:太少不行,太多更糟

参数设计是工具设计中最需要拿捏的部分。核心矛盾:

  • 参数太少 → 工具不够灵活 → 模型需要多次调用
  • 参数太多 → 模型难以理解 → 填空准确率下降
  • 参数太自由 → 模型生成幻觉参数 → 执行失败

参数设计的黄金法则

法则一:必填参数 ≤ 3 个

超过 3 个必填参数,模型的正确填充率断崖式下降。如果需要更多信息,用可选参数 + 合理默认值。

1
2
3
4
5
6
7
8
9
10
11
// ❌ 5 个必填参数
{ "required": ["name", "email", "phone", "department", "role"] }

// ✅ 3 个必填 + 2 个可选(带默认值)
{ "required": ["name", "email"],
"properties": {
"phone": { "description": "...(可选,不填则不发送短信)" },
"department": { "default": "general" },
"role": { "default": "member" }
}
}

法则二:枚举优于自由文本

1
2
3
4
5
6
7
8
9
// ❌ 自由文本
"status": { "type": "string", "description": "用户状态" }

// ✅ 枚举
"status": {
"type": "string",
"enum": ["active", "inactive", "suspended", "pending"],
"description": "用户状态:active(活跃)/inactive(不活跃)/suspended(已停用)/pending(待激活)"
}

枚举给了模型一个”选择题”而非”填空题”,准确率天差地别。

法则三:参数描述要对齐模型的理解方式

1
2
3
4
5
// ❌ 技术思维
"path": { "type": "string", "description": "文件的绝对路径" }

// ✅ 用户思维
"path": { "type": "string", "description": "你要读取的文件路径。可以是从项目根目录开始的相对路径(如 src/main.ts),也可以是绝对路径。" }

模型是从用户语境推断参数的。你的参数描述越贴近用户的表达习惯,模型推理越准确。

法则四:相互排斥的参数用 oneOf/anyOf

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"parameters": {
"type": "object",
"properties": {
"user_id": { "type": "string" },
"email": { "type": "string" }
},
"oneOf": [
{ "required": ["user_id"] },
{ "required": ["email"] }
]
}
}

这明确告诉模型:”二选一,不能两个都传”。


四、工具编排模式

单个工具简单,多个工具的组合才是真正的工程挑战。以下是四种核心编排模式。

4.1 顺序链模式(Sequential Chain)

最基础的模式:A 的输出 → B 的输入 → C 的输入。

1
2
3
4
用户: "部署最新版本到生产环境"
→ get_latest_version() → "v2.3.1"
→ build_project("v2.3.1") → "build_12345"
→ deploy("build_12345", "production") → "部署成功"

适用场景:线性流程、E2E 工作流、数据处理管线。

设计要点

  • 每个工具的返回值应结构化(JSON),方便下游解析
  • 在描述中说明前置条件(如”仅在 build_project 成功后调用”)
  • 添加进度反馈(”Step 1/3: 获取版本号…”)

4.2 并行扇出模式(Parallel Fan-out)

多个独立工具同时执行,汇总结果。

1
2
3
4
5
用户: "检查所有微服务的健康状态"
→ check_service("user-service") ┐
→ check_service("order-service") ├ 并行
→ check_service("payment-service") ┘
→ 汇总: { user: ok, order: degraded, payment: ok }

适用场景:批量查询、多数据源聚合、健康检查。

设计要点

  • 工具必须无依赖关系(纯只读、无状态)
  • 需要汇总工具或模型自动汇总
  • 考虑部分失败时的降级策略

4.3 条件路由模式(Conditional Routing)

工具链根据中间结果动态选择路径。

1
2
3
4
5
用户: "处理这张图片"
→ analyze_image() → "类型: 照片, 含人脸, 偏暗"
→ 含人脸? → enhance_portrait()
→ 偏暗? → auto_brightness()
→ 最终 → save_image()

适用场景:智能路由、异常处理、多策略选择。

设计要点

  • 中间工具返回结构化的”决策信号”(如 { type: "portrait", needs_enhance: true }
  • 不要让模型”猜”路径——用工具返回明确的状态码或枚举
  • 提供兜底路径

4.4 ReAct 循环模式(Reasoning + Acting)

这是最智能也最难驾驭的模式。模型在”思考→执行→观察→再思考”的循环中自主决策。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
模型思考: 需要知道项目结构
→ 执行: list_files("/project")
→ 观察: 有 src/, tests/, package.json

模型思考: 需要看 package.json 的依赖
→ 执行: read_file("package.json")
→ 观察: 用了 React 18, 需要升级到 19

模型思考: 升级 React 需要改哪些文件?
→ 执行: search_code("ReactDOM.render")
→ 观察: 3 个文件用了旧 API

模型思考: 逐一修改
→ 执行: edit_file("src/index.tsx", ...)
→ 执行: edit_file("src/App.tsx", ...)
→ 执行: edit_file("src/components/Legacy.tsx", ...)

模型思考: 全部完成,确认 → 结束

适用场景:开放式的复杂任务(编程 Agent、研究 Agent)。

设计要点

  • 工具粒度要小(原子化),给模型最大的组合空间
  • 每个工具必须返回明确的结果(成功/失败/部分成功 + 数据)
  • 设置最大步数限制,防止无限循环
  • 错误信息要能指导模型的下一步决策

五、工具系统的工程实践

5.1 错误处理:不是返回字符串就完了

工具执行的错误处理,是三件事:告诉模型出了什么问题、为什么、接下来怎么处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# ❌ 简陋的错误处理
def search_docs(query):
results = db.search(query)
if not results:
return "没有找到结果"
return results

# ✅ 结构化的错误处理
def search_docs(query):
try:
results = db.search(query)
if not results:
return {
"success": True,
"data": [],
"count": 0,
"message": "未找到匹配 '{query}' 的文档。建议:1) 尝试更通用的关键词 "
"2) 检查拼写 3) 使用 list_docs 查看所有可用文档"
}
return {
"success": True,
"data": results,
"count": len(results),
"snippets": [r["title"] for r in results[:5]]
}
except TimeoutError:
return {
"success": False,
"error": "TIMEOUT",
"message": "搜索超时。数据库可能负载过高,建议:"
"1) 缩小搜索范围 2) 等待几秒后重试 3) 使用本地缓存。",
"retryable": True
}
except Exception as e:
return {
"success": False,
"error": "INTERNAL",
"message": f"搜索失败: {e}。这不是你的问题,请告知用户稍后重试或联系管理员。",
"retryable": False
}

规范化的返回值结构

1
2
3
4
5
6
7
{
"success": true | false, // 是否成功
"data": ..., // 成功时的数据
"error": "...", // 失败时的错误码(枚举值)
"message": "...", // 给人(和模型)读的消息,包含下一步建议
"retryable": true | false // 是否可以重试
}

关键:message 不只是给用户看,更是给模型看的。模型会基于 message 的内容决定下一步行为。所以 message 要包含”下一步建议”。

5.2 超时与重试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DEFAULT_TIMEOUT = 30    # 秒
MAX_RETRIES = 3
RETRY_DELAY = 1 # 秒,指数退避

def execute_tool(name, params):
for attempt in range(MAX_RETRIES):
try:
result = call_with_timeout(name, params, DEFAULT_TIMEOUT)
return result
except TimeoutError:
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY * (2 ** attempt))
continue
return error_response("TIMEOUT", "重试 3 次后仍超时", retryable=False)
except RateLimitError:
# 限流错误应该等待更久
time.sleep(5 * (2 ** attempt))
continue

超时策略

工具类型 建议超时 理由
数据库查询 5-10s 索引查询应很快;慢查询是 bug
HTTP API 15-30s 外部服务不可控
文件操作 3-5s 本地 I/O 应该很快
LLM 调用 60-120s 生成可能需要时间
编译构建 300-600s 大型项目构建慢

5.3 幂等性设计

对于写操作工具,幂等性是关键——同一个请求执行多次,结果应该一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用业务 ID 保证幂等
def create_user(email, name):
existing = find_user_by_email(email)
if existing:
return {"success": True, "data": existing, "message": "用户已存在,跳过创建"}
user = insert_user(email, name)
return {"success": True, "data": user, "message": "用户创建成功"}

# 使用请求 ID(idempotency key)
def charge_order(order_id, amount, idempotency_key):
existing_charge = find_charge_by_key(idempotency_key)
if existing_charge:
return {"success": True, "data": existing_charge, "message": "重复请求,返回已有结果"}
# ... 实际扣款

5.4 权限与安全

模型调用工具时,你永远不知道用户会要求它传什么样的参数。所以工具的权限校验必须在服务端,不能依赖模型自觉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 工具层的安全栅栏
def delete_file(path):
# 1. 路径白名单
if not path.startswith(ALLOWED_DIRS):
return error("PERMISSION_DENIED", f"无权限操作 {path} 路径")

# 2. 危险操作确认
if path.endswith((".env", ".pem", ".key", "credentials.json")):
return error("DANGEROUS_OPERATION",
f"该文件包含敏感信息,拒绝删除。如需强制删除请使用 force_delete 工具并明确确认。")

# 3. 备份
backup_path = create_backup(path)

# 4. 执行
os.remove(path)

return {"success": True, "message": f"已删除 {path},备份保存在 {backup_path}"}

安全设计清单

  • 文件操作限定在白名单目录内
  • 网络请求限制目标域名/IP
  • 数据库操作用只读账号(查询工具)
  • 写操作需要额外的确认步骤
  • SQL 注入防护(工具层再次做参数化)
  • 敏感信息过滤(不返回密码、密钥)
  • 操作审计日志

六、工具集的演变:从 Function 到 MCP 到 Plugin

6.1 第一代:内联 Function Calling

最原始也最简单的模式。工具定义直接写在代码中,作为 prompt 的一部分传给模型。

1
2
3
4
5
6
7
8
9
10
tools = [
{"name": "get_weather", "description": "...", "parameters": {...}},
{"name": "search_docs", "description": "...", "parameters": {...}},
]

response = client.chat.completions.create(
model="gpt-4",
messages=messages,
tools=tools,
)

优点:简单,零外部依赖。
缺点:工具绑定在 Agent 代码中,不可复用。

6.2 第二代:MCP(Model Context Protocol)

Anthropic 提出的 MCP 将工具抽离为独立的 Server。Agent 通过标准协议发现和调用工具。

1
2
3
4
5
6
7
8
9
┌──────────┐     MCP 协议      ┌──────────────┐
│ Agent │ ←──────────────→ │ MCP Server │
│ (Host) │ tools/list │ (文件系统) │
│ │ tools/call │ │
└──────────┘ └──────────────┘

┌──────┴──────┐
│ 文件系统 │
└─────────────┘

MCP 的核心价值

  1. 工具与 Agent 解耦:同一个文件系统 MCP Server 可以被 Claude Desktop、Cursor、自定义 Agent 等任何 MCP 客户端使用
  2. 生态复用:社区可以贡献各种 MCP Server(GitHub、Slack、Postgres、浏览器自动化…),你只需配置
  3. 标准协议:统一的工具发现(tools/list)、调用(tools/call)和资源访问(resources/read

什么时候用 MCP

  • 工具需要在多个 Agent 之间共享
  • 工具需要独立部署和管理
  • 想利用社区已有的 MCP 生态

6.3 第三代:Plugin 系统

比 MCP 更进一步,Plugin 不仅包含工具,还包含运行环境、权限声明、UI(可选)。

1
2
3
4
5
6
7
8
9
10
{
"name": "github-plugin",
"version": "1.0.0",
"tools": ["create_issue", "list_prs", "review_code"],
"permissions": ["repo:*", "user:email"],
"config": {
"base_url": "https://api.github.com",
"rate_limit": 5000
}
}

对比三种模式

维度 Function Calling MCP Plugin
复杂度
复用性 跨 Agent 跨 Agent + 生态
部署方式 内联 独立进程 独立进程 + 沙箱
权限模型 基础 细粒度声明
适用规模 个人项目 团队/企业 平台/生态
学习成本

七、实战案例:为前端项目搭建工具集

理论够多了,来做点实际的。假设你有一个前端项目,需要为 AI 编程助手设计一套工具。以下是完整的设计流程。

7.1 第一步:梳理工具需求矩阵

先列出所有需要的操作,按粒度和类型分类:

工具名 类型 粒度 必要性
list_files 只读 原子 必须
read_file 只读 原子 必须
search_code 只读 原子 必须
write_file 写入 原子 必须
edit_file 写入 原子 必须
run_command 写入 原子 必须
install_package 写入 原子 推荐
run_tests 写入 复合 推荐
lint_check 只读 原子 推荐
format_code 写入 原子 可选
check_types 只读 原子 可选
deploy_preview 写入 复合 可选

7.2 第二步:设计核心工具

edit_file 为例,设计一个完整的工具定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"name": "edit_file",
"description": "精确编辑文件中的某段内容。"
+ "当需要修改现有文件时使用此工具(而非 write_file,后者用于创建新文件或完全覆写)。"
+ "通过 old_string 定位要修改的位置,替换为 new_string。"
+ "注意:old_string 必须在文件中唯一存在,否则操作失败。"
+ "建议先使用 read_file 获取文件内容,确认 old_string 精确匹配后再调用。",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "要编辑的文件路径(相对于项目根目录)。例如 'src/components/Header.tsx'"
},
"old_string": {
"type": "string",
"description": "要被替换的原始文本。必须与文件中的内容完全一致(包括空格、缩进)。"
+ "如果有多处匹配,操作会失败——请使用更长的上下文使其唯一。"
},
"new_string": {
"type": "string",
"description": "替换后的新文本。如果与 old_string 完全相同,操作会被拒绝(无效编辑)。"
},
"replace_all": {
"type": "boolean",
"description": "是否替换所有匹配的 old_string。默认 false(仅替换第一个)。"
+ "当需要全局重命名时设为 true。",
"default": false
}
},
"required": ["file_path", "old_string", "new_string"]
}
}

实现层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def edit_file(file_path, old_string, new_string, replace_all=False):
# 1. 路径校验
if not file_path.startswith(PROJECT_ROOT):
return error("PERMISSION_DENIED", f"只能编辑项目内的文件: {PROJECT_ROOT}")

# 2. 读取文件
content = Path(file_path).read_text()

# 3. 匹配检查
matches = content.count(old_string)
if matches == 0:
return error("NOT_FOUND",
f"未找到 old_string。请确认内容完全一致(含空格和缩进)。"
f"建议:read_file('{file_path}') 获取文件精确内容。")
if not replace_all and matches > 1:
return error("AMBIGUOUS_MATCH",
f"old_string 在文件中出现了 {matches} 次。"
f"建议:1) 提供更长的上下文使其唯一 2) 设置 replace_all=true 替换全部。")

# 4. 执行替换
if replace_all:
new_content = content.replace(old_string, new_string)
else:
new_content = content.replace(old_string, new_string, 1)

# 5. 备份 + 写入
backup_path = Path(file_path).with_suffix('.bak')
Path(file_path).write_text(content) # backup
Path(file_path).write_text(new_content)

return {
"success": True,
"data": {
"file": file_path,
"replacements": 1 if not replace_all else matches,
"backup": str(backup_path)
}
}

7.3 第三步:工具的发现策略

工具太多,模型会”选择困难”。50 个工具时,模型的准确率会比 10 个工具时显著下降。

解决方案:动态工具注册

不是一次性把所有工具都注册,而是根据当前上下文动态暴露:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ToolManager:
def __init__(self):
self.all_tools = load_all_tools()

def get_active_tools(self, context):
"""根据上下文过滤可用的工具"""
active = []
if context.get("mode") == "code":
active = [t for t in self.all_tools if t.category in ("file", "terminal", "test")]
elif context.get("mode") == "plan":
active = [t for t in self.all_tools if t.is_read_only()]
elif context.get("mode") == "deploy":
active = [t for t in self.all_tools if t.category in ("build", "deploy")]

# 限制最大工具数
return active[:20] # 工具过多反而降低准确率

7.4 第四步:工具使用的监控与优化

工具质量好不好,数据说了算。你需要监控这些指标:

指标 说明 健康值
工具选择准确率 模型选了正确的工具吗? > 90%
参数正确率 参数填对了吗? > 85%
工具执行成功率 工具调用成功了吗? > 95%
平均调用次数/任务 完成任务用了几个工具调用? 依任务复杂度
重复/无效调用率 有多少调用是冗余的? < 10%
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 工具调用日志
{
"timestamp": "2026-07-02T09:30:00Z",
"tool": "edit_file",
"params": {"file_path": "src/App.tsx", "old_string": "import React", ...},
"result": "success",
"duration_ms": 45,
"task_id": "task_12345"
}

# 基于日志优化
# 1. edit_file 的 old_string 匹配失败率 15% → 改进错误提示,引导模型先 read_file
# 2. search_code 返回结果过多导致模型超时 → 增加 limit 默认值从 50 → 10
# 3. 模型频繁用 write_file 而非 edit_file → 在 write_file 描述增加 "修改现有文件请用 edit_file"

八、常见反模式与避坑指南

反模式一:万能工具

1
2
3
4
5
6
7
8
9
// ❌ 一个工具包揽一切
{
"name": "do_whatever",
"description": "执行任何操作",
"parameters": {
"action": { "type": "string", "description": "要做什么" },
"data": { "type": "object", "description": "相关数据" }
}
}

问题:模型不知道该传什么参数,瞎猜的结果是 80% 的调用都会失败。

正确做法:拆成原子工具。

反模式二:工具重名或语义重叠

1
2
3
// ❌ 两个工具几乎一样
{ "name": "read_file", "description": "读取文件内容" }
{ "name": "get_file", "description": "获取文件" }

问题:模型在两者之间摇摆不定,增加选择错误率。

正确做法:明确区分语义,或合并为一个。

反模式三:隐藏副作用

1
2
3
4
5
def create_user(name):
user = db.insert(name)
send_welcome_email(user.email) # 隐藏的副作用
log_to_analytics(user.id) # 隐藏的副作用
return user

问题:工具的”表面行为”和”实际行为”不一致。模型以为只是创建用户,实际还发了邮件。

正确做法:副作用显式化。要么拆成独立工具,要么在工具描述中明确说明。

反模式四:忽略版本兼容

1
2
3
4
5
6
7
# 工具 v1
def deploy(env): # 单参数
...

# 后来加了参数,模型还在用旧参数
def deploy(env, strategy="rolling", health_check=True): # v2
...

问题:工具升级后,模型的调用模式可能不兼容,导致大量失败。

正确做法

  1. 新增参数用可选 + 默认值,不破坏旧调用
  2. deprecated: true 标记废弃参数
  3. 工具版本号化,迁移时做灰度

九、总结

工具是 Agent 的手和脚,工具的数量、粒度、描述质量、错误处理直接决定了 Agent 能干多少活、干得多好。

核心原则回顾

  1. 描述即产品:工具描述是给 LLM 看的 UI,值得投入至少 30% 的开发精力
  2. 原子优先,复合慎用:默认用小粒度工具,只在模型频繁犯错时引入复合工具
  3. 参数 ≤ 3 必填:超过 3 个必填参数,模型填充准确率断崖下降
  4. 错误信息是模型的路标:不仅说”失败了”,还要说”为什么失败、接下来怎么处理”
  5. 安全校验在服务端:永远不要信任模型的参数,路径、权限、注入攻击全都要服务端校验
  6. 工具即产品,需要迭代:监控工具的使用数据,基于数据持续优化

设计工具集的五步流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
第一步:需求梳理
├── 列出所有需要的操作
├── 按类型和粒度分类
└── 标注必要性和优先级

第二步:原子化拆解
├── 每个工具只做一件事
├── 明确输入/输出/副作用
└── 检查是否有语义重叠

第三步:编写描述
├── 场景:什么时候用
├── 行为:做什么
├── 参数:怎么传(≤3个必填)
├── 输出:返回什么
└── 限制:注意什么

第四步:实现与安全
├── 路径/域名白名单
├── 权限校验
├── 结构化错误
├── 超时与重试
└── 幂等性保证

第五步:监控与迭代
├── 记录每次调用日志
├── 分析选择准确率/参数正确率
├── 识别反模式
└── 持续优化描述和实现

掌握了这套方法论,你就可以为任何项目——无论是一个前端脚手架工具,还是企业级 AI 平台——设计出高质量的 Agent 工具集。


延伸阅读


本文写于 2026 年 7 月 2 日,所有设计模式基于当前主流 LLM 工具调用能力(GPT-4、Claude、Gemini 系列),适用于 Function Calling、MCP 及各类 Agent 框架。