- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条) - queue 加 conversationId 字段,按会话精准 drain - regenerate/editMessage 只清本会话排队消息 - newConversation 保留旧会话排队消息 - AiError 只清出错会话的队列项
14 KiB
AI Chat 真实会话实测分析(系统消息污染 + 死循环重调)
日期:2026-07-07 | 方法:从生产 DB(
%APPDATA%/top.1216.devflow/devflow.db)实测最近两个会话全量消息反推,Python 只读统计 基线会话:9357c27c(HaoGamePlatfPro,prompt 644,586 / completion 3,865)+43b75e49(wk-hszd,prompt 601,028 / completion 4,134) 产物:2 P0 / 3 P1 / 2 P2 性质:实测数据反推(非静态代码审查)。finding 的源码根因为"假设",须独立 grep/read 核验后再定论 关联:aichat-session-analysis-2026-06-22 aichat-techdebt-audit-2026-06-21 devflow-generating-statemachine devflow-compress-ui-no-fold
会话概况
| 项 | 9357c27c | 43b75e49 |
|---|---|---|
| 标题 | HaoGamePlatfPro | 查看 E 盘 wkhszd 目录内容 |
| 跨度 | 2026-06-30 14:31 ~ 15:04(~33 分钟) | 2026-07-01 ~ 07-03(中间大间隔) |
| 消息数 | 97 | 77 |
| prompt / completion tokens | 644,586 / 3,865 | 601,028 / 4,134 |
| completion/prompt 比 | 0.60% | 0.69% |
| 工具调用次数 | list_directory 22 / read_file 22 / search_files 13 / list_projects 2 / update_project 1 / get_task_count 1 | list_directory 16 / read_file 17 / list_projects 2 / list_project_modules 3 / bind_directory 3 / create_project 1 / update_project 3 |
| 工具结果总量 | 195.4 KB | 62.3 KB |
| 用户输入字数 | 34 字(2 条) | 51 字(5 条) |
| 用户催促 | 0 | 1(核对) |
| 末尾收尾 | assistant(seq96,正常) | assistant(seq76,正常) |
| task | 浏览 HaoGamePlatfProject 代码补充项目信息 | 把 wk-hszd 注册为 DevFlow 多工程项目 |
两会话共同特征:极少用户输入(34~51 字)产生 60 万级 prompt,completion/prompt 比 < 1%,极端输入重。
P0(2 项)
[P0-1] seq0 system 消息污染:assistant 思考流被截断重复拼接 41 次 + role 错位 + 时序倒挂
- 现象(9357c27c seq0):
role=system,但内容是 assistant 语义:"我已经充分了解了项目结构。让我快速查看几个关键文件..."- 同一短语
"现在更新项目信息:基于代码分析,这是一个大型综合游戏平台后端。让我更新项目描述和技术栈。"重复 41 次,拼成 1879 字符 - seq0 timestamp
1782971672713比 seq11782971504351晚 168 秒(时序倒挂)
- db 证据:
ai_messagesseq0 row,role=system,status=active,content 长 1879 - 根因假设(强):系统 prompt 注入路径把"上一轮 assistant 流式片段"误当 system 上下文写入;且写入库时陷入重复拼接循环(疑似字符串 builder 在流式 chunk 累积时无去重/无终止条件)。这是 aichat-session-analysis-2026-06-22 P2-1 的复发且恶化——上次只见 1 条 assistant 语义残留,本次是 41 次死循环拼接。
- 影响:
- system 消息会作为 prompt 头部送入 LLM,1879 字符无意义噪声污染每一轮上下文
- role 错位会误导模型(system 应是约束指令,却变成"我说的"内容)
- 时序倒挂破坏压缩/排序/计费
- 待核验源码点:
- system prompt 注入路径(
ai_messages写入 system 行的位置,src-tauri/src/commands/ai/) - 流式 chunk 累积逻辑(查
for/push_str/+=类字符串拼接,是否有循环退出条件) - timestamp 赋值逻辑(为何 system 行用了"插入时刻"而非"会话起始时刻")
- system prompt 注入路径(
- 关联:印证 aichat-techdebt-audit-2026-06-21 P2 区"system 消息混入 assistant 内容"项;与 aichat-session-analysis-2026-06-22 P2-1 同源(前次未根治)
[P0-2] 工具死循环重调:LLM 无"已调用过"记忆,同参同路径重复触发
- 现象(9357c27c):
read_file(application.xml)读了 17 次,其中两次返回完全相同的 8457c 全文(同一文件无变化)search_files(pattern='.java', path=...src)连调 13 次,每次返回结构几乎一致(items=0, has_more=True,5.5~5.8KB),LLM 完全没意识到在重复list_directory(...src)2 次,第 2 次返回 30,516c(第 1 次 6,144c,深度参数不一致但 LLM 未说明理由)
- db 证据:13 条 search_files 调用
arguments完全相同,result长度方差 < 5% - 根因假设:
- 无工具结果缓存层:会话级无"同参工具调用 → 返回缓存命中提示"机制,LLM 每次发起都真执行
- prompt 未告知 LLM 已读内容:read_file 返回的文件指纹/路径未在后续 prompt 中显式标注"已读",LLM 失忆反复读
- search_files has_more=True 的分页语义被 LLM 误解:可能 LLM 以为重调能翻页,但实际每次返第一页
- 影响:
- prompt 64 万的直接元凶之一:13 次 search × ~5.7KB + 17 次 read × 平均 2.7KB ≈ 75KB 仅重复工具结果(占 prompt 字符量的主要部分)
- 死循环消耗 30+ 轮 LLM 调用,用户等待感极差
- 待核验源码点:
- 工具调度层(
tool_registry.rs/agentic/mod.rs)——是否有seen_tool_calls去重 - read_file 是否返回 file_hash 供 LLM 引用(对照 aichat-techdebt-audit-2026-06-21 P1「expected_hash schema 契约破裂」)
- search_files 分页契约(为何 has_more=True 但无 page/cursor 参数)
- 工具调度层(
- 关联:印证 aichat-session-analysis-2026-06-22 P2-2(同文件反复 read)+ 新增 search_files 死循环变体
P1(3 项)
[P1-1] 压缩实质失效:compressed 消息平均长度比 active 还长(205%) 【误报,2026-07-07 核验撤销】
- 原现象(9357c27c):58/97 条 status=compressed,但 compressed 平均 2641c/条,active 平均 1287c/条——压缩后比未压缩还长 2 倍。
- 核验撤销:进一步按 role 分组统计发现,compressed tool 消息长(4608c)是因为会话前期是大工具调用(list_directory/read_file 大输出),active tool 消息短(1714c)是因为后期转为小操作(update_project)。这是会话业务节奏的自然结果,不是压缩机制问题。
- prompt_tokens 语义澄清:
prompt_tokens=644,586是会话级累积值(每轮 prompt 累加,见agentic/mod.rs:1366 tokens.add()),不是最后一轮值。97 条消息 × 平均 6645 tokens/轮 ≈ 644K,完全合理。每轮实际 prompt 只 ~6.6K(active content 49KB ≈ 13K token,压缩后只进 active)。 - 结论:06-22 报告 P0-1 修复(is_active 白名单让 compressed 不进 prompt)生效。本项撤销,无需修复。
[P1-2] LLM 自检缺失 + 宣称完成即停,用户被迫人工纠错
- 现象(43b75e49 wk-hszd):
- seq71 assistant 自检发现 bug:
path被最后一次bind_directory覆盖成hszd-test,stack变成["Go"] - 但这是 assistant 自己在 seq74"再次核对"时才发现——说明 seq68 用户催"核对"前,assistant 已认为完成
- 3 次
bind_directory把 path 从hszd-api→hszd-admin→hszd-test,每次都覆盖前值,assistant 没意识到多工程 monorepo 的 path 字段不该是单子工程
- seq71 assistant 自检发现 bug:
- db 证据:bind_directory 3 次调用 args 显示 path 被依次覆盖,最终 assistant 自己回滚用
update_project(path='E:/wk-hszd')修正 - 根因假设:
- agent 无"宣称完成前的强制自检"(对照 aichat-session-analysis-2026-06-22 P1-1)
bind_directory的path字段语义对多工程 monorepo 有歧义(LLM 以为是"当前操作的子工程",实际是"项目根")
- 影响:用户对 agent 完成度零信任,需人工核对每个字段
- 待核验/落点:
- agent 行为策略(prompt 加自检 checklist)
bind_directoryschema 注释补"monorepo 应用项目根路径,非子工程"
- 关联:aichat-decision-capability aichat-b-route-parallel-multiround
[P1-3] 工具拒绝消息破坏 JSON 协议:raw 文本"用户拒绝了此操作"
- 现象(9357c27c seq95):
update_project被用户拒绝,tool role 消息 content ='用户拒绝了此操作'(8 字符裸文本),非 JSON - db 证据:61 条 tool 消息中 60 条 JSON-parseable,1 条 raw-text(就是这条拒绝)
- 根因假设:工具拒绝路径(
ai_reject/ 审批拒绝)走的是错误字符串直接返回,未包装成{"status":"rejected","reason":"..."}结构 - 影响:
- 若 LLM provider 严格解析 tool role 内容(部分 provider 要求 JSON),会触发协议错误
- 与 aichat-techdebt-audit-2026-06-21 P1「审批状态字符串双轨(executed/completed)」同家族——工具返回协议不统一
- 待核验源码点:审批拒绝 handler(
commands.rs拒绝分支)的返回序列化
P2(2 项)
[P2-1] system 消息 timestamp 用"插入时刻"而非"会话时刻",时序倒挂
- 现象(9357c27c):seq0 system ts=1782971672713,seq1 user ts=1782971504351,seq0 比 seq1 晚 168 秒
- 根因假设:system prompt 注入是在会话进行中"补写"的(可能是压缩/续轮时插入),用了补写时刻
- 影响:排序、压缩窗口计算、计费全受污染
- 关联:与 P0-1 同源(都是 seq0 system 行的写入 bug)
[P2-2] bind_directory 多工程语义错配:单值 path 字段无法表达 monorepo
- 现象(43b75e49):wk-hszd 是 Go+Vue+Test 三子工程 monorepo,assistant 3 次 bind_directory 分别绑三个子工程路径,前两个被第三个覆盖
- 根因假设:
projects.path是单值字段,而 DevFlow 的"多工程"概念(project_modules表)与"项目根 path"语义未在 schema 注释中澄清 - 影响:多工程项目的 path 字段长期处于"最后绑的子工程"错误状态,直到 LLM 自检或人工纠正
- 待核验/落点:
bind_directoryIPC 的 schema 文档 + 多工程 monorepo 的 path 字段规范(可能应允许 null 或 readonly,工程信息走project_modules)
落点建议(未实施,遵守 session-role-diagnose-only)
| 项 | 优先级 | 落点 |
|---|---|---|
| P0-1 system 消息污染 + 重复拼接 | P0 | system prompt 注入路径 + 流式 chunk 累积循环(查 push_str/+= 类拼接) |
| P0-2 工具死循环重调 | P0 | 工具结果缓存层(会话级 seen_tool_calls)+ read_file 返回 file_hash + search_files 分页契约 |
| P1-1 压缩产出反向膨胀 | P0/P1 | compress_old_messages 产出从拼接改真摘要 |
| P1-2 自检 + bind_directory 语义 | P1 | prompt 自检 checklist + bind_directory schema 注释 |
| P1-3 拒绝消息 JSON 协议 | P1 | 审批拒绝 handler 返回结构化 JSON |
| P2-1 timestamp 时序 | P2 | system 行 timestamp 用会话起始时刻 |
| P2-2 多工程 path 语义 | P2 | bind_directory/path schema 文档 |
最高杠杆:P0-1(系统消息污染直接污染每一轮 prompt)+ P0-2(工具死循环是 prompt 64 万的主因)。两者叠加,即使只修这两项,类似会话的 prompt 可降一个量级。
2026-07-07 修复落地(本次实施)
| 项 | 位置 | 修复 |
|---|---|---|
| P0-1 LLM 退化输出污染 system 消息 | commands/ai/compress.rs |
clean_summary 加 is_degenerated_repetition 退化检测,同一片段重复 ≥5 次返空 → 调用方据空值降级走关键词兑底 |
| P0-2 工具死循环重调 | commands/ai/audit/cache.rs + audit/mod.rs |
新增 find_cached_readonly_result(只读幂等工具白名单:read_file/read_symbol/list_directory/search_files/grep),Low 风险执行前查会话历史,同参同成功(completed)调用直接回填缓存跳过真执行 |
| P1-3 拒绝消息破 JSON 协议 | commands/ai/commands/chat.rs + audit/mod.rs |
三处拒绝消息(用户拒绝此操作/路径授权拒绝/路径黑名单拒绝)改结构化 JSON {status,reason,message} |
| 额外:Windows 上 .ssh/.aws 黑名单绕过 | state/allowed_dirs.rs |
check_path_authorization 黑名单检查提前到路径规范化之前(对原始路径也判一次)。根因:Windows 上 /home/user/.ssh/config 的 is_absolute() 返 false,persistent 为空时旧逻辑直接返 NeedsAuthorization 跳过黑名单 |
| P1-2 bind_directory 多工程语义错配 | commands/ai/tool_registry.rs |
工具描述补充 monorepo 语义:path 应为项目根路径(非子工程),多次调用会覆盖,子工程信息走 list_project_modules |
| — | 误报撤销:prompt_tokens 是会话级累积值(非单轮值),按 role 分组统计后压缩机制正常 |
测试:cargo test -p devflow --lib 通过(compress 11 + audit 19 + allowed_dirs 20,含新增 6 个回归测试)。原 test_grep_blacklisted_path_denied(此前失败)现已通过。
复现方法
# 只读查询生产 DB
python -c "
import sqlite3, os
db = os.path.expandvars(r'%APPDATA%/top.1216.devflow/devflow.db')
con = sqlite3.connect(f'file:{db}?mode=ro', uri=True)
# seq0 system 污染证据
print(con.execute('select seq, role, length(content), timestamp from ai_messages where conversation_id=? and seq<2 order by seq',
('9357c27c-aeba-488f-9f4f-12c2d0506f63',)).fetchall())
# search_files 13 次重复
print(con.execute('select count(*), min(length(result)), max(length(result)) from ai_tool_executions where conversation_id=? and tool_name=?',
('9357c27c-aeba-488f-9f4f-12c2d0506f63','search_files')).fetchall())
"
# 表:ai_conversations / ai_messages / ai_tool_executions
数据快照:2026-07-07 抓取自 devflow.db(生产库,非 dev)。
与 06-22 报告的对比
| 项 | 06-22 报告 | 07-07 实测 | 趋势 |
|---|---|---|---|
| 工具返回裸文本(raw) | patch_file/read_file 大量 raw(6204c) | 仅 1 条(拒绝消息) | ✅ 已修复(P0-2 性质修正有效) |
| 重复 tool 结果落库 | seq 11/12/13/14 同结果 4 份 | 0 条 tool_call_id 重复 | ✅ 已修复(P0-3 撤销成立) |
| system 消息混 assistant 语义 | seq0 残留 1 句 | seq0 重复 41 次拼接 | ❌ 恶化(P2-1 → P0-1) |
| 同文件反复 read | mysql_guide read 4× | application.xml read 17× | ❌ 恶化(P2-2 → P0-2) |
| 上下文管理实质失效 | compressed 未降 prompt(已隐式修复) | compressed 产出反向膨胀 205% | ⚠️ 新变体(P0-1 → P1-1) |
| 中断(tool 后无 assistant) | seq85 停尸 | 无(两会话均 assistant 收尾) | ✅ 已修复 |
结论:06-22 后修复的"工具返回协议/落库去重/中断"三项确实生效;但"system 污染"和"工具死循环"两类问题反而恶化——前者从残留 1 句变成 41 次拼接,后者从 4× 变成 17×。说明这两类是未根治的根因,须单独立项。