# 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`)。