Skip to content

工具与技能

技能可以不要,工具能不能也不要?

不能混为一谈:

层次作用不用它时
Tool(工具)AI 通过 function calling 真正执行的能力(调接口、改数据、发消息等)若仍要让 AI 办事,就必须保留对应 Tool;否则 AI 只能 纯聊天,无法替你操作插件。
Skill(SKILL.md / SkillFeature)可选:长说明、activate_skill、粗筛里帮模型缩小工具范围可以 不写 SKILL;工具仍可通过 keywords/tags 和全量收集参与筛选。Core 不提供 declareSkill

非 AI 路径:不需要「技能」时,能力可以完全放在 命令 addCommand、HTTP API、CLI、定时任务 上——不注册 Tool 即可,与 SKILL 无关。

通用性建议:SKILL 里只写 跨平台通用的流程与约束;平台差异放进 Tool 的 description / parameters。重复、无激活价值、只为凑关键词的 SKILL 可以 直接删掉,避免「垃圾技能」;保留精简工具与清晰 keywords 往往更稳。


工具(Tool)是 AI 可调用的具体操作;技能(Skill)是可选的语义层,用于说明与粗筛(当前推荐磁盘 SKILL.md)。

概念关系

AI Agent 处理消息时常见流程:

  1. 粗筛:用户消息匹配 Skill(含关键词)或直接从工具池按相关性选 Tool
  2. 细筛:权限过滤 + 相关性排序(无 Skill 时仍可对 全部已注册 Tool 做细筛)

工具(Tool)

注册工具

使用 addTool(ToolFeature 扩展方法)注册工具:

typescript
import { usePlugin } from 'zhin.js'

const { addTool } = usePlugin()

addTool({
  name: 'search_music',
  description: '按关键词搜索音乐,返回歌曲名、歌手和链接',
  parameters: {
    type: 'object',
    properties: {
      keyword: { type: 'string', description: '搜索关键词' },
      limit: { type: 'number', description: '返回数量,默认 5' },
    },
    required: ['keyword'],
  },
  tags: ['音乐', '搜索'],
  keywords: ['音乐', '歌', '听歌', '搜歌'],
  execute: async (args) => {
    const results = await musicAPI.search(args.keyword, args.limit || 5)
    return results
  },
})

Tool 接口

Tool 支持泛型参数推断(默认 Record<string, any>,向后兼容):

typescript
interface Tool<TArgs extends Record<string, any> = Record<string, any>> {
  // 必填
  name: string                           // 工具名称(全局唯一)
  description: string                    // 描述(AI 用来理解工具用途)
  parameters: ToolParametersSchema<TArgs>  // 参数定义(JSON Schema 格式)
  execute: (args: TArgs, context?: ToolContext) => MaybePromise<ToolResult>  // 执行函数
  
  // 可选 - AI 发现
  tags?: string[]                 // 分类标签
  keywords?: string[]             // 触发关键词
  
  // 可选 - 约束
  platforms?: string[]            // 限定平台(如 ['icqq'])
  scopes?: ('private'|'group'|'channel')[]  // 限定场景
  permissionLevel?: ToolPermissionLevel     // 权限要求
  hidden?: boolean                // 对 AI 隐藏
  preExecutable?: boolean         // 允许预执行(无副作用的只读工具)
  
  // 可选 - 元数据
  source?: string                 // 来源标识
  kind?: string                   // 工具分类(如 file / shell / web)
}

ToolResult 返回类型

工具的 execute 返回 ToolResult,支持多种形式:

typescript
type ToolResult =
  | string               // 直接作为文本回复
  | { text: string }     // 结构化文本
  | { data: any; format?: string }  // 结构化数据
  | void | null | undefined  // 无回复
  | any                  // 其他类型自动 JSON.stringify

使用 ZhinTool 链式 DSL

ZhinTool 提供更简洁的链式写法:

typescript
import { usePlugin, ZhinTool } from 'zhin.js'

const { addTool } = usePlugin()

addTool(
  new ZhinTool('get_weather')
    .desc('查询城市天气')
    .param('city', 'string', '城市名称', true)
    .param('unit', 'string', '温度单位(C/F)', false)
    .platform('icqq')           // 仅 ICQQ 平台可用
    .scope('group')             // 仅群聊可用
    .permission('user')         // 所有用户可用
    .execute(async (args) => {
      return await fetchWeather(args.city, args.unit)
    })
)

使用 defineTool(类型安全)

defineTool<TArgs> 利用 Tool<TArgs> 的泛型支持,让 executeargs 参数获得完整的类型提示:

typescript
import { defineTool } from 'zhin.js'

const weatherTool = defineTool<{ city: string; unit?: string }>({
  name: 'get_weather',
  description: '查询城市天气',
  parameters: {
    type: 'object',
    properties: {
      city: { type: 'string', description: '城市名称' },
      unit: { type: 'string', description: '温度单位' },
    },
    required: ['city'],
  },
  execute: async (args) => {
    // args 类型为 { city: string; unit?: string }
    return await fetchWeather(args.city, args.unit)
  },
})

注意: 旧的 ToolDefinition<TArgs> 已废弃,现在是 Tool<TArgs> 的类型别名。直接使用 Tool<TArgs> 即可。

文件化 Tool(*.tool.md)

除了程序化注册,还可以通过 *.tool.md 文件声明工具——无需写 TypeScript 代码即可让 AI 拥有新能力。框架自动扫描、注册、热重载。

发现顺序

  1. 工作区 cwd/tools/
  2. ~/.zhin/tools/
  3. data/tools/(框架默认数据目录)
  4. 已加载插件:根插件与直接子插件包根目录下的 tools/

同名 Tool 先发现者优先。程序化注册的同名 Tool 优先于文件化版本。

文件结构

支持两种组织方式:

text
tools/
├── greeting.tool.md              # 扁平:纯模板,无需 handler 文件
└── calculator/
    ├── calculator.tool.md        # 嵌套:带 handler
    └── handler.ts                # 执行逻辑

带 handler 的 Tool

markdown
---
name: calculator
description: 计算数学表达式,支持加减乘除和括号
parameters:
  expression:
    type: string
    description: 数学表达式
    required: true
keywords: [计算, ]
tags: [utility, math]
handler: ./handler.ts
---

Handler 文件导出默认函数:

typescript
// tools/calculator/handler.ts
export default async function(args: { expression: string }) {
  const sanitized = args.expression.replace(/[^0-9+\-*/().%\s]/g, '')
  const result = new Function(`return ${sanitized}`)()
  return `${args.expression} = ${result}`
}

纯模板 Tool(无 handler)

当没有 handler 字段时,body 作为模板, 自动替换为参数值:

markdown
---
name: greeting
description: 生成个性化问候语
parameters:
  name:
    type: string
    description: 用户名
    required: true
  time:
    type: string
    description: 时间段
    enum: [morning, afternoon, evening]
tags: [utility]
---

你好,{{name}}!{{time}}好,欢迎来到 Zhin 机器人世界。

Frontmatter 字段一览

字段类型必填说明
namestring工具名称(全局唯一)
descriptionstring工具描述
parametersobject简写参数定义(见下)
handlerstringhandler 文件路径(相对于 .tool.md)

| keywords | string[] | — | 触发关键词 | | tags | string[] | — | 分类标签 | | platforms | string[] | — | 限定平台 | | scopes | string[] | — | 限定场景 | | permissionLevel | string | — | 权限级别 | | kind | string | — | 工具分类 | | hidden | boolean | — | 是否隐藏 |

参数简写格式(自动转换为 ToolParametersSchema):

yaml
parameters:
  city:
    type: string
    description: 城市名称
    required: true
  unit:
    type: string
    description: 温度单位
    enum: [C, F]
    default: C

热重载

工作区 cwd/tools/ 目录支持热重载——新增、修改、删除 *.tool.md 文件后,框架会在 400ms 内自动重新发现并注册。

内置工具

框架内置了一组 AI 可直接使用的工具(由 @zhin.js/agent 提供),无需手动注册:

工具说明
bash执行 Shell 命令(受 execSecurity 策略约束)
read_file读取文件内容
write_file写入文件
edit_file编辑文件(基于 diff)
list_dir列出目录
glob按模式匹配文件
grep搜索文件内容
web_search网页搜索
web_fetch抓取网页内容
ask_user向用户提问并等待回答
chat_history查询对话历史(关键词触发)
user_profile读写用户偏好(关键词触发)
schedule_followup安排定时跟进提醒(关键词触发)
spawn_task创建后台子任务(关键词触发)
activate_skill激活已安装的技能
install_skill从 URL 安装技能

ask_user — 用户确认工具

ask_user 工具允许 AI 主动向用户提问并等待回答。典型场景:

  • 危险操作确认:当 execAsk: true 且命令不在白名单时,AI 用 ask_user 向用户确认
  • 信息补全:AI 需要更多信息才能完成任务时主动询问
  • 选择确认:提供选项让用户选择
json
{
  "name": "ask_user",
  "arguments": {
    "question": "要执行 npm install 吗?",
    "choices": ["是", "否"]
  }
}

ask_user 通过 IM 消息发送问题,等待用户回复后将答案返回给 AI 继续处理。超时未回复则返回超时提示。

Agent 预设(*.agent.md)

Agent 预设用于声明领域专长 Agent,AI 可自动识别场景并委派子任务。

发现顺序

  1. 工作区 cwd/agents/
  2. ~/.zhin/agents/
  3. data/agents/
  4. 已加载插件包根目录下的 agents/

文件格式

markdown
---
name: code-reviewer
description: 代码审查专家,擅长发现 bug 和优化建议
keywords: [代码, 审查, review, bug]
tags: [development]
tools: [read_file, grep, edit_file]
model: gpt-4o
maxIterations: 8
---

你是一个资深代码审查员,专注于安全和性能问题。

## 审查规则
1. 检查输入验证和 SQL 注入风险
2. 检查资源泄漏(未关闭的连接、定时器)
3. 检查异步错误处理

Frontmatter 字段

字段类型必填说明
namestringAgent 名称
descriptionstringAgent 描述
keywordsstring[]触发关键词
tagsstring[]分类标签
toolsstring[]关联工具名列表
modelstring首选模型
providerstring首选 Provider
maxIterationsnumber最大迭代次数

Body(frontmatter 之后的正文)作为 Agent 的 systemPrompt 注入。

插件清单(plugin.yml)

插件可在包根目录放置 plugin.yml 声明元数据:

yaml
name: my-plugin
description: 我的示例插件
version: 1.0.0

通过 plugin.manifest getter 访问:

typescript
const plugin = usePlugin()
console.log(plugin.manifest)
// → { name: 'my-plugin', description: '...', version: '1.0.0' }

如果 plugin.yml 不存在,会自动 fallback 到 package.jsonname/description/version

技能(Skill)

技能目录与发现顺序

文件化技能(SKILL.md)的发现与 activate_skill 查找路径一致,优先级为:

  1. 工作区 cwd/skills/<name>/SKILL.md
  2. ~/.zhin/skills/<name>/SKILL.md
  3. data/skills/<name>/SKILL.md(框架默认数据目录)
  4. 已加载插件:根插件与直接子插件包目录下的 skills/<name>/SKILL.md(插件根 = path.dirname(plugin.filePath)

install_skill 默认仍安装到工作区 skills/。工作区 skills/ 支持热重载(见下文及 AI 文档)。

在插件中声明(推荐:文件化)

当插件提供多个相关工具时,在插件包目录下增加 skills/<技能名>/SKILL.md(frontmatter 含 namedescriptionkeywordstags;可选 tools 列表关联工具名)。Agent 会通过 discoverWorkspaceSkills 与同路径的 activate_skill 发现该技能。

text
plugins/utils/my-plugin/skills/my-plugin/SKILL.md

在适配器中声明

包内 skills/<name>/SKILL.md

各适配器包内提供 skills/<适配器名>/SKILL.md(含 namedescriptionkeywords 等 frontmatter),工具通过 addTool / 群管自动生成注册。Adapter.declareSkill 已从 Core 移除,仅保留文件化技能。

群管理能力(推荐:覆写方法自动检测)

群管理是 IM 的通用能力。Adapter 基类声明了 IGroupManagement 接口中的可选方法规范,适配器只需覆写自己平台支持的方法,start() 会自动检测并生成群管 Tool(Skill 粗筛依赖 SKILL.md 或工具 keywords,不再由适配器代码注册 Skill):

typescript
class IcqqAdapter extends Adapter<IcqqBot> {
  // 覆写标准群管方法 —— 内部委托给 Bot 的原生 API
  async kickMember(botId: string, sceneId: string, userId: string) {
    const bot = this.bots.get(botId)
    if (!bot) throw new Error(`Bot ${botId} 不存在`)
    return bot.kickMember(Number(sceneId), Number(userId), false)
  }

  async muteMember(botId: string, sceneId: string, userId: string, duration = 600) {
    const bot = this.bots.get(botId)
    if (!bot) throw new Error(`Bot ${botId} 不存在`)
    return bot.muteMember(Number(sceneId), Number(userId), duration)
  }

  async listMembers(botId: string, sceneId: string) {
    const bot = this.bots.get(botId)
    if (!bot) throw new Error(`Bot ${botId} 不存在`)
    const memberMap = await bot.getMemberList(Number(sceneId))
    return { members: Array.from(memberMap.values()), count: memberMap.size }
  }

  async start() {
    this.registerIcqqPlatformTools()  // 注册平台特有工具(头衔、公告、戳一戳等)
    await super.start()               // 自动检测上述 3 个方法 → 生成 Tool → 注册 Skill
  }
}

目前 10 余个 IM 适配器(含 ICQQ、OneBot11、Milky、QQ 官方、Telegram、Discord、KOOK、Slack、钉钉、飞书等)已采用此模式,Satori、OneBot 12 等协议适配器见 适配器 一览:

适配器覆写的标准方法保留的平台特有工具
ICQQkick, mute, muteAll, setAdmin, setNickname, setGroupName, listMembers头衔、群公告、戳一戳、禁言列表等
OneBot11kick, mute, muteAll, setAdmin, setNickname, setGroupName, listMembers, getGroupInfo头衔
Milkykick, mute, muteAll, setAdmin, setNickname, setGroupName, listMembers, getGroupInfo
Telegramkick, unban, mute, setAdmin, setGroupName, getGroupInfo置顶、投票、反应、贴纸、权限等
Discordkick, ban, unban, mute, setNickname, listMembers, getGroupInfo角色管理、帖子/论坛、反应、Embed
KOOKkick, ban, unban, setNickname, listMembers角色管理、黑名单
QQ 官方kick, mute, muteAll, listMembers, getGroupInfo频道/子频道、角色管理
Slackkick, setGroupName, listMembers, getGroupInfo邀请、话题、归档、反应等
钉钉kick, setGroupName, getGroupInfo部门管理、工作通知等
飞书kick, listMembers, getGroupInfo, setGroupName管理员设置、解散群等

可用的群管理方法规范:

方法说明权限级别
kickMember踢出成员group_admin
muteMember禁言(duration=0 解除)group_admin
setMemberNickname设置群昵称/名片group_admin
setAdmin设置/取消管理员group_owner
listMembers获取成员列表user
banMember封禁成员group_admin
unbanMember解除封禁group_admin
setGroupName修改群名称group_admin
muteAll全员禁言/解除group_admin
getGroupInfo获取群信息user

群管理使用指南

AI 在调用群管理工具时会遵循以下规则(已内置到 Skill 描述中):

  1. 用户名到 ID 的解析 — 当用户只提供昵称/名片时,AI 会先调用 list_members 查询成员列表,匹配目标用户的 user_id,再执行后续操作
  2. 禁言场景mute_member 适用于违规发言、刷屏、骚扰等需要临时限制发言的场景。duration 单位为秒,传 0 表示解除禁言,默认 600 秒(10 分钟)
  3. 管理员操作set_admin 需要群主权限,普通管理员无法操作;enable=false 为取消管理员
  4. 踢人与封禁的区别kick_member 是将成员移出群聊(可再次加入),ban_member 是永久拉黑
  5. 操作前确认 — AI 会确认目标用户正确后再执行,避免误操作

平台特有工具约束

不同平台的特有工具有各自的使用限制:

平台工具约束
ICQQicqq_poke每次请求只戳一次,不重复调用
ICQQicqq_send_user_like每人每天最多 20 次
ICQQicqq_list_muted仅查询,不执行禁言操作
ICQQicqq_set_title需要群主权限

平台特有工具

对于标准群管以外的平台特有操作(如 ICQQ 的头衔/群公告、Discord 的角色管理/Embed 等),在 start() 中通过 addTool() 手动注册;平台级 AI 说明放在适配器包 skills/<adapter>/SKILL.md,由 Agent 扫描与 activate_skill 使用。

typescript
class IcqqAdapter extends Adapter<IcqqBot> {
  async kickMember(...) { /* ... */ }
  async muteMember(...) { /* ... */ }

  async start() {
    this.addTool({ name: 'icqq_set_title', ... })
    this.addTool({ name: 'icqq_announce', ... })
    this.addTool({ name: 'icqq_poke', ... })
    await super.start()
  }
}

Skill 接口

typescript
interface Skill {
  name: string              // 技能名称
  description: string       // 描述(含 conventions)
  tools: Tool[]             // 包含的工具
  keywords?: string[]       // 触发关键词
  tags?: string[]           // 分类标签
  pluginName: string        // 来源插件
}

权限控制

权限级别

typescript
type ToolPermissionLevel = 
  | 'user'          // 普通用户(默认,所有人可用)
  | 'group_admin'   // 群管理员
  | 'group_owner'   // 群主
  | 'bot_admin'     // 机器人管理员
  | 'owner'         // 机器人拥有者

两层校验

第一层:AI 前过滤 在工具收集阶段,权限不足的工具不会出现在 AI 的可选列表中:

发送者是普通用户 → AI 只能看到 permissionLevel: 'user' 的工具
发送者是群管理员 → AI 能看到 'user' + 'group_admin' 的工具

第二层:运行时校验 工具执行时,ToolContext 会注入到 execute 函数中,适配器在执行前再次校验权限:

typescript
execute: async (args, context) => {
  this.checkPermission(context, 'group_admin')  // 运行时二次校验
  // ... 执行实际操作
}

Tool 与 Command 互转

工具自动生成命令

去重机制

当同一个工具同时通过 Skill 路径和 externalTools 路径被收集时,collectTools 会自动去重:

  • Skill 路径优先
  • 同名工具只保留第一次收集到的

完整示例

typescript
import { usePlugin, MessageCommand, ZhinTool } from 'zhin.js'

const { addTool, addCommand, logger } = usePlugin()

// 工具 1:搜索音乐
addTool(
  new ZhinTool('search_music')
    .desc('搜索音乐')
    .param('keyword', 'string', '搜索关键词', true)
    .param('limit', 'number', '返回数量', false)
    .execute(async (args) => {
      const results = await musicAPI.search(args.keyword, args.limit || 5)
      return { songs: results, count: results.length }
    })
)

// 工具 2:获取歌词
addTool({
  name: 'get_lyrics',
  description: '获取指定歌曲的歌词',
  parameters: {
    type: 'object',
    properties: {
      songId: { type: 'string', description: '歌曲 ID' },
    },
    required: ['songId'],
  },
  keywords: ['歌词', '词'],
  execute: async (args) => {
    return await musicAPI.getLyrics(args.songId)
  },
})

// 另:在插件包 skills/my-music/SKILL.md 写 name/description/keywords,供 Agent 发现(无 declareSkill API)

// 同时也注册一个命令(传统调用方式)
addCommand(
  new MessageCommand('music <keyword:string>')
    .desc('搜索音乐')
    .action(async (_, result) => {
      const data = await musicAPI.search(result.params.keyword, 3)
      return data.map(s => `${s.name} - ${s.artist}`).join('\n')
    })
)

基于 MIT 许可发布