第 6 章 · Tools 类型化工具
本章目标:掌握
defineTool()四要素定义与useTool()挂载,理解工具调用的完整生命周期与错误语义,写出模型爱用、用得对的工具。
6.1 工具是什么、不是什么
工具是你写的函数,把"模型可能需要做的事"暴露给它:查订单、建工单、退款。关键分工:
- 模型决定何时调用——它读工具的名字、描述、参数模式后自主决策;
- 你的代码决定如何执行——run 函数里是你 100% 可控的应用逻辑。
与技能(第 7 章)的区别:技能教"流程知识",工具给"执行能力";与沙箱的区别:沙箱给文件与 shell 环境,工具连的是你的应用系统。
6.2 defineTool 四要素
// src/tools/lookup-order.ts
import { defineTool } from '@flue/runtime';
import * as v from 'valibot';
import { orders } from '../shared/orders.ts';
export const lookupOrder = defineTool({
// ① name:模型调用时使用的名字(全局唯一,snake_case 惯例)
name: 'lookup_order',
// ② description:模型唯一的"说明书"——写清做什么、何时用、返回什么
description: '按订单号查询订单当前状态。当用户询问订单进度或配送时间时使用。',
// ③ input:Valibot 顶层对象 schema,模型参数先过校验再进 run
input: v.object({
orderId: v.string(),
}),
// ④ run:你的执行代码。data 是校验后的强类型参数
async run({ data }) {
const order = await orders.get(data.orderId);
// 结果信封:{ output } 而不是裸值
return { output: { status: order.status, eta: order.eta } };
},
});// src/agents/order-assistant.ts —— 挂载工具
'use agent';
import { useModel, useTool } from '@flue/runtime';
import { lookupOrder } from '../tools/lookup-order.ts';
export function OrderAssistant() {
useModel('anthropic/claude-haiku-4-5');
useTool(lookupOrder); // 一行挂载,可挂多个
return '帮助客户查询订单状态与预计送达时间。';
}defineTool() 会校验定义并冻结返回,天然适合放在 src/tools/ 目录跨 agent 共享;一次性工具也可以把定义对象直接内联写进 useTool({...})。
6.3 一次工具调用的完整旅程
模型看到 name+description+input(JSON Schema)
│ 模型决定调用,产出参数
▼
Flue 用 input schema 校验参数
│ 校验失败 → 错误回传给模型(run 不执行),模型自行修正重试
▼
run({ data, signal, log, toolCallId }) 执行
│ throw → 变成模型可见的错误结果,agent 不崩溃
▼
{ output } 序列化后回传模型,继续推理三个必须理解的语义:
// 语义一:输出信封。output 必须可 JSON 序列化;
// 裸字符串是 { output: <string> } 的简写
const simple = defineTool({
name: 'ping',
description: '连通性测试。',
async run() {
return 'pong'; // 简写形式,等价 { output: 'pong' }
},
});
// 语义二:terminate。结束当前轮次(同 finish/give_up 契约)
const submitReport = defineTool({
name: 'submit_report',
description: '提交最终审计报告并结束本轮工作。',
input: v.object({ markdown: v.string() }),
async run({ data }) {
await reports.save(data.markdown);
return { output: '报告已提交', terminate: true };
},
});
// 语义三:throw 不崩溃。错误是模型可见的反馈
const risky = defineTool({
name: 'deploy_service',
description: '部署指定服务到生产环境。',
input: v.object({ service: v.string() }),
async run({ data }) {
if (!allowedServices.has(data.service)) {
// 模型会看到这条消息,并尝试换一种方式或告知用户
throw new Error(`服务 ${data.service} 不在白名单内,禁止部署`);
}
return { output: await doDeploy(data.service) };
},
});别吞错误
模型只能对"它看得见的失败"做出反应。捕获异常后返回模糊成功, 模型会误以为操作成功了——这比直接 throw 更危险。
6.4 run 的额外上下文与输出校验
除 data 外,run 还能拿到:
const searchCode = defineTool({
name: 'search_code',
description: '在仓库中搜索代码片段。',
input: v.object({ query: v.string() }),
// 可选:声明 output schema,让返回值也被校验与强类型化
output: v.object({ matches: v.number(), files: v.array(v.string()) }),
async run({ data, signal, log, toolCallId }) {
// signal:中止信号。传给你的异步操作,取消时及时停止
const res = await github.search(data.query, { signal });
// log:进度日志。流式进入会话事件,模型看不到
log.info(`搜索 "${data.query}" 命中 ${res.total_count} 处`);
// toolCallId:本次调用的唯一 ID,用于关联外部副作用
metrics.track('code_search', { callId: toolCallId });
return { output: { matches: res.total_count, files: res.items.map(i => i.path) } };
},
});命名冲突规则:同一次渲染中工具名必须唯一,且不能占用框架保留名(task、activate_skill、read_skill_resource),否则装配工具集时直接抛错。
带沙箱的 agent 还自动获得内置工具:read、write、edit、bash、grep、glob——没有沙箱就没有这些工具,模型无法调用不存在的工具。
6.5 工具设计最佳实践
// ❌ 反面教材:描述模糊、参数宽泛、无错误路径
const badTool = defineTool({
name: 'do_stuff',
description: '处理数据。',
input: v.object({ payload: v.any() }),
async run() { /* ... */ return { output: 'ok' }; },
});
// ✅ 好工具:描述含触发时机、参数精确、错误信息可行动
const refundOrder = defineTool({
name: 'refund_order',
description:
'为订单发起退款。仅当用户明确要求退款且订单状态为 delivered 时使用。'
+ '返回退款流水号;订单不满足条件时返回拒绝原因。',
input: v.object({
orderId: v.describe(v.string(), '订单号,形如 ORD-2024-xxxx'),
reason: v.picklist(['damaged', 'not_received', 'changed_mind']),
}),
async run({ data }) {
const order = await orders.get(data.orderId);
if (order.status !== 'delivered') {
throw new Error(`订单状态为 ${order.status},仅 delivered 订单可退款`);
}
const refund = await payments.refund(order, data.reason);
return { output: { refundId: refund.id, amount: refund.amount } };
},
});描述含触发时机("仅当…时使用")、参数用 picklist 收窄、错误信息告诉模型下一步怎么办——这三点直接决定模型调用工具的准确率。
本章小结
- 工具 = 模型决策 + 你的代码执行;四要素:name / description / input / run;
- 参数先过 Valibot 校验再进 run;输出走
{ output, terminate? }信封; - throw 变成模型可见的错误反馈,agent 不会崩溃,但不要吞错误;
- run 还能拿到 signal / log / toolCallId;output schema 可选校验返回值;
- 好描述 = 能力 + 触发时机,这是模型正确调用工具的关键。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. defineTool 定义中,模型唯一能"看到"并据此决策的部分不包括?
2. run 函数收到的参数校验失败时会发生什么?
3. 工具 run 中抛出异常的后果是?
4. 下列哪个自定义工具命名会导致装配时报错?
🛠️ 动手实践
- 为一个公开 API(如天气、汇率)封装一个工具,描述里写清触发时机,测试模型调用的准确率。
- 给工具加上
outputschema,故意返回错误类型,观察运行时的校验行为。 - 写一个带白名单校验的工具,用不合法参数触发 throw,确认模型能理解错误并换路。
Agent 会用工具了。下一章学习用 Skills 把领域知识打包成可复用资产。