OpenCode 源码解析
第 6 讲:Tool 工具注册与执行——LLM 的"手"与"眼"
基于 dev 分支源码 · 2026-07-12
一、工具系统:LLM 的能力边界
前五讲我们覆盖了 OpenCode 的整体架构、CLI 入口、Session 核心、Server 网络层和 Plugin 插件系统。这一讲进入Tool 工具注册与执行系统——这是 LLM 与外部世界交互的核心通道,也是 OpenCode 最关键的运行时组件之一。
📦 本讲核心文件
packages/opencode/src/tool/tool.ts(183 行)— 工具定义抽象
packages/opencode/src/tool/registry.ts(420 行)— 工具注册表服务
packages/opencode/src/tool/truncate.ts(156 行)— 输出截断与持久化
packages/core/src/tool/tool.ts(162 行)— V2 工具定义与结算
packages/core/src/tool/registry.ts(147 行)— V2 Location 作用域注册表
packages/core/src/tool/builtins.ts(48 行)— 内置工具集合
工具实现文件 28 个,总计约 4825 行
二、工具系统的双层架构
OpenCode 的工具系统分为两层:V1 层(packages/opencode/src/tool/)面向当前运行时,V2 层(packages/core/src/tool/)面向下一代 Location 作用域架构。两层并行存在,V2 正在逐步接管核心能力。
V2 Core 层 (packages/core/src/tool/)
⬇️ 提供基础抽象
Tool.make() — 不透明工具值
⬇️
ApplicationTools — 进程级注册
⬇️
ToolRegistry — Location 级注册 + 结算
⬇️
Materialization — 定义导出 + 执行结算
V1 Opencode 层 (packages/opencode/src/tool/)
⬇️ 当前活跃运行时
Tool.define() — 工具定义
⬇️
ToolRegistry — 内置 + 插件 + 自定义工具
⬇️
Truncate — 输出截断与持久化
三、V1 工具定义抽象
tool.ts 定义了工具的核心抽象,包括 Def(工具定义)、Info(工具信息)、Context(执行上下文)和 ExecuteResult(执行结果)。
1. 工具定义接口
📄 tool.ts (第 55-65 行)
export interface Def<
Parameters extends Schema.Decoder<unknown> = Schema.Decoder<unknown>,
M extends Metadata = Metadata,
> {
id: string
description: string
parameters: Parameters
jsonSchema?: JSONSchema7
execute(args: Schema.Schema.Type<Parameters>, ctx: Context): Effect.Effect<ExecuteResult<M>>
formatValidationError?(error: unknown): string
}
设计要点:每个工具声明 Parameters(Effect Schema 解码器)定义输入参数类型,execute 返回 Effect.Effect 而非 Promise,使工具执行天然嵌入 Effect 错误处理与追踪管线。
2. Tool.define() 工厂函数
📄 tool.ts (第 151-169 行)
export function define<
Parameters extends Schema.Decoder<unknown>,
Result extends Metadata,
R,
ID extends string = string,
>(
id: ID,
init: Effect.Effect<Init<Parameters, Result>, never, R>,
): Effect.Effect<Info<Parameters, Result>, never, R | Truncate.Service | Agent.Service> & { id: ID } {
return Object.assign(
Effect.gen(function* () {
const resolved = yield* init
const truncate = yield* Truncate.Service
const agents = yield* Agent.Service
return { id, init: wrap(id, resolved, truncate, agents) }
}),
{ id },
)
}
关键机制:define() 将 Truncate.Service 和 Agent.Service 注入到工具初始化中,所有工具自动获得输出截断和 Agent 信息的能力。工具 ID 同时附加到 Effect 对象上(Object.assign(..., { id })),方便静态引用。
3. wrap() — 参数解码与输出截断管线
📄 tool.ts (第 99-149 行)
function wrap<Parameters extends Schema.Decoder<unknown>, Result extends Metadata>(
id: string,
init: Init<Parameters, Result>,
truncate: Truncate.Interface,
agents: Agent.Interface,
) {
return () =>
Effect.gen(function* () {
const toolInfo = typeof init === "function" ? { ...(yield* init()) } : { ...init }
// 编译一次解析器闭包,避免每次调用重新分配
const decode = Schema.decodeUnknownEffect(toolInfo.parameters)
const execute = toolInfo.execute
toolInfo.execute = (args, ctx) => {
return Effect.gen(function* () {
const decoded = yield* decode(args).pipe(
Effect.mapError((error) =>
new InvalidArgumentsError({
tool: id,
detail: toolInfo.formatValidationError
? toolInfo.formatValidationError(error)
: String(error),
}),
),
)
const result = yield* execute(decoded, ctx)
// 自动截断输出
const agent = yield* agents.get(ctx.agent)
const truncated = yield* truncate.output(result.output, {}, agent)
return {
...result,
output: truncated.content,
metadata: { ...result.metadata, truncated: truncated.truncated,
...(truncated.truncated && { outputPath: truncated.outputPath }),
},
}
}).pipe(Effect.orDie, Effect.withSpan("Tool.execute", { attributes: attrs }))
}
return toolInfo
})
}
执行管线:LLM 调用工具时,参数首先经过 Schema.decodeUnknownEffect 解码,解码失败抛出 InvalidArgumentsError(包含友好的模型可读错误信息)。执行成功后,输出自动经过 Truncate.output() 截断管线,超出 2000 行或 50KB 的结果被保存到临时文件。
四、工具注册表 ToolRegistry
registry.ts 是工具系统的调度中心,负责收集、过滤和提供所有可用工具。它维护三类工具:内置工具(15 个)、插件工具、自定义脚本工具。
1. 内置工具清单
| 工具 ID | 文件 | 功能 |
|---|---|---|
| shell | shell.ts (645 行) | 终端命令执行 |
| read | read.ts (386 行) | 读取文件/目录 |
| edit | edit.ts (737 行) | 精确字符串替换编辑 |
| write | write.ts (104 行) | 文件写入 |
| apply_patch | apply_patch.ts (313 行) | 统一补丁应用(GPT 专属) |
| glob | glob.ts (76 行) | 文件模式匹配 |
| grep | grep.ts (112 行) | 正则内容搜索 |
| task | task.ts (346 行) | 子代理任务委托 |
| todowrite | todo.ts (46 行) | 任务清单管理 |
| websearch | websearch.ts (143 行) | 网络搜索(Exa/Parallel) |
| fetch | webfetch.ts | 网页抓取 |
| skill | skill.ts (70 行) | 技能加载 |
| question | question.ts | 向用户提问 |
| lsp | lsp.ts | LSP 诊断查询 |
| plan | plan.ts | 计划模式退出 |
条件注册:部分工具根据运行时标志动态启用:question 仅在 app/cli/desktop 客户端可用,lsp 需要 experimentalLspTool 标志,plan 需要 experimentalPlanMode 且仅 CLI 可用。
2. 工具过滤策略
注册表根据模型 ID 和 Provider 动态过滤工具列表:
📄 registry.ts (第 266-278 行)
const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) {
const filtered = (yield* all()).filter((tool) => {
// WebSearch 仅在 OpenCode 内置 Provider 或 Exa/Parallel 标志开启时可用
if (tool.id === WebSearchTool.id) {
return webSearchEnabled(input.providerID, { exa: flags.enableExa, parallel: flags.enableParallel })
}
// GPT 模型使用 apply_patch 而非 edit/write(非 GPT-4)
const usePatch =
input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4")
if (tool.id === ApplyPatchTool.id) return usePatch
if (tool.id === EditTool.id || tool.id === WriteTool.id) return !usePatch
return true
})
// ...
})
智能路由:GPT 模型(非 GPT-4)使用 apply_patch 统一补丁工具,其他模型使用 edit/write 精确编辑工具。这种区分是因为 GPT 系列在统一 diff 格式上表现更好。
3. 插件工具与自定义工具
除了 15 个内置工具,注册表还支持两类扩展工具:
1. 插件工具:通过 Plugin 系统注册的 npm 插件工具,支持 Zod Schema 和 JSON Schema 两种参数格式
2. 自定义脚本工具:自动扫描项目 {tool,tools}/*.{js,ts} 目录,动态加载用户编写的工具脚本
📄 registry.ts (第 171-185 行)
const matches = dirs.flatMap((dir) =>
Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true, symlink: true }),
)
for (const match of matches) {
const namespace = path.basename(match, path.extname(match))
const mod = yield* Effect.promise(() => import(pathToFileURL(match).href))
for (const [id, def] of Object.entries(mod)) {
if (!isPluginTool(def)) continue
custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
}
}
五、V2 工具架构:Location 作用域与结算
V2 工具系统(packages/core/src/tool/)采用了更严格的抽象设计,核心变化是不透明工具值和结算(settlement)机制。
1. Tool.make() — 不透明工具值
📄 core/src/tool/tool.ts (第 71-132 行)
export function make<
Input extends SchemaType<any>,
Output extends SchemaType<any>,
Structured extends SchemaType<any> = Output,
>(config: Config<Input, Output, Structured>): Definition<Input, Structured> {
const tool = Object.freeze({}) as Definition<Input, Structured>
const definitions = new Map<string, ToolDefinition>()
runtimes.set(tool, {
definition: (name) => {
const cached = definitions.get(name)
if (cached) return cached
return new ToolDefinition({
name,
description: config.description,
inputSchema: toJsonSchema(config.input),
outputSchema: toJsonSchema(config.structured ?? config.output),
})
},
settle: (call, context) =>
Schema.decodeUnknownEffect(config.input)(call.input).pipe(
Effect.flatMap((input) => config.execute(input, context).pipe(
Effect.flatMap((output) =>
Schema.encodeEffect(config.output)(output)),
)),
),
})
return tool
}
设计亮点:工具值本身是冻结的空对象 Object.freeze({}),真正的运行时逻辑存储在 WeakMap 中。这种"不透明值"设计防止外部代码直接访问工具内部实现,只能通过 definition()、settle() 等受控接口交互。
2. 结算管线 (Settlement)
V2 的 settle 方法封装了完整的工具执行管线:
LLM 工具调用
⬇️
① Schema.decodeUnknownEffect — 参数解码
⬇️
② config.execute(input, context) — 工具执行
⬇️
③ Schema.encodeEffect(output) — 输出编码
⬇️
④ toModelOutput — 内容格式转换
⬇️
ToolOutput — 返回给 LLM
3. V2 注册表与 Location 作用域
V2 注册表(core/src/tool/registry.ts)是 Location 作用域的,与 V1 的进程全局注册表不同:
📄 core/src/tool/registry.ts (第 42-125 行)
const registryLayer = Layer.effect(
Service,
Effect.gen(function* () {
const applications = yield* ApplicationTools.Service
const resources = yield* ToolOutputStore.Service
type Registration = { readonly identity: object; readonly tool: AnyTool }
const local = new Map<string, Array<{ readonly token: object; readonly registration: Registration }>>()
const settleWith = Effect.fn("ToolRegistry.settle")(function* (input, advertised?) {
const registration =
local.get(input.call.name)?.at(-1)?.registration
?? applications.entries().get(input.call.name)
if (!registration)
return { result: { type: "error", value: `Unknown tool: ${input.call.name}` } }
// 执行结算 + 输出边界管理
const pending = yield* settle(registration.tool, input.call, {
sessionID: input.sessionID, agent: input.agent,
assistantMessageID: input.assistantMessageID, toolCallID: input.call.id,
})
const bounded = yield* resources.bound({ sessionID, toolCallID, output })
return { result, output: bounded.output, outputPaths: bounded.outputPaths }
})
// ...
}),
)
优先级链:Location 级注册(local Map)优先于应用级注册(ApplicationTools),最新注册覆盖同名旧工具。结算时自动绑定 ToolOutputStore 管理输出路径。
六、核心工具实现分析
1. Shell 工具 — 命令执行引擎
shell.ts(645 行)是最复杂的工具,支持多行命令执行、文件操作检测、权限请求和输出截断。关键设计:
1. 文件操作自动检测:内置 FILES、CMD_FILES、FLAGS、SWITCHES 集合,自动识别 rm/cp/mv/mkdir 等文件操作命令并触发权限请求
2. Shell 解析:使用 ShellPrompt(基于 tree-sitter)解析命令,支持 Bash 和 PowerShell 两种语法
3. 工作空间安全:containsPath 检查确保命令操作在工作空间范围内
2. Edit 工具 — 精确字符串替换
edit.ts(737 行)实现了精确的 oldString/newString 替换编辑:
📄 edit.ts (第 47-56 行)
export const Parameters = Schema.Struct({
filePath: Schema.String.annotate({ description: "The absolute path to the file to modify" }),
oldString: Schema.String.annotate({ description: "The text to replace" }),
newString: Schema.String.annotate({
description: "The text to replace it with (must be different from oldString)",
}),
replaceAll: Schema.optional(Schema.Boolean).annotate({
description: "Replace all occurrences of oldString (default false)",
}),
})
设计要点:使用文件级 Semaphore 锁防止并发编辑冲突,支持 BOM 检测与保留,编辑后自动触发 LSP 诊断检查和代码格式化。
3. Task 工具 — 子代理委托
task.ts(346 行)支持将任务委托给子代理,支持前台和后台两种模式:
1. 前台模式(默认):等待子代理完成并返回结果
2. 后台模式:异步启动子代理,完成后自动通知
3. 任务恢复:通过 task_id 参数恢复之前的子代理会话
七、输出截断系统
truncate.ts(156 行)管理工具输出的截断与持久化,防止过大的输出占用 LLM 上下文窗口:
| 参数 | 默认值 | 说明 |
|---|---|---|
| MAX_LINES | 2000 | 最大行数限制 |
| MAX_BYTES | 50 KB | 最大字节数限制 |
| RETENTION | 7 天 | 截断文件保留期 |
| direction | "head" | 截断方向(保留头部/尾部) |
截断文件存储在 TRUNCATION_DIR 目录,每 1 小时自动清理超过 7 天的过期文件。截断时生成友好提示,引导 LLM 使用 Grep 或 Read(带 offset/limit)访问完整内容。
八、工具执行流程图
LLM 生成工具调用
⬇️
ToolRegistry.tools() — 获取工具列表
⬇️
wrap().execute() — 参数解码 + Schema 校验
⬇️
ctx.ask() — 权限请求(如需)
⬇️
工具实际执行(文件操作/命令/网络等)
⬇️
Truncate.output() — 输出截断与持久化
⬇️
ExecuteResult — 返回给 LLM 继续对话
九、架构设计总结
OpenCode 工具系统的核心设计原则:
🔹 Effect 原生:所有工具执行返回 Effect.Effect,天然支持错误处理、追踪和组合
🔹 Schema 驱动:参数和输出通过 Effect Schema 定义,编译时类型安全 + 运行时校验
🔹 自动截断:输出截断透明嵌入工具管线,开发者无需手动处理
🔹 动态过滤:根据模型能力和 Provider 特性智能选择可用工具集
🔹 双层架构:V1 面向当前运行时,V2 面向 Location 作用域的未来架构
🔹 扩展友好:插件工具和自定义脚本工具通过相同的注册表管线集成
📖 系列导航
← 第 5 讲:Plugin 插件系统
→ 第 7 讲:Provider 模型抽象层
关注公众号获取更多技术干货 · 源码地址:https://github.com/opencode-ai/opencode
夜雨聆风