跳到主要内容

工具系统:MCP 工具 + 本地 agent 工具

本章一句话: 模型眼里只有「一堆工具」,根本不知道某个工具是外部 MCP 服务器提供的、还是 DeepChat 自带的本地能力——这层屏蔽就是工具系统的价值。

1. 它要解决的小问题

DeepChat 的「手脚」来自三类异构来源:外部 MCP 服务器(标准协议)、本地 agent 工具(读写文件、跑命令、搜代码……直接在主进程做)、以及 ACP agent 自带工具。它们调用方式、权限模型都不同。如果让聊天 runtime 去分辨「这个工具该怎么调」,会到处是 if-else。ToolPresenter 把它们抹平成一个统一接口

2. 思路:聚合 + 来源映射

怎么读:左边是聊天 runtime,它只跟 ToolPresenter 打交道。ToolPresenter 内部
用 ToolMapper 记住「每个工具名来自哪」,调用时按来源分流。

AgentRuntimePresenter
│ getAllToolDefinitions() ← 拿统一的 MCPToolDefinition[]
│ callTool(name, args) ← 按名字调,不关心来源

┌─────────────── ToolPresenter ───────────────┐
│ ToolMapper: toolName ─► 'mcp' | 'agent' │
└───────┬───────────────────────────┬──────────┘
│ source = mcp │ source = agent
▼ ▼
┌───────────────┐ ┌────────────────────────┐
│ McpPresenter │ │ AgentToolManager │
│ 外部 MCP 服务 │ │ ├ FileSystemHandler │
└───────────────┘ │ ├ BashHandler (exec) │
│ ├ FffSearchHandler │
│ ├ chatSettingsTools │
│ ├ SubagentOrchestrator │
│ ├ AgentPlanTool │
│ └ TapeTools / MemoryTools│
└────────────────────────┘

3. 聚合工具定义

ToolPresenter.getAllToolDefinitions() 按顺序做(见 docs/architecture/tool-system.md):

  1. mcpPresenter 拉 MCP 工具。
  2. AgentToolManager 拉本地 agent 工具。
  3. ToolMapper 记来源;重名时优先保留 MCP 工具
  4. 过滤 disabled 的 agent 工具,并为每个 conversation 维护独立映射。

结果:agentRuntimePresenter 只持有统一的 MCPToolDefinition[],完全不知道某个工具真实出身。这就是上一章 processStreamtools 参数的来源。

4. 本地 agent 工具有哪些

AgentToolManager 装配的 model-facing 工具(名字见 agentToolManager.ts):

工具名干什么处理器
read / write / edit读写/编辑文件AgentFileSystemHandler
glob / grep代码/文件搜索(FFF backed)AgentFffSearchHandler
exec / process执行命令 / 管理后台进程 sessionAgentBashHandler
question(QUESTION_TOOL_NAME)反问用户一个结构化问题runtime 内置
update_plan(agent-core/update_plan)更新任务计划(只改 plan state)AgentPlanTool
subagent_orchestrator编排子 agent 并行/串行跑任务SubagentOrchestratorTool
image_generate图像生成AgentImageGenerationTool
tape_info/tape_search/tape_anchors/tape_handoff/tape_context读/搜索磁带、打 handoff anchorAgentTapeToolHandler
skill_list / skill_view列出/查看 SkillsSkillTools

搜索策略很明确:prompt 引导模型走 globgrepread,shell 搜索命令不在 model-facing 代码搜索路径上(docs/architecture/tool-system.md,FFF Search 一节)。

5. 权限:预检查 + 暂停授权

本地工具不直接碰旧 presenter,而是通过注入的 runtime port(toolPresenter/runtimePorts.ts,AgentToolRuntimePort)拿到:conversation 工作目录、已批准路径、settings 审批消费、会话上下文桥接。

权限按能力拆成三个服务:

能力权限服务触发的错误
文件访问filePermissionServiceFilePermissionRequiredError
settings 变更settingsPermissionService
shell/命令CommandPermissionServiceCommandPermissionRequiredError

流程上,危险操作不是「先做了再问」:AgentToolManager 在调用前做 preCheckToolPermission,需要授权就抛出对应的 *PermissionRequiredError;processStream 把它转成 pending interaction,循环暂停,授权块回到 renderer 等用户点批准(见 02-chat-loop.md 的「三个出口」)。这也是为什么 read 这种只读工具被列进 PARALLEL_READ_ONLY_AGENT_TOOLS(dispatch.ts:129)——只读可并行,写操作要串行 + 授权。

6. Subagent:把活拆给子会话,再 merge / discard 结果

subagent_orchestrator(subagentOrchestratorTool.ts:9)让父 agent 把任务派给最多 5 个子 agent(subagentOrchestratorSchema,:25-29)。每个子任务跑在一个独立子会话里(sessionKind='subagent',见 docs/FLOWS.md 第 3 节)。

关键在结果如何回到父会话——这里和 Tape 紧密咬合:

subagentOrchestratorTool.ts:414-416
await this.runtimePort.mergeSubagentTape?.(parentSessionId, task.sessionId, meta)
// 或
await this.runtimePort.discardSubagentTape?.(parentSessionId, task.sessionId, meta)

子会话以 ${parentSessionId}::fork::${forkId} 的 fork 形式存在(01-tape-system.md 第 6 节);跑完后父会话要么 merge(把子磁带吸收进来)、要么 discard(整段丢弃)。因为一切都是 append-only,merge/discard 也只是「在父磁带上追加结果或一个丢弃标记」,不破坏任何过程。

7. 巧妙之处

  • 来源透明:聊天 runtime 持有统一 MCPToolDefinition[],加新工具种类(本地/ MCP / ACP)不动主循环。
  • 权限即暂停:把「要授权」实现成抛错 → 暂停交互,复用了 stream 循环已有的 paused 机制,不需要工具系统自己搞一套阻塞 UI。
  • subagent = fork + merge:子 agent 不是临时内存对象,而是真正的 fork 会话,结果通过 tape merge/discard 落到父会话,可追溯可丢弃。

8. 边界与局限

  • 重名工具一律 MCP 优先,本地同名工具会被遮蔽——加本地工具要避开常见 MCP 名。
  • FFF 不可用时 glob/grep 直接报工具错误(不静默降级),保证模型能感知搜索失败(docs/architecture/tool-system.md)。

9. 代码地图

主题文件路径符号名
聚合 / 路由src/main/presenter/toolPresenter/index.tsToolPresentergetAllToolDefinitions
来源映射src/main/presenter/toolPresenter/toolMapper.tsToolMapper
本地工具装配src/main/presenter/toolPresenter/agentTools/agentToolManager.tsAgentToolManager
runtime portsrc/main/presenter/toolPresenter/runtimePorts.tsAgentToolRuntimePort
子 agent 编排src/main/presenter/toolPresenter/agentTools/subagentOrchestratorTool.tsSubagentOrchestratorToolSUBAGENT_ORCHESTRATOR_TOOL_NAME
Tape 工具src/main/presenter/toolPresenter/agentTools/agentTapeTools.tsAgentTapeToolHandlerTAPE_TOOL_NAMES
命令权限src/main/presenter/permission/commandPermissionService.tsCommandPermissionServiceCommandPermissionRequiredError