399 lines
19 KiB
Markdown
399 lines
19 KiB
Markdown
# 审计设计演进:从 Session Trace 到 Exact Run
|
||
|
||
今天看到的审计系统包含 exact Run、决策 Timeline、Agent Step、Tool Invocation、Token 对账和独立 Reasoning。它看起来像一套预先设计好的完整架构,但真实过程并不是这样。
|
||
|
||
这套设计是被一系列具体问题推出来的:先是答案无法回放,然后是 Tool 记录语义不一致,再后来是同一 Session 多轮串线,最后 Single ReAct 重构又让旧审计链失去所有权。每次变化都解决了当时最紧迫的问题,也留下了下一阶段才看得见的新缺口。
|
||
|
||
这篇文章不按提交逐条记流水账,而是解释六次能力跃迁。第一次阅读只看第 1、2、4、7 和 10 节,就能抓住主线。
|
||
|
||
## 1. 先看完整演进
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["只能看到最终答案"] --> B["01 可查询 Trace<br/>答案可以回放"]
|
||
B --> C["02 Evidence 与质量语义<br/>记录开始可比较"]
|
||
C --> D["03 exact Run<br/>多轮不再串线"]
|
||
D --> E["04 Canonical Tool Truth<br/>事实与长期审计分层"]
|
||
E --> F["05 Single ReAct 审计重建<br/>责任回到 Harness"]
|
||
F --> G["06 Timeline / Token / Reasoning<br/>决策、成本与敏感正文分治"]
|
||
```
|
||
|
||
六个阶段分别改变了审计系统回答问题的能力:
|
||
|
||
| 阶段 | 审计开始能够回答 |
|
||
|---|---|
|
||
| 01 可查询 Trace | 一次诊断大致执行过哪些 Agent Step 和 Tool |
|
||
| 02 语义加固 | Tool 成功、无证据、失败以及 Prompt/规则版本分别是什么 |
|
||
| 03 exact Run | 这些记录究竟属于同一 Session 中的哪一轮 |
|
||
| 04 Canonical Tool Truth | 当前发布校验依赖的完整事实,与长期审计元数据如何分开 |
|
||
| 05 Single ReAct 重建 | 新 Harness 中谁负责记录 Agent 和 Tool,审计失败如何处理 |
|
||
| 06 决策、成本与敏感正文 | 决策顺序、模型成本、Reasoning 可用性和治理缺口是什么 |
|
||
|
||
这不是从“简单”机械升级到“复杂”。每一步都在重新定义审计的边界。
|
||
|
||
## 2. 第一阶段:先让最终答案可以被回放
|
||
|
||
### 当时的问题
|
||
|
||
系统已经能执行诊断并返回结果,但演示者只能展示最终答案。面试官如果继续问“Agent 调了哪些 Tool、Verifier 做了什么、证据在哪里”,只能翻数据库或日志临时解释。
|
||
|
||
### 当时的选择
|
||
|
||
2026-07-03 的 MVP Trace 变更增加了独立只读 API:
|
||
|
||
```http
|
||
GET /api/diagnosis/{sessionId}/trace
|
||
```
|
||
|
||
它直接聚合已有持久化数据:
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
API["Trace API"] --> SESSION[("diagnosis_session")]
|
||
API --> STEP[("agent_step")]
|
||
API --> TOOL[("tool_invocation")]
|
||
SESSION --> RESP["聚合 Trace Response"]
|
||
STEP --> RESP
|
||
TOOL --> RESP
|
||
```
|
||
|
||
这个阶段没有改变 Chat 主流程,也没有新增数据库表。目标很克制:先把已经存在的执行记录变成一个稳定、只读、可演示的查询入口。
|
||
|
||
### 没有选择什么
|
||
|
||
- 没有把 Trace 塞进 `/api/chat` 响应;
|
||
- 没有为了演示制造全离线假运行时;
|
||
- 没有先设计通用事件平台;
|
||
- 没有重写已有 Agent 和 Tool 持久化。
|
||
|
||
### 解决了什么
|
||
|
||
系统第一次可以从最终答案回到 Agent Step 和 Tool 记录,演示流程也形成了“Chat → Trace → Feedback”的闭环。
|
||
|
||
### 新暴露的问题
|
||
|
||
Trace 能查出来,不代表记录语义已经可靠:
|
||
|
||
- 不同 Tool 的持久化路径并不统一;
|
||
- `success`、无结果和失败的含义不稳定;
|
||
- Trace 仍以 `sessionId` 为唯一身份;
|
||
- 记录能展示,但还不能稳定支撑评测。
|
||
|
||
第一阶段解决的是“有没有回放入口”,不是“审计是否已经正确”。
|
||
|
||
## 3. 第二阶段:从“有记录”走向“有稳定语义”
|
||
|
||
### 当时的问题
|
||
|
||
评测系统准备使用 Tool Trace 计算证据质量时,发现不同 Tool 对状态的表达不一致:有的调用统一走 recorder,有的在本地 helper 中写表;无命中、失败和降级路径也可能被混成一个模糊结果。
|
||
|
||
与此同时,AIOps 还是一条独立入口。它能执行自动诊断,却没有与 Chat 相同的 Trace 故事。
|
||
|
||
### 当时的选择
|
||
|
||
2026-07-04 到 07-09 的一组变化没有急着增加新表,而是先收紧契约:
|
||
|
||
1. 统一 Tool recorder 和 evidence summary 语义;
|
||
2. 区分调用成功、没有证据和真正失败;
|
||
3. 覆盖 Verifier 缺失、非法输出和 degraded path;
|
||
4. 让 AIOps 复用已有 Trace 基础设施;
|
||
5. 用 compact `prompt_audit` 和 Gatekeeper rule-set version 记录决策版本,不保存完整 Prompt。
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
TOOL["Tool 调用"] --> S1["执行是否成功"]
|
||
S1 --> S2["是否找到 Evidence"]
|
||
S2 --> S3["Verifier / Gatekeeper 如何判断"]
|
||
S3 --> S4["使用哪一版 Prompt / Rule"]
|
||
S4 --> EVAL["稳定 Trace 与确定性评测"]
|
||
```
|
||
|
||
### 没有选择什么
|
||
|
||
- 没有把完整 Prompt 存进 Trace;
|
||
- 没有使用 Prompt 内容 hash 作为频繁变化的版本协议;
|
||
- 没有引入 LLM-as-judge 代替确定性 fixture;
|
||
- 没有为 AIOps 另建一套 Trace 数据模型。
|
||
|
||
### 解决了什么
|
||
|
||
审计记录开始拥有可比较的语义。评测可以区分“Tool 正常但无证据”和“Tool 本身失败”,也能知道某次判断使用了哪版 Prompt 与规则。
|
||
|
||
AIOps 当时被定义为 Chat 的兄弟入口:不同触发方式,共享同一套持久化 Trace。这在当时是合理的复用选择。
|
||
|
||
### 新暴露的问题
|
||
|
||
两个入口共享 Trace,并没有解决最根本的身份问题。只要同一个 `sessionId` 连续执行多轮,所有 Step 和 Tool 仍会混到一起。
|
||
|
||
而且审计维度越丰富,跨轮污染的破坏越大:不仅回放错误,Feedback、Verifier、评分和案例沉淀都会读错对象。
|
||
|
||
## 4. 第三阶段:Session 不是一次执行,必须引入 exact Run
|
||
|
||
### 触发它的真实故障
|
||
|
||
2026-07-10 的 E2E 使用同一个 `sessionId` 连续请求两轮。Redis 多轮上下文表现正常,但 MySQL 出现了另一种现实:
|
||
|
||
```text
|
||
diagnosis_session
|
||
query / answer 被后一轮覆盖
|
||
|
||
agent_step
|
||
第一轮 + 第二轮持续追加
|
||
|
||
tool_invocation
|
||
第一轮 + 第二轮持续追加
|
||
```
|
||
|
||
主记录表达最新一轮,明细却表达多轮混合。Trace 不再代表某一次诊断,Feedback 也不知道评价的是哪一轮结果。
|
||
|
||
### 当时的选择
|
||
|
||
系统没有只在旧表上补一个轮次字段,而是拆分两种生命周期:
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
S["chat_session<br/>多轮对话目录"] --> R1["diagnosis_run 1<br/>第一轮执行"]
|
||
S --> R2["diagnosis_run 2<br/>第二轮执行"]
|
||
R1 --> D1["step / tool by run_id"]
|
||
R2 --> D2["step / tool by run_id"]
|
||
```
|
||
|
||
核心决策包括:
|
||
|
||
- `sessionId` 表示多轮会话;
|
||
- `runId` 成为正式 API 字段,表示一次可回放执行;
|
||
- `agent_step` 和 `tool_invocation` 增加 `run_id`;
|
||
- Trace、Feedback、Evaluation 和案例来源优先绑定 Run;
|
||
- 指定 `runId` 时必须校验它属于 path 中的 `sessionId`。
|
||
|
||
### 没有选择什么
|
||
|
||
- 没有继续让 `diagnosis_session` 同时承担会话与执行;
|
||
- 没有尝试根据时间戳把历史混合 Trace 伪造成多个真实 Run;
|
||
- 没有在这个阶段引入 `diagnosis_trace_event`;
|
||
- 没有立即删除旧表和旧客户端兼容。
|
||
|
||
“当时没有引入 Timeline”很重要。这个阶段只解决身份隔离,复用 `agent_step` 与 `tool_invocation` 表达 Trace;typed Timeline 是后续才增长出来的能力。
|
||
|
||
### 解决了什么
|
||
|
||
两轮 E2E 得到同一个 Session 下两个不同 Run:
|
||
|
||
- run1 exact Trace 只返回 run1;
|
||
- run2 exact Trace 只返回 run2;
|
||
- step/tool mixed row check 为 0;
|
||
- 多轮对话上下文仍然连续。
|
||
|
||
审计终于拥有稳定的最小单位:**一次 Run 可以独立回放、评分、反馈和沉淀。**
|
||
|
||
### 新暴露的问题
|
||
|
||
Run 隔离修复了“记录属于谁”,但没有回答“Tool 的完整事实由谁保管”。旧 JPA ToolInvocation 更像长期 preview,无法成为 EvidenceGuard 的独立验真来源。
|
||
|
||
兼容策略也留下了债务:不传 `runId` 时读取 latest run、旧表保留、历史 mixed trace 只能映射为 compatibility run。
|
||
|
||
## 5. 第四阶段:长期 Trace 不能同时充当证据真理源
|
||
|
||
### 当时的问题
|
||
|
||
Single ReAct + Harness 设计要求 EvidenceGuard 独立验证 Agent 引用。如果 Guard 只能读取 Agent 已经看过的裁剪结果,等于让模型用自己的输入证明自己。
|
||
|
||
旧 `tool_invocation` 又不能直接升级为完整事实存储:长期保存 raw response 会扩大敏感数据、存储体积和保留治理范围。
|
||
|
||
### 当时的选择
|
||
|
||
2026-07-21 的 ToolBoundary 设计把一次 Tool 调用拆成不同用途的数据面:
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
RAW["Tool Raw Result"] --> CAN["Canonical Invocation<br/>Redis 短期完整真相"]
|
||
CAN --> OBS["Agent Observation<br/>有界投影视图"]
|
||
CAN --> GUARD["EvidenceGuard<br/>独立验真"]
|
||
CAN -.-> DUR["Durable Audit<br/>MySQL 长期元数据"]
|
||
```
|
||
|
||
Canonical Invocation 保存当前 Run 所需的 request、raw response、Agent projection、状态和时间,并拥有 `PROJECTING / READY / ERROR` 生命周期。Agent 只能获得投影结果,不能访问 Redis 或完整 raw。
|
||
|
||
### 没有选择什么
|
||
|
||
- 没有让 JPA `tool_invocation` 保存全部 raw;
|
||
- 没有让 Agent 获取 Redis key 或 canonical record;
|
||
- 没有把无证据和调用失败合并;
|
||
- 没有让 raw 超限时静默截断后继续充当完整真相。
|
||
|
||
### 解决了什么
|
||
|
||
系统第一次明确区分:
|
||
|
||
- **当前 Run 的验真事实**:完整、短期、Harness-only;
|
||
- **Agent 的推理观察**:有界、稳定、可消费;
|
||
- **长期审计记录**:适合复盘,但不复制完整 raw。
|
||
|
||
这也奠定了后来的失败语义:canonical store 缺失时无法验真,需要 fail-closed;durable audit 写入失败不应改变 Tool observation,可以 fail-open。
|
||
|
||
### 新暴露的问题
|
||
|
||
新 ToolBoundary 有了 canonical store,却尚未接回长期 `tool_invocation` 审计。与此同时,旧 recorder 依赖 ThreadLocal 和旧 Tool 副作用,不能直接带进新 Harness。
|
||
|
||
换句话说,新架构已经有了“事实”,但一度失去了“长期记录”。
|
||
|
||
## 6. 第五阶段:Single ReAct 后,审计必须重新确定所有权
|
||
|
||
### 当时的问题
|
||
|
||
旧系统的审计依附于多 Agent、`@Tool`、ThreadLocal 和旧 `AgentLoggingHook`。Single ReAct 清理删除 Planner、Executor、Verifier、Composer 和独立 AIOps 入口后,继续保留这些记录路径会出现三个问题:
|
||
|
||
- 审计逻辑仍依赖已经退出生产链的结构;
|
||
- Tool backend 可能通过旧 annotation/recorder 产生重复副作用;
|
||
- Agent hook 可能长期保存正文和 Thought,违反新的数据边界。
|
||
|
||
### 当时的选择
|
||
|
||
2026-07-22 的收尾没有给旧 recorder 增加更多兼容分支,而是重建 Harness-native 审计:
|
||
|
||
| 旧方式 | 新方式 |
|
||
|---|---|
|
||
| 多 Agent 名称和旧 Hook 决定步骤语义 | `HarnessAgentAuditHook` 记录单体 Diagnosis Agent 步骤 |
|
||
| ThreadLocal 回退传递身份 | exact RunContext / run-scoped tracker |
|
||
| Tool 自身带 recorder 副作用 | ToolBoundary 统一触发 durable audit port |
|
||
| Agent 输入输出正文与 Thought 混入普通记录 | 普通 Step 以 roles、count、Tool names、bytes 等 metadata 为主 |
|
||
| JPA 审计与 canonical truth 概念混合 | Redis canonical fail-closed,JPA durable audit fail-open |
|
||
| Chat 与 AIOps 两条公开诊断入口 | 统一 `/api/chat` + Intent Router |
|
||
|
||
### 没有选择什么
|
||
|
||
- 没有把 `/api/ai_ops` 迁移成第二个 Harness use case;
|
||
- 没有保留旧 `@Tool` 与新 ACI 双注册;
|
||
- 没有让 audit DB 故障直接改变 Agent Observation;
|
||
- 没有为了兼容继续使用旧多 Agent 身份。
|
||
|
||
### 解决了什么
|
||
|
||
审计责任终于与当前执行架构一致:Agent Step 由 Agent Hook 负责,Tool durable audit 由 ToolBoundary 负责,最终 Run 由应用和 Release 收口。
|
||
|
||
这一步也说明演进不是只做加法。07-04 保留 AIOps 兄弟入口是当时的合理方案;07-22 删除它,则是“唯一入口、唯一 Harness”新目标下的必要替换。
|
||
|
||
### 新暴露的问题
|
||
|
||
metadata-only 解决了普通 Trace 的泄漏风险,却无法满足“为什么 Agent 选择这个 Tool”的深度审计需求。Run 虽然有 Step 和 Tool,也仍缺一条统一的决策 Timeline 和完整模型成本账本。
|
||
|
||
## 7. 第六阶段:从执行记录扩展到决策、成本与 Reasoning
|
||
|
||
### 新架构暴露的新问题
|
||
|
||
真实 E2E 出现过一条失败 Run:Diagnosis Agent 重复调用 `lookup_knowledge`,累计 12 次 Tool、45,087 Token 后才进入 `BUDGET_EXHAUSTED`;Evidence Repair 还出现过结构解析失败。
|
||
|
||
旧 Trace 可以看见许多 Step 和 Tool,却不容易直接回答:
|
||
|
||
- Agent 为什么继续查询,什么时候没有信息增益;
|
||
- Token 消耗来自 Router、Agent、Repair 还是 SemanticGuard;
|
||
- Evidence、Semantic 和 Release 决策按什么顺序发生;
|
||
- Provider 是否真的返回 Reasoning,还是系统只保存了 assistant text。
|
||
|
||
### 当前选择
|
||
|
||
2026-07-23 之后,审计继续分化为三个正交能力:
|
||
|
||
1. **typed Decision Timeline**:用 RUN、ROUTING、AGENT、TOOL、EVIDENCE、SEMANTIC、RELEASE 阶段记录决定顺序;
|
||
2. **Model Token Ledger**:按组件和轮次记录 usage,在 Run 结束执行总额对账;
|
||
3. **Reasoning Audit**:独立保存 Provider Reasoning 与 assistant text,普通 Trace 只暴露可用性和 bytes 等 metadata。
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
RUN["exact Run"] --> TL["Decision Timeline<br/>决定如何发生"]
|
||
RUN --> STEP["Agent Step<br/>模型轮次"]
|
||
RUN --> TOOL["Tool Invocation<br/>外部行为"]
|
||
RUN --> TOKEN["Token Ledger<br/>组件成本"]
|
||
RUN --> RSN["Reasoning Audit<br/>敏感正文"]
|
||
TL --> TRACE["普通 Trace"]
|
||
STEP --> TRACE
|
||
TOOL --> TRACE
|
||
TOKEN --> TRACE
|
||
RSN -.-> RAPI["独立受限查询"]
|
||
```
|
||
|
||
### 没有选择什么
|
||
|
||
- 没有把 Reasoning 塞回普通 Trace 或 SSE;
|
||
- 没有在 Provider 不返回 Reasoning 时伪造思考过程;
|
||
- 没有只在 Run 结束写一个无法解释的 Token 总数;
|
||
- 没有把 Timeline 变成用于重建业务状态的完整 Event Sourcing。
|
||
|
||
### 解决了什么
|
||
|
||
当前 Trace 不仅能展示“发生过哪些调用”,还可以解释决定顺序、组件成本和发布依据。`tokens_reconciled` 能检查分项与总额,`usage_available=false` 能区分真实零消耗与供应商未返回 usage。
|
||
|
||
### 仍未完成什么
|
||
|
||
- Reasoning 访问控制、保留期限和加密尚未闭环;
|
||
- 非 DeepSeek Provider 和 reasoning unavailable live 样本仍需补充;
|
||
- `agent_step.thought` 仍存在兼容镜像语义;
|
||
- latest-run 与 legacy Trace fallback 仍然存在;
|
||
- durable audit fail-open 意味着业务成功仍可能伴随审计缺口。
|
||
|
||
演进到这里,系统不再把“记录越多”当作审计完成,而开始治理哪些记录应该存在、谁能读取、缺失时如何表达。
|
||
|
||
## 8. 哪些设计被保留,哪些被替换
|
||
|
||
| 早期设计 | 当前结果 | 判断 |
|
||
|---|---|---|
|
||
| Trace 与 Chat 响应分离 | 保留独立只读 Trace API | 核心边界正确,持续保留 |
|
||
| 聚合持久化 Step/Tool | 保留,并增加 Run、Timeline 和对账 | 基础能力被扩展 |
|
||
| Session 作为 Trace 身份 | 改为 exact Run,Session 只作目录 | 被真实串线问题替换 |
|
||
| AIOps 作为兄弟入口 | 删除,统一 `/api/chat` | 阶段性合理,后续架构收敛后退出 |
|
||
| ToolInvocation 同时承担事实与审计 | 拆为 canonical truth 与 durable audit | 责任过载,被数据分层替换 |
|
||
| 旧 ThreadLocal/Tool 副作用 recorder | 改为 Harness-native Hook、Tracker 和 Audit Port | 与当前执行架构重新对齐 |
|
||
| 普通 Step 保存 Thought/正文 | 转向 metadata + 独立 Reasoning | 安全边界收紧,但兼容镜像尚未完全清除 |
|
||
| 一个 Token 总数 | 组件轮次账本 + Run 对账 | 从统计值升级为可解释成本 |
|
||
|
||
演进中真正稳定下来的不是某张表,而是三条原则:执行身份要精确、数据责任要分层、缺失和失败要诚实可见。
|
||
|
||
## 9. 为什么不能一开始就设计成现在这样
|
||
|
||
站在今天回看,很容易认为最初就应该有 Run、Timeline、Reasoning 和 canonical store。但当时缺少三个关键事实:
|
||
|
||
1. 没有多轮 E2E,就无法证明 Session 身份会造成怎样的跨轮污染;
|
||
2. 没有 EvidenceGuard,就无法看清长期 Tool audit 与当前验真 truth 的责任冲突;
|
||
3. 没有真实 Token 和 Reasoning 样本,就无法确定哪些字段应该对账、哪些正文必须隔离。
|
||
|
||
过早一次性设计完整审计平台,很可能得到通用事件总线、万能 JSON 和全量 raw 存储,却没有解决 MVP 当时最重要的问题。
|
||
|
||
这套演进采用的是另一种方式:先围绕可观察故障收紧最小契约,再让新的真实运行暴露下一层边界。
|
||
|
||
## 10. 用一张图记住设计为什么变成现在这样
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
P1["答案无法解释"] --> D1["独立 Trace API"]
|
||
P2["Tool 状态语义不一致"] --> D2["Evidence / Quality 契约"]
|
||
P3["同一 Session 多轮串线"] --> D3["exact Run"]
|
||
P4["长期审计不能独立验真"] --> D4["Canonical Truth / Durable Audit 分层"]
|
||
P5["旧审计依赖旧多 Agent 与 ThreadLocal"] --> D5["Harness-native Audit"]
|
||
P6["决策顺序、成本和 Reasoning 不可解释"] --> D6["Timeline / Token / Reasoning 分治"]
|
||
|
||
D1 --> NOW["当前可回放、可对账、有数据边界的审计系统"]
|
||
D2 --> NOW
|
||
D3 --> NOW
|
||
D4 --> NOW
|
||
D5 --> NOW
|
||
D6 --> NOW
|
||
```
|
||
|
||
一句话概括:
|
||
|
||
> 审计系统最初只是把已有 Session 记录聚合出来;真实 E2E 先迫使它建立稳定 Evidence 语义,再用 exact Run 修复多轮身份,随后 Single ReAct 重构又把 Tool 真理源与长期审计分开,最终才发展出统一 Timeline、Token 对账和独立 Reasoning 治理。
|
||
|
||
## 11. 事实来源与延伸阅读
|
||
|
||
- `devflow/projects/2026-07-03-mvp-demo-trace-acceptance/`:独立只读 Trace API 的起点;
|
||
- `devflow/projects/2026-07-04-evidence-trace-hardening/`:Evidence 状态和 degraded path 契约加固;
|
||
- `devflow/projects/2026-07-04-aiops-traceable-diagnosis-entry/`:早期 AIOps 兄弟入口与共享 Trace;
|
||
- `devflow/projects/2026-07-09-interview-demo-quality-audit/`:Prompt/Rule version 与确定性质量审计;
|
||
- `devflow/projects/2026-07-10-session-run-trace-isolation/`:多轮串线、exact Run 决策和两轮 E2E;
|
||
- `devflow/projects/2026-07-21-single-react-tool-invocation-store/`:canonical Tool truth 与 durable audit 分层;
|
||
- `devflow/projects/2026-07-22-single-react-cleanup-e2e/`:Single ReAct 审计所有权重建;
|
||
- [ISS-015 诊断运行质量与 Reasoning 审计收敛](../../issues/active/ISS-015-diagnosis-runtime-quality-and-reasoning-audit.md):Timeline、Token、Reasoning 和当前缺口;
|
||
- [审计主设计](审计系统设计-从调试日志到可回放的决策证据.md):当前设计不变量与关键取舍;
|
||
- [真实 Run 案例](从一次诊断Run看审计系统如何记录决策.md):当前审计能力的具体回放方式。
|
||
|