跳到主要内容

第 03 章 · 工具、MCP 与审批

本章讲 agent 的「手脚」:它能调的工具从哪来、注册成什么样、工具太多塞不下上下文怎么办、危险工具怎么让用户批准。读完你能讲清 Cherry Studio 的工具工程三件套——统一注册表、deferred tools、主进程审批。

1. 先建直觉:工具的三个来源,一个注册表

agent 能调的工具有三类来源,但都收进同一个进程级注册表 ToolRegistry:

来源例子谁注册
内建工具web_search / web_fetch / kb_search / kb_listbuiltin/index.tsregisterBuiltinTools
MCP 工具mcp__gmail__sendMessagemcp/mcpTools.tssyncMcpToolsToRegistry
元工具tool_search / tool_inspect / tool_invoke请求时按需注入(见 §4)

ToolEntry 的形状(src/main/ai/tools/adapters/aiSdk/types.ts,见 docs/references/ai/tool-registry.md):

// 示意,非源码:工具目录里一条记录长什么样
interface ToolEntry {
name: string // 模型在 tool_calls 里写的「线名」,如 web_search
namespace: string // 分组:web / kb / mcp:<id> / meta
description: string // 一行简介,给 tool_search 看
defer: 'never' | 'always' | 'auto' // 是否折叠(见 §4)
tool: Tool // AI SDK 的 Tool:schema + execute + needsApproval
applies?(scope): boolean // 这次请求要不要启用它(纯函数)
}

registry(src/main/ai/tools/adapters/aiSdk/registry.tsToolRegistry,模块级单例)在工具文件 import 时登记,在请求时被 buildAgentParams 读取。selectActive(scope) 跑每条的 applies 谓词挑出本次启用的工具——applies 必须是纯函数(它每次 selectActive 都跑,副作用会破坏确定性)。

两套工具系统并存。 注意:这个 aiSdk ToolRegistry 服务普通聊天AgentClaude Code agent 会话有独立的一套——tools/adapters/claudeCode/agentTools.ts 直接从 MCP 服务器和内建描述符构建 Claude 的工具策略,不消费这个 ToolRegistry(见 tool-registry.md 开头)。别把两者混了。

2. 线名约定:__ 分段

双下划线作段分隔符(让工具内部的单 _ 不歧义):

来源线名模式例子
内建固定线名 <namespace>_<verb>web_search, kb_search
MCPmcp__<camelCase(server)>__<camelCase(tool)>mcp__gmail__sendMessage
元工具tool_<verb>tool_search, tool_invoke

3. MCP 工具:读目录永不阻塞启动

MCP(Model Context Protocol)是一个标准协议,把外部能力(Gmail、浏览器、文件系统…)包装成「服务器」,模型把它们当工具调。McpRuntimeService(src/main/ai/mcp/McpRuntimeService.ts)负责连这些服务器——支持 stdio(子进程)、Streamable HTTP / SSE(远程)、内存(进程内的内建服务器,如浏览器、文件系统)三种 transport。

最值得学的一点:读工具目录和刷新目录被拆成两条路。 McpCatalogService 把目录读取分成:

listTools(serverId) ← cache-only,只读共享缓存 mcp.tools.<serverId>,永不连服务器
▲ 所有构建工具面的热路径都走它(Claude SDK 桥、agent 工具策略、aiSdk 同步)
│ → 一个死的/慢的 MCP 服务器,绝不会卡住 agent/chat 启动(issue #16242)

refreshTools(serverId) ← 真正连接、列举、写缓存的「写路径」
只由后台 warmer 驱动(启动预热 / onToolListChanged / 手动刷新 / 启用开关)

listTools 第一次见到一个从没预热过的服务器(缓存 undefined,区别于「预热过但空 []」)时,会非阻塞地触发一次 refreshTools——这样无头/定时启动能自我预热,又不会反复探测死服务器。

代价:工具可用性是最终一致的。 一个启动时缓存还冷的服务器,这次会话贡献不了工具,下次才出现——而且 Claude Agent SDK 按 session 快照工具列表,所以这没法在会话中途变热(见 tool-registry.mdTool catalog reads never block on MCP)。这是「永不阻塞启动」换来的取舍。

MCP 工具同步:syncMcpToolsToRegistry({ selectedToolIds })(mcp/mcpTools.ts)从缓存读每个选中服务器的工具,把每个注册成一条 ToolEntry,其 tool.execute 经 MCP transport 代理过去。同步幂等,陈旧项下次覆盖。

它要解决的小问题。 你挂了 50 个 MCP 工具,每个工具的 schema 都要塞进系统提示——光工具定义就吃掉小上下文模型一半的窗口,还没开始聊。

思路。 把不常用的工具折叠起来:系统提示里只放三个元工具,让模型自己「先搜再调」。

不折叠(工具少): 折叠(工具多):
tool A schema tool_search ← 按 namespace 搜可用工具
tool B schema tool_inspect ← 看某个工具的完整签名
tool C schema ──────▶ tool_invoke ← 按名字带 JSON 参数调任意工具
...(50 个,撑爆) + 一段 <DEFERRED_TOOLS> 列出有哪些命名空间

什么时候折叠? shouldDefer(entries, contextWindow)(exposition/shouldDefer.ts)——除了「估算 token 超过上下文 10% 阈值」外,还有两道闸:

  • MIN_AUTO_DEFER_COUNT(=5):auto 池得够大,搜索-再-调才比直接内联划算。
  • META_TOOLS_OVERHEAD_TOKENS(=500):省下的 token 得超过三个元工具本身的提示开销。

没有这两道闸,小工具集 + 小上下文模型会触发折叠却净亏 tokendefer: 'always' 的工具总是折叠;defer: 'auto' 的按上面判定;defer: 'never' 永不折叠。

tool_search 长什么样。只暴露被折叠的工具(已经内联的搜出来是冗余),按命名空间分组返回简介;verbose 时返回完整 schema 并记进「已检视」账本,这样第一次 tool_invoke 不会被「没先 inspect」的门挡掉(createToolSearchTool,meta/toolSearch.ts)。注入由 applyDeferExposition(exposition/applyDeferExposition.ts)在「确实发生折叠时」才做——它把折叠的名字从 tools 里剥掉、注入三个元工具、返回 <DEFERRED_TOOLS> 段要列举的命名空间。

还有第四个元工具 tool_exec,故意不注入。 它是把整个注册表当全局 API 的沙箱 JS 执行器(meta/exec/),worker_threads + new Function 跑模型写的代码,有全 Node 权限——属于提权面,被刻意留在门外,等有明确需求时再用 Preference 开关放出(tool-registry.mdtool_exec is not injected)。

5. 工具审批:主进程独家裁决

它要解决的小问题。 agent 要跑 rm -rf 或发邮件,得让用户先点「允许」。但多窗口、可能掉线、可能 renderer 崩——谁是审批状态的权威?

铁律:Main 是审批状态的唯一写者。 renderer 只是渲染审批卡、收集用户决定、把决定 POST 给 Main;Main 把决定写进 DB 权威行、落库、恢复流。renderer 从不自己写 approved/denied(见 docs/references/ai/tool-approval.md)。

① execute 时,wrapper 查 tool.needsApproval + 助手的自动批准策略
需要审批 → 写一个 approval-requested part,把工具 promise 挂起

② AiStreamManager 把 topic 状态翻成 awaiting-approval(共享缓存,所有窗口原子看到)

③ 用户点允许/拒绝 → useToolApprovalBridge → IPC Ai_ToolApproval_Respond

④ Main 按 transport 分支:
• Claude-Agent 快路径:交给 AgentSessionRuntimeService.respondToolApproval
解决活的 canUseTool promise,流继续(命中活注册项时早返回,不读 DB)
• MCP 路径:读 anchor 行的 parts,应用决定,仅当 DB 行上确有该 part 才写
全部审批决定齐了 → 派发 continue-conversation 让流续上

⑤ 续流一广播 pending,awaiting-approval 立刻翻回,审批卡同 tick 消失

审批门工具永不被折叠(两者咬合的设计)。 一个 force-prompt 的 MCP 工具注册时是 defer: 'never'——mcp/mcpTools.ts 读一次 isMcpToolForcePromptBySource 同时驱动 deferneedsApproval——所以它留在内联,SDK 的原生审批门才能在它身上触发。若把它折叠掉,它就从 SDK 工具集里消失,审批门永不触发,只能经 tool_invoke 调到、绕过审批卡。作为运行时兜底,tool_invoke / tool_exec 在执行时还会调 isApprovalGated 拒绝审批门工具,把模型引回内联调用(isApprovalGated.ts,见 tool-registry.mdApproval-gated tools are never deferred)。

Claude Code agent 会话的审批走 ToolApprovalRegistry(src/main/ai/runtime/claudeCode/ToolApprovalRegistry.ts)+ SDK 的 canUseTool;权限模式有 default / acceptEdits / bypassPermissions / plan(AgentPermissionModeSchema,src/shared/data/api/schemas/agents.ts:27),改 agent 配置会经 applyPolicyUpdate 热更到温热连接上(第 02 章 §2)。

6. 巧妙之处

  • cache-only 读目录。 把「死 MCP 服务器不能拖垮 agent 启动」做成架构级保证(读/写两条路),而不是到处加超时。
  • deferred tools 带成本闸。 不是「工具多就折叠」,而是「折叠的净收益真的为正才折叠」——两道闸防止小模型反而亏 token。
  • defer 与审批咬合。 审批门工具 defer:'never' 这条规则,把「安全」和「上下文优化」两个看似无关的子系统正确地连了起来——折叠不能把审批门折没了。
  • 审批 Main 独写。 多窗口/掉线/崩溃下审批状态仍单点权威,renderer 永远只读。

7. 边界与局限

  • 工具可用性最终一致:启动时缓存冷的 MCP 服务器,这次会话没它的工具。
  • Claude 会话工具按 session 快照:中途加 MCP 服务器要等下一个会话。
  • tool_exec 默认关闭:模型写代码跑全权限 Node 的能力存在但不开。
  • 普通聊天的 ToolRegistry 与 Claude agent 的工具策略是两套,改一处不影响另一处——要小心别以为改了注册表就改了 agent 工具。

8. 代码地图

主题文件符号
工具注册表src/main/ai/tools/adapters/aiSdk/registry.tsToolRegistry, registry
内建工具注册src/main/ai/tools/adapters/aiSdk/builtin/index.tsregisterBuiltinTools
MCP → 注册表同步src/main/ai/tools/adapters/aiSdk/mcp/mcpTools.tssyncMcpToolsToRegistry
折叠判定src/main/ai/tools/adapters/aiSdk/exposition/shouldDefer.tsshouldDefer
折叠注入src/main/ai/tools/adapters/aiSdk/exposition/applyDeferExposition.tsapplyDeferExposition
tool_searchsrc/main/ai/tools/adapters/aiSdk/meta/toolSearch.tscreateToolSearchTool
MCP 运行时src/main/ai/mcp/McpRuntimeService.tsMcpRuntimeService
Claude agent 工具策略src/main/ai/tools/adapters/claudeCode/agentTools.tsbuildClaudeToolPolicy, listClaudeAgentToolDescriptors
Claude 审批注册表src/main/ai/runtime/claudeCode/ToolApprovalRegistry.tstoolApprovalRegistry
权限模式src/shared/data/api/schemas/agents.tsAgentPermissionModeSchema

下一章:把前三章里点到为止的几处「最烧脑也最精彩」的机制——steer-boundary 滚动时序、resume、warm query、压缩穿透——展开,并给出整体的边界与横向对比。