OpenCode 自定义工具(Custom Tools)开发实战指南:为 AI 编程助手打造专属能力

引言

用过 OpenCode 的朋友都知道,它内置了一套相当强大的工具:readwriteeditbashgrepglobwebfetch 等,正是这些工具让 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/

三、用 tool() 助手创建第一个工具

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 会据此生成符合格式的参数。

四、用 Zod 定义参数

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_addmath_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.$。同理,你也可以调用 nodego runsh,乃至团队内部编译好的二进制文件——底层能力想怎么接都行。

九、与权限系统配合使用

自定义工具和内置工具一样,受 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"
  }
}

> 小提示:内置工具里,writeapply_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 工具生态。