Files
SuperBizAgent-java/docs/reports/修复报告-多轮对话时间查询缓存问题.md
zhuyongxin 60be51f4a5 docs: 重构文档结构,分离学习笔记和 MVP 架构设计
**变更概述:**
- 将 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/(问题分析和重构计划)
2026-06-23 14:14:51 +08:00

436 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 多轮对话时间查询缓存问题 - 修复报告
> **问题发现时间**: 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 (基于用户反馈)
**验证状态**: 🟡 待用户验证
**下次回顾**: 验证通过后可以归档