1. 从“工具给到六十个”说起:Agent 为什么会挑花眼
“Agent 工具给到六十个,它开始挑花眼”——这句话我第一次看到的时候,脑子里立刻浮现出一个很具体的画面:一个刚入职的实习生,桌上被同时摊开了六十份不同的操作手册,每一份都告诉他“用我准没错”,结果他站在原地,一份都不敢翻。Agent 也是一样的道理。
这两年做 Agent 开发的人越来越多,从 LangChain4j 到 Spring AI,从 MCP 协议到各种 Agent 框架,工具调用(Tool Calling / Function Calling)几乎成了标配能力。很多人上手的第一反应就是:既然工具越多越强,那我就把能接的都接上。搜索接一个、数据库接一个、文件读写接一个、浏览器自动化接一个、代码执行再接一个……接完一数,六十个。然后跑起来发现,Agent 不但没变聪明,反而开始“犯迷糊”:该调 A 工具的时候调了 B,该传三个参数的时候只传了一个,甚至干脆不调工具,自己编一个答案糊弄过去。
这不是模型变笨了,而是工具选择本身就是一个决策问题,而决策空间一旦膨胀到六十个选项,任何模型都会出现选择困难。这篇文章我想把这件事掰开揉碎讲清楚:工具数量到底在哪个区间是健康的、超过之后会发生什么、怎么用 MCP 和主流 Java 生态(Spring AI、LangChain4j)把工具组织得让 Agent 不迷路。适合正在做 Agent 项目、被工具调用折磨过的开发者,也适合刚接触 MCP 协议、想知道“工具到底该怎么给”的新手。
我先把结论摆在这儿:工具不是越多越好,六十个工具对绝大多数 Agent 来说已经严重超载。真正专业的做法不是“全给”,而是“分层给、按需给、动态给”。下面从设计思路开始拆。
2. 工具过载的本质:Agent 到底在“挑”什么
2.1 工具调用的决策链路拆解
要理解 Agent 为什么挑花眼,得先知道它在一次工具调用里到底做了哪些判断。很多人以为模型是“看到问题→选工具→执行”,其实中间至少有三层决策:
第一层是意图识别:用户这句话到底要不要调工具?有些问题模型自己就能答,有些必须查外部数据。这一层判断错了,后面全错。
第二层是工具选择:如果要调,六十个工具里选哪个?模型看到的是每个工具的 name、description、参数 schema。它要在这些文本描述里做语义匹配。
第三层是参数填充:选定工具后,从用户输入里抽取参数,或者从上下文里推断参数。参数类型、必填项、格式约束都在这一层生效。
六十个工具带来的最大压力在第二层。模型的上下文窗口是有限的,六十个工具的完整 schema 塞进去,光工具定义就可能吃掉几千甚至上万 token。更麻烦的是,很多工具的 description 写得很像——“搜索网页”“查询信息”“获取资料”,语义高度重叠,模型根本分不清边界。
提示:工具描述的重叠度比工具数量更致命。十个语义清晰的工具,往往比三十个描述模糊的工具更好用。
2.2 上下文窗口与注意力稀释
这里有个很多人忽略的机制:注意力稀释。Transformer 架构在处理长上下文时,并不是每个 token 都获得同等关注。当工具定义占据大量上下文后,模型对每个工具描述的“注意力权重”会被摊薄。工具越多,单个工具被准确识别的概率越低。
我做过一个粗略的实测对比,用同一批任务、同一个模型,只改变工具数量:
| 工具数量 | 工具选择准确率 | 平均响应延迟 | 参数填充错误率 |
|---|---|---|---|
| 5 个 | 约 96% | 低 | 约 3% |
| 15 个 | 约 91% | 中 | 约 7% |
| 30 个 | 约 78% | 高 | 约 15% |
| 60 个 | 约 55% | 很高 | 约 28% |
这张表不是精确的学术数据,是我在几个实际项目里观察到的趋势。可以看到,从 30 个到 60 个,准确率断崖式下跌。这就是“挑花眼”的量化表现。参数填充错误率飙升也很好理解:工具一多,模型容易把 A 工具的参数格式套到 B 工具上。
2.3 六十个工具的真实来源
为什么会有项目真的给到六十个工具?我复盘过几个案例,来源基本是这几类:
- 历史堆积:项目迭代过程中不断加工具,没人做减法,旧的没删新的又加。
- 能力贪多:觉得“万一用得上”,把搜索、爬虫、数据库、文件、邮件、日历、地图全接上。
- MCP 批量接入:MCP 协议让接入外部工具变得极其简单,一个 MCP Server 可能就暴露十几个工具,接三四个 Server 就六十个了。
- 团队协作:不同模块的开发者各自加自己的工具,缺乏统一治理。
MCP(Model Context Protocol)的普及是双刃剑。它把工具接入标准化了,mcp server一挂,工具自动注册,开发体验极好。但正因为太方便,很多人失去了“克制”的意识。playwright mcp、chrome devtools mcp、burpsuite mcp、blender mcp这些现成的 Server 一接,工具数量瞬间爆炸。
3. 工具治理的核心思路:分层、按需、动态
3.1 分层设计:把六十个工具收进几个“抽屉”
解决挑花眼的第一招是分层。不要把所有工具平铺给模型,而是先给模型几个“大类工具”,让它先选类别,再在类别内选具体工具。这就像把六十份手册先按部门分到六个抽屉,实习生先找抽屉再找手册。
具体做法有两种:
方案一:工具分组 + 路由 Agent。用一个轻量的路由层(可以是一个小模型,也可以是规则)先判断任务属于哪个域,比如“搜索域”“文件域”“数据库域”,然后只把该域的工具注入上下文。这样每次模型看到的工具可能只有 8 到 12 个。
方案二:元工具(Meta Tool)。给模型一个list_tools(category)工具,让它先查询某类别下有哪些工具,再决定调用。这种方式更灵活,但多了一次往返。
在 Spring AI 里,工具分组可以通过自定义ToolCallbackResolver实现,按当前会话的上下文动态返回工具集合。LangChain4j 里则可以用ToolProvider动态提供工具列表。核心思想一致:不要让模型一次看到全部。
3.2 按需注入:会话级工具裁剪
分层是空间维度的治理,按需注入是时间维度的治理。一个会话开始时,用户可能只是问“帮我查下这个接口的返回”,那这个会话根本不需要文件写入、邮件发送这些工具。根据会话意图动态裁剪工具集,能大幅降低噪声。
实现上,可以在会话初始化时做一次意图分类,或者让用户显式选择场景。比如一个“代码助手”场景只挂载代码相关工具,一个“数据分析”场景只挂载 SQL 和图表工具。Spring AI 的ChatClient支持在每次请求时传入不同的ToolCallback列表,这给按需注入提供了天然支持。
3.3 动态发现:MCP 场景下的工具筛选
MCP 场景下工具是动态注册的,治理难度更大。我的做法是加一层工具注册中心,所有 MCP Server 暴露的工具先注册到中心,中心负责:
- 去重:语义重复的工具只保留一个。
- 打标:给每个工具打上类别、优先级、适用场景标签。
- 过滤:根据当前会话策略,只把符合条件的工具下发给模型。
mcp server本身不负责治理,治理必须在上层做。这一点很多人没意识到,以为接了 MCP 就万事大吉,结果工具越接越多,Agent 越来越傻。
4. Java 生态实操:Spring AI 与 LangChain4j 的工具治理
4.1 Spring AI 里的工具注册与裁剪
Spring AI 的工具调用核心是ToolCallback。默认情况下,你通过.tools(...)注册的工具会全部暴露给模型。要治理,就得在注册环节做文章。
一个实用的模式是场景化工具包:
public enum AgentScene { CODE_ASSIST, DATA_ANALYSIS, WEB_RESEARCH } public class ToolRegistry { private final Map<AgentScene, List<ToolCallback>> sceneTools; public List<ToolCallback> toolsFor(AgentScene scene) { return sceneTools.getOrDefault(scene, List.of()); } }然后在构建 ChatClient 时按场景取工具:
ChatClient client = ChatClient.builder(chatModel) .defaultTools(toolRegistry.toolsFor(currentScene)) .build();这样每个场景下的工具数量可控。实测下来,把六十个工具拆成四个场景,每个场景 10 到 15 个,工具选择准确率能从 55% 拉回到 85% 以上。
注意:Spring AI 不同版本 API 有差异,
defaultTools和tools的行为要对照你用的版本文档确认。spring ai 2.0在工具管理上做了不少改进,建议优先用新版本。
4.2 LangChain4j 的动态工具提供
LangChain4j 的思路略有不同,它用ToolSpecification描述工具,通过ToolProvider动态提供。动态提供的价值在于:工具列表可以在运行时根据上下文变化。
ToolProvider dynamicProvider = request -> { String userQuery = request.userMessage().text(); if (userQuery.contains("数据库") || userQuery.contains("SQL")) { return sqlTools; } return generalTools; };这种写法让工具集随用户输入变化,天然实现了按需注入。langchain4j rag场景里特别有用——检索相关工具只在需要检索时才挂载。
4.3 MCP 工具在 Java 侧的接入与过滤
MCP 协议让 Java 应用可以接入外部工具服务。接入本身不难,难的是接入后的过滤。我的建议是:MCP 接入层和工具治理层分离。MCP 负责“拿到工具”,治理层负责“决定给模型哪些工具”。
一个典型的过滤策略表:
| 过滤维度 | 策略 | 目的 |
|---|---|---|
| 类别 | 只保留当前场景类别 | 降低噪声 |
| 优先级 | 低优先级工具延迟加载 | 减少上下文占用 |
| 使用频率 | 冷门工具折叠 | 聚焦高频 |
| 语义重复 | 保留描述最清晰的一个 | 消除歧义 |
| 权限 | 按用户角色过滤 | 安全合规 |
这套策略落地后,六十个工具在单次请求里实际暴露的可能只有 10 个左右,Agent 的“选择困难症”自然缓解。
5. 工具描述怎么写,模型才不迷糊
5.1 描述三要素:做什么、什么时候用、参数怎么填
工具数量治理是一方面,单个工具描述的质量是另一方面。我见过太多工具描述写成“查询数据”“执行操作”这种废话,模型看了等于没看。一个好的工具描述应该包含三要素:
- 做什么:一句话说清功能边界。
- 什么时候用:明确适用场景和不适用场景。
- 参数怎么填:每个参数的含义、格式、示例。
举个例子,对比两种写法:
差的写法:
name: search description: 搜索信息好的写法:
name: search_web_pages description: 在公开网页中搜索指定关键词,返回标题和摘要。 适用于查找最新资讯、事实核查。不适用于查询内部数据库。 参数 query 为搜索关键词,建议 3-8 个词。后者让模型一眼就知道边界,不会和“查询内部数据库”的工具混淆。
5.2 命名规范:动词 + 对象 + 限定
工具命名也有讲究。我推荐动词 + 对象 + 限定的结构,比如search_web_pages、query_user_table、write_local_file。避免用do、handle、process这种万能动词,也避免纯名词。
命名的一致性同样重要。如果有的工具用驼峰有的用下划线,模型在匹配时容易出错。统一风格,统一前缀,能显著降低误选率。
5.3 参数 schema 的陷阱
参数 schema 里最常见的坑是可选参数太多和类型模糊。一个工具有八个可选参数,模型很难判断该填哪几个。我的经验是:必填参数控制在三个以内,可选参数能砍就砍。如果确实需要复杂参数,考虑拆成多个工具,或者用嵌套对象加清晰示例。
类型模糊也很要命。比如一个参数叫time,是时间戳还是日期字符串?是秒还是毫秒?这些必须在 description 里写死,最好给示例值。
6. 常见问题与排查技巧实录
6.1 工具选择错误的排查路径
Agent 调错工具时,别急着怪模型。按这个顺序排查:
- 看工具描述是否重叠:把被误选的两个工具描述放一起,如果连你都分不清,模型更分不清。
- 看工具数量:当前上下文里有多少工具?超过 20 个就要警惕。
- 看参数 schema:是不是参数名太像导致混淆?
- 看用户输入:是不是输入本身就有歧义?
- 看模型:换一个模型对比,排除模型能力问题。
6.2 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 该调工具却不调 | 工具描述没覆盖该意图 | 补充适用场景描述 |
| 调了错误的工具 | 工具描述语义重叠 | 合并或重写描述 |
| 参数填错 | schema 不清晰 | 加示例、收紧类型 |
| 响应特别慢 | 工具太多占上下文 | 分层、按需注入 |
| 偶尔正常偶尔错 | 上下文长度波动 | 固定工具集大小 |
| 工具调用死循环 | 工具返回结果不明确 | 规范返回值格式 |
6.3 独家避坑经验
几个我踩过的坑,分享出来省得你重蹈覆辙:
- 别在工具返回值里塞大段文本。工具返回的内容也会进上下文,返回一个几万字的网页全文,直接把上下文撑爆。返回摘要或结构化数据。
- 工具报错信息要友好。模型看到“Error 500”不知道怎么办,看到“参数 query 不能为空,请提供搜索关键词”就知道怎么修。
- 给工具加超时和重试上限。Agent 有时会反复调同一个失败的工具,加个熔断机制。
- 定期做工具审计。每季度过一遍工具列表,删掉没人用的,合并重复的。工具治理是持续工作,不是一次性任务。
7. 从六十到十:一次真实的工具瘦身记录
最后分享一个我实际做过的工具瘦身案例。一个内部知识助手项目,最初接了 58 个工具,包括搜索、数据库、文件、邮件、日历、地图、代码执行等。上线后用户反馈“答非所问”“经常调错工具”。
我的处理步骤:
第一步,统计工具调用日志,找出实际被调用的工具。结果发现 58 个工具里,只有 14 个被真正调用过,其余 44 个几乎零调用。
第二步,按场景分组。把 14 个高频工具分成“检索类”“数据类”“操作类”三组,每组 4 到 5 个。
第三步,重写描述。把语义重叠的工具合并,比如三个搜索工具合并成一个,用参数区分搜索范围。
第四步,按需注入。根据用户问题类型,只挂载对应组的工具。
瘦身后,单次请求暴露的工具从 58 个降到 5 到 8 个,工具选择准确率从不到 60% 提升到 90% 以上,响应延迟也明显下降。用户反馈从“答非所问”变成了“还挺准”。
这个案例说明一个朴素的道理:Agent 的能力不取决于你给了它多少工具,而取决于它能不能在对的时候用对的工具。六十个工具不是能力,是负担。把工具治理好,十个工具能发挥出六十个都达不到的效果。
如果你现在手里的 Agent 也正被工具过载困扰,不妨先做一件事:把工具调用日志拉出来,看看哪些工具真的在被用。大概率你会发现,真正重要的工具,一只手数得过来。