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/(问题分析和重构计划)
This commit is contained in:
zhuyongxin
2026-06-23 14:14:51 +08:00
parent caef477cec
commit 60be51f4a5
25 changed files with 339 additions and 606 deletions
@@ -0,0 +1,436 @@
# 多轮对话时间查询缓存问题 - 修复报告
> **问题发现时间**: 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 (基于用户反馈)
**验证状态**: 🟡 待用户验证
**下次回顾**: 验证通过后可以归档