ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent工具过载:六十个工具为何让模型挑花眼?

Agent工具过载:六十个工具为何让模型挑花眼?

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 调错工具时,别急着怪模型。按这个顺序排查:

  1. 看工具描述是否重叠:把被误选的两个工具描述放一起,如果连你都分不清,模型更分不清。
  2. 看工具数量:当前上下文里有多少工具?超过 20 个就要警惕。
  3. 看参数 schema:是不是参数名太像导致混淆?
  4. 看用户输入:是不是输入本身就有歧义?
  5. 看模型:换一个模型对比,排除模型能力问题。

6.2 常见问题速查表

现象可能原因解决方向
该调工具却不调工具描述没覆盖该意图补充适用场景描述
调了错误的工具工具描述语义重叠合并或重写描述
参数填错schema 不清晰加示例、收紧类型
响应特别慢工具太多占上下文分层、按需注入
偶尔正常偶尔错上下文长度波动固定工具集大小
工具调用死循环工具返回结果不明确规范返回值格式

6.3 独家避坑经验

几个我踩过的坑,分享出来省得你重蹈覆辙:

  • 别在工具返回值里塞大段文本。工具返回的内容也会进上下文,返回一个几万字的网页全文,直接把上下文撑爆。返回摘要或结构化数据。
  • 工具报错信息要友好。模型看到“Error 500”不知道怎么办,看到“参数 query 不能为空,请提供搜索关键词”就知道怎么修。
  • 给工具加超时和重试上限。Agent 有时会反复调同一个失败的工具,加个熔断机制。
  • 定期做工具审计。每季度过一遍工具列表,删掉没人用的,合并重复的。工具治理是持续工作,不是一次性任务。

7. 从六十到十:一次真实的工具瘦身记录

最后分享一个我实际做过的工具瘦身案例。一个内部知识助手项目,最初接了 58 个工具,包括搜索、数据库、文件、邮件、日历、地图、代码执行等。上线后用户反馈“答非所问”“经常调错工具”。

我的处理步骤:

第一步,统计工具调用日志,找出实际被调用的工具。结果发现 58 个工具里,只有 14 个被真正调用过,其余 44 个几乎零调用。

第二步,按场景分组。把 14 个高频工具分成“检索类”“数据类”“操作类”三组,每组 4 到 5 个。

第三步,重写描述。把语义重叠的工具合并,比如三个搜索工具合并成一个,用参数区分搜索范围。

第四步,按需注入。根据用户问题类型,只挂载对应组的工具。

瘦身后,单次请求暴露的工具从 58 个降到 5 到 8 个,工具选择准确率从不到 60% 提升到 90% 以上,响应延迟也明显下降。用户反馈从“答非所问”变成了“还挺准”。

这个案例说明一个朴素的道理:Agent 的能力不取决于你给了它多少工具,而取决于它能不能在对的时候用对的工具。六十个工具不是能力,是负担。把工具治理好,十个工具能发挥出六十个都达不到的效果。

如果你现在手里的 Agent 也正被工具过载困扰,不妨先做一件事:把工具调用日志拉出来,看看哪些工具真的在被用。大概率你会发现,真正重要的工具,一只手数得过来。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进