Files
DevFlow/docs/05-代码审查/aichat-会话实测分析-2026-07-07.md
绝尘 e9e3578d26 修复: DeepSeek 400 全量扫描 + 队列 per-conv 隔离
- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条)
- queue 加 conversationId 字段,按会话精准 drain
- regenerate/editMessage 只清本会话排队消息
- newConversation 保留旧会话排队消息
- AiError 只清出错会话的队列项
2026-07-20 00:19:50 +08:00

14 KiB
Raw Blame History

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 比 seq1 1782971504351 晚 168 秒(时序倒挂)
  • db 证据:ai_messages seq0 row,role=system,status=active,content 长 1879
  • 根因假设(强):系统 prompt 注入路径把"上一轮 assistant 流式片段"误当 system 上下文写入;且写入库时陷入重复拼接循环(疑似字符串 builder 在流式 chunk 累积时无去重/无终止条件)。这是 aichat-session-analysis-2026-06-22 P2-1 的复发且恶化——上次只见 1 条 assistant 语义残留,本次是 41 次死循环拼接。
  • 影响:
    1. system 消息会作为 prompt 头部送入 LLM,1879 字符无意义噪声污染每一轮上下文
    2. role 错位会误导模型(system 应是约束指令,却变成"我说的"内容)
    3. 时序倒挂破坏压缩/排序/计费
  • 待核验源码点:
    • system prompt 注入路径(ai_messages 写入 system 行的位置,src-tauri/src/commands/ai/)
    • 流式 chunk 累积逻辑(查 for / push_str / += 类字符串拼接,是否有循环退出条件)
    • timestamp 赋值逻辑(为何 system 行用了"插入时刻"而非"会话起始时刻")
  • 关联:印证 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%
  • 根因假设:
    1. 无工具结果缓存层:会话级无"同参工具调用 → 返回缓存命中提示"机制,LLM 每次发起都真执行
    2. prompt 未告知 LLM 已读内容:read_file 返回的文件指纹/路径未在后续 prompt 中显式标注"已读",LLM 失忆反复读
    3. 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-apihszd-adminhszd-test,每次都覆盖前值,assistant 没意识到多工程 monorepo 的 path 字段不该是单子工程
  • db 证据:bind_directory 3 次调用 args 显示 path 被依次覆盖,最终 assistant 自己回滚用 update_project(path='E:/wk-hszd') 修正
  • 根因假设:
    1. agent 无"宣称完成前的强制自检"(对照 aichat-session-analysis-2026-06-22 P1-1)
    2. bind_directorypath 字段语义对多工程 monorepo 有歧义(LLM 以为是"当前操作的子工程",实际是"项目根")
  • 影响:用户对 agent 完成度零信任,需人工核对每个字段
  • 待核验/落点:
    • agent 行为策略(prompt 加自检 checklist)
    • bind_directory schema 注释补"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_directory IPC 的 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_summaryis_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/configis_absolute() 返 false,persistent 为空时旧逻辑直接返 NeedsAuthorization 跳过黑名单
P1-2 bind_directory 多工程语义错配 commands/ai/tool_registry.rs 工具描述补充 monorepo 语义:path 应为项目根路径(非子工程),多次调用会覆盖,子工程信息走 list_project_modules
P1-1 压缩产出反向膨胀 误报撤销: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×。说明这两类是未根治的根因,须单独立项。