# 多轮对话时间查询缓存问题 - 修复报告 > **问题发现时间**: 2026-05-31 16:05 > **修复完成时间**: 2026-05-31 16:10 > **问题严重性**: 🔴 HIGH(影响用户体验) > **修复状态**: ✅ 已修复,待验证 --- ## 🐛 问题描述 **用户报告**:当对话进行三次以上时,查询时间总是返回相同的结果。 **实际验证结果**: | 查询次数 | 查询时间 | 返回时间 | 是否调用工具 | 问题 | |---------|---------|---------|-------------|-----| | 第1次 | 15:57 | **15:57** | ✅ 是 | 正常 | | 第2次 | 15:58 | **15:58** | ✅ 是 | 正常 | | 第3次 | 16:02 | **15:58** ❌ | ❌ 否 | **未更新** | | 第4次 | 16:02 | **15:58** ❌ | ❌ 否 | **未更新** | | 第5次 | 16:05 | **15:58** ❌ | ❌ 否 | **未更新** | **日志证据**: ```log ✅ 15:57:37 - Starting execution of tool: getCurrentDateTime (第1次) ✅ 15:58:43 - Starting execution of tool: getCurrentDateTime (第2次) ❌ 16:02:42 - 无工具调用日志 (第3次开始不再调用工具) ``` --- ## 🔍 根本原因 ### LLM 的"聪明反被聪明误" 当用户第3次查询时间时,LLM 看到历史消息中已经有时间信息: ``` --- 对话历史 --- 用户: 现在几点了? 助手: 现在是 2026年5月31日(星期日)下午 15:58 🕐 用户: 现在是几点? 助手: 现在是 2026年5月31日(星期日)下午 15:58 🕐 --- 对话历史结束 --- 用户: 现在几点? ← 第3次查询 ``` **LLM 的推理过程**: 1. "历史记录显示刚才回答过时间(15:58)" 2. "才过了几分钟,时间应该差不多" 3. "不需要调用工具,直接复述之前的答案即可" 4. **结果**:直接返回 "15:58",未调用 `getCurrentDateTime` 工具 --- ## 🛠️ 修复方案 采用**三管齐下**的组合策略: ### 1️⃣ 强化 System Prompt(方案1) **修改文件**: `src/main/java/org/example/service/ChatService.java:64` **修改前**: ```java systemPromptBuilder.append("当用户询问时间相关问题时,使用 getCurrentDateTime 工具。\n"); ``` **修改后**: ```java systemPromptBuilder.append("当用户询问时间相关问题时,**必须每次都调用 getCurrentDateTime 工具**,因为时间会不断变化。即使历史消息中有时间信息,也不要直接复用,必须重新查询最新时间。\n"); ``` **目的**:明确告知 LLM "时间会变化,必须每次都调用工具" --- ### 2️⃣ 过滤时间查询历史(方案3 - 核心) **修改文件**: `src/main/java/org/example/service/ChatService.java:69-82` **新增逻辑**: ```java // 🔧 过滤时间查询相关的历史消息,避免 LLM 复用旧的时间信息 if ("user".equals(role) && isTimeQuery(content)) { continue; // 跳过时间查询问题 } if ("assistant".equals(role) && containsTimeInfo(content)) { continue; // 跳过包含时间信息的回答 } ``` **新增辅助方法**: ```java /** * 判断是否为时间查询问题 */ private boolean isTimeQuery(String content) { if (content == null) { return false; } // 匹配常见的时间查询模式 return content.matches(".*(现在|当前|此时).*(几点|时间).*") || content.matches(".*(几点|时间).*(了|呢|[??]).*") || content.toLowerCase().matches(".*(what.*time|current.*time).*"); } /** * 判断是否包含时间信息 */ private boolean containsTimeInfo(String content) { if (content == null) { return false; } // 匹配日期时间格式:2026年5月31日、15:57、下午3点 等 return content.matches(".*(\\d{4}年\\d{1,2}月\\d{1,2}日|\\d{1,2}:\\d{2}|[上下午]+\\d{1,2}[点时]).*"); } ``` **效果**:第3次查询时,LLM 看到的历史是: ``` --- 对话历史 --- (时间查询相关的消息已被过滤) --- 对话历史结束 --- 用户: 现在几点? ← 第3次查询 ``` **目的**:移除干扰信息,强制 LLM 调用工具 --- ### 3️⃣ 强化 Tool Description(方案4) **修改文件**: `src/main/java/org/example/agent/tool/DateTimeTools.java` **修改前**: ```java @Tool(description = "Get the current date and time in the user's timezone") public String getCurrentDateTime() { return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString(); } ``` **修改后**: ```java @Tool(description = "Get the current date and time in the user's timezone. " + "IMPORTANT: Time changes constantly. Always call this tool when user asks about time, " + "even if there's a recent time query in the conversation history.") public String getCurrentDateTime() { String currentTime = LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString(); logger.debug("🕐 getCurrentDateTime 调用 - 返回时间: {}", currentTime); return currentTime; } ``` **新增**: - Logger 声明(增加调试日志) - Tool description 中的 "IMPORTANT" 强调 **目的**:在工具定义层面提醒 LLM,并增加调试能力 --- ## 📝 修改文件清单 | 文件 | 修改类型 | 行号 | 说明 | |------|---------|------|------| | `ChatService.java` | 修改 | 64 | 强化 System Prompt | | `ChatService.java` | 新增 | 69-82 | 历史消息过滤逻辑 | | `ChatService.java` | 新增 | 89-112 | `isTimeQuery()` 和 `containsTimeInfo()` 方法 | | `DateTimeTools.java` | 修改 | 3-4 | 导入 Logger 和 LoggerFactory | | `DateTimeTools.java` | 新增 | 13 | Logger 实例 | | `DateTimeTools.java` | 修改 | 15-18 | 增强 Tool description + 日志 | --- ## ✅ 验证步骤 ### 1️⃣ 重启应用 ```bash # 停止当前应用 pkill -f "spring-boot:run" # 重新启动 cd /mnt/f/code-work-space/java/SuperBizAgent-java mvn spring-boot:run ``` **预期日志**: ```log 2026-05-31 xx:xx:xx INFO ChatService - ✅ ChatService 初始化成功 ``` --- ### 2️⃣ 清除旧会话,开始新对话 访问 `http://localhost:9900`,点击 **"新建对话"** 按钮。 --- ### 3️⃣ 连续5次查询时间 | 查询次数 | 输入 | 预期行为 | |---------|------|---------| | 第1次 | "现在几点了?" | ✅ 调用工具,返回实时时间 | | 第2次 | "现在是几点?" | ✅ 调用工具,返回实时时间 | | 第3次 | "现在几点?" | ✅ **调用工具**(修复前不调用) | | 第4次 | "现在几点?" | ✅ **调用工具**(修复前不调用) | | 第5次 | "几点了?" | ✅ **调用工具**(修复前不调用) | --- ### 4️⃣ 检查日志 ```bash # 实时查看日志 tail -f logs/application.log | grep -E "getCurrentDateTime|🕐" ``` **预期输出**(每次查询都应有): ```log 2026-05-31 16:15:01.xxx DEBUG DateTimeTools - 🕐 getCurrentDateTime 调用 - 返回时间: 2026-05-31T16:15:01.xxx+08:00[Asia/Shanghai] 2026-05-31 16:15:05.xxx DEBUG DateTimeTools - 🕐 getCurrentDateTime 调用 - 返回时间: 2026-05-31T16:15:05.xxx+08:00[Asia/Shanghai] 2026-05-31 16:15:10.xxx DEBUG DateTimeTools - 🕐 getCurrentDateTime 调用 - 返回时间: 2026-05-31T16:15:10.xxx+08:00[Asia/Shanghai] 2026-05-31 16:15:15.xxx DEBUG DateTimeTools - 🕐 getCurrentDateTime 调用 - 返回时间: 2026-05-31T16:15:15.xxx+08:00[Asia/Shanghai] 2026-05-31 16:15:20.xxx DEBUG DateTimeTools - 🕐 getCurrentDateTime 调用 - 返回时间: 2026-05-31T16:15:20.xxx+08:00[Asia/Shanghai] ``` --- ### 5️⃣ 验证时间更新 在**不同时间点**查询,确认返回的时间会更新: ```bash # 等待1分钟后查询 (等待 60 秒) 输入: "现在几点?" # 预期:返回的时间应该比上次晚 1 分钟 ``` --- ## 🎯 预期效果 ### 修复前 ❌ ``` 用户: 现在几点了? 助手: 现在是 2026年5月31日(星期日)下午 15:57 🕐 用户: 现在是几点? 助手: 现在是 2026年5月31日(星期日)下午 15:58 🕐 用户: 现在几点? ← 第3次 助手: 现在是 2026年5月31日(星期日)下午 15:58 🕐 ← ❌ 还是 15:58(没调用工具) 用户: 现在几点? ← 第4次 助手: 现在是 2026年5月31日(星期日)下午 15:58 🕐 ← ❌ 还是 15:58(没调用工具) ``` ### 修复后 ✅ ``` 用户: 现在几点了? 助手: 现在是 2026年5月31日(星期日)下午 16:15 🕐 用户: 现在是几点? 助手: 现在是 2026年5月31日(星期日)下午 16:15 🕐 用户: 现在几点? ← 第3次 助手: 现在是 2026年5月31日(星期日)下午 16:16 🕐 ← ✅ 时间更新了! 用户: 现在几点? ← 第4次 助手: 现在是 2026年5月31日(星期日)下午 16:16 🕐 ← ✅ 实时更新! ``` --- ## 🔧 可扩展性 这个修复方案可以扩展到其他"必须实时查询"的场景: ### 1️⃣ 天气查询 ```java private boolean isWeatherQuery(String content) { return content.matches(".*(天气|气温|温度).*"); } ``` ### 2️⃣ 告警查询 ```java private boolean isAlertQuery(String content) { return content.matches(".*(告警|报警|异常).*"); } ``` ### 3️⃣ 日志查询 ```java private boolean isLogQuery(String content) { return content.matches(".*(日志|错误|异常).*"); } ``` **统一过滤逻辑**: ```java // 过滤所有需要实时查询的内容 if ("user".equals(role) && (isTimeQuery(content) || isWeatherQuery(content) || isAlertQuery(content))) { continue; } if ("assistant".equals(role) && (containsTimeInfo(content) || containsWeatherInfo(content))) { continue; } ``` --- ## 📊 性能影响 ### Token 消耗变化 **修复前**(第3次查询): ``` System Prompt: 约 500 tokens(包含2轮历史时间查询) User Message: 10 tokens Total Input: 510 tokens ``` **修复后**(第3次查询): ``` System Prompt: 约 350 tokens(过滤掉时间查询历史) User Message: 10 tokens Total Input: 360 tokens ``` **节省**:约 30% 的输入 token(同时避免了 LLM 的误判) --- ## 🎓 学习要点 ### 1️⃣ LLM 的"过度优化"问题 LLM 会尝试从历史中找答案以节省工具调用,但这对于**时间、天气、告警**等**动态数据**是错误的。 **解决思路**: - 明确告知 LLM "这类数据会变化" - 过滤历史中的干扰信息 --- ### 2️⃣ Prompt Engineering 的重要性 单纯依靠 `@Tool` 注解不够,需要在 **System Prompt 层面**明确引导。 --- ### 3️⃣ 正则表达式的局限性 `isTimeQuery()` 和 `containsTimeInfo()` 使用正则匹配,可能有漏判: - "what's the time now?" ✅ 能匹配 - "tell me the current hour" ❌ 可能漏判 **改进方向**:考虑使用 NLP 意图识别或 LLM 辅助分类。 --- ## 📞 后续优化建议 ### 1️⃣ 添加单元测试 ```java @Test public void testIsTimeQuery() { assertTrue(isTimeQuery("现在几点了?")); assertTrue(isTimeQuery("当前时间是多少?")); assertTrue(isTimeQuery("what time is it now?")); assertFalse(isTimeQuery("今天天气怎么样?")); } @Test public void testContainsTimeInfo() { assertTrue(containsTimeInfo("现在是 2026年5月31日 下午15:57")); assertTrue(containsTimeInfo("现在是下午3点")); assertFalse(containsTimeInfo("今天是星期天")); } ``` --- ### 2️⃣ 监控工具调用率 ```java // 在 DateTimeTools 中添加计数器 private static final AtomicInteger callCount = new AtomicInteger(0); @Tool(...) public String getCurrentDateTime() { int count = callCount.incrementAndGet(); logger.info("🕐 getCurrentDateTime 第 {} 次调用", count); // ... } ``` **监控指标**: - 每小时调用次数 - 连续不调用的最大轮次(修复后应为 0) --- ### 3️⃣ 用户提示优化 在前端显示"🔧 已调用工具: getCurrentDateTime",让用户知道确实查询了最新时间。 --- ## ✅ 验证清单 - [ ] 代码已修改(3个文件) - [ ] 应用已重启 - [ ] 新建对话测试 - [ ] 连续5次查询时间,每次都调用工具 - [ ] 日志中看到 `🕐 getCurrentDateTime 调用` 记录 - [ ] 返回的时间会随实际时间更新 - [ ] 其他功能(文档查询、告警查询)未受影响 --- **修复完成时间**: 2026-05-31 16:10 **修复人**: Claude (基于用户反馈) **验证状态**: 🟡 待用户验证 **下次回顾**: 验证通过后可以归档