最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
工具塞满上下文窗口怎么办:深度拆解 AI Agent Tool Search 按需加载实现原理
时间:2026-07-16 08:41:56 编辑:袖梨 来源:一聚教程网
本文将讲解 AI Agent 里的 tool search 机制——当工具数量多到占满上下文窗口时,怎么按需加载模型需要的工具。本文内容覆盖实现原理、匹配算法、缓存影响,并配合 x-code-cli 的源码来讲解。
Tool 和 MCP Tool
LLM 本身不能执行操作
大语言模型(LLM)是纯文本生成器,只能接收文本输入并输出文本应答,无法主动执行外部操作。
AI Agent 之所以能做这些事,是因为模型可以通过工具调用的方式来执行这些操作。例如模型输出指令——“我要调用 readFile 工具,参数是 package.json”——客户端(这里指 MCP 客户端,即实现了 MCP 协议的 AI Agent 应用,如 x-code-cli、Claude Code、Cursor 等)代码将解析这条指令,并执行实际的文件读取操作,再把读到的内容传回给模型。模型拿到文件内容后继续推理,决定下一步做什么。
这些可被模型调用的操作——读文件、执行命令、搜索代码、查数据库、联网搜索——统称为 Tool(工具)。每个工具由三部分组成:一个名字(name)、一段自然语言描述(description,告诉模型这个工具能做什么)、一份 JSON Schema(inputSchema,定义工具接受什么参数)。以 x-code-cli 的 readFile 工具为例:
{"name": "readFile","description": "Read the contents of a file at the specified path.","inputSchema": {"type": "object","properties": {"path": {"type": "string","description": "The absolute path of the file to read"},"offset": {"type": "number","description": "Line number to start reading from (1-based)"},"limit": {"type": "number","description": "Maximum number of lines to read"}},"required": ["path"]}}
这三部分会被序列化后放进每次 API 请求的 tools 数组。模型根据 description 判断什么时候用这个工具,根据 inputSchema 生成合法的参数。一个工具的定义越复杂(参数多、描述长),占用的 token 就越多。
下面是 x-code-cli 里 Agent Loop 的执行流程:
sequenceDiagramautonumberactor U as 用户participant C as 客户端(x-code-cli)participant LLM as LLM APIU->>C: "帮我把 utils.ts 里的 foo 函数重命名为 bar"C->>LLM: 用户消息 + 工具列表(readFile, edit, shell, grep...)LLM-->>C: tool_call: readFile({path: "src/utils.ts"})Note over C: 客户端执行 readFile,读取文件内容C->>LLM: tool_result: "export function foo() { ... }"LLM-->>C: tool_call: edit({path: "src/utils.ts", old: "foo", new: "bar"})Note over C: 客户端执行 edit,替换字符串C->>LLM: tool_result: "文件已修改"LLM-->>C: 文本回复: "已将 foo 重命名为 bar"C->>U: 展示回复
在整个过程中,模型自主决定调用什么工具、传什么参数。客户端只负责执行和传递结果。这个循环可能执行一次,也可能执行很多次,主要取决于任务的复杂度。
工具数量的增长
内置工具
一个 Agent 产品刚上线时,内置工具的数量可能不多。但是随着产品的迭代,内置工具会逐渐增加。例如联网搜索、网页抓取、子 agent 委派、待办管理、后台任务管理等工具。Claude Code、Codex、Cursor 这些主流 AI Agent 产品都经历了这个增长过程,一个成熟的 AI Agent 产品拥有几十个内置工具是很正常的。
MCP 工具
MCP(Model Context Protocol,模型上下文协议)是一套让 Agent 接入外部服务的开放协议。例如接入 GitHub MCP server,Agent 就能创建 issue、合并 PR;接入一个数据库 MCP server,Agent 就能执行 SQL 查询。
各个 MCP Server 会通过 tools/list 接口向客户端注册自身对外开放的全部工具;该接口采用全量返回模式,客户端在建立与某一服务端的连接后,即可一次性获取该服务端完整的工具定义清单。
sequenceDiagramparticipant C as MCP 客户端(AI Agent)participant S1 as MCP Server A(交易所)participant S2 as MCP Server B(数据库)C->>S1: 建立连接C->>S1: tools/listS1-->>C: 返回 400 个工具定义C->>S2: 建立连接C->>S2: tools/listS2-->>C: 返回 15 个工具定义Note over C: 客户端拿到所有 server 的完整工具列表<br/>共 415 个工具,全部放进后续的 LLM 请求
MCP tool 和内置 tool 在结构上没有区别,都是名字 + 描述 + JSON Schema。区别只在于是谁提供的:内置 tool 是产品代码里写死的,MCP tool 是外部 server 动态注册的。
一个 MCP server 提供的工具数量往往远超内置工具。以一个加密货币交易所的 MCP server 为例,它覆盖现货交易、合约交易、资产划转、行情查询、余额查询等操作,一共 400 个工具。如果用户本身又接了一些其他的 MCP server,工具总数很容易超过 400 个。
工具数量增多带来的问题
如果要让大模型调用这些工具,所有工具定义必须随请求一起发给模型。由于大语言模型本身是无状态的,每次发起推理请求时,都需要重新携带完整的工具列表。
一个中等复杂的工具定义大约 150–300 token(名字几个 token、描述一两行、JSON Schema 里的属性名和类型描述是大头)。400 个工具 × 平均 160 token ≈ 64,000 token。用户还没开始提问,光工具定义就占掉了 64k token。
我们来看一下 x-code-cli 里调用 LLM 时的请求结构。下面是 packages/core/src/agent/loop.ts 里 runTurn 函数的核心代码(简化后):
result = streamText({model, // LLM 模型实例system: cached.system, // System Prompt(系统提示),约 2-5k tokenmessages: cached.messages,// Messages(对话历史 + 用户消息)tools: cached.tools, // Tools(工具定义)← 全量注入时 64k tokenmaxRetries: 3,abortSignal: options.abortSignal,maxOutputTokens: getMaxOutputTokens(options.modelId),providerOptions: mergedProviderOptions,})
一次 API 请求的输入由 system、tools、messages 三个字段组成。token 的分布大致是这样的:
一次 API 请求的 Token 构成:┌────────────────────────────────────┐│ system(系统提示)│约 2-5k token├────────────────────────────────────┤│ tools(工具定义) │← 全量注入时:64k token(400 个工具)├────────────────────────────────────┤│ messages(对话历史 + 用户消息)│根据对话长度变化└────────────────────────────────────┘如果模型的上下文窗口是 128k,工具列表占掉了将近一半
关于费用需要说明一点:主流 LLM API 都有前缀缓存(prefix caching)。system + tools 这些每轮不变的前缀部分,第一次请求按全价计费,后续请求命中缓存后按折扣价计费(通常是原价的 10%–50%),所以不是每轮都需要付全价。
但 64k token 的工具列表即使命中缓存,也在持续产生开销:它占用了上下文窗口的物理空间,增加了首字延迟(TTFT),挤压了留给对话历史和用户消息的空间。工具列表越大,能用来做实际对话的窗口就越小。
这就引出了本文的主题——tool search。
Tool Search
tool search 是一个用于“按需加载工具定义”的内置工具。默认情况下,所有工具的完整定义(名字 + 描述 + JSON Schema)都会放进每次请求的 tools 数组里。tool search 改变了这个策略:只在系统提示里放一份工具名字清单,不放完整定义;当模型判断需要调用某个工具时,先通过内置的 toolSearch 工具加载该工具的完整 schema,下一轮再执行调用。
模型仍然知道有哪些工具——系统提示里列出了所有工具的名字。但每个工具的完整定义(描述 + JSON Schema)不会预先放进请求,而是等模型实际要调用时再加载。这样上下文占用就从全量 64k 降到了只加载工具名字清单的 3-5k,再加上按需加载的少数几个工具的 schema。
Deferred 机制
Deferred 直译是“延迟”。在 tool search 的语境下,deferred 工具就是被延迟加载的工具——它的完整定义(描述 + JSON Schema)不随请求发送,只在模型通过 toolSearch 主动请求时才加载进来。与之对应的是直接加载的工具(directly loaded),即每轮请求都带着完整 schema 的核心工具。
下面来看按需加载具体如何实现,主要有以下几个步骤:
第一步:客户端照常从 MCP server 全量拿到工具。 tools/list 接口还是全量返回。客户端启动时从每个 MCP server 拉到所有工具的完整定义——名字、描述、JSON Schema——全部存在本地内存里。这一步和没有 tool search 时完全一样。
第二步:只把工具名字告诉模型。 完整的工具 schema 不放进请求的 tools 数组。工具名字按 server 分组,写在系统提示的 ## Deferred Tools 段里。模型每轮都能看到这份名字清单,知道有哪些工具可用,但看不到每个工具接受什么参数。
下面是 x-code-cli 实际生成的 ## Deferred Tools 段(截取部分):
## Deferred ToolsThe tools below are available but NOT loaded — only their names are listed,with no schema. To use one, first call `toolSearch` (keyword search, or`select:<exact_name>` to load specific tools by name); its schema is thenadded to your tool set and it becomes directly callable on your next step.Core tools (readFile, writeFile, edit, shell, grep, glob, listDir, task)are always loaded — never search for those.### Built-in- webSearch, webFetch, todoWrite, activateSkill### Server: gate-mcp- mcp__gate__cex_spot_get_ticker, mcp__gate__cex_spot_create_order,mcp__gate__cex_spot_get_balance, mcp__gate__cex_futures_get_ticker,... (共 400 个工具)### Server: github- mcp__github__create_issue, mcp__github__list_pull_requests,mcp__github__merge_pull_request, ...
可以看到,这段文字包含两部分信息:一是使用说明(告诉模型怎么通过 toolSearch 加载工具),二是按来源分组的工具名字清单。模型每轮都能看到这份清单,但看不到任何工具的参数定义。
第三步:给模型一个内置的 toolSearch 工具。 当模型需要调用某个 deferred 工具时,它先调用 toolSearch,传入关键词或工具名。客户端在内存里的工具目录上做匹配,把命中的工具加入下一轮请求的 tools 数组。模型在下一轮就能像调任何普通工具一样调用它。
sequenceDiagramparticipant S as MCP Serverparticipant C as 客户端(MCP 客户端)participant LLM as LLMNote over S,C: 启动时C->>S: tools/listS-->>C: 400 个工具的完整定义Note over C: 全部存在内存里Note over C,LLM: 用户提问:"帮我查一下 BTC/USDT 的最新价格"C->>LLM: 核心工具 schema + toolSearch schema + deferred 名字清单Note over LLM: 看到名单里有 cex_spot_get_ticker,<br/>但没有它的 schema,无法直接调用LLM-->>C: toolSearch({query: "select:mcp__gate__cex_spot_get_ticker"})Note over C: 在内存目录里匹配,把这个工具加入 activated 集合C->>LLM: 核心工具 + toolSearch + cex_spot_get_ticker(刚激活的 schema)LLM-->>C: cex_spot_get_ticker({currency_pair: "BTC_USDT"})
要强调的一点:deferred 是客户端做的事。MCP 协议的 tools/list 依旧全量返回,协议本身没有按需发现的能力。deferred 机制完全是客户端在拿到全量工具列表之后,自行决定哪些放进请求、哪些延迟加载——这是客户端的策略,不是协议的功能。
模型只看到名字,怎么找到对的工具?
如果只提供工具名称、不附带对应的 Schema,模型无法明确该工具的入参结构。为了解决该问题,系统设计了多层校验保障机制。
工具名称自身已具备充足的语义描述能力。 mcp__github__create_issue、cex_spot_get_ticker 这种名字,模型一看就知道是什么功能。MCP 工具名一般遵循 server__action 或 server__resource__verb 的命名规范,名字本身就是一层语义索引。如果工具名全是 tool_001、tool_002,这套方案就不成立了。
名单放在模型看得到的地方。 系统提示的 ## Deferred Tools 段按 server 分组列出了所有 deferred 工具名。模型不是盲目搜索——它看到了完整名单,只是没看到每个工具的参数定义。
客户端完成 toolSearch 的匹配运算。 toolSearch 支持两种调用方式:关键词搜索和精确匹配。
方式一:关键词搜索。 模型不确定工具的确切名字时使用。客户端拿 query 参数和每个工具的预处理文本做关键词匹配,按得分排序,返回前几个。
用一个例子来说明。假设 deferred 目录里有这几个工具:
工具目录(每个工具在注册时预处理好搜索文本):工具名搜索文本(名字拆词 + 描述 + schema 属性名)─────────────────────────────────────────────────────────────────────────mcp__gate__cex_spot_get_ticker gate cex spot get ticker 获取现货行情 currency_pairmcp__gate__cex_spot_create_order gate cex spot create order 创建现货订单 currency_pair side amount pricemcp__gate__cex_spot_cancel_order gate cex spot cancel order 取消现货订单 order_idmcp__github__create_issuegithub create issue 创建 issue title body labels
模型传入 toolSearch({query: "spot ticker"}) 后,客户端对每个工具打分:
query = "spot ticker"mcp__gate__cex_spot_get_ticker:"spot" → 名字拆词里有 "spot" → +10"ticker" → 名字拆词里有 "ticker" → +10总分 = 20 ✓mcp__gate__cex_spot_create_order:"spot" → 名字拆词里有 "spot" → +10"ticker" → 名字拆词里没有,搜索文本里也没有 → +0总分 = 10mcp__gate__cex_spot_cancel_order:"spot" → 名字拆词里有 "spot" → +10"ticker" → 名字拆词里没有,搜索文本里也没有 → +0总分 = 10mcp__github__create_issue:"spot" → 都没有 → +0"ticker" → 都没有 → +0总分 = 0结果(按分数降序):1. mcp__gate__cex_spot_get_ticker (20)← 返回2. mcp__gate__cex_spot_create_order (10)← 返回3. mcp__gate__cex_spot_cancel_order (10)← 返回4. mcp__github__create_issue(0) ← 过滤掉
Codex 的搜索逻辑类似,也是给每个工具预先生成搜索文本,但打分用的是 BM25 算法(信息检索领域的经典排序算法,搜索引擎用它来给搜索结果排序)。核心思路一样:拿 query 的关键词去匹配工具的搜索文本,按相关度排序。
两种方式都不需使用 embedding,也不需要调用模型,都是做的字符串匹配——同样的 query、同样的工具集,得到的结果完全一致。
另外,关键词搜索可能会同时命中多个工具。toolSearch 按打分排序取前 N 个(x-code-cli 默认 5 个),所有命中的工具都会被激活——加入 activated 集合,下一轮全部出现在 tools 数组里。模型拿到这几个工具的 schema 后,自己判断该使用哪个,客户端不会进行限制。
比如用户说“帮我下一个限价单买 BTC”,模型调用 toolSearch({query: "spot order"}),可能会同时命中 cex_spot_create_order、cex_spot_get_ticker、cex_spot_cancel_order 等多个工具。多激活几个工具的代价是每个工具多占一份 schema 的 token,但比全量注入 400 个工具要好得多。而且激活是持久的——一旦激活,后续轮次不用重复搜索同一个工具,可以直接使用。
方式二:select: 精确匹配。 模型直接传 select:工具名,客户端按名字精确查找,不需要打分。
模型怎么知道确切的工具名?因为系统提示的 ## Deferred Tools 段里列出了所有 deferred 工具的完整名字。模型每轮都能看到这份名单,只要它在名单里找到了目标工具名,就可以直接用 select: 加载。
什么时候用哪种方式?这个逻辑写在 toolSearch 工具自身的 description 里,模型在调用前会读到这段说明:
Pass `query` as either:- keywords describing the capability you need(e.g. "search the web", "github create issue")→ returns the best-matching deferred tools, or- "select:<name>,<name>" to load specific tools by theirEXACT name from the Deferred Tools list(prefer this when you already know the name).
也就是说工具描述直接告诉模型:如果已经知道名字,优先用 select:;不确定名字时,用关键词描述能力。 实际使用中,模型大多数情况会走 select:,因为它能直接从名单里看到工具名。关键词搜索是模型对工具名不确定时的备选路径——比如工具名比较长或者有多个相似工具时,模型可能用 "spot order" 这样的关键词让客户端帮它筛选。
两种调用方式对比:关键词搜索(不确定工具名时用):toolSearch({query: "spot ticker"})→ 客户端对所有工具做关键词打分,返回得分最高的几个精确匹配(已知工具名时用,推荐方式):toolSearch({query: "select:mcp__gate__cex_spot_get_ticker"})→ 客户端直接按名字查找,找到就返回,不打分
关键词搜索是一层保底机制。模型大多数情况下能直接从名单里找到目标工具名,走 select: 精确匹配。只有当模型不确定具体用哪个工具时,才会退而使用关键词搜索让客户端帮它筛选。
哪些工具该 defer,哪些直接加载
不是所有的工具都需要 defer,一些核心工具会直接加载,而一些偶尔才用的工具才会被 defer。
直接加载的工具(每轮请求都带完整 schema):
这类工具是 Agent 的基础操作能力,用户随便问个问题都可能用到:
| 工具 | 功能 | 为什么必须直接加载 |
|---|---|---|
readFile | 读文件 | 几乎所有编码任务都需要先读文件 |
edit | 编辑文件 | 写代码的核心操作 |
writeFile | 写文件 | 创建新文件 |
shell | 执行命令 | 运行测试、安装依赖、git 操作 |
grep | 搜索代码 | 定位代码位置 |
glob | 查找文件 | 找文件路径 |
task | 子 agent | 委派子任务 |
toolSearch | 工具搜索 | 加载 deferred 工具的唯一入口 |
延迟加载(defer)的工具:
- 非核心内置工具:
webSearch(联网搜索)、webFetch(抓取网页)、todoWrite(TODO 清单)。这些工具不是每个任务都会被用到,所以需要时再加载。 - 所有 MCP 工具:MCP 工具由外部 server 提供,面向特定场景(交易、GitHub 操作、数据库查询等),不存在“每个任务都会用到”的 MCP 工具。
用一个具体例子来说明分类过程。假设 Agent 注册了以下工具:
分类示例:readFile→ 核心内置工具,每个任务都用→ 直接加载edit→ 核心内置工具,每个任务都用→ 直接加载shell → 核心内置工具,每个任务都用→ 直接加载toolSearch→ 加载入口,必须直接可用 → 直接加载webSearch → 非核心内置,偶尔才用 → deferwebFetch→ 非核心内置,偶尔才用 → defermcp__gate__cex_spot_get_ticker→ MCP 工具→ defermcp__gate__cex_spot_create_order→ MCP 工具→ defermcp__github__create_issue → MCP 工具→ defer...(其余 MCP 工具全部 defer)
下面是判断逻辑的流程图:
完整循环:搜索 → 激活 → 调用
我们来看一个具体的示例,用“查 BTC 价格”来演示一遍实际的调用过程:
sequenceDiagramautonumberactor U as 用户participant C as 客户端participant LLM as LLMU->>C: "帮我查一下 BTC 的现货价格"C->>LLM: 用户消息 + 核心工具 + toolSearch + deferred 名字清单Note over LLM: 看到名单里有 cex_spot_get_tickerLLM-->>C: toolSearch({query: "select:mcp__gate__cex_spot_get_ticker"})Note over C: 精确匹配命中,加入 activated 集合C-->>LLM: tool_result: "Loaded 1 tool(s) — now callable directly on your next step"Note over C: 下一轮请求的 tools 数组追加 cex_spot_get_ticker 的完整 schemaC->>LLM: 核心工具 + toolSearch + cex_spot_get_ticker(已激活)LLM-->>C: cex_spot_get_ticker({currency_pair: "BTC_USDT"})Note over C: 执行 MCP 工具调用,拿到价格C->>LLM: 工具执行结果LLM-->>C: "BTC 现货价格是 63,521.30 USDT"C->>U: 展示回复
注意步骤 3 和步骤 5 之间:模型在第一轮调用了 toolSearch 工具,但不能在同一轮里调用 cex_spot_get_ticker——因为这个工具在第一轮的 tools 数组里还不存在。必须等客户端把它的 schema 加进去,下一轮模型才能调用。
这就是 deferred 方案的代价:首次使用某个 deferred 工具时,至少需要 2 轮 API 调用——先搜索,下一轮才能调用。
整个流程里 state 的变化
从启动到多轮对话,各个 state 字段的变化过程:
| 阶段 | state 变化 |
|---|---|
| 启动 | catalog = 从所有 MCP server 拿到的 400 个 deferred 工具的完整定义 |
activated = 空集合 | |
systemPromptCache = 系统提示,包含 ## Deferred Tools 名字清单 | |
baseTools = [readFile, edit, shell, grep, ..., toolSearch] | |
| 第 1 轮请求 | tools = composeTurnTools(baseTools, activated) = baseTools(activated 为空) |
| 用户:“查 BTC 价格” | 发给 LLM:system + tools(9 个核心工具)+ messages |
| 第 1 轮响应 | LLM 返回:toolSearch({query: "select:mcp__gate__cex_spot_get_ticker"}) |
handleToolSearch() 在 catalog 里精确匹配命中 | |
activated = { "mcp__gate__cex_spot_get_ticker" } | |
返回 tool_result 给模型:Loaded 1 tool(s) — now callable directly on your next step: | |
- mcp__gate__cex_spot_get_ticker: 获取现货行情 | |
| 第 2 轮请求 | tools = [...baseTools, cex_spot_get_ticker 的完整 schema](尾部多了一个工具) |
messages 里追加了上一轮的 tool_call + tool_result | |
发给 LLM:system + tools(10 个工具)+ messages,前缀缓存 miss 一次 | |
| 第 2 轮响应 | LLM 返回:cex_spot_get_ticker({currency_pair: "BTC_USDT"}) |
客户端执行 MCP 工具调用,拿到价格。activated 不变 | |
| 第 3 轮 | tools 同第 2 轮(activated 没变,前缀稳定,缓存命中) |
| LLM 返回文本回复 | |
| 后续轮次 | activated 持久保留,不用再搜 cex_spot_get_ticker |
| 如果激活新工具 → activated 增长 → tools 追加 → 再 miss 一次 |
几个关键点:
catalog和systemPromptCache在启动时固定,整个 session 不变activated只增不减tools数组每轮动态合成 =baseTools+ activated 对应的 schema- 前缀缓存只在 activated 增长的那一轮 miss,之后稳定
工程细节
前面讲的是 tool search 的核心流程。这一节主要讲一下几个实现过程中遇到的问题以及解决方法。
阈值判断:什么时候该启用 defer。 工具少的时候,全量注入更简单,也不需要多一轮往返。x-code-cli 用 DEFERRAL_THRESHOLD_PERCENT 做判断:把所有候选 deferred 工具的 schema 序列化后估算 token 量,如果不超过模型上下文窗口的 10%,就跳过 defer,退回全量加载。
模型兼容性。 tool search 依赖模型主动去调 toolSearch。如果模型不支持或不配合,deferred 工具就永远不会被加载。各家的处理方式不同:
- Codex 的做法最精确:在
models.json里按具体模型版本配置supports_search_tool布尔字段。只有明确标记为支持的模型才启用 tool search,未知模型默认关闭。 - Claude Code 判断的是 API 能力而非模型智力:用
modelSupportsToolReference()检查模型是否支持tool_reference协议块。默认排除列表只有['haiku'],但可以通过远程配置动态更新,不需要发版。 - x-code-cli 目前用的是粗粒度的字符串匹配:
WEAK_MODEL_PATTERNS = ['haiku', 'nano', 'glm-4v'],模型名包含这些子串就退回全量注入。这个方案的问题是区分不了同一系列的不同版本——比如 Claude 3 Haiku 和 Claude 3.5 Haiku 能力差距很大,但都会被一刀切地禁用。
系统提示的字节稳定性。 LLM API 的前缀缓存依赖请求前缀不变。system + tools 组成的前缀在后续请求中保持一致,就能命中缓存,按折扣价计费。这要求系统提示里的 deferred 名单不能在运行中动态修改。x-code-cli 的做法是在启动时生成 systemPromptCache 并冻结,整个 session 不再改动。
这带来一个副作用:工具激活后,系统提示里的名单仍然写着这个工具是 deferred 状态,但 tools 数组里已经有了它的完整 schema。模型看到的系统提示和实际的工具列表之间存在不一致:系统提示说工具未加载,但工具实际上已经在列表里了。
在实际使用中,这个不一致很少造成问题。强模型(Sonnet、GPT-4o 等级)在收到第一次 toolSearch 的 tool_result 后,会记住工具已经加载,后续直接调用而不会重复搜索。重复搜索只在极端情况下出现——比如对话轮次非常多、早期的 tool_result 被压缩掉了、模型又看到系统提示里写着"未加载"时,才可能再搜一次。x-code-cli 加了一层防御:如果搜索的工具已经在 activated 集合里,返回 Already loaded — call xxx directly now.,避免模型陷入搜索循环。这是防御性代码,不是常规路径。
Claude Code 和 Codex 从架构上避免了这种不一致。Claude Code 靠 Anthropic API 的 tool_reference 机制,激活的工具由服务端展开 schema,客户端不改 tools 数组。Codex 靠 OpenAI Responses API 的 tool_search_output 历史项,同样由服务端处理。两者都不需要在系统提示里维护一份可能过时的状态。x-code-cli 因为要跨多个 provider,没法依赖单一 API 的服务端能力,只能走客户端重注入——但从实际效果看,强模型下这种不一致几乎不会触发重复搜索。
子 agent 不开 defer。 子 agent 的工具集由白名单控制,大多数子 agent 只有几个内置工具。比如 explore 子 agent 只有 readFile、glob、grep、listDir、shell 五个工具,plan 子 agent 更少,只有四个只读工具。工具这么少,加 tool search 没有意义。唯一的例外是 general-purpose 子 agent——它继承了父 session 的 MCP 工具,工具数量可能很多,但即便如此,子 agent 目前也走全量注入,因为子 agent 的生命周期短、轮次少,defer 带来的节省不明显。
总结
tool search 的完整流程回顾:启动时从 MCP server 全量拿到工具定义,按使用频率分成直接加载和 deferred 两类。直接加载的工具,完整 schema 放进每轮请求的 tools 数组;deferred 工具只在系统提示里列名字。模型需要某个 deferred 工具时,调 toolSearch 加载它的 schema,下一轮就能正常调用。
几个主流 AI Agent CLI 的实现对比:
| 全量注入? | 动态机制 | 匹配算法 | |
|---|---|---|---|
| Claude Code | 否 | ToolSearch 工具 | 关键词打分 + select: |
| Codex | 否 | tool_search 工具 | BM25 全文检索 |
| Gemini CLI | 是 | 无 | — |
| x-code-cli | 否 | toolSearch 工具 | 关键词打分 + select: |
Claude Code、Codex、x-code-cli 三家的思路几乎一样,区别在于激活后的 schema 怎么对模型可用——Claude Code 和 Codex 靠各自 API 的服务端能力,x-code-cli 靠客户端重注入(直接把 schema 加进 tools 数组)。Gemini CLI 目前没有内置 tool search。
我的其他 AI Agent 文章
- 让 AI Agent 系统自己发现 bug、自己提修复 PR:自我进化的 Harness
- AI Agent 工程师入门指南
- 如何从零开始实现一个 AI Agent CLI
- 掘金小册--从零打造一个 AI Agent CLI
参考资料
- Claude Code 源码
- Codex 源码
- x-code-cli 源码
- MCP 协议规范
相关文章
- 围产保健卡使用手册 07-16
- 桦树叶绘制技巧 07-16
- 寻踪皮影师绳武 挑战清河区域影舞 BOSS 07-16
- 围产手册办理指引 07-16
- 石家庄围产保健本办理指引 07-16
- 桦字组词全集 07-16