Flue 框架从入门到精通
Part 2 · 核心能力

§05 工具(Tools):给 Agent 一双会干活的手

defineTool 的四部分,以及工具不是鉴权边界

📑 本章目录

05.1 模型只会说话,工具让它动手

我第一次给 Agent 接工具时,激动得不行。因为那一刻它从"只会聊"变成"真能干活"。

模型本身只能"想"和"说"。查数据库、发短信、改文件,它一样都干不了。

工具就是它的手。这是 Flue 里最关键的一步——把你的业务能力交给 Agent。

05.2 一个工具的四部分

我用 defineTool 定义工具。它有四个零件:

// src/shared/order-tools.ts
import { Type, defineTool } from '@flue/runtime';

export const lookupOrderStatus = defineTool({
  name: 'lookup_order_status',   // 1. 模型调用的名字
  description: 'Look up order fulfillment status.', // 2. 帮模型判断何时用
  parameters: Type.Object({      // 3. 输入参数(用 Type,非 valibot)
    orderId: Type.String({ description: 'Order ID like order_1234' }),
  }),
  execute: async ({ orderId }) => {  // 4. 真正干活的代码
    // 这行是关键:返回字符串给模型
    return `Order ${orderId} is packed.`;
  },
});

四部分拆开看:名字、说明、参数、执行体。

注意execute 必须返回字符串。

返回对象会出错。要返回结构化数据,用 JSON.stringify(...) 包一层。

05.3 工具不是鉴权边界

这点特别重要。模型的输入不可信,但凭证必须由你的代码给。

// 正确:客户 id 由代码定,模型只能选订单号
export default createAgent(({ id: customerId }) => ({
  model: 'anthropic/claude-haiku-4-5',
  tools: [
    defineTool({
      name: 'lookup_customer_order',
      parameters: Type.Object({ orderId: Type.String() }),
      execute: async ({ orderId }) => {
        // 模型选订单号,但查哪个客户由代码定
        return await orders.getStatus(customerId, String(orderId));
      },
    }),
  ],
}));

模型能选订单号,但选不了客户。凭证和越权防护,永远握在你代码手里。

标叔的经验:工具里别放密钥。

我见过有人把 API Key 写进工具参数描述,想让模型自己带。危险。密钥在 env 里,代码注入,模型只传业务参数。

05.4 接 MCP 生态

你不用自己写所有工具。MCP 服务器能提供远程工具:

const inventory = await connectMcpServer('inventory', {
  url: env.INVENTORY_MCP_URL,
  headers: { Authorization: `Bearer ${env.INVENTORY_MCP_TOKEN}` },
});
// 连上后,工具名会变成 mcp__inventory__lookup_item
const harness = await init(agent, { tools: inventory.tools });
方式 适合 标叔的结论
自己写 defineTool 业务逻辑、内部 API 最灵活,首选
接 MCP 服务器 现成生态工具 省事,但多一跳网络

工具讲透了。下一章讲"技能"和"子智能体",让 Agent 既专业又会分工。