工具与 schema
怎样设计模型好用的工具。内容包括命名、描述、用 zod(内置插件)或 JSON Schema(MCP)定义输入、如实声明效果、只返回数据,以及处理用户可以补救的状态。
工具(tool)是插件的 API。模型调用它,你自己的页面也调用它。好的工具很小,只做一件事;它的描述写清楚什么时候该用,返回的数据模型拿来就能用。
本页介绍怎样用代码编写工具,包括内置插件(zod)和远程插件(JSON Schema)。插件文档里的工具写法见声明式插件,本页关于描述、效果和返回结果的建议同样适用。工具背后的概念见工具与效果。
命名
一个工具有几个名字,分别给不同的读者看:
| 名字 | 规则 | 示例 |
|---|---|---|
| 工具名 | ASCII 小驼峰的动词或名词,插件内唯一 | cardBalance、search、reserveSeat |
| 工具 id | <plugin>.<name>(自动生成) | wallet.cardBalance |
| 模型名 | <plugin>__<name>(自动生成) | wallet__cardBalance |
title | 给用户看的简短中文名称 | 校园卡余额 |
App 每次请求都会把所有启用的工具发给模型,所以相似的工具越多,每次请求消耗的 token 就越多。相关的读取操作应该合并成一个带筛选参数的工具,不要写十个几乎一样的工具。
给用户看的文字
用户会在聊天、授权确认和设置页里看到工具的 title。title 用中文写,英文版写在 i18n.en.tools.<name>.title。
插件文档还可以给工具写一个 summary:一行不超过 80 个字符的说明,用平实的话讲清楚工具做什么。安装审核页和插件市场的插件页会把它显示在工具名下方。英文版写在 i18n.en.tools.<name>.summary;缺少英文版时,App 不会给使用英文界面的用户显示这一行。内置插件没有 summary 字段。
下一节介绍的 description 从不显示给用户。
描述
description 是写给模型看的,中文或英文都可以。它应该写清楚三件事:
- 什么时候用这个工具,用同学们自己会说的话来写。例如:「Use when the user asks which canteen is least crowded」。
- 返回什么:有哪些字段,单位是什么(元、分钟、北京时间),数据有多新。
- 限制和后续步骤:分页大小、必须遵守的调用顺序(「call
treehole__sendCodefirst」),以及遇到verification_required时该怎么办。
不要在描述里重复输入 schema。每个字段的说明请用 .describe() 写,因为模型会在字段旁边看到这些说明。
如果描述文字取决于设备,description 可以写成一个接收宿主服务的函数:
description: (services) => `Show a native UI in the chat… Components:\n${services.genui.describeCatalog()}`,输入
内置插件使用 zod 4。模型看到的是由 zod schema 生成的 JSON Schema。无论调用来自助手、页面还是定时任务,App 都会在 execute 运行之前校验输入,所以 execute 可以放心使用它。
input: z.object({
from: z.string().trim().max(20).optional().describe('boarding place, e.g. 燕园 / 新燕园'),
at: z.string().optional().describe('Beijing time "YYYY-MM-DD HH:mm", default now'),
count: z.number().int().min(1).max(10).optional().describe('departures per direction, default 3'),
}),- 每个数字和字符串都要设上下限(
.min、.max),默认值写在描述里。 - 取值范围固定的集合用枚举,例如
z.enum(['weekday', 'holiday'])。 - 需要模型填写时间时,让它写本地时间字符串
"YYYY-MM-DD HH:mm"。SDK 的scheduleInputSchema和parseLocal在处理日程时已经采用这种格式。 - 远程插件:设备会用
z.fromJSONSchema把 MCP 的inputSchema(即 JSON Schema)转换成 zod。请只用 JSON Schema 的基础特性,以保证转换能成功。
效果
每个工具只能有一种效果(effect)。效果告诉 App 这个工具做的是哪一类事情,用户会按效果分别决定助手能否不经询问直接执行(见权限)。
effect: 'read', // 读取:余额、列表、搜索
effect: 'write', // 修改:添加待办、关注帖子、预约房间
effect: 'money', // 花钱:创建支付订单、充值、把校园卡切换为电子卡
effect: 'public', // 公开:在树洞发帖或回复有些工具还要加上另外两个标记。返回第三方文本(比如帖子或通知)的工具要设置 untrusted: true。调用这样的工具后,助手这一轮剩下的操作都会被标记为「受污染」(tainted),这一轮里会长期保存的写入要等用户确认(见安全)。只能由用户发起的操作,比如批准或解锁某样东西,要设置 userOnly: true。这样工具对模型不可见,来自助手或定时任务的调用都会被拒绝。很少用到的工具可以设置 onDemand: true:对话开始时不提供给模型,助手用 plugins.use 加载插件后才提供,这样每次请求都更小。
返回结果
execute: async ({ name, sort = 'least' }, ctx) => {
const { time, canteens } = await fetchCrowd(ctx.signal);
return { time, canteens: canteens.filter((c) => !name || c.name.includes(name)), source: CROWD_SOURCE };
},结果会交回给模型,所以要按模型方便阅读的方式组织:
- 返回 JSON 对象,字段名和单位要一看就懂。
- 需要时注明数据来源和时效(
source、time)。 - 返回模型可以传给下一个工具的 id。
- 把中止信号
ctx.signal传给网络请求,这样调用被取消时请求能尽快停止。 - 失败时抛出
Error,附上用户能看懂的简短中文信息,例如throw new Error('校园卡登录已失效')。助手会把它转告给用户。
有些情况是预料之中的,需要用户介入,比如短信验证。这时请返回一个状态对象,而不是抛出异常。助手就能向用户索要所需的信息,再重新调用工具:
export type VerificationRequired = { status: 'verification_required'; kind: 'login'; methods: ['sms']; message: string };
// 在 execute 里:
if (needsSms) {
return {
status: 'verification_required',
kind: 'login',
methods: ['sms'],
message: '树洞需要短信验证。先调用 treehole__sendCode 发送验证码,再带上 code 重试。',
} satisfies VerificationRequired;
}上下文与说明
插件可以给模型提供两种额外的文字。
instructions 写在系统提示词里,每次请求都会发送。它适合写简短的规则:什么问题用哪个工具,以及「以预约系统为准」之类的注意事项。
context({ query, kv, services }) 在每轮对话之前运行,返回和当前消息相关的事实,比如用户记忆的快照。它的结果会被标记为数据,函数必须在 800 ms 内返回。没有相关内容时返回 undefined。
两者每次请求都会消耗 token,也都会改变提示词的开头部分,降低模型服务商复用缓存的机会。所以要写得短,而且内容要稳定。不要写入日期和时间,App 每轮都会自动加上。
在界面里调用工具
页面和小组件也会调用工具。invoke 以用户身份运行工具,host.useToolData 通过 App 的共享缓存读取工具结果:
// 在页面或小组件里(actor: 'user',不弹授权确认)
await invoke('focus.start', { label: '高数' });
// 通过宿主的共享缓存实时读取
const data = host.useToolData<{ minutes: number }>('focus.today', {}, { ttlMinutes: 1 });检查清单
- 每个工具只负责一件事,读取和修改分成不同的工具。
- 每个工具都有中文
title、i18n.en.tools里的英文标题,以及写给模型的description。 - 每个输入字段都有范围限制和说明。
-
effect如实声明;需要untrusted或userOnly的地方都设置了。 - 返回的是数据:体积小,带单位,不含密钥,也不含指令。
- 错误信息简短、用户能看懂;可以补救的状态用状态对象表示。