# AI Chat 跑题改进试验记录

> 创建：2026-06-20
> 范围：AI Chat 长对话「抓不住重点/跑题」根因治 — 测试计划 + 迭代过程 + 改进前基线 + 遗留
> 关联提交：P0 `102d398` / P1 `013ce21` / P2 `a2db5c7`
> 关联设计：`意图识别层论证-2026-06-19.md` / `多主题上下文管理愿景-2026-06-19.md`
> 三原则对齐：优雅 / 可靠 / 易迭代

---

## 一、测试计划

四层覆盖，从机制单测到实跑场景逐层外扩。机制层已闭环，实跑层待人工验证。

### ① 单测（机制正确性）— 189 测，全过

| 模块 | 测数 | 覆盖点 |
|------|------|--------|
| `intent.rs` 意图识别 + filter | 43 | 空 subset 命中 / 置信三重 fallback（<0.7、过滤<3、空 subset→全量）/ 顺序漂移 / 工具收敛 29→5-10 |
| `context_helpers.rs` 压缩+工具压缩+tokenize | 18 | `extract_keyword_summary`（全停用词、全漂移、top-10）/ `should_summarize_tool_result`（2KB 边界、占比 40% 边界）/ `extract_key_info`（错误行 error/panic/失败、首尾 5 行、空/短直回）/ **2-gram 汉字滑窗**（"压缩架构"→"压缩/缩架/架构"、不跨边界） |
| `context.rs` 上下文+主题检测 | 32 | TrackedMessage.topic 推断 / `pending_topic_marker` 双高置信置位 / 任一 None 不标防误报 / take 消费 |
| `prompt.rs` 系统提示 | 6 | 中文「聚焦准则」段 / 英文「Focus」段 / 兜底 lang 回落 / 压缩 prompt 未波及回归保护 |

**df-ai crate 共 189 测全过**（commit `a2db5c7` 自验：`cargo test -p df-ai` EXIT 0）。

### ② 苛刻测（边界/对抗，agent 在跑）

针对每条机制的极端输入，验证不崩溃、不误报、有兜底：

- 全漂移（user 消息全是无关内容 → 意图 Unknown → fallback 全量工具，不卡死）
- 全停用词（`extract_keyword_summary` 返回空字符串，不 panic）
- 全错误行（`extract_key_info` 命中所有 error/panic 行，不超长）
- 2KB 边界（`should_summarize_tool_result` 恰好 2000/2001 字节，阈值判定稳定）
- 连续主题切换（连续两条 user topic 不同 → `pending_topic_marker` 只标末两条，不累积）

> 状态：部分已并入单测（见 `context_helpers.rs` 对抗段），其余 agent 审查阶段补跑。

### ③ db 基线分析（改进前跑题现象）— 本文 §三

挑 3 个长对话（均 600KB 量级，glm-5.2），解析 messages JSON，归类跑题现象，作为改进前基线。
**结论：跑题主因是「工具结果挤占 + 全量工具分散 + 压缩丢主题」三类，P0-P2 改进恰好对症。**

### ④ 实跑场景（重启 dev 对话验证）— 待人工执行

改进机制已测过，LLM 实际响应效果需实跑验证。建议场景：

| 场景 | 验证改进 | 预期 |
|------|---------|------|
| 单主题长对话（连续 5+ 轮同一主题） | 改进3 压缩增强保留主题锚点 | 续接时不丢主线 |
| 主题切换（话题 A→B） | 改进5 主题标记软提示 | 切换后不把旧话题带进新回答 |
| 多主题交织（A/B 轮番） | 改进1 聚焦准则 + 改进5 | 始终围绕当前轮核心 |
| 大工具结果（>2KB / >10KB） | 改进4 工具结果压缩 | tool_result 不挤占上下文 |
| 工具并发（4-12 个并行） | 改进2 意图收敛 29→5-10 | LLM 可见工具收敛，不被工具列表稀释意图 |

---

## 二、迭代过程（P0 → P1 → P2）

三批改进，每批：根因 → 改进 → 单测 → 审查 → 效果。常量开关均可回退（调参退回接入前行为）。

### P0（`102d398`）— 系统提示聚焦 + 意图接入 loop 收敛工具

**根因**：
1. 系统 prompt 把「聚焦」混进「行为准则」段被稀释，LLM 无独立抓手
2. loop 每轮把全量 29 个工具定义塞给 LLM，工具列表本身稀释意图（LLM 在 29 个工具里挑，而非围绕 user 目标）

**改进**：
- 改进1 `prompt.rs` 加 `## 聚焦准则`（中）/ `## Focus`（英）独立段 4 条，机制描述（"始终围绕当前请求核心目标""切换话题以最新为准""先结论后解释""不主动展开无关上下文"），不混进行为准则
- 改进2 `intent.rs` `filter_tool_defs` 按意图收敛工具 29→5-10；`agentic:487` 三重 fallback：置信<0.7 / 过滤后<3 / 空 subset → 回全量；执行路径不变（audit/execute 走完整 registry，filter 只改 LLM 可见 `tool_defs`）

**单测**：prompt 4（中/英/兜底 lang/压缩未波及）+ intent filter 7（空 subset/命中/漂移/顺序）= 11
**审查**：CR-25，🟡2（Debug 加 Data 域 + threshold 注释失准）→ 已修 ✅ PASS
**常量开关**：`INTENT_CONF_THRESHOLD = 0.7`（调 1.0 关闭收敛）

### P1（`013ce21`）— 压缩增强 + 工具结果压缩

**根因**：
3. `compress_old_messages` 压缩时丢主题词（用户反复提及的实体/技术名词是续接锚点），续接时 AI 找不回主线
4. tool_result 原样塞回上下文，>2KB 的搜索/读文件结果直接挤占 token（基线：95% 内容是 tool_result）

**改进**：
- 改进3 `context_helpers.rs` 压缩 prompt 加「主题/关键词保留段」；失败兜底从「裸裁剪」改 `extract_keyword_summary`（user 消息词素 top-10 去停用词），首位插摘要作锚点
- 改进4 `should_summarize_tool_result`（>2KB 或占比>40% 触发）+ `extract_key_info`（错误行 error/panic/失败 + 首尾各 5 行中间省略）；view-only 不改持久化（原始 tool_result 仍在 DB，`build_for_request` clone 视图）

**单测**：compress/keyword 2 + should_summarize + extract_key_info 14 = 16
**审查**：CR-26，🟡1（tokenize 中文锚点弱：每汉字单成词素致"压缩/架构"2 字词被拆滤）→ P2 修 ✅ PASS
**常量开关**：`KEYWORD_FALLBACK_ENABLED` / `TOOL_RESULT_COMPRESS_ENABLED`

### P2（`a2db5c7`）— 主题检测保守 + tokenize 2-gram 汉字

**根因**：
5. 主题切换无感知，切换后旧话题被带进新回答（基线 d6614e0b：用户问"写文件为什么总写错日期"，AI 仍在追旧话题"授权体验"的尾巴）
6. CR-26 🟡1：汉字单字词素导致 2 字中文锚点（"压缩""架构""审批"）被 tokenize 拆滤，关键词摘要抓不住中文主题

**改进**：
- 改进5 `context.rs` TrackedMessage.topic 字段（push 推断 topic：意图置信>=0.7 且非 Unknown）+ `pending_topic_marker`（末两条 user topic 双高置信且不同才标，任一 None 不标防误报）+ loop 保守标记（`agentic:571` 顶部读 marker → insert system 软提示，不参与裁剪/压缩）
- tokenize 2-gram 汉字滑窗（`context_helpers.rs:294`）：每汉字既单成词素又产出相邻 2-gram（"压缩架构"→"压缩/缩架/架构"），非汉字边界重置窗口不跨边界组词

**单测**：topic 检测 6 + tokenize 2-gram（含不跨边界）= 测并入 context 32 + context_helpers 增量
**审查**：CR-26 🟡1 已闭环
**常量开关**：`TOPIC_MARKER_ENABLED = true`（false 跳过标记，排障/对比用）
**保守性**：topic 不参与裁剪/压缩、TrackedMessage 不 Serialize（只 ChatMessage 落库）、软提示非强制

### 三原则对齐

| 原则 | 落地 |
|------|------|
| **优雅** | filter 只改 LLM 可见 `tool_defs` 不动执行路径；view-only 不改持久化；topic 软提示不参与裁剪；常量开关可回退 |
| **可靠** | 三重 fallback（意图置信/过滤数/空 subset）；压缩失败兜底关键词摘要；view-only clone 视图；topic 双高置信才标防误报 |
| **易迭代** | 4 个常量开关（`INTENT_CONF_THRESHOLD`/`KEYWORD_FALLBACK_ENABLED`/`TOOL_RESULT_COMPRESS_ENABLED`/`TOPIC_MARKER_ENABLED`）可独立开关排障；每批独立 commit + 审查回填；tokenize 2-gram 是纯函数增量无破坏 |

---

## 三、改进前基线（db 对话分析）

> 数据源：`C:/Users/23780/AppData/Roaming/top.1216.devflow/devflow-dev.db`，`ai_conversations.messages` JSON
> 挑样：raw_len>500K 的长对话 3 个（均为 glm-5.2，改进前数据）

### 量化指标（3 对话均值）

| 指标 | f89d129a | a95f5d6d | d6614e0b | 均值 |
|------|---------|---------|---------|------|
| 总消息数 | 251 | 213 | 211 | 225 |
| user / assistant / tool | 22 / 111 / 118 | 22 / 88 / 103 | 22 / 96 / 93 | 22 / 98 / 105 |
| tool_calls 总数 | 118 | 103 | 93 | 105 |
| 单轮 ≥3 工具并发 | 6 | 4 | 3 | 4 |
| tool_result >2KB | 39 | 34 | 44 | 39 |
| tool_result >10KB | 24 | 14 | 14 | 17 |
| **空 assistant（纯工具 echo）** | **69/111 (62%)** | **32/88 (36%)** | **41/96 (43%)** | **47%** |
| user 「继续」 | 5 | 2 | 0 | 2.3 |
| user ≤12 字短问 | 14/22 | 7/22 | 13/22 | 52% |
| **asst 文本 / tool 文本 比** | **0.05** | **0.04** | **0.05** | **0.05** |

**关键信号**：
- **asst 文本仅占 4-5%**，95% 上下文是 tool_result → 工具结果挤占严重（改进4 对症）
- **47% assistant 轮次是纯工具 echo**（content 空、只发 tool_calls）→ LLM 不产出围绕 user 目标的文本，被工具流程推着走（改进1+2 对症）
- **单轮 4-12 工具并发** → 工具列表稀释意图（改进2 收敛对症）

### 跑题现象归类（5 类，含实例）

#### 类型1：工具结果挤占（最高频，3/3 对话）

- **现象**：tool_result 占 95% token，asst 文本仅 4-5%。AI 回复被大块搜索/读文件结果淹没，user 要的「结论」淹没在工具回显里。
- **实例**（f89d129a [58]）：用户问"我做哪些决策，多角度论证"，AI 先并发 12 个工具读 4 个文件（[9]-[49] 占 50+ 条消息），最后才在 [58] 一次性吐 6760 字决策分析。论证正确但被工具流程拖到 50 条消息后。
- **对症改进**：P1 改进4 工具结果压缩（>2KB 触发 extract_key_info 保留错误行+首尾 5 行）。

#### 类型2：全量工具分散意图

- **现象**：每轮把 29 个工具全塞 LLM，AI 在工具列表里挑而非围绕 user 目标，表现为"读一堆无关文件试探"。
- **实例**（d6614e0b [92]-[116]）：用户问"消息发送交付功能"，AI 并发 3 工具读 useAiEvents/useAiStream/useAiSend/useAiConversations/ai.ts/stream_recv/commands.rs/agentic.rs/persistence 9 个文件，最后才产出分析。
- **对症改进**：P0 改进2 意图 filter_tool_defs 收敛 29→5-10。

#### 类型3：压缩丢主题（长对话续接跑题）

- **现象**：`compress_old_messages` 裁掉主题词后，续接时 AI 找不回主线，绕旧话题。
- **实例**（d6614e0b [174]→[181]）：用户问"最新 aichat 权限"，AI 先读旧路径 `2025-07-14.md`（不存在），再搜索才发现文件已被手工改名为 `07-15`，绕了一圈。主题锚点（"授权方案文件"）在压缩中被丢。
- **对症改进**：P1 改进3 压缩增强 + extract_keyword_summary 保留主题锚点。

#### 类型4：多主题交织（切换不干净）

- **现象**：用户切换话题，AI 仍带旧话题尾巴进新回答。
- **实例**（d6614e0b [140]→[153]）：用户从"消息交付功能不足"切到"写文件为什么写错日期"，AI 在 [141]-[152] 还在读 useAiSend/ai_tools 等旧话题文件，[153] 才答到日期根因。
- **对症改进**：P2 改进5 主题检测 + pending_topic_marker 软提示。

#### 类型5：无聚焦（系统提示无独立抓手）

- **现象**：系统 prompt 把「聚焦」混在「行为准则」被稀释，AI 无明确"围绕当前请求核心"的指令约束。
- **实例**（3 对话通用）：47% assistant 轮次纯工具 echo 不产出围绕 user 目标的文本。
- **对症改进**：P0 改进1 系统提示独立聚焦段（中「聚焦准则」/ 英「Focus」4 条）。

### 基线小结

5 类跑题现象与 P0-P2 五条改进**一一对应**，改进设计精准对症基线，无遗漏主因。改进后预期：

| 基线现象 | 改进 | 机制测过 | 实跑预期 |
|---------|------|---------|---------|
| 工具结果挤占 | 改进4 | ✅ 14 测 | tool_result 压缩释放 token |
| 全量工具分散 | 改进2 | ✅ 7 测 | 工具收敛 29→5-10 |
| 压缩丢主题 | 改进3 | ✅ 2 测 | 主题锚点保留 |
| 多主题交织 | 改进5 | ✅ 6 测 | 切换软提示 |
| 无聚焦 | 改进1 | ✅ 4 测 | 独立聚焦段 |

---

## 四、遗留与风险

### 已知遗留

1. **LLM 实际响应效果需实跑验证** — 单测覆盖机制正确性（filter/压缩/tokenize/topic 标记），但 LLM 在改进后 prompt + 收敛工具下的实际「抓重点」表现，需重启 dev 对话按 §一④ 场景实跑。机制正确 ≠ LLM 行为改善。
2. **主题检测 F1 天花板** — topic 推断依赖意图置信>=0.7（非 Unknown），意图识别本身的准确率决定 topic 检测上限。短问（基线 52% user ≤12 字）意图置信常 <0.7，这类 topic 推断为 None，不参与标记（保守防误报的代价）。
3. **中文 tokenize 2-gram 仍有边界** — 3+ 字词（"状态机""工作流"）能由 2-gram 组合近似，但语义级词（"推进链"3 字）仍可能被切；已优于单字方案，非完美。

### 未覆盖的跑题场景（建议补改进，基于基线）

基线 5 类已被 P0-P2 覆盖，未发现遗漏的**主因**。但有两类**次生现象**改进未直接覆盖，建议观察实跑后决定是否补：

1. **「继续」疲劳**（f89d129a 5 次"继续"）— AI 被工具流程拖到长尾，user 不得不催。根因是工具流程过长（类型1+2 的次生），P0-P2 收敛工具+压缩后应缓解，但若实跑仍高频出现，可考虑加「单轮工具调用数硬上限」或「长工具流程中途主动给中间结论」。
2. **空 assistant 纯工具 echo**（47%）— P0 改进1 聚焦段是软约束，不强制 assistant 每轮产出文本。若实跑 echo 率仍高，可考虑 prompt 加「工具调用前用一句话说明意图与当前请求的关系」的硬约束（当前基线已有"执行操作前简要说明你的意图"，但未要求与当前请求关联）。

> 两项均为**次生/可选**，不阻塞当前迭代，建议实跑验证后再定。

---

## 附：数据可复现

db 分析脚本核心逻辑（python sqlite3，标准库）：

```python
import sqlite3, json
conn = sqlite3.connect(r'C:/Users/23780/AppData/Roaming/top.1216.devflow/devflow-dev.db')
# 按长度排长对话
conn.execute("SELECT id,title,LENGTH(messages) FROM ai_conversations ORDER BY 3 DESC LIMIT 5")
# 解析单对话
msgs = json.loads(row['messages'])
# 量化：空 assistant（content 空但 tool_calls 非空）/ asst 文本 vs tool 文本比 / 单轮工具并发数
```

复现命令：见本任务分析脚本（已离线保存于 `/tmp/analyze_chat.py` + `/tmp/metrics.py`）。
