openinterpreter 中 tool_search 工具描述模板:Apps/Connectors 工具发现的 BM25 实现解析
本文围绕 tool_description.md 这份 tool_search 工具描述模板展开:先逐句解读模板本身的提示词设计与 {{app_descriptions}} 占位符机制,再深入 ToolSearchHandler 与 create_tool_search_tool 的源码实现,说明描述文本如何被动态渲染、BM25 检索引擎如何构建、搜索结果又如何转化为下一次模型调用可用的工具规格。读完本文,你可以完整理解本项目"延迟加载工具 + 关键词检索发现"这一工具发现机制从提示词到执行链路的落地方式。
1. 模板定位:一段注入给模型的"工具发现说明书"
模板文件 codex-rs/core/templates/search_tool/tool_description.md 全文如下:
# Apps (Connectors) tool discovery
Searches over apps/connectors tool metadata with BM25 and exposes matching tools for the next model call.
You have access to all the tools of the following apps/connectors:
{{app_descriptions}}
Some of the tools may not have been provided to you upfront, and you should use this tool (`tool_search`) to search for the required tools and load them for the apps mentioned above. For the apps mentioned above, always use `tool_search` instead of `list_mcp_resources` or `list_mcp_resource_templates` for tool discovery.
它不是给人看的说明文档,而是作为 tool_search 工具的 description 文本注入到模型上下文中的一段提示词。逐句拆解其设计意图:
- 标题
# Apps (Connectors) tool discovery:限定这份描述服务于 Apps/Connectors(应用/连接器)场景下的工具发现,与 MCP 通用工具发现相区分。 - 第一句功能声明:明确说明该工具"基于 BM25 对工具元数据做检索,并把命中的工具暴露给下一次模型调用"。这一句同时交代了检索算法(BM25)与结果用途(供下一轮调用加载使用),让模型知道调用它不会立即执行工具,而是"解锁"工具。
{{app_descriptions}}占位符:运行时会被当前已启用的应用/连接器清单及描述替换,告诉模型"这些应用的工具你都可能用到"。- "部分工具可能没有提前提供给你":解释了延迟加载(deferred loading)策略——工具元数据不会一次性全部塞进上下文,而是按需检索加载,以此控制 token 开销。
- "对上述应用始终用
tool_search而不是list_mcp_resources/list_mcp_resource_templates做工具发现":这是一条硬性路由规则,避免模型绕过检索直接枚举 MCP 资源,保证发现路径统一走 BM25。
与 MCP 通用场景对应的描述由 Rust 代码动态生成,见 tool_search_spec.rs:
let description = format!(
"# Tool discovery\n\nSearches over deferred tool metadata with BM25 and exposes matching tools for the next model call.{source_section}Some of the tools may not have been provided to you upfront, and you should use this tool (`{TOOL_SEARCH_TOOL_NAME}`) to search for the required tools. For MCP tool discovery, always use `{TOOL_SEARCH_TOOL_NAME}` instead of `list_mcp_resources` or `list_mcp_resource_templates`."
);
两者结构几乎同构:都是"功能声明 + 来源清单 + 延迟加载提示 + 路由规则"四段式。从源码结构看,模板文件承载的是"来源为 Apps/Connectors"的变体,而 Rust 侧 create_tool_search_tool 负责通用 MCP 场景及来源清单的渲染,两者共享同一套提示词骨架。
同目录下还有一份姊妹模板 request_plugin_install_description.md,定义 request_plugin_install 工具的使用约束:仅当用户明确要求某个已知未安装的插件/连接器、且 tool_search 不可用或已尝试但未命中时才允许调用,并给出完整五步工作流(先检查活跃工具列表 → 对照已知清单精确匹配 → 插件优先于连接器 → 携带 tool_type/action_type/tool_id/suggest_reason 参数发起安装请求 → 安装完成后决定继续搜索还是放弃)。它与 tool_search 构成互补关系:tool_search 解决"已启用来源里找不到工具",request_plugin_install 解决"工具根本还没安装"。
2. 工具 Schema:query 必填、limit 默认 8
tool_search 的参数定义同样在 create_tool_search_tool 中:
let properties = BTreeMap::from([
("query".to_string(),
JsonSchema::string(Some("Search query for deferred tools.".to_string()))),
("limit".to_string(),
JsonSchema::number(Some(format!(
"Maximum number of tools to return. Defaults to {default_limit}."
)))),
]);
query(string,必填):检索词,对应 JSON Schema 的required: ["query"];limit(number,可选):返回工具数量上限,默认值来自常量 TOOL_SEARCH_DEFAULT_LIMIT,其值为8:
pub const TOOL_SEARCH_TOOL_NAME: &str = "tool_search";
pub const TOOL_SEARCH_DEFAULT_LIMIT: usize = 8;
参数在运行时的校验逻辑见 ToolSearchHandler::handle_call:
query去除首尾空白后为空 → 返回RespondToModel("query must not be empty")错误,让模型自行修正;limit == 0→ 返回RespondToModel("limit must be greater than zero");- 无匹配来源时直接返回空的
ToolSearchOutput,不报错。
这种"把错误回传给模型而不是崩溃"的设计,使模型可以在同一会话内自我修正参数。
3. BM25 检索引擎:从 search_text 到 SearchEngine
tool_description.md 中"with BM25"这句声明的实现在 ToolSearchHandler:
pub(crate) fn new(
search_infos: Vec<ToolSearchInfo>,
source_listing: ToolSearchSourceListing,
) -> Self {
let search_source_infos = search_infos
.iter()
.filter_map(|search_info| search_info.source_info.clone())
.collect::<Vec<_>>();
let spec = create_tool_search_tool(
&search_source_infos,
TOOL_SEARCH_DEFAULT_LIMIT,
source_listing,
);
let documents: Vec<Document<usize>> = search_infos
.iter()
.map(|search_info| search_info.entry.search_text.clone())
.enumerate()
.map(|(idx, search_text)| Document::new(idx, search_text))
.collect();
let search_engine =
SearchEngineBuilder::<usize>::with_documents(Language::English, documents).build();
// ...
}
关键点:
- 每个可检索工具(
ToolSearchInfo)都携带一段search_text字段,它即是被索引的"文档"。BM25 引擎以英文语言配置构建(Language::English),文档 ID 就是该工具在search_infos数组中的下标; - 检索时 search 方法 用
search_engine.search(query, limit)拿到按相关性排序的文档 ID,再映射回ToolSearchEntry中的output(LoadableToolSpec),最后经coalesce_loadable_tool_specs合并——同一命名空间(如mcp__calendar)下命中的多个工具会被归并为一个LoadableToolSpec::Namespace,减少重复的工具声明。单元测试 mixed_search_results_coalesce_mcp_namespaces 验证了 MCP 工具与动态命名空间工具混合命中时的归并结果,并确认每个工具都带defer_loading: Some(true)标记。
4. 来源清单渲染:去重、512KB 预算与按字符边界截断
模板里的 {{app_descriptions}} 对应的来源清单渲染逻辑在 tool_search_spec.rs:
- 按名称去重:以
BTreeMap汇总来源名与描述,重复名称保留先出现的描述(测试 create_tool_search_tool_deduplicates_and_renders_enabled_sources 验证了"同名来源只取首个非空描述、输出保持字典序"的行为); - 512KB 描述预算:常量
MAX_TOOL_SEARCH_SOURCE_DESCRIPTION_BYTES = 512 * 1024,渲染前先扣除来源名等固定开销,再逐个来源分配描述字节数;超预算的来源被跳过,且截断借助 take_bytes_at_char_boundary 在多字节字符边界处安全切割(测试 create_tool_search_tool_bounds_aggregate_source_descriptions 用 2 万个 🦀 字符构造了极端用例); - Omit 模式:
ToolSearchSourceListing::Omit时完全不渲染来源清单(当世界状态已在别处宣告了这些来源时启用),测试 create_tool_search_tool_omits_sources_when_world_state_advertises_them 断言此时描述中不再出现来源名。
这套预算控制保证了无论接入了多少应用/连接器,注入模型上下文的那段 description 都不会失控增长——这正是"延迟加载 + 按需检索"策略能成立的前提之一。
5. 暴露控制与缓存:何时模型才能看到 tool_search
tool_search 是否对模型可见由配置开关控制。mcp_tool_exposure.rs 中的逻辑根据 search_tool_enabled 布尔值决定工具暴露策略(search_tool_enabled 为 true 时走"经检索发现"的暴露路径),其配套单测文件 mcp_tool_exposure_test.rs 覆盖了 apps_enabled 与 search_tool_enabled 四种组合下的暴露结果。
性能侧,ToolSearchHandlerCache 用 Mutex<Option<Arc<ToolSearchHandler>>> 缓存已构建的检索器:当新的 search_infos 与 source_listing 与缓存完全一致时直接复用(避免每轮对话重建 BM25 索引),任一字段变化则重建。单测 cache_reuses_handler_for_identical_search_infos_and_rebuilds_for_changes 验证了"相同输入复用同一 Arc、修改一条 search_text 即触发重建"的行为。端到端集成测试位于 tests/suite/search_tool.rs 与 tests/suite/request_plugin_install.rs,后者同时验证了"必须先耗尽 tool_search 才允许请求安装"的约束。
6. 小结:一条完整的工具发现链路
把模板与源码串起来,tool_search 的完整链路是:
- 描述注入:模板/代码生成的 description(含来源清单与 BM25 声明)进入模型上下文,模型据此知道"哪些应用的工具需要检索才可用";
- 参数校验:
query非空、limit > 0(默认 8),错误以可回传模型的方式返回; - BM25 检索:对每个工具的
search_text建立英文 BM25 索引,按相关性取前limit个; - 规格归并:命中工具按命名空间合并为
LoadableToolSpec,携带defer_loading标记暴露给下一轮调用; - 兜底协同:检索不到时,若用户显式要求某个已知插件/连接器,走姊妹模板约束下的
request_plugin_install安装流程。
这一机制的本质,是用"提示词声明发现规则 + 客户端 BM25 检索 + 延迟加载标记"三层配合,让模型在工具数量膨胀时无需背负全部工具 schema 的上下文成本,同时保留确定性的发现路径。若要进一步阅读,建议从 tool_search_spec.rs 与 tool_search.rs 两个文件及其内嵌测试入手,再对照 codex-rs/core/templates/search_tool/ 下的两份模板,即可完整还原该功能的提示词面与实现面。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00