Skip to content

第 6 章 · Tools 类型化工具

本章目标:掌握 defineTool() 四要素定义与 useTool() 挂载,理解工具调用的完整生命周期与错误语义,写出模型爱用、用得对的工具。

6.1 工具是什么、不是什么

工具是你写的函数,把"模型可能需要做的事"暴露给它:查订单、建工单、退款。关键分工:

  • 模型决定何时调用——它读工具的名字、描述、参数模式后自主决策;
  • 你的代码决定如何执行——run 函数里是你 100% 可控的应用逻辑。

与技能(第 7 章)的区别:技能教"流程知识",工具给"执行能力";与沙箱的区别:沙箱给文件与 shell 环境,工具连的是你的应用系统。

6.2 defineTool 四要素

typescript
// 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 } };
  },
});
typescript
// 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 一次工具调用的完整旅程

text
模型看到 name+description+input(JSON Schema)
        │ 模型决定调用,产出参数

Flue 用 input schema 校验参数
        │ 校验失败 → 错误回传给模型(run 不执行),模型自行修正重试

run({ data, signal, log, toolCallId }) 执行
        │ throw → 变成模型可见的错误结果,agent 不崩溃

{ output } 序列化后回传模型,继续推理

三个必须理解的语义:

typescript
// 语义一:输出信封。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 还能拿到:

typescript
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) } };
  },
});

命名冲突规则:同一次渲染中工具名必须唯一,且不能占用框架保留名(taskactivate_skillread_skill_resource),否则装配工具集时直接抛错。

带沙箱的 agent 还自动获得内置工具:readwriteeditbashgrepglob——没有沙箱就没有这些工具,模型无法调用不存在的工具。

6.5 工具设计最佳实践

typescript
// ❌ 反面教材:描述模糊、参数宽泛、无错误路径
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. 下列哪个自定义工具命名会导致装配时报错?

🛠️ 动手实践

  1. 为一个公开 API(如天气、汇率)封装一个工具,描述里写清触发时机,测试模型调用的准确率。
  2. 给工具加上 output schema,故意返回错误类型,观察运行时的校验行为。
  3. 写一个带白名单校验的工具,用不合法参数触发 throw,确认模型能理解错误并换路。

Agent 会用工具了。下一章学习用 Skills 把领域知识打包成可复用资产