19 KiB
ISS-010 同 session 多轮诊断 Trace 隔离
状态:已归档
严重程度:高
发现时间:2026-07-10
来源:同一 sessionId 多轮 Chat E2E 验证
归档日期:2026-07-10
OpenSpec:openspec/changes/archive/2026-07-10-session-run-trace-isolation
实现提交:52bf030、26d5529、027aed1、d928a19、78c1477、f9df943
归档提交:3578709
背景
归档结论:当前 MVP 已将“会话态”和“运行态”拆开。sessionId 表示多轮会话目录和 Redis 上下文;runId 表示一次可回放诊断执行。Trace、Feedback、Evaluation、AIOps 和案例沉淀的新路径都按 runId 隔离。
原问题中 Chat 链路同时存在两类“会话”语义:
Redis SessionContext
-> 保存同一 sessionId 的多轮对话历史
-> 用于下一轮模型上下文
MySQL diagnosis_session / agent_step / tool_invocation
-> 保存诊断 Trace
-> 用于 Trace API、Verifier、Evidence score、Feedback 和评测
多轮对话需要继续复用 sessionId,否则无法保留上下文。但一次诊断 Trace 应该是可独立回放、可独立评分、可独立反馈的执行单元。
当前实现只按 sessionId 关联 Trace,导致同一个 sessionId 下多轮诊断的 step/tool 记录混在一起。
当前实现已改为:
chat_session(sessionId)
-> diagnosis_run(runId)
-> agent_step.run_id
-> tool_invocation.run_id
旧 diagnosis_session 保留为历史兼容和回滚表,新 Chat/AIOps 执行不再写入新的运行态。
归档结果
- OpenSpec 已归档到
openspec/changes/archive/2026-07-10-session-run-trace-isolation。 - 主规格已同步到
openspec/specs/session-run-trace-isolation/spec.md。 mvp/architecture/和mvp/tables/已更新为chat_session -> diagnosis_run -> agent_step/tool_invocation(run_id)模型。- Demo 脚本和 Trace UI 已支持
sessionId + runId精确 Trace 和 Feedback。 - Maven E2E、
scripts/query_mysql.pyDB 检查、logs/日志检查和 baseline drift 检查均已通过;未观察到 baseline drift。 devflow/projects/2026-07-10-session-run-trace-isolation/已保存 brief、evidence、decisions、acceptance。
E2E 证据
本次使用 mvp-demo profile 通过 Maven 启动服务,并用同一个 sessionId 连续请求两轮 /api/chat:
sessionId = e2e-multiturn-codex-20260710-1615
round 1 = 支付接口最近出现超时,请结合知识库、日志和指标判断可能原因,并给出修复建议。
round 2 = 基于上一轮结论,只列出目前最缺的三类证据,以及下一步应该优先查哪个系统。
验证结果:
- 第一轮成功,走多 Agent:
planner -> executor -> verifier -> composer。 - 第二轮成功,日志显示进入请求时
会话历史消息对数: 1,说明 Redis 历史上下文被复用。 /api/chat/session/{sessionId}返回messagePairCount=2。diagnosis_session只有一行,query被第二轮问题覆盖。agent_step返回 14 行,包含第一轮多 Agent step 和第二轮intelligent_assistantstep。tool_invocation返回 19 行,包含两轮工具调用。self_evaluation.verifier_evaluation仍保留第一轮 Verifier 结果;第二轮简单问答没有新的 Verifier,但 rule evaluation 会基于同 session 全部工具调用重新计算。
关键入库形态:
diagnosis_session
session_id = e2e-multiturn-codex-20260710-1615
query = round 2 question
status = SUCCESS
step_count = 14
tool_call_count = 19
agent_step
round 1: planner, executor..., verifier, composer
round 2: intelligent_assistant...
tool_invocation
round 1 tools + round 2 tools all under same session_id
本次验证产物保存在:
target/e2e/request-round1.jsontarget/e2e/response-round1.jsontarget/e2e/request-round2.jsontarget/e2e/response-round2.jsontarget/e2e/trace-after-round2.json
核心问题
P0:Trace 不是单次诊断的稳定回放
GET /api/diagnosis/{sessionId}/trace 会聚合同一 sessionId 下所有 agent_step 和 tool_invocation。
多轮之后,Trace 不再表示某一轮诊断,而是混合历史执行轨迹。
P0:Verifier 和评分可能读取跨轮证据
Verifier、Gatekeeper、ToolTraceSummaryService 和 EvaluationService 当前主要按 sessionId 查询工具调用。
如果上一轮和当前轮证据混在一起,当前轮可能引用或评分到历史工具结果。
P1:反馈语义不清晰
feedback 当前在 diagnosis_session 上按 sessionId 保存。
多轮之后,用户反馈的是哪一轮答案不再明确。useful 反馈沉淀到 case_library 时也可能关联到最新主表答案,而不是用户实际评价的那一轮。
P1:diagnosis_session 字段被覆盖但子表追加
主表 query/answer/status/self_evaluation/step_count/tool_call_count 表示最新运行或混合统计,子表却保留多轮历史。
这会让 Trace summary、数据库统计和人工排查产生歧义。
已确认决策
D1:runId 是正式 API 字段
/api/chat 和 /api/ai_ops 的响应或 SSE 消息需要暴露本次执行的 runId。
sessionId = 多轮对话上下文 ID
runId = 本轮诊断执行 ID
新客户端应优先用 runId 查询 Trace 和提交 Feedback。旧客户端只传 sessionId 时,服务端兼容解析该 session 的最新 run。
D2:拆分会话态和运行态
不再把 session、run、trace 全部塞进 diagnosis_session 一张主表。
新增两张主表:
chat_session
-> 多轮对话上下文主表
diagnosis_run
-> 单次诊断执行主表
Trace 继续使用现有明细表表达:
agent_step
tool_invocation
暂不新增单独的 diagnosis_trace 或 trace_event 主表。
D3:runId 格式
使用 run- + UUID 全量字符串。
run-550e8400-e29b-41d4-a716-446655440000
D4:Trace API 兼容旧路径
GET /api/diagnosis/{sessionId}/trace
-> 查该 session 最新 run
GET /api/diagnosis/{sessionId}/trace?runId=run-xxx
-> 查指定 run
指定 runId 时必须校验该 run 属于 path 中的 sessionId。
最新 run 建议按 diagnosis_run.created_at DESC, id DESC 解析,避免旧 run 因反馈或异步评分更新 updated_at 后被误认为最新。
D5:Feedback 优先绑定 run
Feedback request 支持 runId。
- 有
runId:绑定指定 run。 - 无
runId:短期兼容绑定该sessionId最新 run,并显式标记 fallback。 case_library.diagnosis_id新数据保存run_id。
兼容语义:历史 case_library.diagnosis_id 可能保存 diagnosis_session.session_id;本 change 之后自动沉淀的新数据保存 diagnosis_run.run_id。查询、幂等和文档需要在过渡期识别两种来源,避免把旧案例误判为无效数据。
D6:所有 /api/chat 执行请求都创建 run
只要请求通过参数校验并进入 ChatService.executeChatWithStrategy,就创建新的 diagnosis run。
- 简单问答也创建 run。
- 复杂诊断也创建 run。
- 空问题等参数校验失败不创建 run。
D7:AIOps 同步纳入 run 隔离
每次 /api/ai_ops 执行也创建新的 diagnosis run。AIOps 的 step、tool invocation 和 rule evaluation 都按 runId 隔离。
阶段说明:AIOps 可作为独立实现切片排在 Chat 之后,但必须在本 change 整体完成前落地;Chat-only 的中间状态只能作为过渡验证状态,不能作为生产完成状态归档。
D8:chat_session 第一阶段只保存会话元数据
chat_session 是会话目录/索引表,不保存完整对话历史正文。
建议保存:
session_id
status
message_pair_count
created_at
last_active_at
expires_at
完整多轮对话历史继续放在 Redis SessionContext.messageHistory,用于下一轮 prompt 上下文。
每轮需要长期审计的用户问题和最终回答保存到 diagnosis_run.query / diagnosis_run.answer。
chat_session.expires_at 只表示 MySQL 会话目录的过期/清理元数据;Redis TTL 到期后,SessionContext.messageHistory 可能不再存在,但已经持久化的 diagnosis_run、agent_step 和 tool_invocation 仍作为审计记录保留。
如果未来需要长期保存完整聊天历史,再单独设计 chat_message 表,不在本阶段引入。
D9:旧 diagnosis_session 表保留但新代码不再写入
新增 diagnosis_run 后,旧 diagnosis_session 不立即删除、不立即改造成 view、不直接重命名。
迁移策略:
- 新增
chat_session/diagnosis_run。 - 为
agent_step/tool_invocation新增 nullablerun_id。 - 将旧
diagnosis_session数据迁移/复制为diagnosis_run兼容记录。 - 为旧
agent_step/tool_invocation回填对应run_id。 - 增加必要索引和查询方法,先保持兼容读取。
- 新代码切换为只写
chat_session和diagnosis_run,并为新 step/tool 写入run_id。 - 验证新旧数据
run_id覆盖情况后,再将新写路径要求run_id非空,并补充索引/约束。 - 旧
diagnosis_session暂时保留,用于历史核对和回滚窗口。 - 后续确认无依赖后,再单独归档或删除旧表。
D10:提供轻量 run 列表 API
新增轻量查询接口,用于查看一个 Chat Session 下有哪些 Diagnosis Run。
GET /api/chat/session/{sessionId}/runs
建议返回字段:
runId
sessionId
query
status
agentFlow
answerPreview
stepCount
toolCallCount
createdAt
updatedAt
该接口只读 diagnosis_run 主表,不展开 agent_step / tool_invocation 大字段。
D11:Feedback 缺少 runId 时短期兼容,长期收紧
Feedback 新协议优先要求 runId。
短期兼容策略:
- 有
runId:绑定指定 run。 - 无
runId:绑定该sessionId最新 run。 - 无
runIdfallback 时,在响应或日志中明确标记fallbackToLatestRun=true,并返回实际绑定的runId。
长期收紧策略:
- 当前端、demo 脚本和外部调用方都完成
runId传递后,再评估是否将缺少runId改为参数错误。
D12:同步更新 demo 脚本和 Trace UI 的 runId 最小支持
本 issue 实施范围包含 demo 脚本和 Trace UI 的最小协议适配。
范围:
- Demo 脚本读取
/api/chat或/api/ai_ops返回的runId。 - Demo 脚本查询 Trace 时传
?runId=...。 - Trace UI 支持 URL 参数
?sessionId=...&runId=...。 - Trace UI 查询时如果有
runId,带上runId。 - 不在本阶段实现完整 run 列表 UI。
目标语义
引入明确的 sessionId / runId 分层:
sessionId = 多轮对话上下文
runId = 单次诊断执行 / 单次可回放 Trace
目标关系:
chat_session(sessionId)
-> one conversation context
-> conversation metadata / TTL / last active state
Redis SessionContext(sessionId)
-> hot messageHistory cache
-> supports prompt context window
diagnosis_run(runId, sessionId)
-> one diagnosis run
agent_step(runId, sessionId)
-> steps of one run
tool_invocation(runId, sessionId)
-> tool calls of one run
建议方案
采用“拆分主表 + 复用现有 Trace 明细表”的方案:
- 新增
chat_session。 - 新增
diagnosis_run。 - 逐步迁移当前
diagnosis_session语义到diagnosis_run。 agent_step新增run_id,继续保留session_id作为冗余筛选和兼容字段。tool_invocation新增run_id,继续保留session_id作为冗余筛选和兼容字段。- Trace API 聚合
diagnosis_run + agent_step + tool_invocation。
建议核心字段:
chat_session
id
session_id unique
status
message_pair_count
created_at
last_active_at
expires_at
diagnosis_run
id
run_id unique
session_id
query
status
agent_flow
answer
self_evaluation
feedback
total_duration_ms
total_token_count
step_count
tool_call_count
created_at
updated_at
agent_step(run_id, step_index)
tool_invocation(run_id, id)
理由:
chat_session只表达会话态,避免会话上下文和诊断结果混在一起。diagnosis_run只表达一次执行,天然隔离每轮 Trace、评分和反馈。agent_step/tool_invocation已足够表达 Trace 明细,暂不需要额外 trace 主表。- 后续如果需要统一时间线,再增加
trace_event,不阻塞本次隔离。
分阶段计划
Phase 0:协议基线和数据边界
目标:先把语义定死,避免实现中反复。
已确认基线:
/api/chat是否返回runId。GET /api/diagnosis/{sessionId}/trace默认查最新 run 还是要求显式传runId。- Feedback 是否优先绑定
runId,只有旧请求缺失runId时才回退最新 run。 - AIOps 是否和 Chat 同步接入
runId。 - 简单问答是否也创建 diagnosis run。
建议默认:
/api/chat返回sessionId + runId。GET /api/diagnosis/{sessionId}/trace兼容查最新 run。GET /api/diagnosis/{sessionId}/trace?runId=...查指定 run。- Feedback 优先按
runId绑定。 - Chat 简单问答也创建 run。
- AIOps 同步接入 run 隔离。
Phase 1:Schema 迁移和历史数据兼容
目标:引入 chat_session / diagnosis_run,并保留旧数据可查询。
任务:
- Flyway 新增
chat_session。 - Flyway 新增
diagnosis_run。 - 为旧
diagnosis_session生成兼容diagnosis_run记录。 - 为旧
agent_step/tool_invocation回填对应run_id。 - 增加
find latest run by sessionId查询。 - 增加
find by runId查询。 - 保留旧
diagnosis_session一段时间,新代码不再写入。
验收:
- 旧 session 的 Trace 仍可查。
- 新索引存在。
- 不改变旧
/api/chat必需字段。 agent_step/tool_invocation支持 nullablerun_id并完成旧数据回填。- 新增 repository 查询可以按
sessionId找最新 run、按runId找指定 run。
Phase 2:Chat 写入切到 runId
目标:每轮 /api/chat 创建一个新的 run,step/tool 按 run 隔离。
任务:
ChatService每次执行生成新的runId。ChatController确保chat_session存在并更新会话态。ChatService按runId创建diagnosis_run。AgentLoggingHook写入agent_step.run_id。ToolInvocationRecorder写入tool_invocation.run_id。SessionContextHolder或新的上下文 holder 同时携带sessionId + runId。backfillSessionMetrics按runId统计。EvaluationService按runId读取工具调用。
验收:
- 同一
sessionId连续两轮后,diagnosis_run有两行不同run_id。 /api/chat响应增加正式字段runId。- 两轮
agent_step/tool_invocation分别按各自run_id查询。 - Redis
messagePairCount仍为 2,证明上下文不被破坏。
Phase 3:Trace API 兼容和精确查询
目标:Trace API 可查最新 run,也可查指定 run,并能列出一个 session 下的 run。
任务:
GET /api/diagnosis/{sessionId}/trace从diagnosis_run默认解析最新 run。- 增加
runIdquery 参数。 - Trace response 增加
runId。 - 新增
GET /api/chat/session/{sessionId}/runs。
验收:
- 不传
runId返回最新 run。 - 传第一轮
runId只返回第一轮 step/tool。 - 传第二轮
runId只返回第二轮 step/tool。 - run 列表 API 只返回轻量 run 摘要,不展开 trace 明细。
- Demo 脚本和 Trace UI 的
runId最小适配按 OpenSpec tasks 放到 Phase 6,避免 Phase 3 同时混入前端/脚本范围。
Phase 4:Feedback 和 CaseLibrary 绑定 run
目标:反馈明确评价哪一轮诊断。
任务:
- Feedback request 支持
runId。 - 旧请求只有
sessionId时短期绑定最新 run,并显式标记 fallback。 case_library.diagnosis_id新数据保存run_id。CaseLibraryService以 run 为来源生成 case,并用run_id做新数据幂等键。- 文档说明
diagnosis_id的过渡语义:旧数据可能是session_id,新数据是run_id。
验收:
- 同 session 多轮后,对第一轮提交 feedback 不会覆盖第二轮。
- useful 生成 case 时能定位到对应 run 的 query/answer。
Phase 5:AIOps 同步 run 隔离
目标:AIOps 使用同样的 run 语义,避免另一条入口继续混杂。
任务:
AiOpsService生成并返回/透出runId。- AIOps
agent_step/tool_invocation按runId隔离。 - AIOps rule evaluation 写入当前 run 的
diagnosis_run.self_evaluation.aiops_rule_evaluation。 - AIOps Trace 查询兼容
sessionId + runId。
验收:
- 同一 AIOps
sessionId重跑不会混合 step/tool。 - AIOps rule evaluation 只读取当前 run 工具调用。
暂不做
- 暂不新增
diagnosis_trace或trace_event主表。 - 暂不做完整 run 列表 UI。
- 暂不删除历史 Trace 数据。
- 暂不改变 Redis 多轮上下文窗口策略。
- 暂不立即物理删除旧
diagnosis_session表。
风险
1. 兼容风险
现有脚本、Trace 页面和反馈接口可能只知道 sessionId。
缓解:保留 sessionId 默认查最新 run 的行为。
2. 异步上下文风险
工具调用和 Agent hook 依赖 ThreadLocal / RunnableConfig 传递上下文。
缓解:统一上下文对象,明确 sessionId 和 runId 必须同时传递。
3. 历史数据回填风险
旧数据没有真实 run 边界,只能按当前 diagnosis_session 生成一条兼容 diagnosis_run。
缓解:旧数据视为单 run,不尝试拆分历史混合数据。
4. 评分口径变化风险
按 runId 隔离后,工具调用数和 evidence score 可能下降,但语义更正确。
缓解:更新 eval fixture 和 baseline,记录这是预期行为变化。
已收敛问题
本 issue 当前已经收敛以下设计边界:
runId是正式 API 字段。chat_session和diagnosis_run拆分为两张主表。- Trace 明细继续由
agent_step/tool_invocation承载。 chat_session只保存元数据,不保存完整对话历史。- 旧
diagnosis_session保留但新代码不再写入。 - 提供轻量 run 列表 API。
- Feedback 缺少
runId时短期兼容、长期收紧。 - Demo 脚本和 Trace UI 做
runId最小支持。
相关文件
mvp/architecture/session-trace-lifecycle.mdmvp/architecture/data-model.mdmvp/tables/聊天会话表-chat_session.mdmvp/tables/诊断运行表-diagnosis_run.mdmvp/archive/2026-07-20-doc-cleanup/tables/诊断会话表-diagnosis_session.mdmvp/tables/Agent步骤表-agent_step.mdmvp/tables/工具调用表-tool_invocation.mdmvp/tables/案例库表-case_library.mdsrc/main/java/com/superbiz/agent/controller/ChatController.javasrc/main/java/com/superbiz/agent/service/ChatService.javasrc/main/java/com/superbiz/agent/service/AiOpsService.javasrc/main/java/com/superbiz/agent/service/DiagnosisTraceService.javasrc/main/java/com/superbiz/agent/service/EvaluationService.javasrc/main/java/com/superbiz/agent/service/FeedbackService.javasrc/main/java/com/superbiz/agent/service/CaseLibraryService.javasrc/main/java/com/superbiz/agent/hook/AgentLoggingHook.javasrc/main/java/com/superbiz/agent/service/ToolInvocationRecorder.javasrc/main/java/com/superbiz/agent/util/SessionContextHolder.javasrc/main/resources/db/migration/V005__create_session_storage.sql