Agent 工具与技能
启用 AI 后,Bot 会用大模型回答没有匹配到命令的消息。本页讲怎样让它做事:
- Tool:AI 可以调用的函数,例如查天气、搜歌、查数据库。
- Skill:一份操作手册,AI 需要时才加载,可以附带专用的 Tool。
- Prompt Section:给 AI 的背景知识,例如产品术语和回答规范。
- Agent:有独立职责、规则和工具的专门助手,可被其他 Agent 联系、委派。
开始前
先运行 npx zhin setup --ai 启用 AI。向导会安装模型 SDK,并声明 @zhin.js/tool 与 @zhin.js/prompt-section,tools/、prompt-sections/ 目录才会被识别。详见 AI 总览。
写第一个工具
推荐使用 zhin.js 内置的 Schema.object() 声明参数(零额外依赖):execute({ city, days }) 会自动推导出 { city: string; days: number },并在调用前自动校验参数、填充默认值!
// tools/weather/index.ts
import { Schema } from 'zhin.js';
import { defineAgentTool } from '@zhin.js/tool';
export default defineAgentTool({
description: '查询城市的实时与未来几天天气',
inputSchema: Schema.object({
city: Schema.string().required().description('城市名称'),
days: Schema.number().default(3),
}),
requiresApproval: 'never',
async execute({ city, days }) {
// city: string,days: number(默认 3 已自动填充)
const response = await fetch(`https://wttr.in/${encodeURIComponent(city)}?format=3`);
return `${await response.text()}(预测 ${days} 天)`;
},
});保存后在私聊里问 Bot "北京今天天气怎么样",AI 会调用这个工具并根据结果回答。
description和inputSchema是写给模型看的:清楚地说明这个工具做什么、参数是什么,模型才会在正确的时机调用它。execute的返回值会交给模型,由模型组织成回复。
参数 Schema 写法
除了推荐的 Schema.object(...)(来自 zhin.js),inputSchema 也兼容 Zod 4 的 z.object(...)(同样支持类型自动推导)以及根节点为 object 的原生 JSON Schema:
// 备选 1:Zod 4(自动推导输入类型)
import { z } from 'zod';
inputSchema: z.object({ city: z.string().min(1), days: z.number().default(3) })
// 备选 2:原生 JSON Schema(配合泛型 defineAgentTool<{ city: string }>)
inputSchema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] }常见需求
限制工具的可用范围
export default defineAgentTool({
description: '踢出群成员',
platforms: ['icqq'], // 只在 QQ 上提供
scopes: ['group'], // 只在群聊中提供
permissions: ['role(master)'], // 只有 master 发起的对话才提供
// ...
});不满足条件时,模型根本看不到这个工具。permissions 与命令的 permit 使用同一套规则。hidden: true 表示注册但不提供给模型,只能按名字调用。
会话冷却等动态状态可以用 isAvailable({ sessionKey, origin }) 在回合开始时判断。按平台会话 限流时,使用 origin.conversation 的完整身份(含 endpoint、parent、thread),因为子任务的 存储 sessionKey 可能不同。它在权限筛选后 同步读取已有状态,返回 false 时,本轮也不能通过 discover / load_tool 加载该工具。 不要在这里发请求、抽概率或消耗额度;执行时仍要复核可变条件。该函数必须同步返回 boolean, 异常会作为错误上报。没有会话的静态能力查询不执行这一检查。
执行前让用户确认
export default defineAgentTool({
description: '删除一条记录',
requiresApproval: 'always',
// ...
});| 取值 | 何时需要用户确认 |
|---|---|
'always' | 每次调用进入当前配置的审批流程(可能由模型审核) |
'human' | 每次调用由人工确认;不复用授权,不接受模型审核或主人身份跳过;无人工渠道时拒绝 |
'once' | 同一会话中第一次调用 |
'on-risk'(默认) | 有风险时。未知的插件操作会确认;bash、文件和网络工具在安全策略已经验证具体命令、路径或 URL 后不再重复确认 |
'never' | 不需要确认 |
'never' 只是跳过确认步骤,不能绕过权限、网络、文件系统和 Shell 等安全策略。
声明危险工具(硬闸)
踢人、禁言、设管理员、发群公告 / 频道帖 / 评论、红包转账、改 Bot 资料这类工具,要声明 risk: 'dangerous':
export default defineAgentTool({
description: '踢出群成员',
risk: 'dangerous',
riskCategory: 'member', // member | publish | funds | identity
// ...
});危险工具走危险工具硬闸,和 requiresApproval 不是一回事:
- 每次调用都要一次新鲜的人工批准(或运营者在 Console 批准过的同参数卡片),
requiresApproval: 'never'、execApprovalMode: bypass、自动审核 Agent、主人身份都不能跳过。 - 被拒、超时或无人可问时本次调用不执行,模型收到「已拒绝、需人工审批、不要重试」;同一回合里同一工具或同类危险工具的后续调用直接拒绝。
- 核心名单里的工具(如各 adapter 的
group_kick、set_mute、send_group_notice)即使不声明也按危险处理;声明risk: 'safe'不能把它们调低。只读查询不需要声明。 - 核心名单按工具本地名匹配;场景管理通用工具的
{prefix}_{suffix}(如discord_remove_member)按至少两段的后缀匹配。改他人群名片(set_group_card、{prefix}_set_nickname)归member;改 Bot 自身昵称的工具请用set_self_nickname/set_bot_nickname之类的本地名,或声明riskCategory: 'identity'。 - 控制面处于
emergency_stop或read_only时,危险工具不弹审批、直接拒绝;muted不拦。
根据配置决定是否提供工具
需要按配置开关或已有资源决定时,在 plugin.ts 的 setup 里注册:
// plugins/utils/lottery/plugin.ts(节选)
import { definePlugin } from 'zhin.js';
import { defineAgentTool } from '@zhin.js/tool';
export default definePlugin({
name: 'lottery',
async setup(context) {
if (!context.config.get().agentToolsEnabled) return; // 配置关闭时不提供
context.addTool('lottery_sync', defineAgentTool({
description: 'Synchronize lottery draws',
requiresApproval: 'always',
inputSchema: { type: 'object', properties: {} },
execute: async (_input, toolContext) => {
const database = toolContext.use(lotteryDatabaseToken);
return database.sync();
},
}));
},
});addTool 与 tools/ 目录的效果完全相同,热重载时会自动替换,不需要手动注销。
在工具里使用插件服务
execute 的第二个参数可以取用插件提供的服务和当前上下文:
| 字段 | 说明 |
|---|---|
config | 本插件配置 |
has(token) | 检查可选的插件服务或 Host token 是否可用(在调用 use(token) 前使用) |
use(token) | 取用插件提供的服务 |
origin / principal | 本次调用来自哪个会话、代表哪个用户 |
selfProfile | 当前平台账号的 Bot 自身信息(昵称、头像、平台角色等) |
locale | 当前会话的语言区域(如 'zh-CN'、'en') |
signal | 当前回合或 generation 的 AbortSignal |
policy | 当前生效的安全策略 |
question | 向用户提问的接口(见下一节) |
$client | 按适配器推断的平台客户端 |
请始终通过这个参数取用依赖,而不是在执行时查找其他插件。这样工具在热重载期间也能使用正确版本的资源。
内部 effects 证据接口
Host 还可能提供可选的内部 effects 证据接口。它不授予权限,也不能替代审批;只有实际动作 owner 能按真实提交结果上报,普通返回文本不能充当提交凭据。工具不能依赖该字段存在,旧 Host 缺失时仍应正常执行;未提供可靠证据的调用不会被当作可安全重放。普通插件与脚手架无需为此修改。
config 默认是 unknown。要给它加类型,用 defineAgentTool.withConfig<WeatherConfig>() 先固定配置类型,再配合 Schema.object() 自动推断输入类型:
import { Schema, databaseHostToken } from 'zhin.js';
import { defineAgentTool } from '@zhin.js/tool';
export default defineAgentTool.withConfig<WeatherConfig>()({
description: '查询天气',
inputSchema: Schema.object({ city: Schema.string().required() }),
async execute({ city }, { config, has, use }) {
const db = has(databaseHostToken) ? use(databaseHostToken) : undefined;
return fetchWeather(city, config.apiKey, db); // city: string,config: WeatherConfig
},
});defineAgentTool<Input, Result, Config>({...}) 的写法同样保留兼容。
在工具里向用户提问
需要用户补充信息时使用 context.question。定时任务等无人值守的场景不会提供它,所以必须处理它不存在的情况。框架内置的 ask_user 工具也基于同一机制,会按会话和用户身份匹配回复。
只在特定场景下提供的工具
工具放在哪个目录,决定了模型什么时候能看到它:
| 位置 | 模型何时能看到 |
|---|---|
tools/<name>/index.ts | 插件启用后,可被发现 |
skills/<skill>/tools/<name>/index.ts | 该 Skill 被加载后 |
agents/<agent>/tools/<name>/index.ts | 选中该 Agent 后 |
agents/<agent>/skills/<skill>/tools/<name>/index.ts | 选中该 Agent 且加载其 Skill 后 |
根目录 tools/ 只放通用、高频、不需要额外说明的工具。只在某个平台、工作流或角色下才有意义的工具,应放进对应的 Skill 或 Agent,避免平时占用提示词。所有工具都会在启动时校验,即使暂时不提供给模型。
让工具常驻
插件工具默认延迟加载:模型只在系统提示的工具索引里看到名称和一句说明,要用时通过 discover / load_tool 拿到参数定义(直接调用也会自动加载)。几乎每轮对话都要用的工具,可以声明 alwaysLoaded: true,让它的参数定义每轮都直接发给模型:
export default defineAgentTool({
description: '把文本翻译成指定语言',
alwaysLoaded: true,
execute: async (input) => { /* ... */ },
})常驻集合要尽量小
每个常驻工具的参数定义每次调用模型都会发送,会占用上下文、增加 token 费用,也会让模型在更多工具之间犹豫。只给真正高频的一两个工具开启;其余保持默认,由模型按需加载。
规则:
- 不写或写
false都是延迟加载,已有插件的行为不变。 - 这个开关只决定“是否常驻”,不放宽准入:
platforms、scopes、permissions不满足时工具仍然不可见,hidden或放在 Skill / Agent 目录下的私有工具也不会因此常驻。 - 每轮最多 8 个插件工具能通过这个开关常驻,超出的按工具顺序保持延迟加载(仍可
discover),日志会提示一次。 - 运维方说了算:
ai.agent.deferredTools.alwaysLoadedMode: replace时,常驻集合完全由alwaysLoadedTools决定,插件声明被忽略;也可以把工具名写进alwaysLoadedTools让它常驻,不受 8 个的限制。 - 只在某个平台提供的工具(设置了
platforms)通过准入后本来就常驻,不需要再声明。
给 AI 补充业务背景
用 Prompt Section 告诉 AI 产品术语、回答规范或工具使用约束:
// prompt-sections/project-rules/index.ts
import { defineAgentPromptSection } from '@zhin.js/prompt-section';
export default defineAgentPromptSection({
title: 'Project rules',
content: 'Answer with repository-local terminology and cite changed files.',
layer: 'context',
order: 70,
retention: 'preferred',
maxChars: 1000,
profiles: ['interactive'],
});目录名就是这段背景的名字,不需要手写 id。保存后,新的对话使用新内容;已经开始的对话继续使用原来的版本。
order决定同一层中的先后顺序;retention决定提示词预算不够时先舍弃谁。字段说明见 参考。- 在 Console 的能力目录里可以看到 Prompt Sections 是否生效(来源、版本、预算策略),但不会显示正文,因为其中可能包含内部策略。可运行示例:
examples/full-bot/prompt-sections/custom/index.ts。 - Prompt Section 只影响模型的上下文,不授予任何工具、数据或审批权限。
手动声明 Prompt Section 能力
如果没有使用 npx zhin setup --ai,需要在 package.json 里同时声明依赖和 Feature:
{
"dependencies": {
"@zhin.js/prompt-section": "latest"
},
"zhin": {
"features": [
{ "package": "@zhin.js/prompt-section", "api": "^1.0.0" }
]
}
}用提示词段(Prompt Segment)精确放置内容
Prompt Section 只能按「层」排序。需要明确"这段属于系统规则、运行环境、平台指引还是会话场景"时,在 plugin.ts 里通过 promptSegmentToken 提供段(ADR 0041):
// plugin.ts
import { definePlugin } from 'zhin.js';
import { promptSegmentToken } from '@zhin.js/agent';
export default definePlugin({
name: 'billing-hints',
setup(context) {
context.resources.provide(promptSegmentToken, {
id: 'billing-hints:platform', // 全局唯一,冲突即报错
kind: 'platform', // system → environment → role_rules → platform → scene
weight: 10, // 同类内权重大者在前
retention: 'preferred', // 预算不够时可被裁剪;required 永不裁剪
inject: () => '# Billing\nQuote amounts in the user\'s currency.',
});
},
});- 文本段(
system/environment/role_rules/platform/scene)拼成系统提示词,类型之间的次序固定,inject返回null即跳过。前四类在同一会话里必须字节稳定:不要放当前时间,时间由每条消息头携带;会随轮次变化的内容放scene(它排在最后,缓存断点之前的部分不受影响)。 - 一个插件可以提供数组;段随该插件所在的 generation 释放,热重载后旧段自动消失。
- 非 Agent 所有者的插件提供的文本段会被标注为
external_content,不能冒充所有者规则或授予权限。 - 消息类段(
user/tool/assistant/state)由mergeMessageSegments按时间稳定归并,规则见 ADR 0041 §3。
用 Skill 封装一套流程
Skill 是 skills/<name>/SKILL.md:一份带 YAML frontmatter 的说明书,平时只向模型展示摘要,需要时才加载全文,并解锁它目录下的专用工具。可以直接复制下面的示例作为模板:
<!-- skills/code-review/SKILL.md -->
---
name: code-review
description: 审查拉取请求或代码变更,输出结构化的问题清单与修复建议
keywords:
- review
- pr
- 代码审查
tools:
- fetch_diff
- post_review_comment
---
# 代码审查流程
1. 先调用 `fetch_diff` 获取目标变更的文件列表与 diff。
2. 按「正确性 → 安全边界 → 可维护性」顺序逐项检查。
3. 发现明确缺陷时,用 `post_review_comment` 指出文件路径、行号与修复建议。一个 Skill 如果包含多个可以独立触发的任务,应拆成多个 Skill,避免加载一个简单任务时带出所有工具。适配器提供的 Skill 放在 agents/<platform>/skills/<name>/,只在对应平台的对话中出现。
平台与场景的分类披露细则(以 ICQQ 为例)
先分类,再披露:适配器负责自己的 Skill 说明、Tool 描述和平台消息段提示;框架根据当前可信入站身份筛选。用 platforms 指定平台,scopes 指定会话类型,按用途划分 Skill,permissions 继续控制权限。不要只在文字中写“仅群聊”。
例如 ICQQ 的点赞放在通用 icqq-interaction,戳一戳和签到放在 icqq-group-interaction;后者的 SKILL.md frontmatter 和每个私有 Tool 都声明 scopes: ['group']。私聊不会获得这个 Skill 的目录、正文或工具,之前加载过也不会绕过本轮筛选。未声明 scopes 表示所有场景,不代表拥有所有权限。
绑定当前会话的工具还可声明 currentConversationBotRoles: ['owner', 'admin'],由可信入站提供 Bot 平台角色。它与发起人的 permissions 同时满足才披露,角色未知时该工具不可发现或加载。角色条件只适用于当前会话:如果工具参数可以选择其他群,须在执行时检查目标群角色,不能用当前群角色代替。入站筛选也不能替代执行前的实时平台检查。
群管理只在群聊披露,服务器/频道管理使用适配器实际归一化后的场景:Slack 的频道是 group,Discord、KOOK、QQ 频道是 channel。创建新群、账号查询、用户资料等通用能力独立保留;混合场景的 Skill 应拆分。
定义子 Agent
子 Agent 是 agents/<name>/ 目录,有自己的职责、边界和工具。它必须包含四个文件:
| 文件 | 内容 |
|---|---|
agent.json | 名称、模型、额外的公共工具、协作声明 collaboration 等 |
system.md | 角色与职责 |
boundaries.md | 不能做什么 |
conventions.md | 工作约定,只能在插件根目录 AGENTS.md 的基础上延伸,不能冲突 |
一个最小可用的 agents/researcher/ 目录与 agent.json 示例:
agents/researcher/
├── agent.json
├── system.md # "# Researcher\n负责检索资料并整理带来源引用的摘要。"
├── boundaries.md # "- 不得直接修改外部业务数据。\n- 未经证实的结论必须标明待核实。"
├── conventions.md # "- 优先引用仓库内文档与权威 API 响应。"
└── tools/
└── search-docs/index.ts// agents/researcher/agent.json
{
"name": "researcher",
"description": "负责检索技术文档并汇总证据链的研究助手",
"tools": ["weather"],
"collaboration": {
"contacts": ["reviewer"],
"accepts_from": ["planner"]
}
}collaboration.contacts 是它可以联系的 Agent 名(不写则不能联系任何人);accepts_from 限定只接受哪些 Agent 的委派(不写则接受任何被允许联系它的 Agent)。被委派时它的权限只会比委派方更小。协作工具与护栏见 AI Agent · 多 Agent 协作。
可选的 workflows/、tools/、skills/、hooks/、knowledge/ 放场景流程、私有工具、私有 Skill、钩子和知识库。反复出现的错误应该写进 conventions.md。
平台适配器的 Agent 会在对应平台的消息到来时自动选中;同一平台有多个自动候选时,路由会报冲突,需要合并职责或由用户显式选择。完整格式见 @zhin.js/agent-feature。
推荐的目录组织
my-plugin/
├── AGENTS.md # 主 Agent 的规则
├── tools/
│ └── short-url/
│ ├── index.ts # 只声明工具
│ └── client.ts # 实现放在同一目录
├── skills/short-url/
│ ├── SKILL.md
│ ├── tools/normalize/index.ts # Skill 专用工具
│ └── hooks/audit/index.ts
├── hooks/audit/index.ts
└── agents/reviewer/
├── agent.json
├── system.md
├── boundaries.md
├── conventions.md
├── workflows/
├── tools/check-result/index.ts # Agent 专用工具
├── skills/review/
│ ├── SKILL.md
│ ├── tools/check-result/index.ts
│ └── hooks/audit/index.ts
├── hooks/audit/index.ts
└── knowledge/- 工具的
index.ts只负责声明,实现放在同一目录;同一 Skill 的多个工具共用的代码放在 Skill 根目录(例如skills/github-account/handlers.ts)。 - 不要从叶子目录用
../../../../src/...引用插件内部实现。确实需要共享的运行时契约,应做成稳定的包 API,或由所属 Skill / Agent 的一个桥接模块集中引入。
参考
Tool 字段
| 字段 | 必填 | 说明 |
|---|---|---|
description | 是 | 给模型看的功能描述 |
inputSchema | 否 | Schema.object(...)(推荐,来自 zhin.js)、Zod 4 对象,或根节点为 object 的 JSON Schema;执行前校验并填充默认值 |
requiresApproval | 否 | 'never' | 'on-risk' | 'once' | 'always' | 'human',默认 'on-risk' |
risk | 否 | 'dangerous' | 'safe';dangerous 走危险工具硬闸,见声明危险工具 |
riskCategory | 否 | 'member' | 'publish' | 'funds' | 'identity',显示在审批卡片上;funds 默认禁用 |
platforms | 否 | 限定平台,如 ['icqq'];为空表示全部 |
scopes | 否 | 限定场景 'private' | 'group' | 'channel';为空表示全部 |
currentConversationBotRoles | 否 | Bot 在当前会话必须具备的平台角色;未知或不匹配时不提供工具 |
permissions | 否 | 发起人的权限规则列表,全部满足才提供 |
hidden | 否 | 注册但不提供给模型,仍可按名字调用 |
alwaysLoaded | 否 | 设为 true 时参数定义每轮常驻给模型;默认 false(延迟加载),见让工具常驻 |
readOnly | 否 | 设为 true 表示工具只读(不改平台、存储,也不影响别人);受限回合(如直播弹幕回合,ADR 0038 §5)只开放声明了 readOnly: true 的工具。不声明视为会写 |
execute(input, context) | 是 | 执行函数,context 见上文 |
权限规则
permissions 由 @zhin.js/permission 统一解析:adapter(name)、group(id,...)、private(id,...)、channel(id,...)、user(id,...)、role(master|trusted|user),以及由适配器判断的平台身份 platform(adapter,perm)(例如群主 / 管理员)。列表中的规则全部满足才通过;一条规则括号内的逗号表示满足其一。无法识别的规则一律拒绝。
Prompt Section 字段
| 字段 | 含义 |
|---|---|
layer | 语义分层:role / task / context / safety 等 |
order | 同一层中,数字越大越靠前 |
retention | 预算不足时:required 不可丢弃(放不下时本轮明确失败,而不是悄悄截断);preferred 优先保留;opportunistic 最先让出 |
maxChars | 这一段的字符上限 |
profiles | interactive(对话)、schedule(定时任务)或两者 |
platforms | 可选,只在指定平台的对话中注入,例如 ['github'] |
scopes | 可选,只在指定 IM 场景注入:private、group、channel,可组合;声明时不能为空 |
总预算由 ai.agent.systemPromptMaxChars 控制。
工具发现配置
工具不会全部塞进提示词。ai.agent.deferredTools(ZhinAgentConfig)控制按需加载:
| 键 | 默认 | 说明 |
|---|---|---|
maxLoadedPerSession | 12 | 每个会话最多同时加载的工具数 |
discoverTopK | 5 | discover 每次返回的条数 |
alwaysLoadedTools | 按场景分档,见按需加载工具 | 始终对模型可见的工具;配置值追加到当前场景档 |
alwaysLoadedMode | 'extend' | 设为 'replace' 时 alwaysLoadedTools 完全替换默认列表 |
mcpServers | {} | 按 MCP 服务器覆盖 alwaysLoaded 名单 |
它是怎么工作的
- 准入。 每轮对话开始时,框架按消息的平台、场景和发送者筛选工具;不满足条件的工具对模型不可见。
- 按需加载。 默认只向模型暴露
alwaysLoadedTools、通过准入的平台专属工具,以及声明了alwaysLoaded: true的插件工具。模型通过discover搜索工具和 Skill(返回名称与简介),用load_tool加载工具的参数定义,用load_skill加载 Skill 全文并解锁其工具。已加载的状态按会话保存,超过上限会逐出。并发的对话和子任务彼此隔离。Anthropic 通道会把未加载的工具以deferLoading标记下发,其他通道只下发已加载的工具。 - 审批与执行。 通过准入后、执行前判断
requiresApproval;执行时绑定工具所属插件的配置和资源。
实现细节
- 两种注册方式写入同一张能力表,由唯一的
ToolIndex发布,不存在第二个动态注册表。addTool()在新版本准备阶段写入,准备失败时从不可见,切换成功后随整个版本一起生效。 - Plugin Runtime 通过
CapabilityIngress按当前回合的可信origin.platform、origin.scope与权限主体筛选 Tool、Skill。提示段也按平台与场景筛选;带 IM 限定的能力不会因非 IM 请求缺少场景而放行。 - 工具的模型可见名(
qualifiedName):根插件的工具保持本地名;子插件的工具由插件路径段与目录名以__连接,例如maps__get-weather。执行时仍绑定原插件的固定版本上下文。 - 按需加载由每轮独占的 deferred controller 管理(
packages/im/agent/src/tool-catalog/deferred-turn-controller.ts),状态保存在DeferredToolSessionSnapshot,不以 IMMessage作为状态键。 ask_user是框架提供的能力,通过当前回合的QuestionPort请求输入;无人值守的回合(如定时任务)不注入该端口,也不会回退到全局消息、适配器或用户队列。- Agent / Skill 私有工具在启动准备阶段统一校验,但不会提前进入模型工具目录。
下一步
- AI 总览:配置模型、触发规则、记忆与 MCP。
- Agent 深入:安全策略、审批、子任务与执行日志。
- AI Agent 学习路径:从 IM Bot 一步步加上 Agent。