多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Agent工具太多怎么选?从function calling到Jev路由决策层

Agent工具太多怎么选?从function calling到Jev路由决策层 1. 先确认一个事实Tool、MCP、Skill 正在把 Agent 的“工具箱”塞爆如果你最近在搭 Agent或者只是在 Codex、Cursor、Trae 里多挂了几个插件多半已经感觉到一个变化工具列表越来越长长到模型开始“选择困难”。先理清三个词因为很多人把它们混着用。Tool 是最底层的能力单元本质上就是一个函数或 API模型通过 function calling 决定要不要调它。MCPModel Context Protocol是接入协议它做的事是把“一堆工具”标准化成一个 server 暴露出来比如 playwright mcp 暴露浏览器操作blender mcp 暴露三维场景操作burpsuite mcp 和 yakit mcp 暴露安全测试能力。Skill 比 Tool 高一层它不只包含工具还捆绑了提示词、流程模板和 few-shot 示例典型如 Codex 的 skill 插件、Cursor 的 skill 市场甚至有人把“数学建模”“领域知识库”也做成了 skill。就这么个生态随便一个认真做的 Agent 项目工具轻松破 50 个。我自己维护过一个安全测试助手挂着 burp 和 yakit 两个 MCP server再加上 playwright、代码执行、文件读写、网页搜索数了数光是顶层工具就有 60 多个还没算各种 skill。这不是夸张是现在做 Agent 的常态。工具多了以后痛感是真实的而且有三个特别典型。第一上下文被工具描述塞满。一个工具的平均描述大概 150 到 250 token60 个工具就是一万多 token。模型每轮都要“读”一遍这些描述再决定调谁有效推理空间被压缩得很厉害用户多聊几轮响应质量肉眼可见地下降。第二相似工具让模型摇摆不定。你同时挂了两个都能“执行浏览器操作”的工具一个走 playwright mcp一个走 chrome devtools mcp模型经常在两个之间反复试。我在日志里见过最典型的报错就是 “tool call cancelled because tool-call flooding was detected”——意思是模型在短时间内连续发太多工具调用被运行时拦下来。这不是模型笨是它真的分不清该用哪个。第三维护成本上来了。工具要升级、要临时下架、要区分权限如果全靠主模型“凭感觉”选你根本没法保证它每次都选到正确的那个。你更没法做灰度因为选择逻辑藏在模型权重里不在你的代码里。所以我一直觉得工具一多“路由”就不是一个可选项而是刚需。问题只是谁来路由怎么路由。2. 路由到底是干什么的Jev 又在这一层里扮演什么角色先说清楚“路由”这个词。在 Agent 系统里路由不是“执行”而是“决策”。它要回答的问题是现在这个用户的请求应该交给哪个 Tool、哪个 MCP server、哪个 Skill而不是先让主模型把 60 个工具全看一遍。传统上我们有三条路可选各有各的死穴。原生 function calling让主模型自己选。实现最简单工具少的时候最好用。但工具一多token 开销和选错概率一起涨。提示词约束在 system prompt 里写“浏览器操作优先用 playwright”。零依赖但规则一多模型照样违反而且你没法动态更新规则。规则和 embedding 路由提前用关键词或向量相似度把请求匹配到工具。便宜、快但覆盖不了自然语言的灵活表达遇到两个描述相似的工具基本废掉。现在的问题是原生 function calling 已经不够用了规则路由又太死所以需要一个专门做“决策”的层。这正是 Jev 这类路由方案出现的原因。从目前能看到的公开信息来判断Jev 的定位是一个独立的、轻量的路由决策层或者说一个专门为工具选择场景设计的路由模型。它不负责干活只负责在很短的响应时间内告诉你“下一步该调谁”。如果你在 Codex 或 Cursor 里搜过 Jev 相关的内容大概会发现它经常和 MCP、Skill 这些词一起出现——因为 MCP 和 Skill 越多越需要有人先做一轮筛选而这个筛选动作本身如果让主模型做成本太高。我先把话说明白Jev 的官方文档和模型细节我目前看到的公开资料不算齐全所以本文不会去编造它的具体参数。下面讲的是一套“路由决策层”的通用架构也是 Jev 这类方案大概率走的路子。你完全可以把这套逻辑迁移到任何类似的模型或自建服务上思路是通用的。Jev 这类路由层和普通 function calling 最大的区别在于它把“选择”从主模型里拆出来了。拆出来之后有几个直接的好处主模型的上下文干净了不再被 60 个工具描述塞满选错时可以单独修路由逻辑不用重新调主模型工具的增删改只影响路由层不影响整体对话质量。代价也很明确多了一次网络请求多了一层要维护的依赖如果路由模型本身选得不准那还不如不拆。路由不是银弹它是把问题从“模型不会选”转变成“如何把工具描述写好”的工程手段。3. 路由决策层的几个关键设计细节既然要自己搭或接路由层就得理解几个核心设计点。很多人以为路由就是“把用户 query 丢给模型让它返回一个工具名”这么想会踩很多坑。3.1 工具注册表描述写的是“使用条件”不是“功能说明书”路由层能不能选对一半的功夫在工具注册表上。每个工具在注册表里至少要包含这些字段工具名、适用场景、反例场景、输入参数 schema、所属 server、优先级、调用成本。这里最容易被忽略的是“反例场景”也就是“什么时候不要用我”。我举个例子。同样是浏览器能力一个工具叫browser_click描述如果写成“点击页面上某个元素”另一个叫browser_fill描述写成“在输入框填入文字”路由模型还能分得清。但如果你有两个都能“打开网页”的工具一个走 playwright、一个走 chrome devtools描述却都写成“打开指定网址”那路由模型只能在两个里瞎猜。正确写法是给其中一个加上“适用于需要等待网络空闲和录制回放时”给另一个加上“适用于需要调试协议层、抓取请求详情时”——也就是把“什么时候选我”写清楚而不是把“我能干什么”写清楚。3.2 先粗筛后精排别让路由模型做“百题选择”第二个关键细节是不要把 60 个工具一次性全塞给路由模型让它直接输出一个答案。输出空间越大模型越容易漂。正确做法是两级先粗筛用关键词、标签或 embedding 把候选从 60 个缩到 10 到 20 个再精排把这十几个候选的简介交给路由模型让它选一个或排个序。粗筛层可以做得非常便宜。比如你事先给每个工具打了标签browser、security、file、data、math再用一个简单的 embedding 模型算 query 和标签的相似度取 Top 15 就够。这一步甚至可以不用 LLM用传统检索就行。精排层才是 Jev 这类模型发挥的地方它要在十几个“都还有点像”的选项里结合当前对话上下文选出最合适的一个。这样既控制了路由模型的输入长度也提高了准确率。3.3 路由层的输入输出结构路由层不要只喂一句用户 query。我踩过几次坑之后现在的标准做法是喂三样东西用户最近的 query、候选工具的“一行简介 使用条件”、当前执行上下文。执行上下文包括已经完成了哪些步骤、上一个工具返回了什么结果、当前对话轮数。举个例子用户说“把这个页面的标题抓下来”。如果只给路由层这一句它可能在“网页截图”和“读取页面标题”两个工具里犹豫。但如果你把“上一步已经打开了页面返回了当前 URL 和页面结构”也告诉它它基本不会选错。路由层的输出建议用结构化 JSON不要只返回一个名字。我一般让它返回这么几个字段route_to选中的工具 id、reason一句话说明为什么选它、confidence0 到 1 的置信度、fallback如果它不可用时的备选工具。reason 字段特别有用排查的时候能直接看到模型当时的判断依据比黑盒好太多了。3.4 成本账什么情况下路由真的划算路由不是白给的它自己也要花 token、花时间。所以值不值得上得算一笔账。我按一个典型场景估过工具 50 个每个工具描述平均 200 token如果走原生 function calling主模型每轮要读 50 个工具描述共计大概 10000 token。如果走路由方案粗筛层把候选缩到 12 个再把每个候选的“一行简介”喂给路由模型大概 1500 token主模型只需要看到被选中工具的完整 schema约 200 到 400 token。算下来每轮能省七八千 token路由模型自己花掉的那一两千 token 根本不算什么。但这里有个前提你的对话轮数够多或者工具调用够频繁。如果就是单个问题单次调用多出来的一次路由请求反而拖慢响应。我的经验值是工具少于 10 个、场景单一、单次响应的延迟极其敏感这三条占了两条就别上独立路由层了。上了反而添乱。3.5 兜底与安全路由失败不能等于系统失败路由层再准也有失误的时候所以一定要设计降级路径。我常用的方案有三个层级第一路由模型置信度过低时回退到主模型原生 function calling让主模型在完整工具列表里选慢一点但不会选不到第二路由选出来的工具调用失败立即用fallback字段里的备选工具重试第三如果重试也失败明确告诉用户“当前能力不足”不要硬编一个结果。安全方面也要注意一点路由结果只是“建议”权限校验不能省。也就是说如果一个工具在当前角色下没有权限无论路由模型怎么建议执行层都必须在调用前再做一道检查。路由模型可能被 prompt 注入误导去推荐一个危险工具但执行层守住权限底线系统就还是安全的。4. 实操把一个路由决策层接进 Agent 工作流理论讲完上实操。下面这套流程我用了挺久核心逻辑在任何环境里都能跑。具体接口以你用的路由服务官方文档为准这里重点讲思路和坑。4.1 前置准备能发起模型调用的环境就够你不需要一套复杂的框架。一个能调用 LLM 的 Python 环境加一个能列出工具列表的 MCP client 或工具注册表就足够开始。我这边用的是 Python 3.10 一个普通的 OpenAI 兼容接口因为大多数路由模型都提供这种兼容格式省去改 SDK 的麻烦。MCP 这边我用的是官方 Python SDK 来拉起 server 并拿到工具清单。4.2 先把工具注册表整理成结构化数据不管工具是从 MCP server 发现的还是代码里手工注册的第一步永远是把它落成结构化数据。下面是一份我常用的 YAML 风格的注册表条目每个工具一条。- id: playwright_open_url name: 打开网页 server: playwright_mcp tags: [browser, navigation] description: 在浏览器中打开指定 URL 并等待加载完成 use_when: 用户需要访问某个网页、读取页面内容、或后续要对该页面做操作时 do_not_use_when: 用户只是想搜索信息且不关心页面渲染结果时应优先走 web_search input_schema: url: type: string required: true注意use_when和do_not_use_when这两个字段是路由的关键。我在 3.1 节说过描述要写使用条件而这两个字段就是把使用条件显式化。另一个容易被忽视的点是MCP server 返回的工具描述往往只写着“这个工具是什么”你要在注册表里补上“什么时候选它”这一步不能偷懒。4.3 实现一个简单的预路由函数接下来是核心代码。思路是把用户 query、候选工具简介、当前上下文合在一起让路由模型返回 JSON。下面是我项目里简化后的版本。import json from openai import OpenAI client OpenAI(base_urlROUTER_BASE_URL, api_keyROUTER_API_KEY) def route(user_query: str, candidates: list[dict], context: str) - dict: candidate_lines \n.join( f- {c[id]}: {c[description]}。适用{c[use_when]}。不适用{c[do_not_use_when]} for c in candidates ) prompt f 当前任务上下文 {context} 用户最新请求 {user_query} 候选工具只从这些里面选 {candidate_lines} 请只输出 JSON不要输出其他内容格式如下 {{route_to: 工具id, reason: 选择理由, confidence: 0-1, fallback: 备选工具id}} .strip() resp client.chat.completions.create( modeljev-router, temperature0.1, messages[{role: user, content: prompt}], response_format{type: json_object}, ) return json.loads(resp.choices[0].message.content)这段代码看着简单但有几个细节值得说明。temperature我固定压到 0.1 左右路由是选择题不是创作题随机性越低越好。response_format强制 JSON省掉一大堆解析容错代码。候选列表不要超过 12 个超过之后准确率明显下降。context 那一项不要省哪怕只是一句“前一步已调用 playwright_open_url返回 200”也能让路由结果稳定很多。4.4 把路由结果接到主模型上路由出来的结果不能直接拿去执行还得把它翻译成主模型能理解的“局部工具列表”。我的做法是主模型每一轮对话前先用路由层选 1 到 3 个工具然后把这几个工具的完整 schema 注入主模型的 tools 参数里。这样主模型每轮只需要在 2 到 3 个工具里选而不是 60 个。def build_tools_for_llm(full_registry, route_result): selected_ids [route_result[route_to]] if route_result.get(fallback): selected_ids.append(route_result[fallback]) selected [t for t in full_registry if t[id] in selected_ids] return [ { type: function, function: { name: t[name], description: t[description], parameters: t[input_schema], }, } for t in selected ]这套“主模型只见局部工具”的做法的好处除了省 token还体现在改工具时不用重新调主模型。工具升级、下架、换 server全在路由层的注册表里改主模型根本感知不到。4.5 在 MCP 场景里落地时的一个隐藏坑如果你用的是 MCP server 加载工具有一个坑特别常见同一类能力可能被多个 server 重复暴露。比如 playwright mcp 暴露了browser_navigatechrome devtools mcp 也暴露了navigate_page。不做处理直接塞进注册表路由层一定会被搞晕。我的建议是在注册表里给每个工具加一个priority字段同一语义的工具只保留最高优先级的一个进入“精排候选”其他的进 fallback 列表。这样既不会丢能力也不会让路由模型在两个等价工具里反复横跳。优先级怎么定看维护频率你自己写的工具优先于第三方 server 的同类工具稳定性好的优先于刚上线的。4.6 在 Codex、Cursor 这些 IDE Agent 里接路由层的注意点现在很多人是在 IDE 里用 Agent 的Codex 里可以挂 skillCursor 也能装很多 skill 插件。这类环境里工具和 skill 往往是混在一起的路由层的注册表就得两种都顾及。Skill 比 Tool 更复杂因为它不只是函数还带提示词和工作流所以注册表里要给 skill 单独加一个trigger_hints字段写清楚“什么样的问题应该唤起这个 skill”。另一个常见的坑是 skill 命名太抽象。市场里的 skill 经常叫“deep research”“refactor expert”这种名字描述又写得很泛。接进路由层之前一定要给每个 skill 重写一遍use_when字段比如“当用户要求对比多个来源信息并生成结构化报告时”。如果这步偷懒路由模型很可能把“帮我调研一下竞品”路由到“代码重构”skill 上我在早期项目里见过不止一次。5. 常见问题与排查技巧实录路由层跑起来之后你会遇到一些反复出现的怪问题。我把实际排查记录整理成了一份速查表基本都是高频场景。5.1 主模型疯狂调工具被运行时拦成 flooding现象日志里出现 “tool call cancelled because tool-call flooding was detected”或者主模型在两三个工具之间反复调用、重复请求。原因多半不是路由不准而是路由结果进了主模型之后主模型拿着同一句话反复试同一个工具没有把上一次的工具返回值回填给路由层。每次路由都是“盲选”模型自然就不收敛。解决办法有两个。第一个是给路由加结果缓存同一个用户、同一个 query、上下文没变化时直接复用上次的 route 结果。第二个是强制把上一个工具的执行结果哪怕是个报错写进 context 再进路由层。这两个加起来基本能消掉 80% 的 flooding。还有一招是在主模型侧设置单轮最大迭代次数比如 5 次超过就打断并让用户重新描述需求。5.2 路由模型在两个相似工具之间反复横跳现象confidence 一直在 0.4 到 0.6 之间reason 里同时提到两个工具选哪个都像是猜。这种问题的根源十有八九是注册表里两个工具的use_when写得不够互斥。比如“打开网页”和“访问页面并等待网络空闲”在很多场景下确实分不清。我的做法是给每个工具补一段“反例”明确说“这个场景不适合我”。还不行的话就再想一层这两个工具是不是可以合并如果能用一个工具加参数搞定就别留两个让人选。5.3 MCP server 连不上路由选了不可用的工具现象路由层正常返回了一个工具但执行时报连接关闭或超时。这个问题和路由本身无关是 MCP server 不稳定。我的应对是加一层健康检查每个 MCP server 在启动时做一个轻量探测失败就把该 server 下的所有工具在注册表里标记为 unavailable路由层直接跳过。另外MCP 的工具列表不是一次拉完就永久有效server 可能中途新增或下线工具所以建议每次会话开始前重新拉一次工具列表而不是复用启动时的缓存。5.4 什么时候不该用独立路由层这不是问题但值得单独说。我在 3.4 节算过成本账如果工具少于 10 个、场景高度单一比如只调用一个代码执行工具独立路由层就是纯开销还会多一次失败点。我见过有的团队为了“架构先进”强行上路由结果路由模型一次选错就比不用路由还难排查。路由层适合的是“工具多、对话轮次多、工具之间差异微妙”的项目判断清楚这一点比学会怎么调参数重要得多。下面这张表是我项目里的排查速查表直接贴出来供参考。现象最常见原因先查哪里快速解法工具调用被 flooding 拦截路由结果未回填执行结果路由 context 字段把上一轮工具返回值写进 context两个工具反复横跳注册表描述不互斥do_not_use_when字段补反例或合并工具路由选到不可用工具MCP server 不稳定server 健康状态启动时探测并标记 unavailable路由结果慢整体延迟增加候选列表太长粗筛层 top_k把候选压到 12 个以内路由 prompt 被用户内容带偏注入写进了工具描述注册表描述来源工具描述只从后端配置读取不拼接用户原文6. 我的实际体会与几个小建议最后说点个人感受也不算总结就是一些踩坑之后的习惯。我在把路由层接进自己的 Agent 项目之后最大的体会是路由模型的调参空间其实很小真正决定效果的是注册表里工具描述的写法。花了两个晚上把几十个工具的use_when和do_not_use_when重写了一遍之后路由准确率直接从大概 60% 提到了 90% 以上。所以如果你时间有限别去纠结路由模型选哪个、temperature 调多少先把工具描述写好。另一个习惯是每新增一个工具我会在注册表里顺手写下三条“典型提问”然后在本地跑一轮路由回归测试确认这三句话分别被路由到正确的工具上。比如新增 playwright 工具时我会问“打开 example.com 并截图”确认它路由到 playwright 而不是 chrome devtools。这个测试不花多少时间但在工具越来越多之后能挡掉大量回归问题。还有一个值得考虑的方向是把路由结果本身做成可观测的。reason 和 confidence 这两个字段别浪费每跑一段时间导出统计一下看看哪些工具的 confidence 中位数特别低。低置信度的工具通常就是描述写得最烂、或者和别的工具重叠最严重的工具优先去优化它们比盲目加新工具有用得多。如果你也在做一个工具超过 20 个的 Agent我的建议是先别急着上复杂框架把工具注册表、粗筛层、路由决策层这三件事按本文的思路搭起来跑一周看数据再决定要不要继续加码。路由这个问题的解法永远是在“选择成本”和“选择准确率”之间做权衡而工具描述写得好不好决定了这两个指标的下限。
返回列表