12.4 结果缓存机制

缓存的必要性

搜索 API 调用通常有以下特点:

  • 成本:每次调用消耗 API 配额
  • 延迟:网络请求需要 2-10 秒
  • 重复性:相同查询可能多次出现

缓存的好处

方面 无缓存 有缓存
API 调用次数 每次都调用 重复查询复用
响应速度 2-10 秒 < 100ms
API 成本 按次数计费 节省 50-80%
用户体验 等待时间长 即时响应

适用场景

适合缓存:

  • ✅ 事实性查询(”React 作者是谁”)
  • ✅ 技术文档查询(”TypeScript 泛型”)
  • ✅ 历史信息查询(”2024 年奥运会”)

不适合缓存:

  • ❌ 实时价格查询(”比特币当前价格”)
  • ❌ 最新新闻查询(”今天头条”)
  • ❌ 时间敏感查询(”现在天气”)

SearchCache 类设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/tools/builtin/search-cache.ts

import type { CacheEntry, CacheStats, TavilyResponse } from '../../search/types.js';

export class SearchCache {
private cache: Map<string, CacheEntry>;
private defaultTTL: number;
private hits: number = 0;
private misses: number = 0;

constructor(defaultTTL: number = 3600) {
this.cache = new Map();
this.defaultTTL = defaultTTL; // 默认 1 小时
}
}

缓存条目结构

1
2
3
4
5
6
7
8
9
10
export interface CacheEntry {
/** 搜索查询 */
query: string;
/** 搜索结果 */
results: TavilyResponse;
/** 时间戳(毫秒) */
timestamp: number;
/** 生存时间(秒) */
ttl: number;
}

核心方法实现

生成缓存键

1
2
3
4
5
6
7
/**
* 生成缓存键
*/
private generateKey(query: string): string {
// 查询标准化:转小写,去除首尾空格
return query.toLowerCase().trim();
}

标准化示例:

1
2
3
"React 19 新特性""react 19 新特性"
" react 19 ""react 19"
"REACT 19 FEATURES""react 19 features"

获取缓存

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
/**
* 获取缓存结果
*/
get(query: string): TavilyResponse | null {
const key = this.generateKey(query);
const entry = this.cache.get(key);

// 缓存不存在
if (!entry) {
this.misses++;
return null;
}

// 计算缓存年龄
const now = Date.now();
const age = (now - entry.timestamp) / 1000; // 转换为秒

// 检查是否过期
if (age > entry.ttl) {
this.cache.delete(key);
this.misses++;
console.log(`[缓存] 查询过期: "${query}" (age: ${Math.floor(age)}s)`);
return null;
}

// 缓存命中
this.hits++;
const remaining = Math.floor(entry.ttl - age);
console.log(`[缓存] 命中: "${query}" (剩余 ${remaining}s)`);
return entry.results;
}

设置缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 设置缓存
*/
set(query: string, results: TavilyResponse, ttl?: number): void {
const key = this.generateKey(query);
const entryTTL = ttl || this.defaultTTL;

const entry: CacheEntry = {
query,
results,
timestamp: Date.now(),
ttl: entryTTL,
};

this.cache.set(key, entry);
console.log(`[缓存] 保存: "${query}" (ttl: ${entryTTL}s)`);
}

删除缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 删除指定查询的缓存
*/
delete(query: string): boolean {
const key = this.generateKey(query);
const deleted = this.cache.delete(key);
if (deleted) {
console.log(`[缓存] 删除: "${query}"`);
}
return deleted;
}

/**
* 清除所有缓存
*/
clear(): void {
const size = this.cache.size;
this.cache.clear();
this.hits = 0;
this.misses = 0;
console.log(`[缓存] 已清除所有缓存 (${size} 条)`);
}

缓存统计

统计信息结构

1
2
3
4
5
6
7
8
9
10
export interface CacheStats {
/** 缓存大小 */
size: number;
/** 缓存的查询列表 */
queries: string[];
/** 命中次数 */
hits: number;
/** 未命中次数 */
misses: number;
}

获取统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 获取缓存统计信息
*/
getStats(): CacheStats {
const queries = Array.from(this.cache.keys());
const total = this.hits + this.misses;
const hitRate = total > 0 ? (this.hits / total) * 100 : 0;

return {
size: this.cache.size,
queries,
hits: this.hits,
misses: this.misses,
};
}

打印统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 打印缓存统计信息
*/
printStats(): void {
const stats = this.getStats();
const total = stats.hits + stats.misses;
const hitRate = total > 0 ? ((stats.hits / total) * 100).toFixed(1) : '0.0';

console.log('\n[缓存统计]');
console.log(` 大小: ${stats.size} 条`);
console.log(` 命中: ${stats.hits} 次`);
console.log(` 未命中: ${stats.misses} 次`);
console.log(` 命中率: ${hitRate}%`);

if (stats.queries.length > 0) {
console.log(` 查询列表:`);
stats.queries.forEach((q, i) => {
const entry = this.cache.get(q)!;
const age = ((Date.now() - entry.timestamp) / 1000).toFixed(0);
console.log(` ${i + 1}. "${q}" (${age}s 前)`);
});
}
console.log('');
}

输出示例:

1
2
3
4
5
6
7
8
9
[缓存统计]
大小: 3 条
命中: 5 次
未命中: 3 次
命中率: 62.5%
查询列表:
1. "react 19 新特性" (234s 前)
2. "typescript 泛型" (456s 前)
3. "北京天气" (123s 前)

TTL 管理

TTL 选择策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 根据查询类型选择 TTL
function getTTLForQuery(query: string): number {
const lowerQuery = query.toLowerCase();

// 实时信息:短 TTL (5 分钟)
if (lowerQuery.includes('价格') || lowerQuery.includes('天气')) {
return 300;
}

// 新闻资讯:中等 TTL (30 分钟)
if (lowerQuery.includes('新闻') || lowerQuery.includes('最新')) {
return 1800;
}

// 技术文档:长 TTL (24 小时)
if (lowerQuery.includes('文档') || lowerQuery.includes('api')) {
return 86400;
}

// 默认:1 小时
return 3600;
}

过期清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 清理过期的缓存条目
*/
cleanup(): number {
const now = Date.now();
let cleaned = 0;

for (const [key, entry] of this.cache.entries()) {
const age = (now - entry.timestamp) / 1000;
if (age > entry.ttl) {
this.cache.delete(key);
cleaned++;
}
}

if (cleaned > 0) {
console.log(`[缓存] 清理了 ${cleaned} 条过期缓存`);
}

return cleaned;
}

自动清理策略

1
2
3
4
5
6
7
8
9
10
// 定时清理(可选)
setInterval(() => {
cache.cleanup();
}, 60000); // 每分钟清理一次

// 或在设置新缓存时触发清理
cache.set(query, results, ttl);
if (cache.size() > 100) { // 缓存过大时清理
cache.cleanup();
}

全局缓存实例

使用单例模式提供全局缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 全局缓存实例
*/
let globalCache: SearchCache | null = null;

/**
* 获取全局缓存实例
*/
export function getGlobalCache(ttl?: number): SearchCache {
if (!globalCache) {
globalCache = new SearchCache(ttl);
}
return globalCache;
}

/**
* 重置全局缓存
*/
export function resetGlobalCache(): void {
if (globalCache) {
globalCache.clear();
}
globalCache = null;
}

在工具中使用

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
// src/tools/builtin/search.ts

export class SearchTool implements IToolPlugin {
private cache: SearchCache;

constructor(apiKey?: string, useCache: boolean = true, cacheTTL: number = 3600) {
// 使用全局缓存
this.cache = getGlobalCache(cacheTTL);
}

async execute(params: SearchToolParams): Promise<ToolResult> {
// 检查缓存
const cached = this.cache.get(params.query);
if (cached) {
return { success: true, output: this.formatResults(cached) };
}

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

// 保存到缓存
this.cache.set(params.query, result);

// ...
}
}

缓存效果示例

场景 1:重复查询

1
2
3
4
5
6
7
8
9
10
用户: React 19 有什么新特性?
[搜索工具] 查询: "React 19 有什么新特性"
[Tavily] 搜索完成,找到 5 个结果
[缓存] 保存: "react 19 有什么新特性" (ttl: 3600s)
Agent: React 19 引入了...

用户(5分钟后): React 19 的新特性有哪些?
[搜索工具] 查询: "React 19 的新特性有哪些"
[缓存] 命中: "react 19 的新特性有哪些" (剩余 3300s)
Agent: React 19 引入了...

场景 2:缓存统计

1
2
3
4
5
6
7
8
9
10
用户: 查看缓存统计
[缓存统计]
大小: 8 条
命中: 15 次
未命中: 8 次
命中率: 65.2%
查询列表:
1. "react 19 新特性" (1234s 前)
2. "typescript 泛型" (567s 前)
...

小结

本节介绍了搜索结果缓存机制的实现:

  • 缓存的必要性和适用场景
  • SearchCache 类设计
  • 核心方法实现(get/set/delete)
  • TTL 管理策略
  • 缓存统计和监控
  • 全局缓存实例

导航

上一篇: 12.3 搜索工具实现

下一篇: 12.5 搜索场景与最佳实践