Qwen Code:延迟工具为何要分“发现”和“执行”?
固定官方仓库
QwenLM/qwen-code@b9840886b86c23f196482cc1ee55d59cbc74df87。本篇只看普通工具声明模式下ToolRegistry → tool_search → tool_call → CoreToolScheduler的延迟工具桥;是静态源码剖面,不是运行实测。
一个工具有三种不同的“存在”
工具可以已经注册,却不在当前发给模型的 function-declaration 列表中。ToolRegistry.getFunctionDeclarations() 默认过滤仍隐藏的 deferred 工具;shouldDefer、alwaysLoad、会话 reveal、visibleTools 等一起决定它是否在列表内。MCP 工具构造时设置 shouldDefer=true,但 alwaysLoad 等例外仍可能使其可见。声明过滤 · 隐藏判定 · MCP 工具
tool_search 可按关键词或 select:<name> 找 schema;关键词模式的候选是仍隐藏的 deferred 工具,精确模式也允许重看已可见工具。返回的是 <functions> 包裹的 schema 文本,不是将工具加入模型当前声明列表,更不是执行工具。查询模式 · 隐藏候选 · schema 返回
模型随后用 tool_call {name, arguments} 调用仍隐藏的目标。resolveDeferredToolCall() 会核对桥、目标、上下文排除规则、deferred/hidden 状态及声明能力;可见工具反而要求直接调用。调度器把 wrapper 请求改写成真实工具名和参数,再走目标工具的调度/权限路径。ToolCallTool.execute() 自己拒绝直接执行,防止绕开调度器。解析与拒绝 · 调度改写 · 不许直执行
设计取舍
这是一个“工具已注册,但完整 schema 按需给模型看”的折中:减少初始声明体积,同时保留发现和调用入口。源码在 setTools() 中取得注册表的声明列表,并向模型聊天对象设置它;tool_search 的文本回答本身不调用 setTools()。setTools · tool_search 实现
但这不是绝对的“deferred 永远隐藏”:小规模 deferred schema 可被预算预加载,历史回放或会话 setup 也可 reveal;CodeModeOnly 有不同声明路径。预算预加载 · 模式分支
边界
这里的“发现”专指已注册工具 schema 的按需检索,不等同于 Skill 文件发现或插件加载;没有验证 MCP 重连、权限弹窗、hooks 的实际顺序,也不声称模型一定先调用 tool_search 才会试 tool_call。拒绝分支是在调度器里兜底检查,执行后的权限、审批和 hook 结果仍须另做实测。桥语义 · 目标策略检查
图的文字说明 · qwen-code-deferred-tools
图上方先区分两个事实:ToolRegistry 已注册目标工具,但 getFunctionDeclarations() 不把仍隐藏的 deferred schema 放进普通模型声明;模型保留可见的 tool_search 与 tool_call 桥。蓝色“发现”轨道是 tool_search 检索并把 <functions> schema 文本返回给模型;这只让模型读到参数形状,不执行目标,也不把目标加入函数声明。橙色“执行”轨道是模型发出 tool_call {name, arguments},resolveDeferredToolCall 检查目标与上下文,CoreToolScheduler 再把 wrapper 请求改写为真实目标并继续调度。后续仍按目标工具的策略执行。图内保留短标签,详细条件以本段和章节为准。两条轨道是机制拆解,不表示源码里有两个独立线程,也不表示每次调用前都必须先搜索。
图只针对仍隐藏的 deferred 工具。已预加载或被 reveal 的工具可直接出现在模型声明中;CodeModeOnly 则使用另一条声明机制。可见工具用 tool_call 包装会被拒绝,须直接调用。固定官方源码 b9840886b86c23f196482cc1ee55d59cbc74df87,证据定位与未验证边界见章节。
在线预览稿:书稿仍在校稿,系统篇以文内固定源码版本为准;静态阅读不等于运行验收。