ProductReadyProductReady

自定义 Agent Tools

创建和使用自定义工具,为 AI agents 提供函数调用能力

自定义 Agent Tools

自定义工具通过函数调用扩展 AI agents 的能力。ProductReady 中的所有 agent providers 都支持 AI SDK 工具接口,实现无缝集成。


概述

工具允许 AI agents:

  • 执行函数 - 在对话过程中调用自定义代码
  • 访问外部数据 - 查询 API、数据库或服务
  • 执行操作 - 修改数据、发送消息或触发工作流
  • 返回结构化结果 - 向 agent 提供类型化的响应

工具接口

所有工具都遵循 AI SDK 标准接口:

import { tool } from "ai";
import { z } from "zod";

const myTool = tool({
  description: "清晰描述此工具的功能",
  inputSchema: z.object({
    // 定义预期输入的 Zod schema
  }),
  execute: async (input) => {
    // 实现逻辑
    return result;
  },
});

创建自定义工具

基础工具示例

import { tool } from "ai";
import { z } from "zod";

const getWeatherTool = tool({
  description: "获取指定位置的当前天气",
  inputSchema: z.object({
    location: z.string().describe("城市名称或地址"),
    units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
  }),
  execute: async ({ location, units }) => {
    // 调用天气 API
    const response = await fetch(
      `https://api.weather.com/v1/current?location=${location}&units=${units}`
    );
    const data = await response.json();
    
    return {
      temperature: data.temp,
      condition: data.condition,
      humidity: data.humidity,
    };
  },
});

访问数据库的工具

import { tool } from "ai";
import { z } from "zod";
import { db } from "~/db/client";
import { users } from "~/db/schema";
import { eq } from "drizzle-orm";

const getUserInfoTool = tool({
  description: "从数据库检索用户信息",
  inputSchema: z.object({
    userId: z.string().describe("要查找的用户 ID"),
  }),
  execute: async ({ userId }) => {
    const user = await db.query.users.findFirst({
      where: eq(users.id, userId),
    });
    
    if (!user) {
      return { error: "未找到用户" };
    }
    
    return {
      name: user.name,
      email: user.email,
      createdAt: user.createdAt,
    };
  },
});

带错误处理的工具

import { tool } from "ai";
import { z } from "zod";

const calculateTool = tool({
  description: "执行数学计算",
  inputSchema: z.object({
    expression: z.string().describe("要计算的数学表达式"),
  }),
  execute: async ({ expression }) => {
    try {
      // 安全计算(生产环境使用专门的数学库)
      const result = evaluateExpression(expression);
      
      return {
        success: true,
        result,
        expression,
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : "计算失败",
        expression,
      };
    }
  },
});

在 Providers 中使用工具

AI SDK Provider 使用工具

import { AiSdkProvider } from "agentlib/providers";
import { tool } from "ai";
import { z } from "zod";

// 定义你的工具
const tools = {
  get_weather: tool({
    description: "获取位置的天气信息",
    inputSchema: z.object({
      location: z.string(),
    }),
    execute: async ({ location }) => {
      return { temperature: 72, condition: "sunny" };
    },
  }),
  
  search_web: tool({
    description: "在网络上搜索信息",
    inputSchema: z.object({
      query: z.string(),
    }),
    execute: async ({ query }) => {
      // 实现网络搜索
      return { results: [] };
    },
  }),
};

// 创建带工具的 provider
const provider = new AiSdkProvider({
  model: "gpt-4",
  apiKey: process.env.OPENAI_API_KEY,
  tools,
});

// 使用 provider
const result = await provider.stream(messages);

Gaia Agent Provider 使用工具

import { GaiaAgentProvider } from "agentlib/providers";
import { tool } from "ai";
import { z } from "zod";

const tools = {
  generate_article: tool({
    description: "生成关于特定主题的博客文章",
    inputSchema: z.object({
      title: z.string(),
      keywords: z.array(z.string()),
      minWords: z.number().default(500),
    }),
    execute: async ({ title, keywords, minWords }) => {
      // 文章生成逻辑
      return {
        title,
        content: "生成的文章内容...",
        wordCount: 750,
      };
    },
  }),
};

const provider = new GaiaAgentProvider({
  model: "gpt-4",
  apiKey: process.env.OPENAI_API_KEY,
  tools,
});

高级工具模式

集成外部 API 的工具

import { tool } from "ai";
import { z } from "zod";

const githubSearchTool = tool({
  description: "搜索 GitHub 仓库",
  inputSchema: z.object({
    query: z.string().describe("搜索查询"),
    language: z.string().optional().describe("编程语言筛选"),
    stars: z.number().optional().describe("最小星标数量"),
  }),
  execute: async ({ query, language, stars }) => {
    const params = new URLSearchParams({ q: query });
    if (language) params.append("language", language);
    if (stars) params.append("stars", `>=${stars}`);
    
    const response = await fetch(
      `https://api.github.com/search/repositories?${params}`,
      {
        headers: {
          Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
        },
      }
    );
    
    const data = await response.json();
    
    return {
      total: data.total_count,
      repositories: data.items.slice(0, 5).map((repo: any) => ({
        name: repo.full_name,
        description: repo.description,
        stars: repo.stargazers_count,
        url: repo.html_url,
      })),
    };
  },
});

文件操作工具

import { tool } from "ai";
import { z } from "zod";
import { promises as fs } from "fs";
import path from "path";

const saveFileTool = tool({
  description: "将内容保存到工作区的文件中",
  inputSchema: z.object({
    filename: z.string().describe("文件名"),
    content: z.string().describe("要写入的内容"),
    directory: z.string().default("output"),
  }),
  execute: async ({ filename, content, directory }) => {
    const dirPath = path.join(process.cwd(), directory);
    const filePath = path.join(dirPath, filename);
    
    // 确保目录存在
    await fs.mkdir(dirPath, { recursive: true });
    
    // 写入文件
    await fs.writeFile(filePath, content, "utf-8");
    
    return {
      success: true,
      path: filePath,
      size: content.length,
    };
  },
});

异步操作工具

import { tool } from "ai";
import { z } from "zod";

const processLargeDataTool = tool({
  description: "异步处理大型数据集",
  inputSchema: z.object({
    datasetId: z.string(),
    operation: z.enum(["analyze", "transform", "export"]),
  }),
  execute: async ({ datasetId, operation }) => {
    // 启动异步操作
    const jobId = await startBackgroundJob(datasetId, operation);
    
    // 轮询完成状态(简化版)
    const result = await waitForJobCompletion(jobId);
    
    return {
      jobId,
      status: "completed",
      result,
    };
  },
});

工具组织

按功能分组工具

// tools/weather.ts
export const weatherTools = {
  get_weather: tool({ /* ... */ }),
  get_forecast: tool({ /* ... */ }),
};

// tools/search.ts
export const searchTools = {
  search_web: tool({ /* ... */ }),
  search_docs: tool({ /* ... */ }),
};

// tools/index.ts
import { weatherTools } from "./weather";
import { searchTools } from "./search";

export const allTools = {
  ...weatherTools,
  ...searchTools,
};

上下文特定工具

import { GaiaAgentProvider } from "agentlib/providers";
import { weatherTools } from "./tools/weather";
import { searchTools } from "./tools/search";
import { databaseTools } from "./tools/database";

function getToolsForContext(context: string) {
  switch (context) {
    case "weather-app":
      return weatherTools;
    case "search-app":
      return searchTools;
    case "admin-panel":
      return { ...searchTools, ...databaseTools };
    default:
      return {};
  }
}

// 在 provider 中使用
const provider = new GaiaAgentProvider({
  model: "gpt-4",
  apiKey: process.env.OPENAI_API_KEY,
  tools: getToolsForContext("weather-app"),
});

最佳实践

1. 清晰的工具描述

// ✅ 好:具体、可操作的描述
const goodTool = tool({
  description: "按关键字、语言和最小星标数搜索 GitHub 仓库。返回前 5 个匹配的仓库及其元数据。",
  // ...
});

// ❌ 差:模糊的描述
const badTool = tool({
  description: "搜索东西",
  // ...
});

2. 详细的输入 Schema

// ✅ 好:文档完善的 schema,带有描述
const goodSchema = z.object({
  query: z.string().describe("搜索查询(例如:'react hooks')"),
  language: z.string().optional().describe("编程语言筛选(例如:'typescript')"),
  minStars: z.number().min(0).optional().describe("最小星标数量(默认:0)"),
});

// ❌ 差:没有描述
const badSchema = z.object({
  q: z.string(),
  lang: z.string().optional(),
  stars: z.number().optional(),
});

3. 健壮的错误处理

const robustTool = tool({
  description: "获取用户资料",
  inputSchema: z.object({
    userId: z.string(),
  }),
  execute: async ({ userId }) => {
    try {
      const user = await fetchUser(userId);
      
      if (!user) {
        return {
          success: false,
          error: "未找到用户",
          userId,
        };
      }
      
      return {
        success: true,
        user,
      };
    } catch (error) {
      console.error("工具执行错误:", error);
      return {
        success: false,
        error: error instanceof Error ? error.message : "未知错误",
      };
    }
  },
});

4. 返回结构化数据

// ✅ 好:结构化、类型化的响应
execute: async ({ location }) => {
  return {
    location,
    temperature: 72,
    unit: "fahrenheit",
    condition: "sunny",
    humidity: 45,
    timestamp: new Date().toISOString(),
  };
}

// ❌ 差:非结构化字符串
execute: async ({ location }) => {
  return `${location} 的天气是 72°F 晴天`;
}

5. 输入验证

const validatingTool = tool({
  description: "处理支付",
  inputSchema: z.object({
    amount: z.number().positive().max(10000),
    currency: z.enum(["USD", "EUR", "GBP"]),
    description: z.string().min(5).max(200),
  }),
  execute: async ({ amount, currency, description }) => {
    // Zod 自动验证输入
    // 额外的业务逻辑验证
    if (amount > 1000 && !description.includes("verified")) {
      return {
        success: false,
        error: "大额支付需要在描述中包含验证信息",
      };
    }
    
    // 处理支付
    return { success: true, transactionId: "..." };
  },
});

工具安全

清理用户输入

import { tool } from "ai";
import { z } from "zod";

const secureSearchTool = tool({
  description: "使用清理后的输入进行搜索",
  inputSchema: z.object({
    query: z.string().max(200),
  }),
  execute: async ({ query }) => {
    // 清理输入
    const sanitized = query
      .replace(/[^\w\s-]/g, "") // 移除特殊字符
      .trim()
      .toLowerCase();
    
    // 使用清理后的输入
    const results = await searchDatabase(sanitized);
    return results;
  },
});

限制文件访问

const safeFileReadTool = tool({
  description: "安全读取文件内容",
  inputSchema: z.object({
    filename: z.string(),
  }),
  execute: async ({ filename }) => {
    // 验证文件名以防止路径遍历
    if (filename.includes("..") || filename.startsWith("/")) {
      return {
        error: "无效的文件名",
      };
    }
    
    // 限制在允许的目录
    const allowedDir = path.join(process.cwd(), "data");
    const filePath = path.join(allowedDir, filename);
    
    if (!filePath.startsWith(allowedDir)) {
      return {
        error: "访问被拒绝",
      };
    }
    
    const content = await fs.readFile(filePath, "utf-8");
    return { content };
  },
});

速率限制

import { tool } from "ai";
import { z } from "zod";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const redis = new Redis({ /* config */ });
const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, "1 m"),
});

const rateLimitedTool = tool({
  description: "带速率限制的 API 调用",
  inputSchema: z.object({
    query: z.string(),
    userId: z.string(),
  }),
  execute: async ({ query, userId }) => {
    // 检查速率限制
    const { success } = await ratelimit.limit(userId);
    
    if (!success) {
      return {
        error: "超出速率限制。请稍后再试。",
      };
    }
    
    // 执行工具逻辑
    const result = await performQuery(query);
    return result;
  },
});

测试工具

单元测试

import { describe, it, expect } from "vitest";
import { getWeatherTool } from "./weather-tools";

describe("getWeatherTool", () => {
  it("应该为有效位置返回天气数据", async () => {
    const result = await getWeatherTool.execute({
      location: "San Francisco",
      units: "celsius",
    });
    
    expect(result).toHaveProperty("temperature");
    expect(result).toHaveProperty("condition");
    expect(result.temperature).toBeTypeOf("number");
  });
  
  it("应该处理无效位置", async () => {
    const result = await getWeatherTool.execute({
      location: "InvalidCity123",
      units: "celsius",
    });
    
    expect(result).toHaveProperty("error");
  });
});

集成测试

import { describe, it, expect } from "vitest";
import { GaiaAgentProvider } from "agentlib/providers";
import { myTools } from "./tools";

describe("带自定义工具的 Agent", () => {
  it("应该在对话中正确使用工具", async () => {
    const provider = new GaiaAgentProvider({
      model: "gpt-4",
      apiKey: process.env.OPENAI_API_KEY,
      tools: myTools,
    });
    
    const messages = [
      { role: "user", content: "东京的天气怎么样?" },
    ];
    
    const result = await provider.stream(messages);
    
    // 验证工具被调用
    expect(result).toBeDefined();
  });
});

故障排除

工具未被调用

问题: Agent 即使应该使用工具也不使用。

解决方案:

  1. 改进工具描述使其更具体
  2. 使输入 schema 描述更清晰
  3. 在描述中提供示例
  4. 检查工具名称是否直观
// 之前
tool({
  description: "获取数据",
  // ...
});

// 之后
tool({
  description: "获取任何城市的当前天气数据。示例:'巴黎的天气怎么样?' 返回温度、状况和湿度。",
  // ...
});

工具执行错误

问题: 工具在执行期间抛出错误。

解决方案:

  1. 添加全面的错误处理
  2. 在 Zod schema 之外验证输入
  3. 添加日志以便调试
  4. 返回有意义的错误消息
execute: async (input) => {
  try {
    // 记录输入
    console.log("工具调用参数:", input);
    
    // 验证
    if (!isValid(input)) {
      return { error: "无效输入" };
    }
    
    // 执行
    const result = await performAction(input);
    return result;
  } catch (error) {
    console.error("工具错误:", error);
    return {
      error: error instanceof Error ? error.message : "未知错误",
    };
  }
}

Schema 验证失败

问题: 输入不匹配预期的 schema。

解决方案:

  1. 使用 .optional() 使可选字段真正可选
  2. 使用 .default() 提供合理的默认值
  3. 为所有字段添加清晰的描述
  4. 用各种输入测试 schema

相关资源

下一步

On this page