使用策略
在 NestJS 中集成大模型(LLM)API 的 Cache 机制,重点不在于缓存 API 的最终响应结果(因为大模型回答通常需要多样性),而是通过管理 Prompt 的结构与生命周期,最大化利用各厂商(OpenAI、Claude、Gemini)的 Prompt/Context Caching 降低成本。同时,针对通用业务数据与特定 LLM 场景,可以建立分层缓存策略。
一、 架构与策略机制设计
在 NestJS 中,推荐采用 两层缓存策略:
- 应用级缓存(NestJS
@nestjs/cache-manager+ Redis): 缓存高频、耗时且确定性强的静态数据(如已提取的 PDF 解析文本、固定工具定义、知识库 Embeddings)。 - 厂商级 Prompt 缓存(Prompt Prefix Optimization): 通过拦截器或服务层优化请求结构,精准触发 OpenAI / Claude / Gemini 的服务端 Prompt Caching。
二、 哪些内容写入 Cache?(判断标准)
遵循 “静态在前,动态在后” 和 “高成本、高重复” 的原则判断:
| 内容类型 | 是否写入/触发 Cache | 原因与策略 |
|---|---|---|
| System Prompt & Role 定义 | 推荐 | 文本量大且高度固定,放置于消息数组最前端,触发厂商 Prompt Cache。 |
| Agent Tools (Function Calling) | 推荐 | Schema 往往长达几千 Token 且很少变化,作为静态 Block 优先标记缓存。 |
| 参考文档 / 知识库 RAG 上下文 | 推荐 | 占 Token 比例最高(>10k Token)。可在本地 Redis 缓存解析后的 Raw Text,并在请求 API 时显式标注 Cache。 |
| 用户历史对话记录 | 选择性 | 仅缓存公共的对话前缀。若追加了新消息,前面未变的部分仍可继续命中 Prompt Cache。 |
| 模型最终生成结果 (Completion) | 不推荐(除非 temperature=0) | 大模型的回答具有随机性,直接缓存 Response 会破坏生成的灵活性,仅适用于固定 FAQ 类场景。 |
三、 什么时候写入、更新与删除?
1. 什么时候写入(Write Strategy)
- 应用级 Redis 缓存: 首次从数据库或文件服务读取长文档/知识库时延迟写入(Cache-Aside),或在知识库上传/更新时主动预热(Write-Through / Warm-up)。
- 大模型 Prompt 缓存: 在构造发往大模型 API 的请求体时即时构建。确保将可缓存的内容打包在请求体的前半部分(满足最小 1,024 Token 门槛)。
2. 什么时候更新或删除(Invalidation Strategy)
- 被动失效(TTL 机制): 设置合理的自然过期时间(例如 Redis TTL 设为 24 小时,匹配知识库更新频率)。
- 主动清理(Event-Driven): 当关联业务逻辑触发更新时(如用户修改了 Agent 规则、上传了新版本文档),通过 NestJS
EventEmitter2发布事件,主动清理 Redis 中的相关缓存键。 - Prompt Cache 自动失效: 厂商侧的 Prompt Caching 会在 5~10 分钟无请求后自动清理,无需手动干预。你只需要保证重新请求时 Prompt 前缀完全一致,就会重新触发写入。
四、 NestJS 代码落地最佳实践
利用 NestJS 的 Interceptor(拦截器) 或 Service 规范请求逻辑。以下展示如何在 NestJS 中封装一个适配多模型 Cache 规则的 AI 服务:
1. 自动化 Prompt 前缀规范化服务
// ai-prompt-builder.service.ts
import { Injectable } from '@nestjs/common';
export interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
@Injectable()
export class AiPromptBuilderService {
/**
* 规范化构建 Prompt,确保前缀绝对一致以命中 OpenAI / Claude 缓存
*/
buildOptimizedPayload(
provider: 'openai' | 'claude',
systemPrompt: string,
ragContext: string,
messages: ChatMessage[],
) {
// 1. 拼接绝对静态的内容作为前缀 (必须超过 1024 tokens)
const staticPrefix = `${systemPrompt}\n\n=== 参考知识库 ===\n${ragContext}`;
if (provider === 'claude') {
// Claude: 显式注入 cache_control
return {
system: [
{
type: 'text',
text: staticPrefix,
cache_control: { type: 'ephemeral' }, // 触发 Claude 显式缓存
},
],
messages: messages.map((m) => ({ role: m.role, content: m.content })),
};
}
// OpenAI: 保持前缀靠前,隐式自动匹配
return {
messages: [
{ role: 'system', content: staticPrefix },
...messages.map((m) => ({ role: m.role, content: m.content })),
],
};
}
}
2. 结合 CacheManager 管理本地/Redis 文本缓存
// rag-knowledge.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
@Injectable()
export class RagKnowledgeService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async getDocumentContext(docId: string): Promise<string> {
const cacheKey = `rag:doc:${docId}`;
// 1. 读取应用缓存
const cachedDoc = await this.cacheManager.get<string>(cacheKey);
if (cachedDoc) {
return cachedDoc;
}
// 2. 缓存未命中,从 DB/存储服务读取并解析
const rawDocText = await this.fetchAndParseFromDB(docId);
// 3. 写入缓存,设置 TTL 12小时
await this.cacheManager.set(cacheKey, rawDocText, 43200 * 1000);
return rawDocText;
}
// 当文档更新时,主动清理缓存
async updateDocument(docId: string, newContent: string) {
await this.saveToDB(docId, newContent);
// 删除缓存,下次请求重新加载
await this.cacheManager.del(`rag:doc:${docId}`);
}
private async fetchAndParseFromDB(docId: string): Promise<string> {
// 模拟 DB 读取
return `文档 ${docId} 的长文本内容...`;
}
private async saveToDB(docId: string, content: string) {}
}
五、 核心避坑指南
- 绝对禁止在前缀中夹杂动态变量: 比如
当前请求时间:${new Date().toISOString()}或当前用户ID:${userId}。这会导致每次请求的前缀哈希值改变,Prompt Caching 完全失效。动态变量必须放在messages的末尾。 - 工具列表 (Tools) 字段必须升序序列化: 如果你的 Agent 会动态加载 Tools,请先对 Tools 数组按名称排序再传入 API,防止顺序颠倒破坏缓存。
- 注意 Token 计数临界值: 确保缓存的前缀内容长度大于 1,024 Tokens(Claude / OpenAI 的硬性要求)。若低于此门槛,添加
cache_control会被忽略或报错。
对比
在多模型 API 开发中,使用 Prompt / Context Caching(提示词缓存) 可以显著降低大模型 API 的成本(最高节省 80%~90%)并减少首字响应延迟(TTFT)。
由于 OpenAI、Anthropic (Claude) 和 Google (Gemini) 三大厂商的 Caching 实现机制不同,使用策略也有所区别:
1. 三大厂商的 Cache 机制对比
| 厂商 | 模型支持 | 触发机制 | 最小 Token 门槛 | 缓存生命周期 (TTL) | 最佳使用场景 |
|---|---|---|---|---|---|
| OpenAI (GPT) | GPT-4o, GPT-4o-mini 等 | 全自动(Implicit) | 1,024 Tokens | 自动刷新(通常 5-10 分钟无请求后过期) | 动态多轮对话、重复长 System Prompt |
| Anthropic (Claude) | Claude 3.5 / 3 系列 | 显式标注(Explicit) | 1,024 Tokens(Sonnet)/ 2,048 Tokens(Haiku) | 5 分钟(每次命中刷新,可手动指定) | 固定大型文档/代码库问答、Agent Tool 定义 |
| Google (Gemini) | Gemini 1.5 Pro / Flash 等 | 显式/隐式均支持 | 32,768 Tokens(或 2,048 Tokens 依模型而异) | 手动创建管理(TTL 可自定义,按小时计费) | 超长视频、音频分析,极大型知识库问答 |
2. 多模型统一架构设计最佳实践
如果你在搭建统一的多模型 API 网关(或使用 LiteLLM / One-API 等工具),建议遵循以下原则以提高 Cache 命中率:
- 严格保持“前缀不变”原则(Prefix Consistency):
- 前固定,后动态:将
System Instructions、Tool Definitions、Base Knowledge永远放在 Prompt 的最顶部。 - 消除动态干扰:切勿在 System Prompt 中放置动态时间戳(如
当前时间是 2026-03-29...)或用户唯一的 Request ID。任何前缀字符变动都会导致整个 Cache 彻底失效。
- 字典与 JSON 键排序(Deterministic Serialization):
- 传递
tools或结构化 JSON 时,务必进行固定的 Key 排序(如json.dumps(obj, sort_keys=True)),防止序列化顺序不同破坏缓存匹配。
- 统一路由适配层(Adapter Pattern):
- 设计抽象层:如果是 Claude 请求,自动在前置大型上下文的最后一个 Block 上追加
cache_control;如果是 OpenAI/Gemini,只需规整好消息的前缀格式即可。
- 注意 Token 阈值过滤:
- 只有当单次固定的 Prompt 超过厂商的最小 Token 限制(OpenAI/Claude 需 $\ge 1024$ Tokens)时,才触发或设置 Cache。对于极短的提示词,开启 Cache 反而可能增加写入开销或无效。