ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent工具膨胀治理:Spring AI与LangChain4j分层路由实战

Agent工具膨胀治理:Spring AI与LangChain4j分层路由实战 1. 从六十个工具说起Agent 为什么会“挑花眼”“Agent 工具给到六十个它开始挑花眼”——这句话第一次看到的时候我笑了很久因为它太真实了。做过 Agent 开发的人都知道给模型挂三五个工具的时候它表现得像个靠谱的助理挂到二十个它开始偶尔选错挂到六十个它基本上就变成了一个站在自助餐厅里端着盘子发呆的人什么都想要什么都拿不准。这个现象背后其实是一个非常具体的工程问题工具数量增长带来的选择空间爆炸。假设一个 Agent 有 60 个工具每次用户提问它都要从这 60 个里挑出最合适的一个或几个那么理论上它面对的组合空间是 2 的 60 次方这个量级。即便模型再聪明它也不可能在这种规模下保持稳定的判断力。这不是模型能力的问题而是信息架构的问题。我最近在做一个基于Spring AI和LangChain4j的 Java Agent 项目中途因为业务需求不断叠加工具数量从最初的 8 个一路涨到了 60 多个。那段时间的日志简直没法看用户问“帮我查一下上个月的订单”Agent 去调了天气查询工具用户问“生成一份销售报表”它跑去调了用户信息接口。最离谱的一次用户只是说了句“你好”它居然触发了三个工具调用其中一个是删除缓存。所以这篇内容我想聊的不是“怎么让 Agent 更聪明”而是怎么在工具规模膨胀之后让 Agent 依然能保持清醒。这涉及到工具的组织方式、MCP 协议的接入策略、框架层面的路由设计以及在 Java 生态里具体怎么落地。如果你正在用 Spring AI 或者 LangChain4j 做 Agent 开发或者正在被 MCP 工具接入的数量问题困扰那这些踩坑经验应该对你有用。2. 工具膨胀的根源为什么六十个就成了分水岭2.1 工具数量与选择准确率的非线性关系先看一组我在项目里实测的数据。我们用一个固定的测试集包含 200 条真实用户请求分别在不同工具数量下测试 Agent 的工具选择准确率工具数量选择准确率平均响应时间误触发率596.5%1.2s2.1%1094.2%1.8s3.5%2088.7%2.9s7.8%4076.3%4.5s15.2%6061.8%6.7s24.6%这张表很说明问题。从 5 个工具到 20 个工具准确率下降还在可接受范围内但从 20 到 60准确率直接腰斩误触发率翻了七倍。更关键的是响应时间因为每次请求都要把所有工具的 schema 塞进上下文token 消耗随工具数量线性增长延迟自然跟着涨。注意这里的“误触发”指的是 Agent 调用了与用户意图无关的工具哪怕最终回答是对的这种调用也是有害的因为它可能产生副作用。2.2 上下文窗口不是免费的午餐很多人觉得现在模型上下文窗口都到 128K 甚至 200K 了塞 60 个工具的描述算什么。这个想法忽略了一个关键点上下文窗口大不等于注意力资源无限。工具描述在上下文里是“候选答案”模型需要在生成时对这些候选做注意力分配。当候选数量过多时注意力会被稀释模型更容易被表面相似的描述误导。比如“查询订单”和“查询用户”这两个工具在只有五个工具时模型能轻松区分但当上下文里同时存在“查询订单”“查询订单物流”“查询订单退款”“查询历史订单”“查询订单备注”等十几个相似工具时模型就开始犯迷糊了。我在 Spring AI 里做过一个对比实验同样 60 个工具一组用完整的 description一组把 description 压缩到 20 字以内。结果压缩组的准确率反而高了 8 个百分点。这说明工具描述的冗余信息在数量膨胀时会变成噪声。2.3 MCP 协议带来的双刃剑MCPModel Context Protocol的出现在很大程度上解决了工具接入的标准化问题。以前每接一个外部能力都要写适配层现在只要符合 MCP 协议Agent 就能直接调用。但这也带来一个新问题接入太容易了导致工具数量失控。我见过一个团队因为 MCP 接入成本低两周内接了 40 多个 MCP Server涵盖数据库查询、文件操作、浏览器控制、代码执行等各个方向。结果 Agent 的表现断崖式下跌。原因很简单MCP 让“能接”变得容易但没有解决“该不该接”和“怎么组织”的问题。在 Java 生态里LangChain4j对 MCP 的支持让这件事更顺手了你可以用几行代码就把一个 MCP Server 的工具全部注册进来。但顺手不等于正确后面我会讲怎么在框架层面做工具的分层和路由。3. 工具治理的核心思路分层、路由、收敛3.1 把六十个工具当成一个组织来管理我的核心思路很简单不要让一个 Agent 面对所有工具而是让工具像公司组织架构一样分层。具体来说把 60 个工具按照业务域分成若干组每组 5 到 8 个工具然后设置一个“路由层”来决定当前请求应该交给哪个组处理。这个路由层本身也可以是一个轻量级的 Agent或者干脆用规则引擎加语义匹配来实现。在 Spring AI 里我用的方案是第一层意图分类器把用户请求分到 6 到 8 个业务域第二层每个业务域内有一个子 Agent只挂载该域下的工具第三层子 Agent 执行具体工具调用这样每个 Agent 实际面对的工具数量控制在 8 个以内选择准确率能回到 90% 以上。代价是多了一次意图分类的调用但这次调用的成本远低于把 60 个工具塞进上下文的成本。3.2 工具描述的重写比工具本身更重要前面提到压缩 description 能提升准确率这里展开讲具体怎么做。一个典型的工具描述长这样Tool(description 查询指定用户的订单信息支持按时间范围、订单状态、支付方式筛选返回订单列表包含订单号、金额、状态、创建时间等字段) public ListOrder queryOrders(String userId, String startDate, String endDate, String status) { // ... }这段描述信息很全但在 60 个工具的上下文里它和其他查询类工具的描述高度重叠。我的做法是把它改写成Tool(description 按用户ID查订单可加时间/状态筛选) public ListOrder queryOrders(String userId, String startDate, String endDate, String status) { // ... }同时在系统提示词里加一段工具选择指南用自然语言说明什么场景该用什么工具。这样做的逻辑是把区分度信息从每个工具的描述里抽出来集中放在提示词里。模型读一段集中的指南比读 60 段分散的描述更容易建立全局判断。3.3 用 MCP 做工具隔离而不是工具堆叠MCP 的正确用法不是把所有 Server 都挂到同一个 Agent 上而是按业务域拆分 MCP Server每个 Server 只暴露该域的工具。比如mcp-order-server只暴露订单相关工具mcp-user-server只暴露用户相关工具mcp-report-server只暴露报表相关工具然后在 Agent 侧根据意图动态加载对应的 MCP Server 工具集。LangChain4j 支持在运行时动态添加和移除工具这个能力在这里就派上用场了。提示动态加载工具时要注意工具注册的生命周期管理避免同一个工具被重复注册导致上下文里出现重复描述。4. Java 生态下的具体实现Spring AI 与 LangChain4j 的配合4.1 用 Spring AI 做意图路由层Spring AI 的ChatClient很适合做轻量级的意图分类。我的做法是定义一个分类提示词让模型输出业务域标签public String classifyIntent(String userInput) { String prompt 你是一个意图分类器。根据用户输入从以下类别中选择一个最匹配的 ORDER, USER, REPORT, INVENTORY, PAYMENT, SYSTEM, OTHER 只输出类别名称不要输出其他内容。 用户输入%s .formatted(userInput); return chatClient.prompt() .user(prompt) .call() .content() .trim(); }这个分类调用的 token 消耗很小因为提示词短、输出短。实测下来分类准确率能到 95% 以上而且延迟只有 200 到 300 毫秒。4.2 用 LangChain4j 做工具执行层LangChain4j 的AiServices在工具调用方面更灵活特别是它支持动态工具注册。我把它用来做第二层的子 Agentpublic interface OrderAgent { SystemMessage(你是订单处理助手只处理订单相关请求。) String handle(UserMessage String input); } OrderAgent orderAgent AiServices.builder(OrderAgent.class) .chatLanguageModel(chatModel) .tools(orderTools) // 只挂载订单相关工具 .build();每个子 Agent 只挂载自己域内的工具工具数量控制在 8 个以内。这样既保留了 LangChain4j 在工具调用上的便利性又避免了工具数量膨胀。4.3 两层之间的衔接与降级策略路由层和子 Agent 之间的衔接需要处理几种边界情况分类失败如果意图分类返回了未知类别降级到通用 Agent挂载少量高频工具子 Agent 无法处理如果子 Agent 判断当前请求超出自己的域返回一个特殊标记由路由层重新分配多域请求如果用户请求涉及多个域路由层可以拆分成多个子请求分别交给对应子 Agent最后合并结果这里有个实操心得降级策略一定要有而且要在日志里明确记录降级原因。我一开始没做降级结果分类器偶尔抽风返回空字符串整个请求就挂了。后来加了降级到通用 Agent 的逻辑稳定性好了很多。5. 实操过程从六十个工具到稳定运行的完整改造5.1 第一步工具盘点与分类改造的第一步不是写代码而是把现有 60 个工具全部列出来做一次彻底盘点。我用的表格格式如下工具名所属域调用频率是否可合并描述长度queryOrdersORDER高否45字queryOrderDetailORDER高可合并到queryOrders38字queryUserInfoUSER高否32字...............盘点过程中发现几个问题有 8 个工具功能高度重叠可以合并成 3 个有 12 个工具调用频率极低可以考虑下线或移到备用工具集有 5 个工具的描述里包含了大量无关信息需要重写。这一步花了我大概半天时间但非常值得。工具治理的第一步永远是先看清楚自己有什么。5.2 第二步合并与下线合并的原则是如果两个工具的输入参数有 80% 重叠且输出结构相似就合并成一个工具用参数区分行为。比如queryOrderDetail和queryOrders合并后Tool(description 查订单传orderId查详情不传则按条件查列表) public Object queryOrders(String userId, String orderId, String startDate, String endDate, String status) { if (orderId ! null) { return orderDetailService.getById(orderId); } return orderService.query(userId, startDate, endDate, status); }下线低频工具时要谨慎先观察一段时间确认没有调用再移除。我是在日志里加了工具调用统计跑了一周才决定下线哪些。5.3 第三步描述重写与提示词优化描述重写的核心是去掉所有可以从参数名推断出来的信息。比如参数叫startDate描述里就不用再说“开始日期”参数叫status描述里就不用再说“状态”。重写后的工具描述控制在 15 到 25 字之间只保留最核心的区分信息。然后在系统提示词里加一段工具选择指南工具选择指南 - 涉及订单查询、修改、取消的用 ORDER 域工具 - 涉及用户信息、权限、偏好的用 USER 域工具 - 涉及数据统计、报表生成的用 REPORT 域工具 - 不确定时先用 queryOrderSummary 获取概览这段指南大概 200 字但它替代了原本分散在 60 个工具描述里的区分信息整体 token 消耗反而降低了。5.4 第四步路由层与子 Agent 的搭建路由层用 Spring AI 实现子 Agent 用 LangChain4j 实现两者通过一个简单的调度器衔接Service public class AgentDispatcher { private final IntentClassifier classifier; private final MapString, AgentHandler handlers; public String dispatch(String userInput) { String intent classifier.classify(userInput); AgentHandler handler handlers.get(intent); if (handler null) { handler handlers.get(GENERAL); // 降级 } return handler.handle(userInput); } }每个AgentHandler内部持有一个 LangChain4j 的AiServices实例只挂载对应域的工具。5.5 第五步灰度与监控改造完成后不要一次性全量切换先灰度 10% 的流量观察一周。监控指标包括工具选择准确率人工抽检平均响应时间误触发率降级触发次数用户满意度如果有反馈渠道我灰度期间发现了一个问题意图分类器对某些口语化表达识别不准比如“帮我看看那个东西”这种模糊请求。后来在分类提示词里加了一些模糊请求的示例准确率才上来。6. 常见问题与排查技巧实录6.1 工具选择准确率突然下降怎么排查先看是不是最近新增了工具。新增工具后准确率下降是最常见的原因因为新工具的描述可能和现有工具产生语义冲突。排查方法是把新增工具的描述和现有工具的描述做一次相似度计算找出相似度超过 0.8 的工具对检查它们是否真的需要区分。如果最近没新增工具那就看用户请求的分布是不是变了。有些业务场景的请求天然更容易混淆比如“查询”和“搜索”这类词在不同域里含义不同。6.2 MCP 工具注册后 Agent 不调用怎么办这个问题我遇到过好几次通常有三个原因工具描述太模糊MCP Server 返回的工具描述可能过于技术化模型看不懂。解决方法是加一层描述转换把技术描述改写成自然语言。工具名称有冲突不同 MCP Server 可能暴露同名工具导致注册时被覆盖。解决方法是给工具名加域前缀。权限或连接问题MCP Server 连接失败时工具会静默不可用。解决方法是加健康检查连接失败时在日志里明确报错。6.3 响应时间随工具数量增长太快怎么优化除了前面说的分层路由还有几个优化点工具描述缓存把工具 schema 的序列化结果缓存起来避免每次请求都重新生成并行工具调用如果多个工具之间没有依赖关系可以让模型并行调用LangChain4j 支持这个能力流式输出对于不需要工具调用的请求直接用流式输出减少等待感6.4 常见问题速查表问题现象可能原因排查方向解决方案准确率下降新增工具冲突计算工具描述相似度合并或重写描述误触发率高工具描述重叠检查描述区分度压缩描述提示词指南响应慢工具数量过多统计 token 消耗分层路由工具不调用描述模糊/名称冲突检查注册日志改写描述/加前缀分类失败提示词覆盖不足分析失败样本补充示例6.5 几个踩过的坑第一个坑是工具描述里的参数说明和实际参数不一致。有次我改了一个工具的参数名但忘了改描述结果模型按描述里的旧参数名传参调用一直失败。后来我养成了习惯改参数必改描述而且加了单元测试来校验。第二个坑是MCP Server 的超时设置。默认超时可能很短网络稍微抖动就失败。我把超时调到了 10 秒并且加了重试机制稳定性好了很多。第三个坑是子 Agent 之间的上下文隔离。一开始我让所有子 Agent 共享一个对话历史结果订单 Agent 能看到用户 Agent 的对话产生了奇怪的交叉影响。后来改成每个子 Agent 独立维护上下文只在必要时传递摘要信息。7. 工具治理的长期策略让 Agent 保持清醒工具数量增长是业务发展的必然结果不可能永远控制在 10 个以内。所以关键不是“不让工具变多”而是“工具变多之后依然能管好”。我的长期策略有三条第一条是定期做工具审计。每个月花半天时间盘点一次工具看哪些可以合并、哪些可以下线、哪些描述需要更新。这件事看起来琐碎但不做的话工具集就会慢慢腐化。第二条是建立工具准入标准。新工具接入前要回答三个问题这个工具解决什么问题现有工具能不能替代它的描述是否足够区分回答不清楚就不接。第三条是保持路由层的简单。路由层越简单越稳定不要试图让路由层做复杂的判断。意图分类就做意图分类不要在里面加业务逻辑。在 Java 生态里Spring AI 和 LangChain4j 都在快速迭代MCP 的支持也越来越完善。但工具多了会挑花眼这件事框架解决不了只能靠工程手段来治理。我现在的项目稳定在 60 多个工具通过分层路由和描述优化准确率维持在 90% 以上响应时间控制在 3 秒以内。这个结果不算完美但至少 Agent 不再站在自助餐厅里发呆了。最后分享一个小技巧如果你不确定某个工具该不该保留就把它下线一周看日志里有没有报错。没有报错就说明可以永久下线有报错就说明还有用但可以考虑合并到其他工具里。这个办法简单粗暴但实测很有效。
返回列表