欢迎光临
我们一直在努力

Next.js全栈AI应用的缓存策略设计:页面缓存、数据缓存与AI响应缓存的层级方案

Next.js全栈AI应用的缓存策略设计:页面缓存、数据缓存与AI响应缓存的层级方案

一、AI应用的成本双重困境:API贵+页面慢

全栈AI应用面临独特的性能-成本冲突:LLM API调用不仅慢(300ms-2s),而且贵(每1万次对话约花费5-20美元)。用户刷新页面如果每次都触发新的LLM调用,成本线性增长。但缓存AI响应又带来"返回过期数据"的风险——用户刚刚更新了日记,缓存版本显示的还是昨天的内容。

这个困境的解决之道不是全局缓存或完全不缓存,而是按数据类型分层的缓存策略:页面缓存(CDN级)、数据缓存(服务端级)和AI响应缓存(语义级),每层有不同的过期策略和失效机制。

二、三层缓存的架构设计

第一层CDN页面缓存:对公开的日记流、食谱推荐列表等非个性化页面,设置Cache-Control: public, max-age=300在CDN上缓存5分钟。第二层Redis数据缓存:对用户个人数据(个人主页、收藏列表),缓存TTL与数据更新频率对齐(60秒-300秒)。第三层AI响应语义缓存:对LLM生成的响应,通过embedding相似度判断新请求是否可以用已有的缓存响应替代——"推荐今晚的家常菜"和"今晚吃什么家常菜"语义相似度0.94,命中缓存。

三、缓存策略的关键实现

// lib/cache/ai-response-cache.ts
/** AI响应的语义缓存层

设计意图:
1. 不是简单的key-value缓存,而是基于embedding语义相似度匹配
2. 相似度阈值需调优:过高(>0.95)导致缓存命中率低,过低(<0.85)可能返回不相关响应
3. 缓存Key使用请求的规范化文本而非原始用户输入
*/
import Redis from 'ioredis';
import { OpenAI } from 'openai';

interface CacheEntry {
response: string;
input_embedding: number[];
created_at: number;
hit_count: number;
}

export class SemanticAICache {
private readonly SIMILARITY_THRESHOLD = 0.92; // 余弦相似度阈值
private readonly MAX_CACHE_AGE = 3600; // 最大缓存1小时
private readonly CACHE_PREFIX = 'ai:semantic:';

constructor(
private redis: Redis,
private openai: OpenAI,
) {}

async get(request: string): Promise<string | null> {
const normalized = this._normalize(request);
const embedding = await this._embed(normalized);

// 扫描最近的缓存条目,逐条计算相似度
const keys = await this.redis.keys(`${this.CACHE_PREFIX}*`);
if (keys.length === 0) return null;

const pipeline = this.redis.pipeline();
keys.forEach((k) => pipeline.get(k));
const results = await pipeline.exec();

let bestMatch: { key: string; similarity: number } | null = null;

for (let i = 0; i < keys.length; i++) {
const raw = results?.[i]?.[1] as string | null;
if (!raw) continue;

const entry: CacheEntry = JSON.parse(raw);
const similarity = this._cosineSimilarity(
embedding,
entry.input_embedding,
);

if (similarity > this.SIMILARITY_THRESHOLD) {
if (!bestMatch || similarity > bestMatch.similarity) {
bestMatch = { key: keys[i], similarity };
}
}
}

if (bestMatch) {
const entry: CacheEntry = JSON.parse(
(await this.redis.get(bestMatch.key))!,
);
// 增加命中计数,用于缓存淘汰策略
entry.hit_count += 1;
await this.redis.set(
bestMatch.key,
JSON.stringify(entry),
'EX',
this.MAX_CACHE_AGE,
);
return entry.response;
}

return null;
}

async set(request: string, response: string): Promise<void> {
const normalized = this._normalize(request);
const embedding = await this._embed(normalized);
const key = `${this.CACHE_PREFIX}${this._hashKey(normalized)}`;

const entry: CacheEntry = {
response,
input_embedding: embedding,
created_at: Date.now(),
hit_count: 0,
};

await this.redis.set(
key,
JSON.stringify(entry),
'EX',
this.MAX_CACHE_AGE,
);
}

private _normalize(text: string): string {
return text.trim().toLowerCase().replace(/\\s+/g, ' ');
}

private async _embed(text: string): Promise<number[]> {
const resp = await this.openai.embeddings.create({
model: 'text-embedding-3-small',
input: [text],
});
return resp.data[0].embedding;
}

private _cosineSimilarity(a: number[], b: number[]): number {
const dot = a.reduce((s, v, i) => s + v * b[i], 0);
const normA = Math.sqrt(a.reduce((s, v) => s + v * v, 0));
const normB = Math.sqrt(b.reduce((s, v) => s + v * v, 0));
return dot / (normA * normB + 1e-8);
}

private _hashKey(text: string): string {
// 简单哈希,用于快速定位存储桶
return text.slice(0, 8).replace(/[^a-z0-9]/g, '');
}
}

四、缓存失效策略的设计取舍

缓存最困难的部分不是存储,而是失效。页面缓存通过revalidatePath(数据变更时主动失效)和max-age(时间到期被动失效)双重保障。AI响应缓存的最大挑战是"用户改了偏好,旧AI响应不能再用了"——通过在用户变更关键信息(饮食偏好、日记主题)时主动清除对应的语义缓存。

缓存击穿(热门key过期导致大量请求同时打到LLM API)的防护:使用singleflight模式,对同一embedding的并发请求只调用一次LLM API,其余请求等待结果共享。

五、总结

本次三层缓存策略的核心结论:

  • CDN→Redis→语义缓存:三层从快到慢、从粗到细的缓存分层,各自覆盖不同的数据新鲜度需求。

  • AI语义缓存的核心是相似度阈值:0.92阈值在命中率和准确率之间取得平衡,过高的阈值导致缓存浪费。

  • singleflight防护缓存击穿:同一请求的并发LLM调用合并为一次,降低API成本。

  • 用户行为驱动的主动失效:数据变更时清除相关缓存,配合TTL被动过期,双重保障数据新鲜度。

  • 缓存层次按访问频率和数据大小配置:高频小数据走Redis,低频大数据走CDN,LLM响应走语义缓存。

  • 赞(0)
    未经允许不得转载:171主机测评 » Next.js全栈AI应用的缓存策略设计:页面缓存、数据缓存与AI响应缓存的层级方案
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址