在 LangChain4j 中实现响应缓存,核心在于利用缓存存储已经计算过的结果,避免重复的 LLM 调用,从而显著降低响应延迟和 API 成本。一个成熟的缓存策略通常结合了本地缓存(如 Caffeine) 的高效性与分布式缓存(如 Redis) 的扩展性,并可根据不同的业务场景进行多级设计。
💡 缓存实现方案
LangChain4j 本身并未提供一个开箱即用的 @Cacheable 注解,但你可以通过集成成熟的 Java 缓存库或利用模型服务商提供的特定缓存功能来实现。
方案一:集成 Caffeine 本地缓存(最通用、最常用)
这是最常见、性能最高的方案,适用于缓存键简单、缓存量可控的场景。你需要手动实现一个缓存管理器,在调用 LLM 之前先检查缓存。
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import dev.langchain4j.model.chat.ChatLanguageModel;
// … 其他导入
public class CachedChatService {
private final ChatLanguageModel model;
// 定义缓存:键为字符串(查询),值为字符串(回答)
private final Cache<String, String> cache;
public CachedChatService(ChatLanguageModel model) {
this.model = model;
// 配置 Caffeine 缓存
this.cache = Caffeine.newBuilder()
.maximumSize(10_000) // 最多缓存 1 万个条目
.expireAfterWrite(1, TimeUnit.HOURS) // 写入 1 小时后过期
.recordStats() // 开启统计信息,便于监控
.build();
}
public String chat(String userMessage) {
// 1. 查询缓存
String cachedResponse = cache.getIfPresent(userMessage);
if (cachedResponse != null) {
System.out.println("Cache hit for: " + userMessage);
return cachedResponse;
}
// 2. 缓存未命中,调用 LLM
System.out.println("Cache miss, calling LLM for: " + userMessage);
String response = model.generate(userMessage);
// 3. 将结果存入缓存
cache.put(userMessage, response);
return response;
}
// 可选:获取缓存统计信息
public CacheStats getStats() {
return cache.stats();
}
}
关键设计点:
- 缓存键设计:示例中直接使用 userMessage 作为键。在更复杂的场景(如带系统消息、带历史上下文、带参数的 AI Service)中,你需要设计一个能唯一代表一次请求的键,例如将系统消息、用户消息、温度等参数组合成一个字符串或生成一个哈希值。
- 并发控制:Caffeine.getIfPresent 是线程安全的。如果担心缓存未命中时多个相同请求同时穿透,可以使用 cache.get(key, k -> loadFromLLM(k)) 方法,它能保证对同一个键只执行一次加载函数。
方案二:利用模型服务商的提示缓存(如 AWS Bedrock)
如果你使用的是支持此功能的模型提供商(如 AWS Bedrock 的 Claude 模型),可以利用其内置的提示缓存功能。它能在 API 层面自动缓存重复的提示前缀(如超长的系统提示、文档上下文),从而降低后续请求的延迟和成本 。
import dev.langchain4j.model.bedrock.BedrockChatModel;
import dev.langchain4j.model.bedrock.BedrockChatRequestParameters;
import dev.langchain4j.model.bedrock.BedrockCachePointPlaceholder; // 或类似的枚举
BedrockChatModel model = BedrockChatModel.builder()
.modelId("anthropic.claude-3-sonnet-20240229")
.defaultRequestParameters(BedrockChatRequestParameters.builder()
// 告诉 Bedrock 在系统消息后自动放置一个缓存点
.promptCaching(BedrockCachePointPlacement.AFTER_SYSTEM)
.build())
.build();
// 之后的所有调用,只要系统消息相同,其后的用户消息都能受益于缓存
这种方式对应用代码侵入性最低,但依赖于特定的云服务商和模型。
方案三:实现持久化缓存(如 Redis)
对于需要跨应用重启、在多实例间共享的缓存数据,你需要实现一个基于 Redis 等中间件的持久化缓存。LangChain4j 也提供了与 Redis 集成的示例 。
其核心思路与 Caffeine 类似,只是缓存存储从本地内存换成了远程的 Redis。你可以自行封装一个 RedisCacheManager,在调用 LLM 前查询 Redis。
🧠 缓存策略设计原则
一个完善的缓存策略需要考虑以下核心维度:
1. 缓存分级设计 (Tiered Caching)
| L1 本地缓存 | Caffeine / Guava Cache | 极高频、极少变化的查询 | 速度最快(纳秒级),无网络开销 | 容量有限,应用重启丢失,数据不一致 |
| L2 分布式缓存 | Redis / Memcached | 需跨实例共享的缓存数据 | 容量大,支持集群和高可用,数据持久 | 有网络延迟(毫秒级),运维成本稍高 |
| L3 模型层缓存 | AWS Bedrock / 其他云厂商 | 超长提示前缀、静态上下文 | 完全无代码侵入,按实际缓存量计费 | 依赖特定云厂商,配置相对固定 |
典型流程:一个请求到来,先查 L1 本地缓存,命中则直接返回;未命中则查询 L2 分布式缓存,命中则返回并回填 L1;若仍未命中,才调用 LLM,并将结果写入 L2 和 L1。
2. 缓存键设计 (Cache Key)
缓存键必须能唯一标识一次请求。对于 AI Service,通常需要包含:
- 用户标识 (@MemoryId):不同用户的数据需要隔离 。
- 模型参数:如 temperature、topP 等,不同参数会产生不同结果。
- 请求内容:包括系统消息、用户消息、历史上下文摘要、RAG 检索到的文档等。
- 工具定义:如果 Agent 使用了工具,工具的签名也应纳入键的一部分。
3. 过期与驱逐策略 (Eviction & Expiry)
- 基于时间的过期 (TTL):为缓存数据设置一个合理的存活时间。例如,新闻类问答可设为 5 分钟,而政策法规类问答可设为 24 小时。
- 基于容量的驱逐 (Eviction):Caffeine 等本地缓存通常使用 LRU (最近最少使用) 或其变种 W-TinyLFU 算法,在缓存达到容量上限时,自动驱逐最不常用的数据。
- 基于事件的失效:当你的知识库数据更新时,应主动使相关缓存失效。例如,产品信息变更后,需要清除所有涉及该产品的问答缓存。
4. 缓存粒度选择
- 细粒度(精确匹配):只缓存完全相同的 userMessage。实现简单,命中率可能不高。
- 粗粒度(语义缓存):对于语义上相近的问题,返回相同的答案。这需要引入嵌入模型和向量数据库,在缓存查询时先对输入进行向量化,然后在缓存库中搜索最相似的已缓存问题 。这能大幅提升命中率,但实现复杂度也更高。
💎 总结
在 LangChain4j 中,实现响应缓存的核心是将 LLM 调用视为一个可缓存的计算结果。你可以:
如果你想进一步了解如何设计基于语义的缓存(即对语义相似的问题返回相同答案),或者想知道如何在 Spring Boot 中更方便地集成 Caffeine,我们可以继续探讨。






