**变更概述:** - 将 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/(问题分析和重构计划)
436 lines
12 KiB
Markdown
436 lines
12 KiB
Markdown
# 多轮对话时间查询缓存问题 - 修复报告
|
||
|
||
> **问题发现时间**: 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 (基于用户反馈)
|
||
**验证状态**: 🟡 待用户验证
|
||
**下次回顾**: 验证通过后可以归档 |