建立自定義工具
在 OpenCode 裡給模型提供可控、可複用的專案動作。
Custom tools 是你寫給 LLM 呼叫的函式。它們和 OpenCode 內建的 read、write、bash、grep 等工具並列存在,適合把專案專有、重複出現、輸入輸出穩定的動作封裝成可控入口。
這一篇用 12 分鐘換什麼:你會知道什麼時候值得寫 custom tool、工具檔案放哪裡、工具名怎麼生成、多個工具怎麼匯出、引數 schema 怎麼寫、context 能拿到什麼,以及哪些安全邊界不能交給模型猜。
先判斷是否真的需要
不要把 custom tool 當成“更高階的 bash”。先按這條路徑判斷:
flowchart LR
Need["一個動作反覆出現"] --> Builtin{"內建工具能解決?"}
Builtin -->|能| UseBuiltin["用 read / grep / bash / edit"]
Builtin -->|不能| External{"是外部通用系統?"}
External -->|是| MCP["優先 MCP"]
External -->|不是| Stable{"輸入輸出穩定?"}
Stable -->|否| Prompt["繼續用提示詞或指令碼"]
Stable -->|是| Custom["封裝 custom tool"]
style UseBuiltin fill:#dcfce7,stroke:#22c55e
style MCP fill:#dbeafe,stroke:#3b82f6
style Custom fill:#fef3c7,stroke:#f59e0b,stroke-width:2px
適合封裝的動作:
- 查詢內部服務狀態。
- 讀取專案專有配置格式。
- 執行固定診斷指令碼並返回摘要。
- 呼叫公司內部 API 的只讀介面。
- 把一段穩定 shell / Python / Node 指令碼包裝成結構化工具。
不適合封裝的動作:一次性命令、危險刪除、生產釋出、資料庫寫入、萬能 shell wrapper、會返回大量日誌的指令碼。
工具一旦進入 OpenCode,就會成為模型可能呼叫的能力。能用 permission 控制內建工具,就不要靠覆蓋內建工具名來“限制”它。
1. 工具放在哪裡
工具定義必須是 TypeScript 或 JavaScript 檔案,但工具定義內部可以呼叫任何語言寫的指令碼。
| 位置 | 適合什麼 |
|---|---|
.opencode/tools/ | 專案級工具,只服務當前儲存庫,推薦從這裡開始 |
~/.config/opencode/tools/ | 全域性工具,影響所有專案,確認穩定後再放這裡 |
大多數專案先用 .opencode/tools/。這樣工具隨儲存庫一起演進,不會把一個專案的假設帶到其他專案。
2. 最小工具結構
最簡單的方式是使用 tool() helper,它提供型別安全和引數校驗。
import { tool } from "@opencode-ai/plugin";
export default tool({
description: "Return current project directory information",
args: {},
async execute(args, context) {
return `directory=${context.directory}\nworktree=${context.worktree}`;
},
});文件名会变成工具名。上面这个文件会创建 project-info 工具。新手先从只读工具开始,确认调用链、输出格式和权限边界都稳定,再考虑写入或外部请求。
3. 一个文件导出多个工具
官方文档说明,一个文件可以导出多个工具。每个 export 会变成独立工具,名字是 <filename>_<exportname>。
例如 .opencode/tools/math.ts 里导出 add 和 multiply,OpenCode 会注册成 math_add 和 math_multiply。
如果工具共享同一组内部 helper,放在一个文件里更容易维护;如果职责不同,拆成多个文件更清楚。不要为了少建文件把无关工具塞到一起。
4. 参数 schema 要写给模型看
tool.schema 就是 Zod。参数越少、描述越具体,模型越不容易误用。
args: {
query: tool.schema.string().describe("Read-only SQL query to execute"),
}也可以直接导入 Zod,返回普通对象:
import { z } from "zod";
args: {
param: z.string().describe("Parameter description"),
}好的参数描述会写清边界,例如“仓库内相对路径”“只读 SQL”“不包含密钥”“只允许 staging 环境”。不要只写 query、path、name 这种模型无法判断风险的描述。
5. context 能拿到什么
工具会收到当前 session(会话)的上下文。官方示例里包括 agent、sessionID、messageID、directory 和 worktree。
context.directory是 session 工作目录(用户启动 OpenCode 时所在的那个文件夹)。context.worktree是 Git worktree(工作树,简单理解为"这个仓库当前 checkout 的文件树根目录"——一个 Git 仓库可以同时有多个 worktree,每个对应一个分支)的根目录。
路径拼接优先基于 context.worktree 或 context.directory,不要假设用户总在项目根目录启动 OpenCode。
6. 调用 Python 或 Shell 脚本
工具定义必须是 TypeScript / JavaScript,但真实逻辑可以放到 Python、Shell 或其他语言脚本里。
核心模式是:工具定义负责 schema、路径和输出摘要,脚本负责实际业务逻辑。例如用 context.worktree 找到项目内脚本,再通过 Bun shell 调用:
const script = path.join(context.worktree, ".opencode/tools/add.py");
const result = await Bun.$`python3 ${script} ${args.a} ${args.b}`.text();
return result.trim();這類結構適合複用已有指令碼。指令碼本身仍要能在終端獨立執行,不要只在 OpenCode 對話裡才“看起來能跑”。
7. 工具名衝突會覆蓋內建工具
Custom tools 按工具名註冊。如果自定義工具和內建工具同名,自定義工具會優先。
例如 .opencode/tools/bash.ts 會替換內建 bash。這不是普通命名問題,而是會改變模型可呼叫的基礎能力。
除非你明確要替換內建工具,否則不要使用 bash、read、write、edit 這類名字。如果只是想停用或收緊內建工具,應該用 permissions。
8. 安全邊界
設計 custom tool 時,預設按“會被模型頻繁呼叫,也可能被錯誤引數呼叫”處理:
- 預設只讀。
- 引數必須校驗,不把模型輸入直接拼 shell。
- 輸出要短,只返回下一步判斷需要的資訊。
- 金鑰從環境變數或憑據系統讀取,不寫進工具檔案。
- 寫入、刪除、釋出、資料庫操作必須有 dry-run、確認和許可權邊界。
- 錯誤返回清楚原因,不把完整堆疊和敏感環境打進上下文。
9. 驗收清單
一個可交付的 custom tool 至少滿足:
- 模型能從
description判斷什麼時候呼叫它。 - 引數 schema 足夠具體,錯誤引數會被拒絕。
- 工具名不和內建工具衝突,除非有明確替換意圖。
- 同一輸入多次執行結果穩定。
- 輸出短、可讀、無金鑰。
- 專案級工具優先,確認穩定後再考慮全域性化。
接下來去哪
工具總覽
先理解 custom tool 和內建工具、MCP、permission 的邊界。
MCP 伺服器
如果需求是外部系統上下文,MCP 往往比自寫工具更合適。
Plugin
只有需要改變 OpenCode 生命週期或註冊更深擴充套件時,才考慮 plugin。
許可權
寫入、shell、外部系統動作要靠 permission 管住。
官方資料
- OpenCode Custom Tools:https://opencode.ai/docs/custom-tools
- OpenCode Tools:https://opencode.ai/docs/tools
- OpenCode Permissions:https://opencode.ai/docs/permissions
- OpenCode Plugins:https://opencode.ai/docs/plugins