docs(mvp): add remaining harness audit guides and engineering index

This commit is contained in:
zhuyongxin
2026-07-30 19:04:59 +08:00
parent e9f1c48d34
commit b39a625e5b
9 changed files with 2629 additions and 2 deletions
+95
View File
@@ -0,0 +1,95 @@
# 审计系统:先从一份已经返回的答案开始
这不是审计表结构手册,而是一页渐进式入门导读。
第一次阅读时,不需要记 `diagnosis_run`、`agent_step` 或事件类型。先回答一个问题:**系统已经给出了诊断答案,为什么还需要审计?**
## 1. 如果只有答案和日志,会发生什么
假设用户收到一份“数据库连接池可能耗尽”的诊断报告。业务日志也显示 Agent 和 Tool 都执行成功。
但系统仍然无法直接证明:
- 报告是否属于本轮请求,而不是混入同一 Session 的上一轮数据;
- Agent 是否真的调用了它声称使用的 Tool;
- Tool 成功是否等于找到了证据;
- EvidenceGuard 和 SemanticGuard 是否真正执行;
- 最终发布结果与 Run 终态、SSE outcome 是否一致;
- Router、Agent 和 Guard 的 Token 能否与总数对上。
这些不是“多打印几行日志”就能稳定解决的问题。它们需要明确的执行身份、决策类型、顺序和关联关系。
## 2. 审计在一次请求中做了什么
```mermaid
flowchart LR
Q["一次诊断请求"] --> R["建立 exact Run<br/>固定本轮身份"]
R --> D["在原始决策点记录<br/>Router / Agent / Tool / Guard / Release"]
D --> S["Run、Timeline、Step、Tool<br/>分别保存各自事实"]
S --> T["Trace 聚合回放<br/>计数与 Token 对账"]
T --> A["回答答案为何产生<br/>也诚实暴露审计缺口"]
```
顺着这条线,审计只做三类事情:
1. **固定身份**:用 `runId` 把一次执行与多轮 Session 分开。
2. **记录决定**:让真正做决定的组件留下有类型、有顺序的结构化事实。
3. **聚合对账**:从最终结果倒推 Timeline、Agent Step、Tool 和 Token,检查它们是否一致。
审计不会替 Agent 诊断,也不会替 Harness 决定能否发布。它负责让这些行为在事后可以被准确解释。
## 3. 先建立这个最小心智模型
```mermaid
flowchart TB
EXEC["一次 Agent 执行"] --> RUN["Run<br/>结果封面"]
EXEC --> TL["Timeline<br/>决策脊柱"]
EXEC --> DETAIL["Step / Tool<br/>行为明细"]
EXEC --> SENSITIVE["Reasoning<br/>独立敏感审计面"]
RUN --> TRACE["普通 Trace"]
TL --> TRACE
DETAIL --> TRACE
SENSITIVE -.-> RTRACE["独立受限查询"]
```
第一次阅读只需要记住:
> Run 告诉我们结果,Timeline 告诉我们决定怎样发生,Step 和 Tool 告诉我们模型与外部世界实际做过什么;它们通过 exact `runId` 组合成一条可回放的证据链。
到这里可以先停下,不需要继续记表名。
## 4. 推荐阅读顺序
```mermaid
flowchart LR
START["先建立直觉"] --> CASE["01 真实 Run 案例<br/>审计怎样使用"]
CASE --> DESIGN["02 审计主设计<br/>为什么这样设计"]
DESIGN --> EVOLUTION["03 设计演进<br/>为什么变成现在这样"]
EVOLUTION --> NEXT["后续专题<br/>数据模型、Token、Tool、Reasoning、失败"]
```
| 顺序 | 先回答的问题 | 阅读 |
|---|---|---|
| 01 | 拿到一份结果后,怎样从后向前回放整次执行? | [从一次诊断 Run 看审计系统如何记录决策](从一次诊断Run看审计系统如何记录决策.md) |
| 02 | 为什么日志不够,为什么需要 exact Run、typed event 和分层数据? | [审计系统设计:从调试日志到可回放的决策证据](审计系统设计-从调试日志到可回放的决策证据.md) |
| 03 | 这套设计经历了哪些真实问题,哪些阶段性方案后来被替换? | [审计设计演进:从 Session Trace 到 Exact Run](审计设计演进-从SessionTrace到ExactRun.md) |
建议一次只读一篇。第一篇建立使用直觉,第二篇理解设计取舍,第三篇再理解这些边界如何被真实问题一步步推出来;数据表、完整事件类型和代码类名都不是第一次阅读的前置知识。
## 5. 遇到具体问题时再往下读
| 当你想知道 | 当前资料 |
|---|---|
| 一次 SUCCESS 诊断的业务链路、字段和真实数值 | [一次诊断全流程 E2E 导读](../diagnosis/一次诊断全流程-E2E导读.md) |
| Session、Run、Trace 和 Reasoning 当前生命周期 | [Session、Run 与 Trace 生命周期](../../architecture/session-trace-lifecycle.md) |
| exact Run 为什么出现,曾经发生过什么串线问题 | [Session-Run-Trace 隔离工程纪要](../diagnosis/Session-Run-Trace隔离-从串线到可回放.md) |
| 如何人工验收一条 Trace 是否完整和安全 | [Trace 检查清单](../../demo/trace-inspection-checklist.md) |
后续本目录会继续补充数据模型、Token、Tool、Reasoning、失败图谱和面试速查。它们会继续保持同样的渐进式结构,不要求从目录头到尾顺序阅读。
## 6. 与 Harness 文档的分工
- [Harness 入门](../harness/README.md)回答“如何让一次非确定性 Agent 执行受控并安全发布”。
- 审计文档回答“这些控制和决定如何被记录、回放和对账”。
- Harness 是执行控制边界,Audit 是决策证据边界;二者协作,但不是同一个职责。
@@ -0,0 +1,255 @@
# 从一次诊断 Run 看审计系统如何记录决策
这篇文章不从表结构和类名开始,而是从一份已经返回给用户的诊断报告开始,倒着追问:**这份答案为什么可以被系统发布?**
先说结论:审计系统不是把运行日志存下来,而是为每一次诊断建立一个独立的 `Run`,再分别记录决策顺序、模型步骤、Tool 调用和最终结果。查询时,这些记录才被重新聚合成一条可以回放、可以对账的执行证据链。
第一次阅读只看第 1、2、3、7 和 8 节即可。先建立直觉,再回来理解为什么要拆成多种记录。
## 1. 只有最终答案,为什么还不够
假设系统返回:
> 当前证据更支持数据库连接池耗尽这一排查方向,但缺少生产指标和实时日志,暂时不能确认最终根因。
这段话看起来很谨慎,但面试官、开发者或评测系统仍然会继续追问:
- 这是本轮请求产生的答案,还是混入了同一会话的上一轮数据?
- Agent 实际调用了什么 Tool,还是只在文本里声称自己查过?
- Tool 返回了候选资料以后,哪个模型步骤使用了它?
- EvidenceGuard 和 SemanticGuard 是否真的执行,最终是谁决定放行?
- 这次请求到底调用了几次模型、消耗多少 Token,数字能否对上?
普通应用日志可以告诉我们“某段代码运行过”,但很难稳定回答这些领域问题。日志行也没有天然的 Run 归属、决策类型和对账关系。
所以审计系统要解决的根问题不是“多记一些信息”,而是:
> 把一次非确定性的 Agent 执行,转换成一组具有明确身份、顺序、责任和边界的可验证记录。
## 2. 先看这次真实 Run
本文使用已有 SUCCESS E2E 样本:
| 项目 | 结果 |
|---|---|
| `session_id` | `e2e-rag-success-20260728162949` |
| `run_id` | `27415045-e674-41af-9cea-de01f27ce040` |
| 最终结果 | `SUCCESS / DIAGNOSIS_REPORT` |
| Agent Step | 2 |
| Tool Invocation | 1 次 `lookup_knowledge` |
| 模型调用 | Router 1 次、Agent 2 次、SemanticGuard 1 次 |
| Timeline | 15 个有序事件 |
| 总 Token | 13,236,`tokens_reconciled=true` |
| 总耗时 | 约 25 秒 |
业务视角看到的是一份诊断报告,审计视角看到的是报告背后的五个问题:
```mermaid
flowchart TB
OUT["用户收到 DIAGNOSIS_REPORT"]
OUT --> RUN["Run 摘要<br/>这次执行最终怎样结束?"]
OUT --> TL["决策 Timeline<br/>先后做过哪些决定?"]
OUT --> STEP["Agent Step<br/>模型每一轮做了什么?"]
OUT --> TOOL["Tool Invocation<br/>实际调用过什么?"]
OUT --> LEDGER["Token 账本<br/>资源消耗能否对上?"]
```
这五个视角不是重复保存同一份内容。它们分别回答不同的问题,并在 `run_id` 下汇合。
## 3. 审计的正确阅读顺序:从结果向前倒推
回放一次诊断时,不应该从第一条日志开始逐行翻。更有效的顺序是:先确认终态,再沿决策链向前寻找依据。
```mermaid
flowchart RL
FIN["RUN_FINISHED<br/>SUCCESS,Token 已对账"] --> REL["RELEASE_DECISION<br/>允许发布"]
REL --> SG["SEMANTIC_GUARD_DECISION<br/>SUPPORTED"]
SG --> EG["EVIDENCE_GUARD_INITIAL<br/>PASSED"]
EG --> A2["Agent Step 1<br/>生成报告草稿"]
A2 --> T1["Tool Invocation<br/>RAG 返回候选证据"]
T1 --> A1["Agent Step 0<br/>发出 Tool Call"]
A1 --> ROUTE["ROUTING_DECISION<br/>DIAGNOSIS"]
ROUTE --> START["RUN_STARTED"]
```
沿着这条链可以得到一个比“请求成功”更具体的结论:
1. 本次 Run 最终正常结束,并发布了诊断报告;
2. Release 放行前,SemanticGuard 给出 `SUPPORTED`;
3. SemanticGuard 之前,EvidenceGuard 已确认引用结构有效;
4. 报告草稿来自 Agent 第 2 轮;
5. 草稿使用的候选证据来自第 1 轮发出的真实 RAG Tool Call;
6. 整条链都属于同一个 exact `run_id`。
审计的价值就在这里:它不是保存一份“成功日志”,而是让最终结果可以逐层找到前置依据。
## 4. 第一层:Run 是这次执行的封面
`sessionId` 表示多轮对话目录,`runId` 才表示一次独立执行。
```mermaid
flowchart TB
S["chat_session<br/>同一个多轮会话"] --> R1["diagnosis_run A<br/>第一轮问题"]
S --> R2["diagnosis_run B<br/>第二轮问题"]
R1 --> D1["自己的 Step / Tool / Timeline"]
R2 --> D2["自己的 Step / Tool / Timeline"]
```
这个拆分来自真实问题:早期系统只使用 `sessionId`。同一会话连续执行两轮后,主记录会被后一轮覆盖,而 Agent Step 和 Tool Invocation 继续追加,最终造成 Trace、Feedback 和评测跨轮混合。
因此当前模型把两种身份分开:
- `sessionId` 回答“这些问题属于哪段对话”;
- `runId` 回答“这条记录属于哪一次执行”。
本次样本的 `diagnosis_run` 像一张封面,保存查询、状态、意图、发布结果、总耗时、总 Token 和最终安全内容。看到它,我们先知道故事结尾,但还不知道过程细节。
## 5. 第二层:Timeline 记录决定,不复制所有正文
本次 Run 的 15 个事件可以压缩成七个阶段:
| 阶段 | 关键事件 | 它证明什么 |
|---|---|---|
| RUN | `RUN_STARTED` | 一次独立执行已经建立 |
| ROUTING | Token、Attempt、Decision | Router 被真实调用,并选择 `DIAGNOSIS` |
| AGENT | 两轮 Token 与 Model Step | Agent 先规划 Tool,后生成草稿 |
| TOOL | `TOOL_INVOCATION` | `lookup_knowledge` 被实际执行 |
| EVIDENCE | `EVIDENCE_GUARD_INITIAL` | 引用和证据结构检查通过 |
| SEMANTIC | Token、Attempt、Decision | SemanticGuard 被调用并判断 `SUPPORTED` |
| RELEASE / RUN | Release、Finish | 报告被允许发布,Run 正常收尾 |
Timeline 只承担“什么时候做了什么决定”。它不保存完整 Tool raw,也不试图替代 Agent Step 和 Tool Invocation 的详细字段。
这是一个有意的设计取舍:如果把所有信息都塞进一张巨大的事件表,查询一条时间线会很方便,但模型步骤、Tool 证据和资源账本都会退化成难以约束的 JSON。当前方案让 Timeline 保持稳定的决策语义,领域明细继续由各自记录负责。
代价是读取时必须做聚合,不能只查一张表。但这个复杂度被集中在 Trace 查询服务中,而不是扩散给每个调用方。
## 6. 第三层:Step 与 Tool 共同证明“模型真的做过什么”
这次诊断有两个 Agent Step:
| Step | 模型行为 | Token | 关联结果 |
|---|---|---:|---|
| 0 | 没有正文,发出 `lookup_knowledge` Tool Call | 2,657 | 产生 1 条 Tool Invocation |
| 1 | 读取 Tool Observation,生成报告草稿 | 4,703 | 进入 EvidenceGuard |
`AgentStepAuditTracker` 在 Step 落库后绑定 `step_id`,Tool 执行时再把这个 ID 写入 `tool_invocation`。因此系统不只知道“这一轮有 Tool Call”和“某处有一次 Tool 调用”,还可以把两者连起来。
```mermaid
flowchart LR
S0["agent_step #0<br/>发出 lookup_knowledge"] -->|"step_id"| T["tool_invocation<br/>READY / EVIDENCE_FOUND"]
T --> O["有界 Observation"]
O --> S1["agent_step #1<br/>生成 Draft"]
```
Tool 审计也没有永久复制完整原始结果。长期记录的是 exact identity、Tool 名称、状态、耗时、请求和结果字节数、稳定错误码,以及 RAG 的检索模式、命中数和相关度等有界元数据。
完整 Tool 结果属于当前 Run 的短期 canonical truth,由 Harness 用于 EvidenceGuard 验真;它与长期 durable audit 的目的不同:
- canonical truth 要回答“当前发布校验依据的事实到底是什么”;
- durable audit 要回答“长期复盘时,这次调用发生了什么”。
把完整 raw 同时写进长期 Trace 虽然排障直接,但会扩大敏感数据、存储体积和保留治理范围,因此没有采用。
## 7. 第四层:Token 不是一个总数,而是一套可对账账本
如果只在 Run 结束时写一个 `total_token_count`,我们无法判断数字是否漏掉 Router、Guard 或隐藏重试。
本次 Run 的模型账本是:
| 组件 | Token |
|---|---:|
| Intent Router | 906 |
| Diagnosis Agent 第 1 轮 | 2,657 |
| Diagnosis Agent 第 2 轮 | 4,703 |
| SemanticGuard | 4,970 |
| 合计 | 13,236 |
对账关系为:
```text
sum(MODEL_TOKEN_USAGE.total_tokens)
= diagnosis_run.total_token_count
= RUN_FINISHED.run_total_tokens
= 13236
```
因此 `tokens_reconciled=true` 不是“记录了一个 Token 数”,而是三个独立视角得到相同结果。
还有一个容易误读的点:`agent_step.tokenCount` 之和只有 7,360,因为它只统计 Diagnosis Agent 的两轮;Run 总额还包括 Router 和 SemanticGuard。审计把组件和轮次分开,就是为了让成本和延迟可以定位,而不是只得到一个无法解释的总数。
Provider 没有返回 usage 时,系统会记录 `usage_available=false`,而不是把缺失伪造成 0 Token。缺数据本身也是需要被审计的事实。
## 8. 这套设计做了哪些关键选择
### 选择一:Trace 独立查询,不塞进 Chat 响应
- **问题**:Chat 面向用户流式返回结果,Trace 面向复盘和评测,生命周期与数据量不同。
- **决策**:通过只读 Trace API 按 `sessionId + runId` 聚合持久化证据。
- **放弃方案**:把完整 Trace 嵌入 `/api/chat` SSE。
- **代价**:调用方需要在收到 metadata 后保存 `runId`,再进行第二次查询。
### 选择二:exact Run 是审计边界
- **问题**:仅按 Session 查询曾造成多轮 Step、Tool、Feedback 和评测串线。
- **决策**:每次有效执行创建独立 Run,所有明细携带同一个 `run_id`。
- **兼容代价**:当前 API 仍保留“不传 `runId` 时读取 latest run”和 legacy 回退;这是迁移兼容,不是推荐的新调用方式。
### 选择三:决策 Timeline 与领域明细分开
- **问题**:单看 Run 摘要不知道过程,单看 Step 或 Tool 又不知道整体决策顺序。
- **决策**:Timeline 保存 typed decision event,Step 和 Tool 保存各自明细,查询时聚合。
- **放弃方案**:一张万能 Trace 表承载所有正文和字段。
- **代价**:需要维护事件与明细之间的计数、身份和顺序一致性。
### 选择四:普通审计长期保存有界信息
- **问题**:完整 Prompt、Reasoning 和 Tool raw 虽然方便排障,却会把敏感数据永久扩散到普通 Trace。
- **决策**:普通 Trace 以 metadata 为主;Reasoning 使用独立审计面,Tool raw 只在当前 Run 的 canonical store 中短期存在。
- **当前缺口**:Reasoning 的认证、权限、加密和保留期限仍未完全闭环;`agent_step.thought` 还存在兼容镜像语义,不能把当前状态描述成彻底隔离。
### 选择五:审计写入失败不改变业务结果
- **问题**:如果长期审计数据库短暂不可用,是否应该让一次本可安全完成的诊断直接失败?
- **决策**:durable audit 采用 fail-open,写入失败告警,但不改变业务结果。
- **边界**:用于 EvidenceGuard 验真的 canonical truth 不是普通审计;它缺失时无法证明证据,必须 fail-closed。
- **代价**:一次业务成功的 Run 可能存在审计缺口,所以 Trace summary 和计数对账必须显式暴露缺失,而不能假装记录完整。
## 9. 审计能证明什么,不能证明什么
审计可以证明:
- 某个结果属于哪个 exact Run;
- 哪些模型和 Tool 被实际调用;
- 决策以什么顺序发生;
- Tool Call 属于哪个 Agent Step;
- Guard 和 Release 给出了什么结果;
- Token、步骤数、Tool 数和 Timeline 数量是否对账。
审计不能自动证明:
- Tool 返回的外部数据本身一定正确;
- `SUPPORTED` 永远不会发生模型误判;
- 没有记录的 Reasoning 可以被事后还原;
- durable audit 写入失败时,缺失的事件仍然存在;
- 有了 Trace 就可以忽略权限、脱敏、保留期限和加密。
这也是为什么审计系统的目标不是“绝对正确”,而是让执行过程的身份、决定、依据和缺口都变得可见。
## 10. 先记住这一句话
> 一次诊断结束后,Run 告诉我们结果,Timeline 告诉我们决定是怎样发生的,Agent Step 和 Tool Invocation 告诉我们模型与外部世界实际做过什么,Token 账本负责对账;它们通过 exact `runId` 组合成一条可回放的决策证据链。
理解这句话,就已经抓住了审计系统的主干。表结构、事件全集、Reasoning 和失败治理都可以在需要时再展开。
## 11. 事实来源与延伸阅读
本文没有重新从源码推导设计,主要依据现有工程资料:
- [一次诊断全流程 E2E 导读](../diagnosis/一次诊断全流程-E2E导读.md):本文 SUCCESS 样本、15 个 Timeline 事件和 Token 数据来源;
- [Session、Run 与 Trace 生命周期](../../architecture/session-trace-lifecycle.md):当前 exact-run、普通 Trace 与 Reasoning 边界;
- `devflow/projects/2026-07-03-mvp-demo-trace-acceptance/`:为什么建设独立 Trace API;
- `devflow/projects/2026-07-10-session-run-trace-isolation/`:Session/Run 串线问题与身份拆分决策;
- `devflow/projects/2026-07-22-single-react-cleanup-e2e/`:Harness-native Agent/Tool durable audit 和 canonical/durable 边界;
- [Trace 检查清单](../../demo/trace-inspection-checklist.md):当前验收边界与兼容镜像说明。
@@ -0,0 +1,328 @@
# 审计系统设计:从调试日志到可回放的决策证据
第一篇文章跟随一条真实 Run,展示了怎样从最终报告倒推出 Router、Agent、Tool、Guard 和 Release。这一篇换一个角度:**为什么这件事不能靠普通日志完成,系统又为什么选择了现在这套审计结构?**
先说核心判断:Agent 系统的审计对象不是代码执行过程,而是一次运行中产生的关键决定。日志可以帮助开发者定位异常,但审计必须让系统回答:这个决定属于哪次执行、由谁作出、依据是什么、先后关系怎样、最终结果是否与过程对得上。
第一次阅读只看第 1、2、3、6 和 9 节即可。它们构成最小设计主线;其余章节用于展开具体取舍。
## 1. 根问题:系统给出了答案,但无法证明答案怎样产生
一个最简单的实现可能只留下这样的日志:
```text
start diagnosis
call lookup_knowledge success
semantic check passed
finish diagnosis
```
这些日志能说明代码大概运行过,却回答不了几个关键问题:
- 它们是否属于同一个请求,还是混入了同一 Session 的另一轮执行?
- `success` 表示 Tool 正常返回、找到证据,还是结论已经被证据支持?
- 哪一轮 Agent 发出了 Tool Call,后续草稿是否真的使用了这次结果?
- `semantic check passed` 前是否完成了引用真实性检查?
- 最终报告、Run 终态和 SSE outcome 是否一致?
- Router、Agent 和 Guard 的 Token 能否与总数对上?
问题不在于日志太少。即使增加更多日志,仍然缺少稳定的身份、类型、顺序和关联关系。
| 普通日志擅长回答 | 审计必须回答 |
|---|---|
| 哪段代码报错了 | 哪一次 Run 在哪个决策阶段停止 |
| 某方法耗时多久 | Router、Agent、Tool、Guard 各自消耗多少 |
| 某次调用返回成功 | 调用是否产生证据,证据是否支持发布 |
| 当前进程发生了什么 | 持久化后能否精确回放同一次执行 |
| 给开发者阅读文本 | 给 API、评测和对账提供稳定结构 |
因此审计不是“更详细的日志”,而是一套独立的数据责任。
## 2. 设计目标:把非确定性执行变成可验证记录
这套审计设计需要满足六条不变量。
### 2.1 每条记录都有 exact Run 身份
`sessionId` 可以跨多轮复用,`runId` 只属于一次执行。Agent Step、Tool Invocation、Timeline 和最终结果必须落在同一个 `runId` 下。
### 2.2 决定在发生的位置被记录
Router 记录路由决定,ToolBoundary 记录真实 Tool 调用,Guard 记录校验结论,Release 记录最终发布决定。审计层不应在事后根据日志文本猜测发生了什么。
### 2.3 先后顺序可以稳定重放
同一个 Run 内,Timeline 使用单调 `sequence_no` 表达决定顺序。回放不依赖不同线程日志的打印时间,也不依赖数据库自增 ID 恰好连续。
### 2.4 不同记录只承担一种责任
Run 保存终态摘要,Timeline 保存决策脊柱,Agent Step 保存模型轮次,Tool Invocation 保存调用元数据,Reasoning 保存独立敏感正文。任何一种记录都不应成为无限扩张的万能 JSON。
### 2.5 长期记录必须有数据边界
普通 Trace 不永久复制完整 Prompt、Tool raw 和任意嵌套参数。缺少 Provider usage 时记录“不可用”,而不是伪造为 0;Reasoning 需要独立治理。
### 2.6 审计缺口必须可见,但不能随意改变业务语义
长期审计写入失败不应把一条本可安全完成的请求变成业务失败;与此同时,查询结果必须通过计数、对账状态和缺失标记暴露审计并不完整。
可以把这六条压缩成一句话:
> 审计记录必须属于精确执行、来自原始决定、顺序稳定、职责单一、内容有界,并且能够诚实表达缺失。
## 3. 整体设计:写入时分工,查询时聚合
审计没有设计成一个集中式拦截器抓取所有内容。真正知道“发生了什么”的组件,在自己的决策点产生结构化记录。
```mermaid
flowchart TB
REQ["一次 Chat 请求"] --> RUN["创建 exact Run"]
subgraph owners["决策所有者"]
ROUTER["Router<br/>路由尝试与决定"]
AGENT["Agent Hook<br/>模型步骤与 Reasoning"]
TOOL["ToolBoundary<br/>真实 Tool 调用"]
GUARD["Evidence / Semantic Guard<br/>验证决定"]
RELEASE["Release<br/>发布结果"]
end
RUN --> ROUTER
RUN --> AGENT
RUN --> TOOL
RUN --> GUARD
RUN --> RELEASE
ROUTER --> TL[("Decision Timeline")]
AGENT --> STEP[("Agent Step")]
AGENT --> RSN[("Reasoning Audit")]
TOOL --> INV[("Tool Invocation")]
AGENT --> TL
TOOL --> TL
GUARD --> TL
RELEASE --> TL
RUN --> SUMMARY[("Diagnosis Run")]
SUMMARY --> QUERY["Trace 聚合查询"]
TL --> QUERY
STEP --> QUERY
INV --> QUERY
RSN -.->|"独立受限端点"| RQUERY["Reasoning 查询"]
```
写入侧保持分工,读取侧由 Trace 服务形成一个面向复盘的 read model:
| 观察面 | 主要回答 |
|---|---|
| `diagnosis_run` | 这次执行最终怎样结束、用了多少资源、发布了什么 |
| `diagnosis_trace_event` | 决策按什么顺序发生 |
| `agent_step` | Diagnosis Agent 每一轮模型调用做了什么 |
| `tool_invocation` | 哪些 Tool 被真实调用,状态、耗时和有界结果怎样 |
| `agent_reasoning_audit` | Provider 是否返回 Reasoning,以及独立保存的敏感正文 |
这是一种“写入分散、读取聚合”的设计。分散不是随意写表,而是让记录权归属于最了解该决定的组件;聚合则把跨组件复杂度集中在 Trace 查询边界。
## 4. 为什么记录 typed decision,而不是事后解析日志
系统当前的 Timeline 使用稳定的 Phase 和 EventType,例如:
```text
RUN_STARTED
ROUTING_DECISION
MODEL_TOKEN_USAGE
AGENT_MODEL_STEP
TOOL_INVOCATION
EVIDENCE_GUARD_INITIAL
SEMANTIC_GUARD_DECISION
RELEASE_DECISION
RUN_FINISHED
```
这些名字表达的是领域事实,而不是实现细节。`SEMANTIC_GUARD_DECISION` 可以稳定表示语义门控的结果,即使以后底层模型客户端或方法名发生变化。
如果改为事后解析日志,会产生三个问题:
1. 日志文案改变就可能破坏审计;
2. 多线程和异步输出使顺序不可靠;
3. “Tool 执行成功”和“Tool 找到证据”很容易被同一个 `success` 混淆。
typed event 让状态语义在写入时就确定。它的代价是新增事件类型需要维护协议和测试,不能随意写一段字符串就算完成审计。
### 这不是 Event Sourcing
Timeline 虽然是追加式事件序列,但系统不会依靠它重建业务状态:
- `diagnosis_run` 仍保存当前终态和发布结果;
- Agent Step 与 Tool Invocation 仍有自己的领域记录;
- Timeline 用于解释“决定怎样发生”,不是整个系统的唯一事实源。
选择完整 Event Sourcing 会引入事件版本、状态重放、快照和迁移复杂度,当前 MVP 没有这项需求。
## 5. 为什么 Timeline 和领域明细必须分开
一种看起来更简单的方案,是把所有信息都写进 `diagnosis_trace_event.details`。这样只查询一张表就能得到全部内容。
但一张万能事件表会同时承担:
- 模型轮次和 Token 字段;
- Tool 请求、状态和 RAG 检索详情;
- Guard 决策;
- Run 终态和最终内容;
- Reasoning 与 assistant text。
最后所有约束都会退化为“不同 EventType 对应不同 JSON 结构”,数据库无法清楚表达关联、索引和数据治理。
当前设计把两类问题分开:
```mermaid
flowchart LR
Q1["什么时候做了什么决定?"] --> TL["Timeline<br/>有序、稳定、轻量"]
Q2["这个对象的详细事实是什么?"] --> DETAIL["Run / Step / Tool<br/>领域字段与关联"]
TL --> TRACE["Trace Read Model"]
DETAIL --> TRACE
```
例如,Timeline 的 `TOOL_INVOCATION` 只需要表达某次调用在决策链中的位置;`tool_invocation` 才负责 `step_id`、Tool 名、状态、耗时、bytes、错误码和 RAG 派生字段。
代价是查询时需要跨表聚合和计数对账,但数据所有权和长期演进更清楚。
## 6. 六个关键决策及其代价
### 决策一:Trace 使用独立只读 API
- **问题**:Chat SSE 面向实时用户体验,Trace 面向复盘、评测和排障,数据量与生命周期不同。
- **选择**:通过 `GET /api/diagnosis/{sessionId}/trace?runId={runId}` 聚合持久化记录。
- **未选**:把完整 Trace 嵌入 `/api/chat` 响应。
- **代价**:客户端必须保存 metadata 中的 `runId`,需要第二次请求才能读取 Trace。
这项决策在最初 MVP Trace 建设时就已确定:可观测性不侵入 Chat 输出协议。
### 决策二:`runId` 而不是 `sessionId` 定义审计边界
- **问题**:同一 Session 连续两轮执行时,主记录覆盖而 Step/Tool 追加,曾导致 Trace、Feedback 和评分跨轮污染。
- **选择**:`chat_session` 表示多轮目录,`diagnosis_run` 表示一次执行,所有明细按 `run_id` 隔离。
- **未选**:继续在旧主表上追加字段,或用“最新一轮”推断明细归属。
- **代价**:接口、反馈、评测和历史迁移都要理解 Session/Run 两级身份。
### 决策三:在原始决策点生成记录
- **问题**:集中式审计器无法准确知道 Router 为什么选择意图、Tool 是否真正执行、Guard 做出了什么领域判断。
- **选择**:让 Application、Router、Agent Hook、ToolBoundary、Guard 和 Release 各自在原始位置记录 typed fact。
- **未选**:请求结束后解析日志或根据最终结果反推中间过程。
- **代价**:每个新决策路径都必须显式接入审计,遗漏不会被“万能拦截器”自动补齐。
### 决策四:Timeline 与领域明细分离
- **问题**:既需要一条简单的决策脊柱,也需要可查询的 Step、Tool 和 Run 字段。
- **选择**:Timeline 记录顺序,领域表记录详情,读取时聚合。
- **未选**:单一超大 Trace 表或全部 JSON event。
- **代价**:需要维护 exact identity、顺序和 persisted/returned count 对账。
### 决策五:普通 Trace 只持久化有界信息
- **问题**:永久保存完整 Prompt、Tool raw、Reasoning 和参数,会放大敏感数据、体积和访问治理风险。
- **选择**:Tool durable audit 只保存有界元数据;完整 Tool truth 只在当前 Run 的 canonical store 中短期存在;Reasoning 使用独立表和独立端点。
- **未选**:为了排障方便,把所有上下文复制到 MySQL Trace。
- **代价**:长期 Trace 不能还原所有原始正文;Reasoning 还需要单独的权限、保留和加密治理。
### 决策六:durable audit fail-open,canonical truth fail-closed
- **问题**:审计数据库不可用时是否让业务失败,以及证据真理源不可用时是否仍允许发布。
- **选择**:长期审计写入失败只告警,不改变业务结果;用于当前 Run 验真的 canonical truth 缺失时不能继续证明证据。
- **未选**:所有审计失败一律中断请求,或所有审计失败都静默忽略。
- **代价**:业务成功不保证审计绝对完整,必须显式暴露 audit gap;canonical store 则成为发布安全的关键依赖。
## 7. 最重要的失败边界:同样叫“记录”,失败语义不同
Tool 执行后会形成两个用途完全不同的数据面:
```mermaid
flowchart TB
TOOL["Tool 执行结果"] --> CAN["Canonical Truth<br/>当前 Run 的完整验真依据"]
TOOL --> DUR["Durable Audit<br/>长期有界元数据"]
CAN -->|"可用"| GUARD["EvidenceGuard 验真"]
CAN -->|"写入失败"| CLOSED["fail-closed<br/>不能证明就不能发布"]
DUR -->|"可用"| TRACE["长期 Trace 可回放"]
DUR -->|"写入失败"| OPEN["fail-open<br/>告警并暴露审计缺口"]
```
为什么不能统一成一种策略?
- canonical truth 参与当前业务决策。没有它,EvidenceGuard 无法独立验证 Agent 引用;继续发布会改变安全语义。
- durable audit 服务长期复盘。它很重要,但让数据库短暂故障覆盖一条已经安全完成的业务结果,会把可观测性变成新的业务单点。
fail-open 不等于“失败无所谓”。审计记录器需要告警,Trace summary 需要对比持久化数量与返回数量,Token 账本需要给出 `tokens_reconciled`,Provider usage 缺失需要写明 `usage_available=false`。
## 8. 查询模型:普通 Trace 与敏感 Reasoning 分开
普通回放入口聚合:
```text
chat_session metadata
+ exact diagnosis_run
+ agent_step metadata
+ tool_invocation metadata
+ ordered diagnosis_trace_event
= DiagnosisTraceResponse
```
Reasoning 使用独立入口:
```http
GET /api/diagnosis/{sessionId}/trace/reasoning?runId={runId}
```
它要求显式 `runId`,并校验该 Run 属于 path 中的 `sessionId`。Provider 没有返回 Reasoning 时也记录 `reasoning_available=false`,不能用 assistant text 或人工摘要伪造。
分开读取的目的不是让敏感正文“换一张表就安全”,而是为后续访问控制、加密和保留期限提供独立治理边界。
## 9. 当前设计没有假装解决所有问题
当前仍有四类明确债务:
1. **兼容查询债务**:普通 Trace 不传 `runId` 时仍可读取 latest run,无 run-backed 数据时还能回退 legacy `diagnosis_session`。这是迁移能力,不是推荐契约。
2. **Reasoning 兼容镜像**:独立 Reasoning 表已经存在,但 `agent_step.thought` 仍可能保存 reasoning 或 assistant text 的兼容镜像,普通 Trace metadata-only 目标尚未完全收口。
3. **敏感治理未完成**:Reasoning 端点的身份认证、权限模型、保留期限和加密仍在 ISS-015 中,不能把“分表”描述成完整安全闭环。
4. **审计天然可能缺失**:durable audit fail-open 意味着必须把缺记录与业务失败分开诊断,不能默认数据库里没有就代表运行中没有发生。
这些不是文章末尾附带的 TODO,而是当前架构代价的一部分。审计系统的可信度来自诚实表达边界,不是把所有状态都包装成完整。
## 10. 方案对比:为什么没有选择看起来更简单的办法
| 方案 | 短期优势 | 没有采用的主要原因 |
|---|---|---|
| 只增加业务日志 | 实现快、开发者熟悉 | 缺少稳定身份、类型、关联和对账协议 |
| Chat 响应携带完整 Trace | 一次请求拿到全部数据 | 污染 SSE 协议,扩大响应和敏感数据暴露 |
| 一张万能 Trace 表 | 查询表面简单 | JSON 结构失控,领域约束、索引和治理困难 |
| 完整 Event Sourcing | 理论上可重放全部状态 | 当前只需解释决策,引入事件版本和状态重建过重 |
| 永久保存全部 raw | 排障最直接 | 敏感数据、成本和保留治理不可控 |
| 所有审计失败都 fail-closed | 记录最完整 | 长期可观测性会成为业务可用性的单点 |
| 所有审计失败都 fail-open | 业务最不易被阻断 | canonical truth 缺失时仍发布会破坏证据安全 |
## 11. 先记住这张图
```mermaid
flowchart LR
EXEC["非确定性 Agent 执行"] --> ID["exact Run 身份"]
ID --> DEC["原始决策点产生 typed record"]
DEC --> SPLIT["Timeline 与领域明细分工"]
SPLIT --> BOUND["普通审计有界<br/>Reasoning 独立"]
BOUND --> READ["Trace 聚合、计数与 Token 对账"]
READ --> EXPLAIN["可以回放,也能说明缺口"]
```
用一句话概括:
> 这套审计设计不是复制运行内容,而是让每个决策所有者在 exact Run 内留下有界、可排序、可关联的结构化事实,再通过独立查询模型把这些事实聚合成可回放、可对账的证据链。
## 12. 事实来源与延伸阅读
- [从一次诊断 Run 看审计系统如何记录决策](从一次诊断Run看审计系统如何记录决策.md):用真实 SUCCESS Run 查看这些设计怎样落到记录中;
- [Session、Run 与 Trace 生命周期](../../architecture/session-trace-lifecycle.md):当前身份、生命周期和查询契约;
- `devflow/projects/2026-07-03-mvp-demo-trace-acceptance/`:独立 Trace API 的初始问题与决策;
- `devflow/projects/2026-07-10-session-run-trace-isolation/`:多轮串线证据和 exact Run 决策;
- `devflow/projects/2026-07-22-single-react-cleanup-e2e/`:Harness-native Agent/Tool audit 与 canonical/durable 失败边界;
- [ISS-015 诊断运行质量与 Reasoning 审计收敛](../../issues/active/ISS-015-diagnosis-runtime-quality-and-reasoning-audit.md):Reasoning 当前完成度和剩余治理缺口。
@@ -0,0 +1,398 @@
# 审计设计演进:从 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):当前审计能力的具体回放方式。