1. 先确认一个事实:Tool、MCP、Skill 正在把 Agent 的“工具箱”塞爆
如果你最近在搭 Agent,或者只是在 Codex、Cursor、Trae 里多挂了几个插件,多半已经感觉到一个变化:工具列表越来越长,长到模型开始“选择困难”。
先理清三个词,因为很多人把它们混着用。Tool 是最底层的能力单元,本质上就是一个函数或 API,模型通过 function calling 决定要不要调它。MCP(Model 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 token,60 个工具就是一万多 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(一句话说明为什么选它)、confidence(0 到 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_url="ROUTER_BASE_URL", api_key="ROUTER_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( model="jev-router", temperature=0.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_navigate,chrome devtools mcp 也暴露了navigate_page。不做处理直接塞进注册表,路由层一定会被搞晕。
我的建议是,在注册表里给每个工具加一个priority字段,同一语义的工具只保留最高优先级的一个进入“精排候选”,其他的进 fallback 列表。这样既不会丢能力,也不会让路由模型在两个等价工具里反复横跳。优先级怎么定?看维护频率:你自己写的工具优先于第三方 server 的同类工具;稳定性好的优先于刚上线的。
4.6 在 Codex、Cursor 这些 IDE Agent 里接路由层的注意点
现在很多人是在 IDE 里用 Agent 的,Codex 里可以挂 skill,Cursor 也能装很多 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,我的建议是:先别急着上复杂框架,把工具注册表、粗筛层、路由决策层这三件事按本文的思路搭起来,跑一周看数据,再决定要不要继续加码。路由这个问题的解法,永远是在“选择成本”和“选择准确率”之间做权衡,而工具描述写得好不好,决定了这两个指标的下限。