第 2 卷
3{ 4"name": "greet",
| 5 | "description":"Greet someone by name.", |
|---|---|
| 6 | "parameters": { |
| 7 | "type":"object", |
| 8 | "properties": { |
| 9 | "name": {"type":"string","description":"The name to greet"} |
| 10 | }, |
| 11 | "required": ["name"] |
| 12 | } |
| 13 | } |
| 14 ] | |
| 15 ---调用greet --- | |
| 16 { isError:false, content: [ {type:'text', text:'Hello, World!'} ], value:'H | |
| 17 ---卸载后再看清单--- | |
| 18 [] |
调用用 root.tools.execute(),参数是一个对象而不是 (name, args):
复制代码 1await root.tools.execute({
| 2 | callId:CallId('call-1'), | //调用标识,用于追踪 |
|---|---|---|
| 3 | name:'greet', | |
| 4 | arguments: { name:'World'}, | |
| 5 | signal: newAbortController().signal, | //取消信号,生产环境由agent loop传入 |
| 6 | }) |
callId 和 signal 都是必填。真实项目里它们由 Agent Loop 提供,这里我手动构造只是为了跑通。
顺带提一句,execute 还有第二个参数 exec,携带 signal(取消信号)、token(身份标识)、agent(当前 Agent 句柄)等运行时上下文——greet 用不到,但写真实工具时你
会靠它拿到取消信号。
模型眼中的工具
清单里只有三个字段:name、description、parameters。这就是模型能看到的全部。
description 值得我们多花点心思,它是模型判断“该不该用、什么时候用”的唯一依据。这里有几条实用经验,分享给你:
说清干什么,也说清不干什么。"Read a file from the filesystem" 不如 "Read a file from the filesystem. Use for reading, not writing; use grep to search across files."
参数描述同样重要。description: 'The name to greet' 能防止模型往里塞 JSON 字符串。
别把实现细节写进去。模型只需要知道“能读文件”,不需要知道你底层用的是本地磁盘还是云存储。
你在工具里写的其他一切,模型都看不见:output、execute、timeoutMs、 isConcurrencySafe……注册表用显式白名单把它们挡在外面。这不是疏漏,而是设计:
只给模型“能干什么、怎么调用”,不给它“怎么实现”,它就不会被实现细节带偏。
register 只是把工具登记进注册表,真正让模型“看见”的,是系统提示词组装。还记得我们刚刚讲要先挂 SystemPrompt 吗?ToolRuntime 依赖的 systemPrompt 服务,会在每
一轮开场时调用 schemas(),把每个工具的 name/description/parameters 拼进发给模型的那段话里。也就是说,你对模型认知边界的控制权,就藏在这三个字段里。
工具的返回值
output 是必填的,不写注册时就抛 TypeError。它由两部分组成:
复制代码
顺便说一句,上面的例子用的是字符串,简单。但如果返回值是一个对象(本讲后面的 env_probe 就返回 { platform, node }),schema 就得写成 type: 'object',这
时有个容易踩的坑:每个显式 object 节点都必须声明 additionalProperties: true | false,缺了注册时就抛 JsonSchemaError(错误码 UNSUPPORTED_SCHEMA)。
复制代码 1schema: {
2type: 'object',
| 3 | additionalProperties: false, | //少这一行会注册失败 |
|---|---|---|
| 4 | properties: { platform: { type:'string'}, node: { type:'string'} }, | |
| 5 | } |
为什么要同时声明 schema 和 render?下一讲我们会提到,模型写的程序要 await tools.<name>(args)</name>,然后用代码处理返回值——程序要的是结构化字段
(value),模型要的是一段排版好的文本(content)。schema 声明前者,render 产出后者,各取所需。如果只返回一个字符串,程序就没法按字段取用。
顺带记住一条硬规矩:execute 返回的是规范 JSON 值,不是内容块,把它翻成模型能看的文本,是 render 的活。注意,调用到这里还没结束:这个值之后还要被校验、可能被改
写、最终才呈现(这条完整流水线,下一讲展开)。所以你的工具只负责执行,执行完给谁看、要不要改,都交给框架。
最后看清 render 的返回类型:它是一个内容块数组,[{ type: 'text', text }] 里的 text 只是其中一种,还可以是图片、工具调用块等。所以 render 返回数组而不是单个
字符串,是因为模型收到的消息本来就是一堆块的序列,文本只是最朴素的一种。
seam:让能力可替换
现在麻烦来了:假设你要提供一个“读环境变量”的能力,但生产、测试要不同的实现。也就是