本节摘要:工具给模型,谁来挑?默认答案是模型自选(function calling):schema 全量装入上下文,模型按任务自行决定调谁。它在工具少(经验上十来个以内,示意值)、任务开放时近乎完美;但工具一多、模型一小、成本一敏感,自选就会退化——选错率上升、L3 层 schema 税变重、小模型在几十个工具里晕头。此时在模型前面加一道路由器:或规则路由(确定性映射),或判断原语路由(约 100ms 的类型安全语义判断,详见《Jev 决策编程》第 6.2 节——它把"这个任务需要哪些工具"变成一次近乎免费的函数调用,LangChain 已将其做成 ModelRouterMiddleware 中间件)。工程上最常用的往往是两段式混合:路由器筛子集、模型在子集内精挑。本节给出三种方式的选择表与两段式的接法。
阅读完本节,你应当能够:
assemble_context 与 dispatch。function calling 的工作方式:上下文 L3 层装入全部工具 schema,模型在每步决策时输出"我要调哪个工具 + 什么参数"。它应该永远是你的第一选择,因为:
registry.schemas() 直接喂给循环;但自选有三个退化信号(经验阈值均为示意,应以自家回归集实测为准):
| 退化信号 | 表现 | 根因 |
|---|---|---|
| 选择准确率下降 | 错选近义工具(该 grep 去读了全文件) | 工具一多,边界描述稀释 |
| L3 税变重 | 几十个 schema 吃掉一至两成窗口 | schema 是常驻成本 |
| 延迟与成本抬升 | 每步都在大清单上做选择 | 选择空间大,决策变贵 |
路由器在模型之前回答一个问题:这个任务/这一步,需要哪些工具? 输出是一个小子集(或直接一个工具)。两种实现:
规则路由(确定性):任务类型 → 工具子集的静态映射。适合边界清晰的组织级场景:客服任务只挂工单类工具、代码任务只挂仓库类工具。零成本、可审计,但写不出语义判断("用户在骂人"这种)。
判断原语路由(语义、轻量):以 Jev 为代表的判断原语——读取程序状态、回答带类型的约束问题、返回类型安全答案 + 校准概率,单次约 100ms、近乎免费(官方自报)——让"该挂哪些工具"这个语义判断便宜到可以放进主循环热路径。典型用法:一个 Choice 问题("该任务属于哪类工作")或一个多选判断,映射到工具子集。LangChain 把这类用法做成了现成的 harness 中间件 ModelRouterMiddleware(官方):路由与护栏都成为可插拔标准件——这正是第 1.2 节"判断原语化"伏笔在工具层的兑现。原理与选型详见《Jev 决策编程》第 6.2 节(模型路由),本书不展开。
| 模型自选 | 规则路由 | 判断原语路由 | |
|---|---|---|---|
| 判断力 | 最强(语境级) | 无(静态映射) | 中(语义级,可校准) |
| 延迟/成本 | 已含在决策里 | ≈0 | ≈100ms、近零成本(官方自报) |
| 可审计性 | 弱 | 强 | 中(概率 + 类型答案) |
| 适用 | 工具少、任务开放 | 任务类型可枚举 | 工具多、需语义分流 |
一条经验分界(示意,以自家实测修正):工具 15 个以内,自选就好;15~40 个,上两段式;40 个以上,路由是必选项,而且应按任务域把工具拆成多个"工具包"分别挂载。分界线也随模型强弱漂移:旗舰模型的舒适区更大,小模型要更早引入路由。
真实的 harness 很少二选一,而是两段式:路由器筛子集,模型在子集内自选:
# routing.py —— 两段式工具路由(写法示意,以实际工程为准) def route_tools(task: str, all_tools: list[str]) -> list[str]: """第一段:筛子集。实现可为规则映射,也可为判断原语调用。""" if 规则可判(task): # 确定性优先 return RULE_MAP[classify_by_rule(task)] return primitive_route(task, all_tools) # 判断原语,约 100ms # primitive_route 详见《Jev 决策编程》第 6.2 节 def assemble_context(state, budget): # 接入 3.3 节的供料策略 subset = state.tool_subset or route_tools(state.task, ALL) msgs = [system_prompt(state), user_task(state), *registry.schemas(only=subset)] # L3 只装子集 ...
两段式的三个收益:L3 税从"全集"降到"子集";小模型在子集上选择准确率回升;安全面收窄——没挂载的工具连被误用的可能都没有(这同时是权限设计的纵深,第 5 章会再见到"不可用即最安全"原则)。
一个量级感受(示意数字,实际以你的工具表与回归集为准):40 个工具的 schema 全集约 6k tokens;路由到 8 个工具的子集约 1.2k tokens——省下的预算够装两三个中型文件,这就是"路由即供料"的直观账本。
⚠️ 路由错了怎么办?给路由器留逃生门:工具子集里永远保留一个
list_tools(列出全部工具并说明用途)与一个"申请换工具集"的说明——模型发现缺工具时可以显式请求扩集。没有逃生门的路由器,会把"筛掉噪音"变成"筛掉氧气"。
逃生门的实现成本极低(一个只读工具 + 一段提示词),却被大量自研 harness 遗漏——它是两段式方案里"信任但可验证"的那颗保险丝。
把本章放回全景图会发现:工具路由表面是"选工具",实质是上下文组装的一部分——它决定的正是 L3 层装什么。由此两条推论:其一,路由策略应该住在 assemble_context 里(如上代码),而不是散在业务代码各处;其二,第 7 章的上下文预算管理天然涵盖 L3 税——工具一多的 harness,"路由 + 压缩"是同一格(供料)里的两只手。
💡 检验你的路由是否"住对了地方":如果换一套路由策略需要改多处业务代码,说明它散了;正确的位置是
assemble_context这一个入口——换策略只改一处,且每次路由决策都能被第 9 章的日志记录下来。
assemble_context 里。工具会被选中、被执行了——但执行不会总成功。错误消息发出后模型看到什么,决定它下一步是自救还是螺旋坠毁。下一节:错误反馈设计。