
1. 工具一多Agent 最先崩的不是执行而是选择当 LangGraph 多工具 Agent 的工具数量从三五个膨胀到几十个时你会发现一个反直觉的现象真正拖垮系统的不是工具执行本身而是 LLM 在几十个工具里选错人。工具少的时候用户问帮我算 15 * 23Agent 直接调 calculator用户问查一下部署文档Agent 调 doc_search。但到了企业级场景画风会立刻变成这样搜索引、内部文档检索、数据库查询、工单系统、CRM、BI 报表、单位换算、计算器、发通知、建任务、发邮件、调工作流、查权限、查日志……一口气几十个工具摆在 Agent 面前。这时候用户说一句查一下用户数据Agent 到底该调用 web_search、doc_search还是 db_query三个工具的描述都可能沾点边web_search 也能搜用户数据的公开资料doc_search 也可能有用户数据说明文档db_query 也能查真正的用户表。工具少的时候靠 LLM 猜一猜也许还能忍工具多了以后如何选对工具会比如何执行工具更难。本文要解决的就是这个编排难题用一套「工具注册中心 优先级 意图路由 多工具编排」机制把几十个工具管起来让 Agent 不再从一大堆工具里盲选而是先分组、再筛选、最后执行。适合正在用 LangGraph 搭建多工具 Agent、已经踩过工具选错坑的开发者。下面所有代码都可以直接复制运行我会把注册表结构、路由配置、工具分组策略、新增工具后的验证方法全部拆开讲清楚。2. 为什么需要多工具治理选择歧义、Token 成本与管理混乱真实业务里的 Agent 通常不是一个聊天机器人而是一个能接入企业系统、自动完成任务的执行层。一旦 Agent 接入的工具变多问题会集中爆发在三个地方。2.1 选择歧义多个工具都像是正确答案比如用户说查一下订单数据这个请求可能命中多个工具db_query 查结构化订单表、doc_search 查订单系统说明文档、web_search 查互联网公开资料、bi_report_query 查 BI 报表、log_search 查订单服务日志。如果没有明确路由规则LLM 很容易在多个工具之间摇摆导致同一个问题这次查库、下次查文档结果不稳定。这种不稳定在演示时看不出来一上生产就会被用户投诉同样的问法答案不一样。2.2 Token 成本工具描述越多Prompt 越长大多数工具调用框架都会把工具的名称、参数 Schema、描述信息一起塞进 LLM 请求里。工具越多请求越长10 个工具还能接受30 个工具明显变慢、变贵80 个工具时 Prompt 可能开始膨胀到难以管理。所以多工具场景下不应该每次都把所有工具暴露给 LLM而应该先判断当前请求大概属于哪个工具组再动态注入少量相关工具。这一步能直接把 Token 消耗砍掉一大半。2.3 管理混乱工具增删改容易牵一发动全身当工具越来越多后你会开始遇到工程治理问题新工具上线后如何不影响旧工具同类工具哪个优先级更高哪些用户能调用有副作用的工具某个工具失败率升高如何监控旧工具被新工具替代如何灰度切换这套问题后端工程师应该很熟悉它本质上不是一个Prompt 技巧问题而是一个服务治理问题。可以把工具理解成微服务工具注册中心约等于 Nacos / Eureka工具分组约等于服务分类优先级约等于权重 / 降级策略意图路由约等于 API Gateway工具编排约等于工作流引擎。2.4 本文要解决的三类问题多工具 Agent 的核心挑战可以拆成三类。第一工具怎么管几十个工具不能散落在代码各处需要有一个统一的注册中心来记录工具名称、所属分组、优先级、权限标签、版本、owner、SLA、是否有副作用。第二工具怎么选不能让 LLM 每次都从几十个工具中海选更稳的方式是两级决策用户问题 → 识别意图 → 定位工具组 → 组内按优先级选择工具把几十选一缩小成先选组再在组内二三选一。第三工具怎么协作复杂任务通常不是一个工具能解决的比如查订单数据并计算总额至少需要查数据库、拿到结果、调用计算器、汇总输出所以多工具 Agent 不只要会选工具还要会编排工具。3. 总体架构先路由到工具组再组内竞争选择本文 Demo 的核心结构可以概括成一句话不要让 Agent 在几十个工具里直接盲选而是先通过意图路由缩小范围再在工具组内按优先级选最合适的工具。整体流程是用户问题 → 意图识别 → 定位工具组 → 组内按优先级选择 → 执行 → 汇总。这个结构的好处很明显降低歧义先定工具组再在组内选工具、降低 Token 成本只把相关工具暴露给 LLM、便于治理所有工具元数据统一注册、方便扩展新增工具时只需注册到对应组、方便灰度调优先级即可影响竞争结果。3.1 工具注册中心把工具从散落函数变成可治理资源工具注册中心负责把工具统一管理起来核心职责有三个注册工具、按分组获取工具、按优先级获取组内最佳工具。下面这段注册代码可以直接复制到你的项目里class ToolRegistry: 工具注册中心 — 管理工具的分组、优先级和元数据。 def __init__(self): self._tools: dict[str, dict] {} self._groups: dict[str, list[str]] {} def register(self, tool_obj, group: str, priority: int 5): 注册工具。priority: 1(最低) - 10(最高) self._tools[tool_obj.name] { tool: tool_obj, group: group, priority: priority, } self._groups.setdefault(group, []).append(tool_obj.name) def get_group_tools(self, group: str) - list: names self._groups.get(group, []) tools [] for name in names: entry self._tools.get(name) if entry: tools.append(entry) return sorted(tools, keylambda x: x[priority], reverseTrue) def get_best_tool(self, group: str): tools self.get_group_tools(group) return tools[0][tool] if tools else None注册时这样调用registry ToolRegistry() registry.register(web_search, search, priority7) registry.register(doc_search, search, priority8) registry.register(db_query, search, priority9) registry.register(calculator, compute, priority8) registry.register(unit_converter, compute, priority6) registry.register(send_notification, action, priority7) registry.register(create_task, action, priority7)三个搜索类工具都属于 search 组但优先级不同db_query 是 9doc_search 是 8web_search 是 7。所以当用户的问题比较模糊比如查用户数据它同时可能命中多个搜索工具但系统会优先选择 db_query因为它在 search 组里优先级最高。没有注册中心时工具只是一个个 Python 函数有了注册中心后工具就带上了工程治理所需的元数据工具名、分组、优先级。后面的路由、竞争选择、动态加载、权限控制、灰度发布都依赖这些元数据。3.2 优先级竞争选择给歧义定先后注册中心中最关键的方法是get_group_tools和get_best_tool。它的逻辑很简单根据 group 找到该组所有工具按 priority 从高到低排序取第一个作为最佳工具。这就是工具竞争选择。当多个工具都可能匹配同一个请求时不再把决定权完全交给 LLM而是给工具设定明确优先级。例如查用户数据匹配到 search 组候选工具是 db_query(9)、doc_search(8)、web_search(7)最终选择 db_query。这解决了开篇那个问题用户问查一下用户数据到底该用哪个工具答案是先路由到 search 组再由优先级决定 db_query 胜出。3.3 意图路由先判断属于哪个工具组工具注册中心解决的是工具如何管理但还需要回答另一个问题用户这句话到底属于哪个工具组这就是意图路由。Demo 中使用关键词规则做意图分类def classify_intent(content: str) - str: if any(kw in content for kw in [综合, 报告, 分析]): return pipeline elif any(kw in content for kw in [数据库, 用户, 订单, 库存, 数据]): return search_db elif any(kw in content for kw in [文档, 部署, 架构, 规范, 内部]): return search_doc elif any(kw in content for kw in [搜索, 新闻, 百科, 互联网]): return search_web elif any(kw in content for kw in [计算, 算, , -, *, /]): return compute_calc elif any(kw in content for kw in [转换, 单位, 温度, 重量]): return compute_convert elif any(kw in content for kw in [通知, 发送, 邮件, 提醒]): return action_notify elif any(kw in content for kw in [任务, 待办, 分配]): return action_task return unknown这个 Demo 用关键词是为了方便理解。在生产环境中你可以把这一步替换成 LLM Router、小模型分类器、embedding 相似度匹配、规则 模型混合路由或基于历史调用数据训练的路由模型。但不管实现方式怎么换关键思想不变先判断用户意图再加载相关工具组而不是一开始就把所有工具交给 LLM。3.4 一个容易踩的坑路由顺序会影响结果在这个 Demo 中pipeline 意图必须放在最前面判断。为什么因为一个综合类问题往往也会包含数据库关键词。例如综合分析查订单数据并计算总额这句话同时包含综合、分析、订单、数据、计算。如果你把 pipeline 判断放在后面它可能先被这段逻辑命中elif any(kw in content for kw in [数据库, 用户, 订单, 库存, 数据]): return search_db于是系统会直接路由到 search_db只调用数据库查询不会进入多工具管道。这就是关键词路由的短路陷阱if / elif 谁在前谁优先命中。所以宽口径、复合型意图要放在前面窄口径、单工具意图放在后面。生产里即使使用 LLM Router也要注意这个问题因为复杂意图往往覆盖多个简单意图如果没有显式优先级路由仍然会飘。3.5 多工具编排的三种模式工具选对只是第一步。真正有用的企业级 Agent还需要让多个工具协作。多工具编排常见有三种模式。A. 扇出式编排同一个请求同时调用多个独立工具然后汇总结果比如分析一个技术方案时查内部文档、查公开资料、查历史工单最后综合输出这些工具之间没有强依赖可以并行或准并行执行。B. 管道式编排A 的输出作为 B 的输入比如先查订单数据再计算 GMV 增长率。这类任务必须注意一点如果 B 依赖 A 的真实输出那么 A 和 B 不能在同一轮一次性生成因为同一轮生成多个 tool_calls 时第二个工具的参数是在第一个工具执行前就生成好的此时它看不到第一个工具的真实结果。所以真正有数据依赖的管道式编排应该走多轮 ReAct第 1 轮调用 db_query第 2 轮把 db_query 结果回传给 LLM第 3 轮 LLM 基于查询结果生成 calculator 调用第 4 轮汇总最终结果。C. 条件式编排根据前一个工具的结果决定是否调用下一个工具比如查库存如果库存低于阈值就创建补货任务如果库存正常就只返回结果。这类编排需要在工具执行后做分支判断不能只靠一次 tool call 完成。3.6 可复制的路由与编排配置片段把上面的思路落成一份可复制的配置你可以直接放进项目里作为工具注册表{ registry_version: 1.0, groups: { search: { description: 信息检索类工具组, tools: [ {name: db_query, priority: 9, side_effect: false}, {name: doc_search, priority: 8, side_effect: false}, {name: web_search, priority: 7, side_effect: false} ] }, compute: { description: 计算与转换类工具组, tools: [ {name: calculator, priority: 8, side_effect: false}, {name: unit_converter, priority: 6, side_effect: false} ] }, action: { description: 有副作用的操作类工具组, tools: [ {name: send_notification, priority: 7, side_effect: true}, {name: create_task, priority: 7, side_effect: true} ] } }, routing: { pipeline: [综合, 报告, 分析], search_db: [数据库, 用户, 订单, 库存, 数据], search_doc: [文档, 部署, 架构, 规范, 内部], search_web: [搜索, 新闻, 百科, 互联网], compute_calc: [计算, 算, , -, *, /], compute_convert: [转换, 单位, 温度, 重量], action_notify: [通知, 发送, 邮件, 提醒], action_task: [任务, 待办, 分配] } }这份配置把工具分组、优先级、副作用标记、路由关键词全部显式化。新增工具时只需要在对应组的 tools 数组里加一条再在 routing 里补充关键词即可不需要改动 Agent 主逻辑。如果你用的是 LangGraph 的 StateGraph把这份配置读进来后在 agent_node 里根据 intent 决定生成哪些 tool_calls就能实现先路由到工具组再组内竞争选择的完整链路。4. 验证请求新增工具后如何确认路由命中与调用链正常配置写完之后最关键的一步是验证。很多人写完注册中心和路由就以为完事了结果上线后发现新工具根本没被调用或者调用链断在中间。下面给出一套可复制的验证流程。4.1 验证工具注册是否生效先跑一段最小验证确认注册中心能正确列出分组和优先级def demo_tool_registry(): registry create_default_registry() print(已注册的工具组:) for group, tool_names in registry.groups.items(): print(f [{group}]: {tool_names}) for t in registry.get_group_tools(group): print(f - {t[tool].name} (优先级: {t[priority]})) print(\n各组最高优先级工具:) for group in registry.groups: best registry.get_best_tool(group) print(f {group} → {best.name if best else N/A}) return registry预期输出类似已注册的工具组: [search]: [web_search, doc_search, db_query] - db_query (优先级: 9) - doc_search (优先级: 8) - web_search (优先级: 7) [compute]: [calculator, unit_converter] - calculator (优先级: 8) - unit_converter (优先级: 6) 各组最高优先级工具: search → db_query compute → calculator action → send_notification如果这里输出的优先级顺序不对说明 register 时的 priority 参数写反了或者 get_group_tools 的排序方向错了。这一步是后面所有验证的基础必须先过。4.2 验证意图路由命中用一组测试用例覆盖每个工具组确认路由能命中预期工具test_cases [ (查一下数据库中的订单数据, db_query), (搜索互联网上的 python 信息, web_search), (查看内部部署文档, doc_search), (帮我计算 15 * 23 7, calculator), (发送通知给团队, send_notification), ] for question, expected_tool in test_cases: result app.invoke({ messages: [HumanMessage(contentquestion)], tool_call_count: 0, tools_used: [], }) actual_tools result.get(tools_used, []) match expected_tool in actual_tools print(f{✓ if match else ✗} {question[:25]}) print(f 期望: {expected_tool} | 实际: {actual_tools})如果某个用例没命中先检查 routing 关键词是否覆盖了这句话里的词再检查 if / elif 的顺序是否被更宽的口径提前拦截。特别是综合分析这类词一定要放在最前面。4.3 验证多工具调用链对于管道式编排要确认一次请求能触发多个工具并且调用链完整result app.invoke({ messages: [HumanMessage(content综合分析查订单数据并计算总额)], tool_call_count: 0, tools_used: [], }) print(f使用的工具: {result[tools_used]}) print(f调用次数 : {result[tool_call_count]}) print(f消息链长度: {len(result[messages])}) for i, msg in enumerate(result[messages]): if isinstance(msg, HumanMessage): print(f [{i}] 用户: {msg.content[:50]}) elif isinstance(msg, AIMessage): if msg.tool_calls: tools [tc[name] for tc in msg.tool_calls] print(f [{i}] Agent 调用工具: {tools}) else: print(f [{i}] Agent: {msg.content[:50]}) elif isinstance(msg, ToolMessage): print(f [{i}] 工具 [{msg.name}]: {msg.content[:50]})预期能看到 HumanMessage、带 tool_calls 的 AIMessage、多条 ToolMessage、最终汇总消息。如果 tools_used 里只有一个工具说明 pipeline 意图没被命中检查综合分析是否在 routing 里且排在最前。4.4 验证竞争选择是否解决歧义最后验证模糊查询下的竞争选择registry create_default_registry() for query, group in [(查用户数据, search), (计算并转换温度, compute)]: group_tools registry.get_group_tools(group) print(f查询: {query}) print(f匹配工具组: {group}) for t in group_tools: print(f - {t[tool].name} (priority{t[priority]})) best group_tools[0] if group_tools else None if best: print(f → 选择: {best[tool].name} (最高优先级))查用户数据应该输出 db_query 胜出。如果输出的是 web_search说明优先级配置反了。这套验证跑通基本可以确认注册中心、路由、竞争选择、多工具编排四条链路都正常。5. 本篇常见错误排查401、local proxy failed、reading choices、OAuth多工具 Agent 在接入模型服务时报错往往集中在几个固定位置。下面按真实报错逐条排查。5.1 401 UnauthorizedKey 没配对或没加载现象是请求直接返回 401日志里能看到401 Unauthorized或invalid api key。原因通常是环境变量没设置或者代码里读的变量名和实际设置的不一致。排查步骤先确认TAOTOKEN_API_KEY这类环境变量在当前 shell 里能echo出来再确认代码里读取的变量名一致最后确认 Key 没有多余空格或换行。如果你用的是 Codex 的 auth.json要确认文件里 Base URL、Key、Model ID 三件套都写全了{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-5 }三件套缺任何一个都会导致 401 或模型找不到。Cline MCP 和 CC Switch 的配置同理Base URL、Key、Model ID 必须同时存在且拼写一致。5.2 local proxy failed本地代理配置冲突现象是local proxy failed或连接被拒绝。这类报错通常和本地网络配置有关先检查是否有残留的代理环境变量HTTP_PROXY、HTTPS_PROXY指向了一个已经关闭的端口。排查方法在终端里unset HTTP_PROXY HTTPS_PROXY后重试如果用的是 IDE 插件检查插件设置里的代理项是否为空。确认网络能正常访问https://taotoken.net/api后再跑一次最小请求验证。5.3 reading choices 报错响应结构解析失败现象是error reading choices或choices is empty。这通常意味着返回的 JSON 结构和代码里解析的字段不匹配。排查步骤先用 curl 直接打一次接口看原始返回长什么样curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-5, messages: [{role: user, content: hello}] }如果 curl 能返回正常结构说明是代码里的解析路径写错了如果 curl 也报错检查 Model ID 是否拼写正确。多工具 Agent 里如果用了自定义的响应解析层要特别注意 tool_calls 字段的嵌套层级不同模型返回的结构可能略有差异。5.4 OAuth 相关报错认证方式不匹配现象是OAuth token expired或invalid grant。如果你用的是 Claude Code 这类工具它可能默认走 OAuth 流程而你的环境配置的是 API Key 模式两者会冲突。排查方法确认当前工具用的是 API Key 还是 OAuth如果用 API Key把 OAuth 相关的缓存文件清掉再重启如果用 OAuth确认 token 没过期。Claude Code 接入时Base URL 填https://taotoken.net/apiKey 填你的 API KeyModel ID 填对应模型三件套对齐后 OAuth 冲突基本能消除。5.5 工具被调用但结果为空现象是 tools_used 里有工具名但 ToolMessage 内容是空的或报未注册的工具。原因通常是 tool_map 构建时漏了某个工具或者工具名大小写不一致。排查方法在 tool_executor 里打印tool_name和tool_map.keys()对比确认注册时用的tool_obj.name和调用时用的 name 完全一致。LangGraph 里工具名默认取函数名如果你用了tool装饰器但没指定 name就以函数名为准。5.6 多轮 ReAct 不收敛一直循环调用现象是 tool_call_count 一直涨Agent 反复调用同一个工具。原因通常是 agent_node 里对 ToolMessage 的处理逻辑有 bug或者 should_continue 判断条件写错。排查方法加一个 MAX_ITERATIONS 上限Demo 里是 6超过就强制返回检查 should_continue 是否在收到 ToolMessage 后正确返回 end确认 agent_node 在收到 ToolMessage 后走的是汇总分支而不是重新生成 tool_calls。6. 语义一致 CTA把注册中心和路由跑起来到这里注册中心、优先级竞争、意图路由、多工具编排四条链路已经完整。你可以先把本文的 JSON 配置和 ToolRegistry 类复制到项目里跑通第 4 节的验证脚本确认每个工具组都能被正确路由和命中。如果验证过程中遇到 401 或 reading choices 这类报错回到第 5 节逐条对照排查。需要申请 API Key 并查看接入文档可以走 API Keys 页面和接入文档想先验证模型对话效果用模型对话页面直接试如果是长期编码或 Agent 场景建议直接上 Coding Plan把 Base URL、Key、Model ID 三件套配好再回到本文的注册中心结构上扩展你自己的工具组。新增工具时记住一个原则先注册到对应组、设好优先级、补上路由关键词再跑一遍第 4 节的验证脚本确认路由命中和调用链都正常才算真正接入完成。