用过 OpenCode 的朋友都知道,它内置了一套相当强大的工具:read、write、edit、bash、grep、glob、webfetch 等,正是这些工具让 AI 能够真正"动手"完成编程任务。但在真实项目中,我们经常需要一些项目专属的能力:查询业务数据库、调用内部 API、跑一段固定的构建脚本、检查某个服务是否健康……这些逻辑通用模型并不知道,也很难用一句普通提示词说清楚。
这时候就需要自定义工具(Custom Tools)出场了。它是 OpenCode 提供的一套轻量扩展机制,让你可以用 TypeScript/JavaScript(甚至任意语言)把"业务能力"封装成一个函数,交给 LLM 在对话中按需调用。本文将从零开始,带你完整掌握自定义工具的创建、参数定义、上下文获取与实战应用。
先看一个场景:你的项目有一个 Postgres 数据库,你想让 AI 帮忙排查线上数据问题。没有自定义工具时,AI 只能猜测表结构、写一堆 psql 命令在 bash 里试错,既慢又危险。有了自定义工具后,你只需提前封装好一个 query_database 工具,AI 就能安全地直接查询,并基于结果继续推理。
自定义工具的价值可以归纳为三点:
能力封装:把重复、易错的操作固化成受控函数,AI 按约定调用,而不是自由发挥。
安全边界:相比开放 bash 全权,自定义工具只暴露你定义好的"接口",天然限制了 AI 能做什么。
跨语言:工具定义用 TS/JS,但实现体可以是 Python、Go、Shell 脚本……团队用什么顺手就用什么。
自定义工具的定义文件(.ts 或 .js)支持两个位置:
.opencode/tools/ 目录下,只对当前项目生效,适合跟随代码仓库一起提交。~/.config/opencode/tools/ 目录下(Windows 上是用户配置目录),所有项目都能用,适合放通用工具。# 项目级目录结构示例 your-project/ ├── .opencode/ │ └── tools/ │ ├── database.ts │ ├── math.ts │ └── python-add.ts └── src/
OpenCode 提供了 tool() 辅助函数,它自带类型安全与参数校验,是推荐的写法。新建 .opencode/tools/database.ts:
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "Query the project database",
args: {
query: tool.schema.string().describe("SQL query to execute"),
},
async execute(args) {
// 你的数据库查询逻辑
return `Executed query: ${args.query}`
},
})
几个关键点:
database 的工具。description 很关键:LLM 靠它判断"什么时候该用这个工具",描述越具体,AI 用对工具的几率越高。args 定义参数:每个字段都要用 tool.schema 描述,AI 会据此生成符合格式的参数。tool.schema 本质就是 Zod schema,支持字符串、数字、布尔、数组、对象等各种类型。你也可以直接引入 Zod,返回普通对象:
import { z } from "zod"
export default {
description: "Tool description",
args: {
name: z.string().describe("User name"),
age: z.number().describe("User age"),
tags: z.array(z.string()).describe("Tags list"),
},
async execute(args) {
return `Hello ${args.name} (${args.age}), tags: ${args.tags.join(", ")}`
},
}
参数校验由 schema 自动完成,AI 传错类型时会得到明确报错,不会污染你的业务逻辑。
除了 default 导出,你还可以从同一个文件导出多个工具。每个导出都会成为一个独立工具,名字格式为 <文件名>_<导出名>:
// .opencode/tools/math.ts
import { tool } from "@opencode-ai/plugin"
export const add = tool({
description: "Add two numbers",
args: {
a: tool.schema.number().describe("First number"),
b: tool.schema.number().describe("Second number"),
},
async execute(args) {
return (args.a + args.b).toString()
},
})
export const multiply = tool({
description: "Multiply two numbers",
args: {
a: tool.schema.number().describe("First number"),
b: tool.schema.number().describe("Second number"),
},
async execute(args) {
return (args.a * args.b).toString()
},
})
上面的文件会生成 math_add 和 math_multiply 两个工具。
自定义工具按工具名做键。如果你的工具与内置工具同名,自定义版本会优先生效。比如你可以用这样一个文件"锁死"内置的 bash 工具,只允许经过你过滤的命令:
// .opencode/tools/bash.ts
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "Restricted bash wrapper",
args: {
command: tool.schema.string(),
},
async execute(args) {
return `blocked: ${args.command}`
},
})
> 建议:除非你有意替换内置工具,否则尽量用不冲突的独特名字。如果只是想禁用某个内置工具(而不是覆盖),更推荐用权限系统(Permissions)里的 deny,那是一种更轻量的做法。
工具的 execute 函数还能拿到第二个参数 context,里面带有当前会话的上下文信息:
// .opencode/tools/project.ts
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "Get project information",
args: {},
async execute(args, context) {
const { agent, sessionID, messageID, directory, worktree } = context
return `Agent: ${agent}, Session: ${sessionID}, Message: ${messageID}, Directory: ${directory}, Worktree: ${worktree}`
},
})
常用字段说明:
directory:当前会话的工作目录,运行工具时以此为基准。worktree:Git worktree 的根目录。agent:当前是哪个 agent 在调用工具(比如主 agent 还是某个子代理)。sessionID / messageID:会话与消息标识,可用于日志追踪。工具定义必须是 TS/JS,但定义内部可以调用任意语言的脚本。官方给了一个用 Python 实现加法的例子。
第一步,写一个纯 Python 脚本 .opencode/tools/add.py:
import sys a = int(sys.argv[1]) b = int(sys.argv[2]) print(a + b)
第二步,创建工具定义 .opencode/tools/python-add.ts,用 Bun.$ 执行脚本:
import { tool } from "@opencode-ai/plugin"
import path from "path"
export default tool({
description: "Add two numbers using Python",
args: {
a: tool.schema.number().describe("First number"),
b: tool.schema.number().describe("Second number"),
},
async execute(args, context) {
const script = path.join(context.worktree, ".opencode/tools/add.py")
const result = await Bun.$`python3 ${script} ${args.a} ${args.b}`.text()
return result.trim()
},
})
这里用了 Bun 内置的 shell 执行能力 Bun.$。同理,你也可以调用 node、go run、sh,乃至团队内部编译好的二进制文件——底层能力想怎么接都行。
自定义工具和内置工具一样,受 permission 字段统一管理。默认所有工具都是 allow(直接放行)。你可以在 opencode.json 里精细控制:
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"edit": "deny",
"bash": "ask",
"webfetch": "allow",
"database": "ask"
}
}
allow:直接执行,不询问。ask:每次执行前征求用户同意。deny:拒绝执行。更妙的是支持通配符,可以一键管控一批工具。比如对某个 MCP 服务器导出的所有工具统一要求确认:
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"mymcp_*": "ask"
}
}
> 小提示:内置工具里,write 和 apply_patch 都归 edit 权限管辖,也就是说只要 edit: deny,这三个文件修改类工具都会被禁掉。
最后我们来一个完整的实战例子。假设我们要给 AI 提供一个"检查服务健康状态"的工具,内部通过 Shell 调用 curl 探测多个端点:
// .opencode/tools/health.ts
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "Check the health status of the dev services",
args: {
services: tool.schema
.array(tool.schema.string())
.describe("List of service names to check, e.g. ['api', 'web']"),
},
async execute(args) {
const results = []
for (const service of args.services) {
const url = service === "api" ? "http://localhost:8080/health" : `http://localhost:3000`
const status = await Bun.$`curl -s -o /dev/null -w "%{http_code}" ${url}`.text()
results.push(`${service}: HTTP ${status.trim()}`)
}
return results.join("\n")
},
})
此后,你只需要在对话里说一句"帮我看看 api 和 web 服务健康不健康",AI 就会自动调用 health 工具并给出结果,完全不用你解释怎么探测。这就是自定义工具带来的效率提升。
自定义工具是 OpenCode 扩展体系中性价比很高的一环:文件即工具、TS/JS 定义、Zod 校验参数、可获取会话上下文、可调用任意语言、可覆盖内置工具,还能与权限系统无缝配合。它把"AI 编程助手"从通用的编码器,升级成了真正懂你项目业务的"团队同事"。
给你的建议是:从团队里最高频、最易错的 2-3 个操作开始封装(比如数据库查询、健康检查、发布脚本),配合 ask 权限控制好执行边界,很快你就能感受到定制化 AI 助手的威力。想继续深入的话,还可以看看官方文档里 MCP 服务器和 Plugins 插件系统的内容,它们与自定义工具互为补充,共同构建完整的 AI 工具生态。