12.3 搜索工具实现

SearchTool 类设计

搜索工具作为 Agent 工具系统的一部分,需要实现 IToolPlugin 接口:

1
2
3
4
5
6
7
8
9
10
// src/tools/builtin/search.ts

import {
IToolPlugin,
ToolMetadata,
ToolResult,
ToolPlugin
} from "../types.js";
import { createTavilyClient } from "../../search/tavily-client.js";
import { getGlobalCache } from "./search-cache.js";

工具元数据

使用 @ToolPlugin 装饰器定义工具元数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@ToolPlugin({
name: "search",
description: "在互联网上搜索信息,获取实时数据。适用于需要最新信息的问题,如天气、新闻、价格、技术文档等。",
category: "builtin",
parameters: {
query: {
type: "string",
description: "搜索关键词或问题",
required: true,
},
num_results: {
type: "number",
description: "返回结果数量 (默认 5,最多 10)",
required: false,
default: 5,
},
search_depth: {
type: "string",
description: "搜索深度 (basic: 快速, advanced: 深度)",
required: false,
default: "basic",
},
days: {
type: "number",
description: "搜索时间范围(天数)",
required: false,
default: 7,
},
topic: {
type: "string",
description: "搜索主题(news, finance, general)",
required: false,
default: "general",
},
},
examples: [
'search: {"query": "北京今天天气"}',
'search: {"query": "最新 AI 新闻", "num_results": 5}',
'search: {"query": "React 19 新特性", "search_depth": "advanced"}',
],
})
export class SearchTool implements IToolPlugin {
readonly metadata!: ToolMetadata;
// ...
}

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export class SearchTool implements IToolPlugin {
private tavilyClient: TavilyClient;
private cache: SearchCache;
private useCache: boolean;

/**
* 创建搜索工具实例
* @param apiKey Tavily API Key (可选,默认从环境变量读取)
* @param useCache 是否使用缓存 (默认 true)
* @param cacheTTL 缓存 TTL (秒,默认 3600)
*/
constructor(
apiKey?: string,
useCache: boolean = true,
cacheTTL: number = 3600
) {
this.tavilyClient = createTavilyClient(apiKey);
this.useCache = useCache;
this.cache = getGlobalCache(cacheTTL);
}
}

execute 方法实现

execute 方法是工具的核心,处理搜索逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
async execute(
params: SearchToolParams,
workspace: string
): Promise<ToolResult> {
try {
console.log(`\n[搜索工具] 查询: "${params.query}"`);
console.log(` 结果数: ${params.num_results || 5}`);
console.log(` 深度: ${params.search_depth || "basic"}`);

// 1. 检查缓存
if (this.useCache) {
const cachedResult = this.cache.get(params.query);
if (cachedResult) {
console.log(`[搜索工具] 使用缓存结果`);
const formatted = this.formatResults(cachedResult);
return {
success: true,
output: formatted,
};
}
}

// 2. 检查服务可用性
if (!this.tavilyClient.isAvailable()) {
return {
success: false,
output: "",
error: "搜索服务不可用,请检查 TAVILY_API_KEY 配置",
};
}

// 3. 构造搜索选项
const searchOptions: SearchOptions = {
query: params.query,
maxResults: Math.min(params.num_results || 5, 10),
searchDepth: params.search_depth || "basic",
days: params.days,
topic: params.topic,
includeAnswer: true,
includeRawContent: false,
};

// 4. 执行搜索
const result = await this.tavilyClient.search(searchOptions);

// 5. 缓存结果
if (this.useCache) {
this.cache.set(params.query, result);
}

// 6. 格式化输出
const formatted = this.formatResults(result);

console.log(`\n[搜索工具] 完成,找到 ${result.results.length} 个结果\n`);

return {
success: true,
output: formatted,
};
} catch (error) {
const errorMsg = `搜索失败: ${
error instanceof Error ? error.message : String(error)
}`;
console.error(`\n${errorMsg}\n`);
return {
success: false,
output: "",
error: errorMsg,
};
}
}

执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
execute() 被调用

打印搜索参数

┌─────────────────┐
│ 检查缓存 │
│ 命中?→ 返回结果 │
└────────┬────────┘
↓ 未命中
┌─────────────────┐
│ 检查服务可用性 │
│ 不可用?→ 返回错误│
└────────┬────────┘
↓ 可用
┌─────────────────┐
│ 构造搜索选项 │
└────────┬────────┘

┌─────────────────┐
│ 调用 Tavily API │
└────────┬────────┘

┌─────────────────┐
│ 缓存结果 │
└────────┬────────┘

┌─────────────────┐
│ 格式化输出 │
└────────┬────────┘

返回结果

结果格式化

formatResults 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 格式化搜索结果
*/
private formatResults(result: TavilyResponse): string {
let output = `🔍 搜索结果: "${result.query}"\n\n`;

// AI 摘要
if (result.answer && result.answer.trim() !== "") {
output += `💡 AI 摘要:\n${result.answer}\n\n`;
}

// 搜索结果
output += `找到 ${result.results.length} 个结果:\n\n`;

result.results.forEach((item, index) => {
output += `${index + 1}. ${item.title}\n`;
output += ` URL: ${item.url}\n`;

// 内容摘要
if (item.content) {
const snippet =
item.content.length > 150
? item.content.substring(0, 150) + "..."
: item.content;
output += ` ${snippet}\n`;
}

// 发布日期
if (item.publishedDate) {
output += ` 发布时间: ${item.publishedDate}\n`;
}

output += `\n`;
});

return output;
}

输出示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
🔍 搜索结果: "React 19 新特性"

💡 AI 摘要:
React 19 引入了多个新特性,包括服务器组件(Server Components)的稳定版本、新的 Actions API、改进的表单处理、以及更好的并发渲染支持...

找到 5 个结果:

1. React 19 发布公告
URL: https://react.dev/blog/2024/12/19/react-19
React 19 正式发布,引入了服务器组件、Actions 等...
发布时间: 2024-12-19

2. React 19 新特性详解
URL: https://dev.to/react-19-features
详细介绍 React 19 的所有新功能...

...

工具注册

搜索工具需要注册到工具注册表中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/tools/registry.ts

import { searchTool } from "./builtin/search.js";

export class ToolRegistry {
private tools = new Map<string, IToolPlugin>();

constructor() {
// 注册内置工具
this.register(new ReadTool());
this.register(new WriteTool());
this.register(new ExecTool());
this.register(searchTool); // 注册搜索工具
}

register(tool: IToolPlugin): void {
this.tools.set(tool.metadata.name, tool);
}

get(name: string): IToolPlugin | undefined {
return this.tools.get(name);
}

getAllMetadata(): ToolMetadata[] {
return Array.from(this.tools.values()).map(t => t.metadata);
}
}

LLM 函数调用

搜索工具通过 OpenAI Function Calling 被 LLM 调用:

工具定义发送给 LLM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const tools = [
{
type: "function",
function: {
name: "search",
description: "在互联网上搜索信息,获取实时数据...",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "搜索关键词或问题"
},
num_results: {
type: "number",
description: "返回结果数量"
},
search_depth: {
type: "string",
description: "搜索深度"
},
// ...
},
required: ["query"]
}
}
}
];

LLM 返回的调用

1
2
3
4
5
6
7
8
9
10
11
12
{
"role": "assistant",
"tool_calls": [
{
"id": "call_abc123",
"function": {
"name": "search",
"arguments": "{\"query\": \"React 19 新特性\", \"num_results\": 5}"
}
}
]
}

Agent 执行工具调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/agent.ts

for (const toolCall of toolCalls) {
const tool = this.toolRegistry.get(toolCall.name);
if (!tool) continue;

const params = JSON.parse(toolCall.arguments);
const result = await tool.execute(params, this.workspace);

// 将结果添加到消息历史
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: result.output
});
}

辅助方法

缓存统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 获取缓存统计信息
*/
getCacheStats() {
return this.cache.getStats();
}

/**
* 清除缓存
*/
clearCache() {
this.cache.clear();
}

/**
* 打印缓存统计
*/
printCacheStats() {
this.cache.printStats();
}

导出函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 导出工具实例创建函数
*/
export function createSearchTool(
apiKey?: string,
useCache?: boolean,
cacheTTL?: number,
): SearchTool {
return new SearchTool(apiKey, useCache, cacheTTL);
}

/**
* 默认导出工具实例
*/
export const searchTool = new SearchTool();

使用示例

在 Agent 中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// src/agent.ts

export class SimpleAgent {
async processTurn(userMessage: string): Promise<string> {
// 添加用户消息
this.messages.push({
role: "user",
content: userMessage
});

// 循环处理
while (true) {
// 调用 LLM
const response = await this.llm.chatStream(
this.systemPrompt,
this.messages,
(chunk) => this.onChunk(chunk)
);

// 处理工具调用
if (response.toolCalls.length > 0) {
for (const toolCall of response.toolCalls) {
// LLM 决定调用 search 工具
const tool = this.toolRegistry.get(toolCall.name);
const params = JSON.parse(toolCall.arguments);
const result = await tool.execute(params, "");

// 添加工具结果到历史
this.messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: result.output
});
}
// 继续循环,让 LLM 基于搜索结果生成最终回答
continue;
}

// 没有工具调用,返回最终回答
return response.content || "";
}
}
}

小结

本节介绍了搜索工具的完整实现:

  • SearchTool 类设计和元数据
  • execute 方法实现
  • 结果格式化
  • 工具注册流程
  • LLM 函数调用集成

导航

上一篇: 12.2 Tavily API 集成

下一篇: 12.4 结果缓存机制