
本文介绍使用 guava 的 loadingcache 实现按用户会话不活跃时长(如 2 分钟)自动过期并清理聊天历史,避免 token 浪费,同时保留其他用户数据不受影响。
本文介绍使用 guava 的 loadingcache 实现按用户会话不活跃时长(如 2 分钟)自动过期并清理聊天历史,避免 token 浪费,同时保留其他用户数据不受影响。
在构建基于 OpenAI 的对话式应用(如 GPT-3.5-turbo 聊天机器人)时,为每个用户维护上下文历史是提升对话连贯性的关键。但长期累积的 ChatMessage 列表会导致请求 token 数激增,显著推高 API 成本与响应延迟。理想方案并非全局清空缓存,而是对单个用户会话实施精准的“不活跃超时”管理:若某用户连续 2 分钟未发送新消息,则其历史记录应自动失效并重置——这正是 Guava LoadingCache 的核心优势所在。
✅ 推荐方案:Guava LoadingCache + expireAfterWrite
LoadingCache<String, List<ChatMessage>> 是比原始 HashMap 更健壮的选择,它原生支持基于写入时间(expireAfterWrite)或访问时间(expireAfterAccess)的条目自动驱逐。针对“用户静默即重置”的场景,expireAfterWrite(2, MINUTES) 更贴切:每次调用 conversations.put(...)(即用户发消息)即刷新该 key 的计时器,确保活跃会话永不过期;而闲置超时后,下次 getIfPresent() 将返回 null,自然触发新会话初始化。
以下是重构后的关键代码(已适配最新 Guava 版本):
// 引入依赖(Maven)
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.2.1-jre</version>
</dependency>private static OpenAiService service;
private static LoadingCache<String, List<ChatMessage>> conversations;
public static void initializeOpenAi() {
service = new OpenAiService(Settings.OPENAI_ACCESS_KEY);
conversations = Caffeine.newBuilder()
.maximumSize(10_000) // 可选:限制总缓存容量
.expireAfterWrite(2, TimeUnit.MINUTES) // ⭐ 关键:2分钟无写入即过期
.build(key -> {
// 自动加载新会话:首次 get 或过期后重建
List<ChatMessage> fresh = new ArrayList<>();
ChatMessage system = new ChatMessage();
system.setRole("system");
system.setContent("Omnis is a chatbot with sarcasm");
fresh.add(system);
return fresh;
});
}
public static String generateChatResponse(User user, String prompt) {
String userId = user.getId();
// ✅ 安全获取:过期则返回 null,否则返回最新列表
List<ChatMessage> conversation = conversations.getIfPresent(userId);
// 若为空(首次访问或已过期),则创建新会话(由 build() 中的 loader 自动完成)
if (conversation == null) {
conversation = conversations.getUnchecked(userId); // 触发 loader 加载
}
// 构造用户消息并追加
ChatMessage userPrompt = new ChatMessage();
userPrompt.setRole("user");
userPrompt.setContent(prompt);
conversation.add(userPrompt);
// 构建并发送 ChatCompletion 请求(逻辑同原代码)
ChatCompletionRequest chatRequest = ChatCompletionRequest.builder()
.model("gpt-3.5-turbo")
.maxTokens(1000)
.temperature(0.9)
.topP(0.3)
.frequencyPenalty(0.9)
.presencePenalty(0.0)
.messages(conversation)
.build();
ChatCompletionResult chatResult;
try {
chatResult = service.createChatCompletion(chatRequest);
} catch (Exception e) {
// 错误处理(保持原逻辑)
return handleError(e);
}
String response = chatResult.getChoices().get(0).getMessage().getContent();
// 追加助手回复(注意:此操作会刷新该 key 的写入时间!)
ChatMessage assistantPrompt = new ChatMessage();
assistantPrompt.setRole("assistant");
assistantPrompt.setContent(response);
conversation.add(assistantPrompt);
// ✅ 不需要显式 put —— 因为 conversation 是缓存中引用的对象,
// 其修改已实时生效;且每次 add 操作都属于“写入”,会重置过期计时器
return response;
}? 关键说明:
立即学习“Java免费学习笔记(深入)”;
- 使用 Caffeine(Guava Cache 的现代继任者)替代已废弃的 CacheBuilder,API 更简洁、性能更优、线程安全开箱即用。
- getUnchecked(key) 在 key 不存在或过期时,会同步调用 loader 创建新值并缓存,避免重复初始化逻辑。
- 无需手动 put():因 conversation 是缓存中可变对象的引用,所有 add() 操作直接作用于缓存实例,且每次 add() 都构成一次“写入”,自动延长其 2 分钟有效期。
- expireAfterWrite 严格基于最后一次写入时间,完美匹配“用户发言即续期”的业务语义。
⚠️ 注意事项与最佳实践
- 线程安全:Caffeine 缓存默认线程安全,多线程并发调用 getIfPresent/getUnchecked 无需额外同步。
- 内存控制:通过 maximumSize() 防止缓存无限增长;若需更精细淘汰(如按访问频次),可结合 weigher() 使用。
- 异常健壮性:loader 中抛出异常将导致本次 get 失败,建议在 loader 内包裹 try-catch 并返回默认值。
-
监控与调试:启用 recordStats() 可统计命中率、加载次数等,便于优化过期策略:
.recordStats() // 后续可通过 conversations.stats() 获取指标
-
替代方案对比:
- ConcurrentHashMap + 定时任务:实现复杂、易出错、无法精确到单 key 级别。
- Spring Cache + @Cacheable:需引入 Spring 生态,灵活性较低。
- 自研 TTL Map:重复造轮子,缺乏成熟度与测试保障。
通过以上改造,你的聊天机器人即可在零成本增加的前提下,智能压缩无效上下文,显著降低 token 消耗,同时保障活跃用户的对话体验丝滑如初。


















