Files
SuperBizAgent-java/mvp/engineering/diagnosis/Session-Run-Trace隔离-从串线到可回放.md

316 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Session、Run、Trace 隔离:一次身份建模错误的修复
**更新日期**:2026-07-29
**性质**:MVP 工程问题与架构决策复盘
**结论状态**:Session / Run 身份模型沿用至当前架构
---
## 1. 问题到底是什么
一句话定义:**系统用同一个 `sessionId`,同时标识“多轮对话”和“一次诊断执行”,导致一次 Trace 不再对应一次真实执行。**
问题由一个两轮 E2E 暴露。用户在同一会话中连续发起两次请求:
```text
Round 1:诊断支付接口超时
Round 2:基于上一轮结论,列出还缺哪些证据
```
两轮复用 `sessionId` 是正确的,因为第二轮需要上一轮上下文。但当时持久化和 Trace 查询也只使用 `sessionId`:
- `diagnosis_session` 是覆盖写,第二轮把第一轮的 query、answer、status 覆盖掉;
- `agent_step` 和 `tool_invocation` 是追加写,两轮明细累积在同一个 `sessionId` 下;
- Trace API 再按 `sessionId` 聚合主表和明细。
结果不是简单的“重复数据”,而是一个系统中从未真实发生过的混合执行:
```mermaid
flowchart LR
R1["Round 1<br/>query A / steps A / tools A"] --> SID["同一个 sessionId"]
R2["Round 2<br/>query B / steps B / tools B"] --> SID
SID --> Main["diagnosis_session<br/>只剩 query B / answer B"]
SID --> Steps["agent_step<br/>steps A + steps B"]
SID --> Tools["tool_invocation<br/>tools A + tools B"]
Main --> Trace["混合 Trace"]
Steps --> Trace
Tools --> Trace
Trace --> Error["无法回答:<br/>哪组证据支持了哪次回答?"]
```
这个错误会沿数据链继续放大:
| 消费方 | 错误结果 |
|---|---|
| Trace 回放 | 一条 Trace 混合两轮步骤和 Tool |
| Verifier / Gatekeeper | 当前轮可能读到上一轮证据 |
| Evaluation | 评分对象和证据集合不再属于同一次执行 |
| Feedback | 无法确定用户评价的是哪一轮回答 |
| CaseLibrary | 可能把反馈沉淀到错误的 query/answer 上 |
因此,核心问题不是某个 Repository 的更新方式,而是**会话边界被错误地当成了证据与审计边界**。
---
## 2. 设计必须守住什么
修复前先定义四条不变量。后续方案不是凭表结构偏好选择,而是看能否同时满足这些约束。
### 不变量 1:一次执行只有一个稳定身份
从请求被接受到最终 `SUCCESS / FALLBACK / FAILED / CANCELLED`,必须有一个不可变 ID。模型步骤、Tool 调用、预算、最终答案和反馈都属于它。
### 不变量 2:一条 Trace 只描述一次执行
Trace 中的 Run、AgentStep、ToolInvocation 和生命周期事件必须使用同一个 exact ID 聚合,不能依赖时间邻近或“最新一条”猜测归属。
### 不变量 3:上下文连续不等于执行合并
同一个 Session 可以包含多个 Run。后一个 Run 可以读取前一轮安全发布结果,但不能继承前一轮的 Tool、Trace、评分或失败状态。
### 不变量 4:兼容不能伪造精度
旧客户端可以有迁移期 fallback,旧数据也可以保留;但系统必须让歧义可观察,不能把无法恢复的历史混合数据伪装成精确多轮记录。
这四条不变量共同导出一个结论:系统需要两个身份,而不是给 `sessionId` 增加更多解释。
---
## 3. 为什么不是在旧表上继续修
设计阶段考虑的不是“拆表还是不拆表”这一个问题,而是如何建立稳定的执行边界。
| 候选方案 | 能解决什么 | 为什么没有选择 |
|---|---|---|
| 继续复用 `sessionId`,修正覆盖逻辑 | 避免主表被覆盖 | 明细仍无法区分轮次,根因未解决 |
| 增加 `round_no` | 可以表示第几轮 | 并发请求、重试和多个执行入口下顺序不稳定;外部引用仍需复合身份 |
| 按时间窗口拆分历史 Step/Tool | 无需改协议 | 时间不能证明归属,会制造看似精确的错误 Trace |
| 每次请求插入一条新的 `diagnosis_session` | 形成一行一次执行 | 实际上已经引入 Run 概念,但名称和 Session 生命周期仍混淆,也缺少会话主实体 |
| 拆分 `chat_session` 与 `diagnosis_run` | 显式表达一对多生命周期 | 需要 Schema、API 和客户端迁移,但能满足全部不变量 |
最终选择最后一种。它的判断依据不是“范式更规范”,而是只有它能让对话连续性和执行可审计性同时成立。
---
## 4. 核心设计
目标模型是一个清晰的一对多关系:
```mermaid
flowchart TB
Client["Client"] --> Session["chat_session<br/>sessionId:多轮对话目录"]
Session --> Run1["diagnosis_run<br/>runId 1:一次执行"]
Session --> Run2["diagnosis_run<br/>runId 2:另一次执行"]
Run2 --> Step["agent_step<br/>模型步骤 metadata"]
Run2 --> Tool["tool_invocation<br/>Tool 审计 metadata"]
Run2 --> Event["diagnosis_trace_event<br/>生命周期 Timeline"]
Run2 --> Reasoning["agent_reasoning_audit<br/>受限原文"]
Run2 --> Trace["普通 Trace API"]
Step --> Trace
Tool --> Trace
Event --> Trace
Reasoning --> Audit["独立 Reasoning API"]
```
这里有三个不同的职责:
- `chat_session` 回答“哪些 Run 属于同一段对话”;
- `diagnosis_run` 回答“这次请求的输入、状态、结果和资源消耗是什么”;
- Trace 回答“这个 Run 具体经历了什么”,它是按 Run 聚合的读模型。
---
## 5. 六项关键决策
### 决策 1:引入正式的 `runId`
**选择**:每次有效 Chat 执行创建新的 `runId`,并通过 SSE metadata 返回 `sessionId + runId`。
**理由**:Run 必须能被 API、数据库、日志、Feedback 和评测独立引用。数据库自增 ID 不适合作为外部协议;轮次编号又不能稳定处理并发和重试。
**代价**:客户端必须保存并向后续 Trace/Feedback 请求传递 `runId`。
**边界**:`runId` 是不透明标识。早期实现使用 `run-` + UUID,后续格式发生过演进,客户端不得解析其前缀或长度。
### 决策 2:拆分会话态与运行态
**选择**:新增 `chat_session` 和 `diagnosis_run`,旧 `diagnosis_session` 停止承载新的运行写入。
**理由**:两者生命周期不同。
| 对象 | 保存内容 | 更新特点 |
|---|---|---|
| `chat_session` | 会话状态、轮次数、最近活动时间等目录信息 | 跨多轮持续更新 |
| `diagnosis_run` | 单次 query、answer、终态、intent、release outcome、预算 | 一次执行内从 RUNNING 走向唯一终态 |
完整多轮正文没有因为拆表就复制到 MySQL。身份拆分解决的是审计归属,不应顺带扩大长期数据保存范围。
### 决策 3:Trace 是 Run 的聚合视图,不另造身份
**选择**:第一阶段复用 `agent_step` 和 `tool_invocation`,增加 `run_id`;不为了修复隔离问题再创建一个独立 `traceId`。
**理由**:隔离所缺的是执行外键,不是第三套身份。引入 `traceId` 只会产生 `sessionId / runId / traceId` 的映射问题。
后续单 Agent + Harness 重构新增 `diagnosis_trace_event`,用于表达统一生命周期 Timeline。这是 Run 下的新明细,不是新的聚合根,也没有改变 `runId` 的边界。
### 决策 4:exact Run 是目标协议,latest Run 只是迁移桥梁
**选择**:目标查询使用:
```http
GET /api/diagnosis/{sessionId}/trace?runId={runId}
```
服务端同时验证 Run 存在且属于 path 中的 Session,防止跨 Session 串读。
旧客户端暂时只传 `sessionId` 时,可以解析 latest Run;但这是显式兼容路径,不是新的业务语义。如果必须计算 latest,应按:
```text
created_at DESC, id DESC
```
而不是 `updated_at`。旧 Run 可能因 Feedback 或异步处理再次更新,最近修改不等于最近执行。
### 决策 5:所有下游语义绑定 Run
**选择**:Trace、Feedback、Evaluation、Tool evidence 和新 Case provenance 都以 `runId` 为执行边界。
**理由**:这些对象评价或引用的是一次回答,不是整段会话。
Feedback 在迁移期缺少 `runId` 时可以绑定 latest Run,但响应必须暴露 `fallbackToLatestRun=true`。兼容如果不可观察,就会从临时措施变成永久歧义。
`case_library.diagnosis_id` 因复用旧列,在过渡期存在历史 `session_id` 和新 `run_id` 两种语义。这是明确接受的迁移成本,而不是应被隐藏的数据一致性。
### 决策 6:历史混合数据不做推测性拆分
**选择**:每条旧 `diagnosis_session` 最多映射为一个 compatibility Run,不根据时间或 Agent 名称猜测真实轮次。
**理由**:旧数据没有记录边界,任何自动拆分都只能产生无法证明的归属。审计系统宁可明确“不知道”,也不能制造虚假的精确回放。
---
## 6. 协议和影响范围
这是一次有意的行为与协议变化,不是纯内部重构。
| 范围 | 变化 | 受影响方 |
|---|---|---|
| Chat / SSE | metadata 增加 `runId` | 前端、脚本、调用方 |
| Trace API | 支持 exact `runId` 查询 | Trace UI、排障工具、评测 |
| Run API | 提供 Session 下的 Run 列表 | 多轮历史浏览 |
| Feedback | request/response 增加 Run 绑定和 fallback 标志 | 前端、CaseLibrary |
| 数据库 | 新增两张主表,明细增加 `run_id` | 持久化、迁移、查询脚本 |
| 其它入口 | 当时的 AIOps 同步采用 Run 边界 | SSE 消费方、Trace |
之所以把当时的 AIOps 一并迁移,是因为它同样会产生可回放执行;只修 Chat 会留下第二条具有同类缺陷的数据链。后续 ISS-014 删除了旧 AIOps 双入口,但这不改变当时“所有执行入口必须共享 Run 边界”的设计判断。
---
## 7. 风险如何处理
### 风险 1:兼容路径继续产生歧义
控制方式是让 fallback 可观察,并把 exact `runId` 定义为目标协议。兼容是迁移机制,不能反向成为领域模型。
### 风险 2:异步链路丢失或串用身份
`sessionId` 与 `runId` 必须作为同一执行上下文传播。当前架构将二者放入显式 `RunContext`,ToolBoundary、审计 Hook 和持久化都校验当前 Run,避免只依赖线程隐式状态。
### 风险 3:历史和新 provenance 共用旧列
保留旧列降低了迁移破坏性,但查询和文档必须承认双语义,不能把旧 `session_id` 当作非法 `run_id` 清理。
### 风险 4:旧混合 Trace 永远无法恢复
这是明确接受的事实。系统保留 compatibility 访问和回滚能力,但不承诺不存在的历史精度。
---
## 8. 如何证明设计成立
验收问题不是“接口里有没有 `runId`”,而是下面五个条件是否同时成立:
```text
同一 Session 连续执行两轮
+ 两轮获得不同 runId
+ 每个 exact Trace 只返回本 Run 明细
+ 数据库不存在跨 Run 混合行
+ 第二轮仍能使用会话上下文
```
真实 E2E 使用:
```text
sessionId = e2e-phase6-chat-codex-20260710-2120
run1 = run-e2a97696-4398-4abc-90e4-28f45c838f92
run2 = run-76ce6a6e-92ab-40c9-800a-eca0c1bb5172
```
结果:
- 两轮 Chat 成功并复用同一个 `sessionId`;
- 两轮返回不同 `runId`;
- run1 exact Trace 只返回 run1,run2 exact Trace 只返回 run2;
- `diagnosis_run` 中存在两条独立运行记录;
- AgentStep:run1 为 10 行,run2 为 9 行;
- ToolInvocation:run1 为 14 行,run2 为 8 行;
- mixed row check 为 0;
- `chat_session.message_pair_count = 2`,上下文连续性没有因隔离而丢失;
- focused tests、baseline diff、日志和数据库核验通过,未观察到 baseline drift。
这组证据同时验证了“该分开的确实分开”和“该连续的仍然连续”。
---
## 9. 后续演进验证了什么
系统后来从多角色 Agent 编排重构为单 Diagnosis ReAct Agent + Harness。执行结构发生了大变化,但 Session / Run 模型没有被替换,反而成为新架构的基础:
- `ChatApplicationUseCase` 创建和结束 Run;
- `RunContext` 显式携带 `sessionId + runId`;
- ToolBoundary 校验 Tool 请求属于当前 Run;
- Redis canonical invocation 使用 `runId + toolCallId` 定位;
- EvidenceGuard 只接受当前 Run 的 READY invocation;
- AgentStep、ToolInvocation、TraceEvent 和 ReasoningAudit 都绑定 Run。
这说明当时解决的不是某一版代码的局部 bug,而是找到了稳定的领域边界。Agent 编排可以替换,Session 与 Run 的生命周期差异不会消失。
---
## 10. 可复用的设计判断
这次问题可以归纳为四条通用经验:
1. **生命周期不同的对象,不应共享同一个聚合身份。**
2. **上下文复用不代表证据、状态和审计记录也可以复用。**
3. **兼容 fallback 必须可观察、可退出,不能静默猜测。**
4. **无法恢复的历史边界应明确降级,不能伪造精确性。**
判断类似系统是否存在同类问题,可以直接问:
- 一次请求是否有独立于 Session 的执行 ID?
- 所有 Step、Tool、Event、Feedback 是否都能精确归属一次执行?
- “查询最新”是否被误当成“查询指定执行”?
- 主表覆盖写、明细追加写是否使用了同一个过宽的关联键?
- 历史迁移是在保留不确定性,还是通过猜测制造精确性?
如果这些问题没有明确答案,那么 Trace 即使看起来完整,也未必能作为可信审计证据。
---
## 11. 资料索引
- [ISS-010:同 session 多轮诊断 Trace 隔离](../../issues/archived/ISS-010-session-run-trace-isolation.md)
- [Session、Run 与 Trace 生命周期](../../architecture/session-trace-lifecycle.md)
- [当前 MVP 架构](../../architecture/current-mvp-architecture.md)
- [数据表索引](../../tables/README.md)
- [devflow brief](../../../devflow/projects/2026-07-10-session-run-trace-isolation/brief.md)
- [devflow decisions](../../../devflow/projects/2026-07-10-session-run-trace-isolation/decisions.md)
- [devflow acceptance](../../../devflow/projects/2026-07-10-session-run-trace-isolation/acceptance.md)
- [devflow evidence](../../../devflow/projects/2026-07-10-session-run-trace-isolation/evidence.md)