写在前面
某个周四下午,运维同学告诉我Python AI服务因为网络波动短暂不可用,几分钟后恢复了。但诡异的事情发生了——用户继续提问,系统却依然返回“AI服务暂时不可用”,而且是毫秒级返回。我第一反应是服务没恢复,检查后发现服务正常,日志里也没有报错。更奇怪的是,新开的会话也会偶发这个问题。一个看似简单的“服务不可用”错误,为什么像病毒一样蔓延?经过整整一下午的排查,我在两个地方抓到了“元凶”——一个是对话历史,一个是Redis缓存。它们叠加在一起,让系统“选择性失明”。今天这篇文章,我会还原整个排查过程、双Bug的根因分析,以及最终采用“双重防护”的解决方案。希望你看完后,遇到类似问题能少走弯路。
一、现象描述:幽灵般的错误信息
某天,Python AI服务因网络抖动返回了错误信息:“AI服务暂时不可用”。运维确认服务在5分钟内恢复正常,但诡异的行为出现了:
关键线索:错误响应速度极快(毫秒级),说明根本没有走到Python服务;新会话也会受影响,说明不是单纯的会话状态残留。
二、根因解剖:两个Bug的叠加效应
排查进入纵深后,我发现这不是单一问题,而是两个相互独立的Bug在不同层面各自作祟,叠加后放大了症状。
Bug #1:对话历史污染 —— LLM 被自己的“黑历史”误导
机制:当Python服务不可用时,Java后端将错误信息“AI服务暂时不可用”作为普通回答返回给前端,并存入会话的对话历史中。后续提问时,Java会将完整对话历史(包含那条错误信息)发给LLM。
LLM的行为:LLM看到上下文中有一条用户问题之后紧接着“AI服务暂时不可用”的助理回复,会认为这是对话的一部分,可能顺着这个思路回答:“由于AI服务不可用,我无法回答……”即使后端已经绕过Python、直接调用了LLM,LLM也会“模仿”之前的错误回复风格。
更隐蔽的后果:如果新会话的上下文恰好包含了之前的错误信息(例如跨会话的记忆被错误加载),新会话也会出现同样问题。
Bug #2:Redis缓存污染 —— 错误回答被永久“刻”进缓存
机制:Java后端在得到错误响应后,仍然将它写入了Redis缓存,key格式为ai:answer:{userId}:{questionMd5}。当服务恢复后,用户问完全相同的问题,系统直接从缓存读取——依然是那条“AI服务暂时不可用”。
为什么“毫秒级返回”?因为完全没走Python,纯Redis读取。
为什么新会话也会出现?新会话的用户ID不变,且问题相同,依然命中缓存。甚至换一个问题,但如果新问题的embedding向量与之前错误缓存的问题相似度过高,某些实现可能错误返回。
这两个Bug单独出现时,影响有限:
只有对话历史污染:切换新会话可恢复,老会话需要清除历史。
只有Redis污染:换一个问法就能绕过缓存。
但叠加之后,清除对话历史无效(缓存还在),换个问法可能仍然因为LLM的“自我延续”而错误。这就是为什么问题表现如此顽固。
三、解决方案:双重防护,截断污染源
修复策略:在错误信息的生成、传播、存储、使用四个环节分别拦截。
3.1 Python端:主动过滤对话历史 + 强化Prompt
核心思路:AI服务自身不应该看到错误信息,即便上游传下来了,也应在发送给LLM之前清理掉。
# python-service/core/llm.py class LLMService: ERROR_KEYWORDS = [ "AI服务暂时不可用", "服务不可用", "系统错误", "无法连接", "网络错误", "超时", "API密钥", "配置错误" ] def clean_conversation_context(self, context: str) -> str: """清理对话上下文中包含的错误信息行""" lines = context.split("\n") cleaned = [] for line in lines: if not any(kw in line for kw in self.ERROR_KEYWORDS): cleaned.append(line) return "\n".join(cleaned) def chat(self, user_id, question, history): # 1. 清理历史中的错误信息 cleaned_history = self.clean_conversation_context(history) # 2. 构建Prompt,明确禁止提及服务状态 prompt = f""" 你是一个知识助手。 重要规则: 1. 不要提及“AI服务不可用”、“系统错误”、“网络故障”等技术问题。 2. 你始终处于正常工作状态,能够回答问题。 3. 如果不知道答案,可以说“我不太确定,建议查阅相关文档”。 对话历史:{cleaned_history} 用户问题:{question} 请回答: """ # 调用LLM... return llm_response同时,Python服务自身在异常时不再返回“AI服务暂时不可用”这种技术性文本,而是返回一个特殊标记(如__SERVICE_UNAVAILABLE__),让Java端识别并转为友好提示,但不存入缓存和历史。
3.2 Java端:智能缓存 + 错误响应降级
修改点1:缓存读取时检测错误内容
// AiServiceImpl.java private boolean isErrorResponse(String answer) { String[] errorKeywords = { "AI服务暂时不可用", "服务不可用", "系统错误", "无法连接", "网络错误", "暂时无法回答", "稍后再试" }; for (String kw : errorKeywords) { if (answer.contains(kw)) { return true; } } return false; } @Override public AiResponse askQuestion(ChatRequest request) { String cacheKey = buildCacheKey(request); AiResponse cached = cacheService.get(cacheKey); // 关键:如果缓存命中但内容是错误响应,则当作未命中,强制刷新 if (cached != null && !isErrorResponse(cached.getAnswer())) { return cached; } if (cached != null) { log.info("Cache hit but contains error response, will refresh."); } // 调用Python服务或LLM AiResponse response = callAiService(request); // 只有正常回答才写入缓存 if (response != null && !isErrorResponse(response.getAnswer())) { cacheService.set(cacheKey, response, Duration.ofHours(1)); } else { // 错误响应使用短时缓存(避免缓存穿透),或者不缓存 log.warn("Error response not cached: {}", response.getAnswer()); } // 如果仍然是错误响应,降级为通用友好提示 if (isErrorResponse(response.getAnswer())) { response.setAnswer(getFriendlyErrorMessage()); } return response; } private String getFriendlyErrorMessage() { String[] messages = { "正在努力处理您的问题,请稍等片刻...", "系统正在维护中,很快就会恢复,请稍后再试", "服务暂时繁忙,请稍后重试", "我正在连接知识库,请耐心等待" }; return messages[ThreadLocalRandom.current().nextInt(messages.length)]; }修改点2:在对话历史保存前也过滤错误响应
// 保存对话消息前 if (isErrorResponse(assistantMessage)) { // 不保存错误响应到历史,或者保存一个占位符 assistantMessage = "系统正在处理,请稍后重试"; } messageService.save(conversationId, "assistant", assistantMessage);3.3 架构层面的“熔断与自愈”
除了上述代码修复,还应该在架构上增加一层健康检查与缓存失效机制:
实际项目中可以用Spring的@Scheduled+ 健康检查端点实现。当服务从DOWN变为UP时,主动清除所有ai:answer:*缓存(或按用户维度清除),避免“僵尸缓存”残留。
四、效果验证与经验沉淀
验证结果![]()
关键经验
不要缓存异常结果:这是最基本但最容易忽视的原则。任何5xx、4xx或业务错误码的响应,都不应进入缓存。
LLM的上下文污染比缓存更难清除:一旦错误信息进入对话历史,LLM会“学习”并延续错误,必须主动过滤输入Prompt。
降级文案要友好且非技术:不要让用户看到“Connection refused”、“NullPointerException”等术语,统一转为可理解的等待提示。
健康检查与缓存联动:服务恢复时主动失效相关缓存,是“自愈系统”的重要一环。
五、总结
一次简单的网络抖动,因为对话历史污染和Redis缓存污染的叠加,演变成了持续数小时的“幽灵错误”。解决它的过程并不复杂——在三个关键节点(历史输入、缓存读写、错误输出)各自加固,就能彻底截断污染源。
这套“双重防护”模式不仅仅适用于AI服务,任何依赖缓存和LLM的系统都应该建立类似的防御策略:
写缓存前校验数据有效性
读缓存后二次验证
对话历史清洗
服务状态联动
下次你的系统突然返回一个早已不存在的错误信息,不妨先检查这两个地方——很可能就是历史对话或缓存里的“僵尸”。
你遇到过类似的“幽灵错误”吗?因为某个服务短暂抖动,结果缓存的异常数据像病毒一样持续影响用户体验?除了本文的过滤+失效策略,你还用过哪些更优雅的“自愈”方案?欢迎在评论区分享你的踩坑经历。