Skip to main content

使用策略​

在 NestJS 中集成大模型(LLM)API 的 Cache 机制,重点不在于缓存 API 的最终响应结果(因为大模型回答通常需要多样性),而是通过管理 Prompt 的结构与生命周期,最大化利用各厂商(OpenAI、Claude、Gemini)的 Prompt/Context Caching 降低成本。同时,针对通用业务数据与特定 LLM 场景,可以建立分层缓存策略。


一、 架构与策略机制设计​

在 NestJS 中,推荐采用 两层缓存策略:

  1. 应用级缓存(NestJS @nestjs/cache-manager + Redis): 缓存高频、耗时且确定性强的静态数据(如已提取的 PDF 解析文本、固定工具定义、知识库 Embeddings)。
  2. 厂商级 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) {}
}


五、 核心避坑指南​

  1. 绝对禁止在前缀中夹杂动态变量: 比如 当前请求时间:${new Date().toISOString()} 或 当前用户ID:${userId}。这会导致每次请求的前缀哈希值改变,Prompt Caching 完全失效。动态变量必须放在 messages 的末尾。
  2. 工具列表 (Tools) 字段必须升序序列化: 如果你的 Agent 会动态加载 Tools,请先对 Tools 数组按名称排序再传入 API,防止顺序颠倒破坏缓存。
  3. 注意 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 命中率:

  1. 严格保持“前缀不变”原则(Prefix Consistency):
  • 前固定,后动态:将 System Instructions、Tool Definitions、Base Knowledge 永远放在 Prompt 的最顶部。
  • 消除动态干扰:切勿在 System Prompt 中放置动态时间戳(如 当前时间是 2026-03-29...)或用户唯一的 Request ID。任何前缀字符变动都会导致整个 Cache 彻底失效。
  1. 字典与 JSON 键排序(Deterministic Serialization):
  • 传递 tools 或结构化 JSON 时,务必进行固定的 Key 排序(如 json.dumps(obj, sort_keys=True)),防止序列化顺序不同破坏缓存匹配。
  1. 统一路由适配层(Adapter Pattern):
  • 设计抽象层:如果是 Claude 请求,自动在前置大型上下文的最后一个 Block 上追加 cache_control;如果是 OpenAI/Gemini,只需规整好消息的前缀格式即可。
  1. 注意 Token 阈值过滤:
  • 只有当单次固定的 Prompt 超过厂商的最小 Token 限制(OpenAI/Claude 需 $\ge 1024$ Tokens)时,才触发或设置 Cache。对于极短的提示词,开启 Cache 反而可能增加写入开销或无效。