自定义 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 即使应该使用工具也不使用。
解决方案:
- 改进工具描述使其更具体
- 使输入 schema 描述更清晰
- 在描述中提供示例
- 检查工具名称是否直观
// 之前
tool({
description: "获取数据",
// ...
});
// 之后
tool({
description: "获取任何城市的当前天气数据。示例:'巴黎的天气怎么样?' 返回温度、状况和湿度。",
// ...
});工具执行错误
问题: 工具在执行期间抛出错误。
解决方案:
- 添加全面的错误处理
- 在 Zod schema 之外验证输入
- 添加日志以便调试
- 返回有意义的错误消息
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。
解决方案:
- 使用
.optional()使可选字段真正可选 - 使用
.default()提供合理的默认值 - 为所有字段添加清晰的描述
- 用各种输入测试 schema