Files
SuperBizAgent-java/mvp/issues/archived/ISS-010-session-run-trace-isolation.md

19 KiB
Raw Permalink Blame History

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.py DB 检查、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_assistant step。
  • 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.json
  • target/e2e/response-round1.json
  • target/e2e/request-round2.json
  • target/e2e/response-round2.json
  • target/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、不直接重命名。

迁移策略:

  1. 新增 chat_session / diagnosis_run。
  2. 为 agent_step / tool_invocation 新增 nullable run_id。
  3. 将旧 diagnosis_session 数据迁移/复制为 diagnosis_run 兼容记录。
  4. 为旧 agent_step / tool_invocation 回填对应 run_id。
  5. 增加必要索引和查询方法,先保持兼容读取。
  6. 新代码切换为只写 chat_session 和 diagnosis_run,并为新 step/tool 写入 run_id。
  7. 验证新旧数据 run_id 覆盖情况后,再将新写路径要求 run_id 非空,并补充索引/约束。
  8. 旧 diagnosis_session 暂时保留,用于历史核对和回滚窗口。
  9. 后续确认无依赖后,再单独归档或删除旧表。

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。
  • 无 runId fallback 时,在响应或日志中明确标记 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 明细表”的方案:

  1. 新增 chat_session。
  2. 新增 diagnosis_run。
  3. 逐步迁移当前 diagnosis_session 语义到 diagnosis_run。
  4. agent_step 新增 run_id,继续保留 session_id 作为冗余筛选和兼容字段。
  5. tool_invocation 新增 run_id,继续保留 session_id 作为冗余筛选和兼容字段。
  6. 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:协议基线和数据边界

目标:先把语义定死,避免实现中反复。

已确认基线:

  1. /api/chat 是否返回 runId。
  2. GET /api/diagnosis/{sessionId}/trace 默认查最新 run 还是要求显式传 runId。
  3. Feedback 是否优先绑定 runId,只有旧请求缺失 runId 时才回退最新 run。
  4. AIOps 是否和 Chat 同步接入 runId。
  5. 简单问答是否也创建 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 支持 nullable run_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。
  • 增加 runId query 参数。
  • 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 工具调用。

暂不做

  1. 暂不新增 diagnosis_trace 或 trace_event 主表。
  2. 暂不做完整 run 列表 UI。
  3. 暂不删除历史 Trace 数据。
  4. 暂不改变 Redis 多轮上下文窗口策略。
  5. 暂不立即物理删除旧 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.md
  • mvp/architecture/data-model.md
  • mvp/tables/聊天会话表-chat_session.md
  • mvp/tables/诊断运行表-diagnosis_run.md
  • mvp/tables/诊断会话表-diagnosis_session.md
  • mvp/tables/Agent步骤表-agent_step.md
  • mvp/tables/工具调用表-tool_invocation.md
  • mvp/tables/案例库表-case_library.md
  • src/main/java/com/superbiz/agent/controller/ChatController.java
  • src/main/java/com/superbiz/agent/service/ChatService.java
  • src/main/java/com/superbiz/agent/service/AiOpsService.java
  • src/main/java/com/superbiz/agent/service/DiagnosisTraceService.java
  • src/main/java/com/superbiz/agent/service/EvaluationService.java
  • src/main/java/com/superbiz/agent/service/FeedbackService.java
  • src/main/java/com/superbiz/agent/service/CaseLibraryService.java
  • src/main/java/com/superbiz/agent/hook/AgentLoggingHook.java
  • src/main/java/com/superbiz/agent/service/ToolInvocationRecorder.java
  • src/main/java/com/superbiz/agent/util/SessionContextHolder.java
  • src/main/resources/db/migration/V005__create_session_storage.sql