Chapter 04

Tools:Zod schema 的类型安全工具调用

Agent 没有 tool 就是一只嘴皮子——能说不能做。Tool 把 API、数据库、文件系统、RAG、子 Agent 等外部能力注入给 Agent,Zod 做 schema,运行时参数/返回值全类型安全。

Tool 的最小形态

import { createTool } from '@mastra/core/tools';
import { z } from 'zod';

export const getWeatherTool = createTool({
  id: 'get-weather',
  description: '查询指定城市当前天气',
  inputSchema: z.object({
    city: z.string().describe('城市中文名,如"北京"'),
  }),
  outputSchema: z.object({
    temp: z.number(),
    condition: z.string(),
  }),
  execute: async ({ context }) => {
    const res = await fetch(`https://api.weather.com?q=${context.city}`);
    const data = await res.json();
    return { temp: data.temp, condition: data.condition };
  },
});

execute 函数的入参

context
Zod 校验过的 input,类型自动从 inputSchema 推导,编辑器里 context. 有智能提示。
runtimeContext
Agent 调用时传入的上下文容器——放 userId、traceId、tenantId 等,贯穿整个 tool 链。
mastra
Mastra 主实例。在 tool 里可以 mastra.getAgent('xxx') 调用其他 Agent(嵌套 Agent 模式)。
abortSignal
上层传下来的取消信号,用来给 fetch 加 signal,避免 Agent 超时后 tool 仍在跑。

Zod:schema 就是文档

Zod 的 .describe() 会作为参数说明喂给模型,写得越清楚模型调用越准:

inputSchema: z.object({
  query: z.string().describe('搜索关键词,不要含时间,时间由 from/to 指定'),
  from: z.string().datetime().describe('ISO8601 时间,搜索起点'),
  to: z.string().datetime().describe('ISO8601 时间,搜索终点'),
  topK: z.number().int().min(1).max(50).default(10),
  tag: z.enum(['news', 'blog', 'paper']).optional(),
}),
技巧:描述要给"时机"
与其写「查询数据库」,不如写「当用户问及具体订单号时调用此工具」。时机性描述能显著减少模型"该调不调"或"不该调乱调"的毛病。

错误处理

export const payTool = createTool({
  id: 'pay',
  description: '发起支付',
  inputSchema: z.object({ amount: z.number().positive() }),
  execute: async ({ context, abortSignal }) => {
    try {
      const res = await fetch('/pay', {
        method: 'POST',
        body: JSON.stringify({ amount: context.amount }),
        signal: abortSignal,
      });
      if (!res.ok) {
        // 业务错误要回给 LLM 让它决策,不要 throw 原始 HTTP 错误
        return { success: false, error: `支付失败:${res.status}` };
      }
      return { success: true, txId: (await res.json()).id };
    } catch (e) {
      return { success: false, error: (e as Error).message };
    }
  },
});
不要让 tool throw
throw 会中断整个 Agent 执行链。把错误包装成结构化返回值交给 LLM,它通常会道歉、重试或转告用户——这是好用的 Agent 该有的韧性。

调用外部 API(带 secrets)

export const githubSearchTool = createTool({
  id: 'github-search',
  description: '搜 GitHub 仓库',
  inputSchema: z.object({ q: z.string() }),
  execute: async ({ context }) => {
    const res = await fetch(
      `https://api.github.com/search/repositories?q=${context.q}`,
      {
        headers: {
          Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
          'User-Agent': 'mastra-agent',
        },
      }
    );
    const data = await res.json();
    return data.items.slice(0, 5).map(r => ({
      name: r.full_name,
      url: r.html_url,
      stars: r.stargazers_count,
    }));
  },
});

查数据库(Drizzle / Prisma)

import { db } from './db';
import { orders } from './schema';
import { eq } from 'drizzle-orm';

export const getOrderTool = createTool({
  id: 'get-order',
  description: '通过订单号查订单详情',
  inputSchema: z.object({
    orderId: z.string().regex(/^ORD\d{8}$/),
  }),
  execute: async ({ context, runtimeContext }) => {
    const userId = runtimeContext.get('userId');
    const [row] = await db.select().from(orders)
      .where(eq(orders.id, context.orderId));

    if (!row) return { found: false };
    if (row.userId !== userId) {
      return { found: false, error: '非本人订单' };
    }
    return { found: true, order: row };
  },
});
安全:永远用 runtimeContext 检查权限
LLM 可以被越权提示注入"查所有订单"。Tool 内部必须拿 userId 做过滤,把权限逻辑写在工具里,不要指望 LLM 自觉。

嵌套 Agent:tool 里再调 Agent

export const translateTool = createTool({
  id: 'translate',
  description: '把英文段落翻译成中文',
  inputSchema: z.object({ text: z.string() }),
  execute: async ({ context, mastra }) => {
    const translator = mastra.getAgent('translator');
    const { text } = await translator.generate(context.text);
    return { translated: text };
  },
});

主 Agent 把翻译任务委托给专门的翻译 Agent。这是多 Agent 协作的最简模式——比手写 orchestrator 简单,适合小规模场景。大规模用 Workflow(下一章)。

Tool 的三种挂载方式

// 1. 静态:Agent 固定可用一批 tools
new Agent({ ..., tools: { getWeatherTool, getOrderTool } })

// 2. 动态:根据运行时上下文决定可见 tools
new Agent({
  ...,
  tools: ({ runtimeContext }) => {
    const tier = runtimeContext.get('tier');
    return tier === 'pro'
      ? { getWeatherTool, payTool, exportTool }
      : { getWeatherTool };
  },
});

// 3. 调用时临时加
await agent.generate(input, {
  toolsets: { admin: { banUserTool } },  // 仅本次可用
});

max steps 与循环防护

await agent.generate(input, {
  maxSteps: 5,    // 最多 5 轮 tool 调用,超出强制停
});

默认 5。复杂任务可调到 10-20。但 maxSteps 过大意味着模型容易在 tool 里"神游"——应同时增强 instructions 约束。

并发工具调用

模型(Claude、GPT-4o)支持在一个 turn 里并发返回多个 tool call。Mastra 会并发 Promise.all 执行,tool 里只要不共享状态就是安全的。

// 同一 turn 模型可能同时调:
//   get-weather(city: "北京")
//   get-weather(city: "上海")
//   get-weather(city: "深圳")
// 三个 fetch 并行,延迟 = max 而不是 sum

Playground 里观察 tool call

发消息后展开 Trace 面板,会看到:

本章小结