**变更概述:** - 将 MVP 架构设计文档独立到项目根目录 `mvp/` - 整理 `docs/` 为纯学习和分析文档目录 - 按类型分类:learning(学习)、analysis(分析)、reports(报告)、guides(指南) **目录结构:** ``` mvp/ # MVP 架构设计(独立) ├── README.md # 数据库设计总览 ├── architecture/ # 架构文档 │ ├── agent-architecture-mvp.md │ ├── implementation-plan.md │ └── ... └── tables/ # 数据表设计 docs/ # 学习和分析文档 ├── learning/ # 学习笔记(00-08 编号) ├── analysis/ # 分析笔记 + 重构计划 ├── reports/ # 临时报告 └── guides/ # 指南文档 ``` **详细变更:** - docs/README.md → mvp/README.md(数据库设计入口) - docs/architecture/ → mvp/architecture/(架构设计) - docs/tables/ → mvp/tables/(数据表设计) - docs/学习笔记-*.md → docs/learning/07-*.md, 08-*.md - docs/项目学习路径.md → docs/learning/00-*.md - docs/功能分析报告.md → docs/analysis/ - docs/修复报告-*.md → docs/reports/ - docs/日志配置*.md → docs/guides/ 或 docs/reports/ - docs/design/ → docs/analysis/(问题分析和重构计划)
12 KiB
多轮对话时间查询缓存问题 - 修复报告
问题发现时间: 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 ❌ | ❌ 否 | 未更新 |
日志证据:
✅ 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 的推理过程:
- "历史记录显示刚才回答过时间(15:58)"
- "才过了几分钟,时间应该差不多"
- "不需要调用工具,直接复述之前的答案即可"
- 结果:直接返回 "15:58",未调用
getCurrentDateTime工具
🛠️ 修复方案
采用三管齐下的组合策略:
1️⃣ 强化 System Prompt(方案1)
修改文件: src/main/java/org/example/service/ChatService.java:64
修改前:
systemPromptBuilder.append("当用户询问时间相关问题时,使用 getCurrentDateTime 工具。\n");
修改后:
systemPromptBuilder.append("当用户询问时间相关问题时,**必须每次都调用 getCurrentDateTime 工具**,因为时间会不断变化。即使历史消息中有时间信息,也不要直接复用,必须重新查询最新时间。\n");
目的:明确告知 LLM "时间会变化,必须每次都调用工具"
2️⃣ 过滤时间查询历史(方案3 - 核心)
修改文件: src/main/java/org/example/service/ChatService.java:69-82
新增逻辑:
// 🔧 过滤时间查询相关的历史消息,避免 LLM 复用旧的时间信息
if ("user".equals(role) && isTimeQuery(content)) {
continue; // 跳过时间查询问题
}
if ("assistant".equals(role) && containsTimeInfo(content)) {
continue; // 跳过包含时间信息的回答
}
新增辅助方法:
/**
* 判断是否为时间查询问题
*/
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
修改前:
@Tool(description = "Get the current date and time in the user's timezone")
public String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
修改后:
@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️⃣ 重启应用
# 停止当前应用
pkill -f "spring-boot:run"
# 重新启动
cd /mnt/f/code-work-space/java/SuperBizAgent-java
mvn spring-boot:run
预期日志:
2026-05-31 xx:xx:xx INFO ChatService - ✅ ChatService 初始化成功
2️⃣ 清除旧会话,开始新对话
访问 http://localhost:9900,点击 "新建对话" 按钮。
3️⃣ 连续5次查询时间
| 查询次数 | 输入 | 预期行为 |
|---|---|---|
| 第1次 | "现在几点了?" | ✅ 调用工具,返回实时时间 |
| 第2次 | "现在是几点?" | ✅ 调用工具,返回实时时间 |
| 第3次 | "现在几点?" | ✅ 调用工具(修复前不调用) |
| 第4次 | "现在几点?" | ✅ 调用工具(修复前不调用) |
| 第5次 | "几点了?" | ✅ 调用工具(修复前不调用) |
4️⃣ 检查日志
# 实时查看日志
tail -f logs/application.log | grep -E "getCurrentDateTime|🕐"
预期输出(每次查询都应有):
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️⃣ 验证时间更新
在不同时间点查询,确认返回的时间会更新:
# 等待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️⃣ 天气查询
private boolean isWeatherQuery(String content) {
return content.matches(".*(天气|气温|温度).*");
}
2️⃣ 告警查询
private boolean isAlertQuery(String content) {
return content.matches(".*(告警|报警|异常).*");
}
3️⃣ 日志查询
private boolean isLogQuery(String content) {
return content.matches(".*(日志|错误|异常).*");
}
统一过滤逻辑:
// 过滤所有需要实时查询的内容
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️⃣ 添加单元测试
@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️⃣ 监控工具调用率
// 在 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 (基于用户反馈)
验证状态: 🟡 待用户验证
下次回顾: 验证通过后可以归档