# 2026-05-26 Multi-Agent 分层诊断工作总结

这份文档记录今天在 `codex/multi-agent-rebuilt` 分支上围绕分层诊断 evaluation 做过的代码改动、最新一轮完整 evaluation 结果集，以及当前评分结论。

## 1. 本次要提交代码的含义

今天的改动主要分成两类：第一类继续补强 evidence layer，让 collector 更稳定地拿到诊断关键证据；第二类补一个非常窄的 timeout salvage，避免 evidence 已经完整产出但 diagnosis 子流程超时后，整条 evaluation 被判成 infra invalid。

### 1.1 Evidence collector 增强

涉及文件：

- `agentkit/skills/builtin/tidb-clinic-diagnosis/scripts/collect_evidence.py`
- `backend/tests/test_collect_evidence_tikv_thread_cpu.py`

这部分不是简单地给某个 case 写死答案，而是把之前比较薄弱的数据源解析补强成更可消费的结构化 evidence。

主要增强点：

- Loki 查询统一转换为 nanosecond timestamp，避免 Loki API 在秒级 epoch 下返回空结果。
- focused log samples 从单一大正则扩展为多组针对性查询，并增加 namespace / container / component fallback。
- raw log sample 会按 panic、recoverable goroutine、`memory.(*Tracker).Consume`、`sqlkiller.go`、`executor:3024`、restart、OOM、coprocessor 等关键词排序，保证稀有但关键的日志不会被普通 WARN/ERROR 淹没。
- 新增 query timeout / recoverable panic 相关 evidence hint，例如 `query_timeout_interrupt_signal_present`。
- Activity Hub events 进入 evidence summary，用于后续关联 version change、configuration change、known issue / fixed release。
- component health、capacity comparison、coprocessor worker、heap/profile、known issue context 等信号继续沉淀到 `critical_evidence_hints`，让 diagnosis 阶段不需要重新从原始数据里猜。
- `queries.json` 增加 panic classification context，方便 judge 和 diagnosis-analyst 稳定消费 restart / panic 边界证据。

整体含义是：以前 agent 可能只看到“日志有 ERROR”或者“metrics 有异常”，现在 collector 会尽量把“什么异常、来自哪里、能指向哪类故障”提前结构化，降低后续 diagnosis 漂移。

### 1.2 Agent 编排与 bounded fast diagnosis

涉及文件：

- `agentkit/config/investibook/agents/evidence-agent.md`
- `agentkit/config/investibook/agents/diagnosis-orchestrator.md`
- `agentkit/config/investibook/agents/diagnosis-analyst.md`
- `backend/investibook/core/system_prompt.py`
- `backend/tests/test_diagnosis_orchestration_config.py`

这部分主要是收紧 sub-agent 之间的边界。

主要改动：

- `evidence-agent` 必须优先跑标准 collector，并把 collector timeout 提到 `900000 ms`，避免长时间 Loki / Activity Hub 采集被提前杀掉。
- `evidence-agent` 优先把原始 ISO 时间传给 collector，由 collector 负责 canonical conversion；只有缺 ISO 时才手动转 epoch。
- `diagnosis-orchestrator` 明确要求把用户/request 里的 ISO window 原样传给 evidence-agent，避免 orchestrator 自己转换出错。
- 当 `critical_evidence_hints` 已经覆盖当前 case family 时，orchestrator 可以要求 diagnosis-analyst 走 bounded fast diagnosis，只读 evidence-agent 输出、summary、report 和 hint 引用的文件。
- `diagnosis-analyst` 新增两个 fast-path mapping：
  - capacity comparison：识别为 node count / capacity reduction 导致 per-node load increase，并明确排除 plan cache 作为主因。
  - expensive query：识别为 read-heavy query path 缺少有效 access path，引发 large scan / full scan / ineffective index 和 TiKV read hotspot。
- system prompt 同步描述 bounded fast diagnosis 和 `bounded_diagnosis_fallback`，保证运行时协议与 agent prompt 一致。

整体含义是：当 evidence 已经足够时，diagnosis 阶段不应该再发散式读取所有文件、继续调用数据源或等待长任务，而应该快速基于结构化 evidence 收敛到对应专项路径。

### 1.3 Evaluation runner timeout salvage

涉及文件：

- `backend/investibook/core/eval/eval_runner.py`
- `backend/tests/test_eval_runner.py`

今天这里有两类小改动。第一类是新增 `_try_bounded_timeout_salvage()`，只在非常窄的条件下触发：

- ACP 结果是 `agent_execution_timeout`。
- workspace 中已经存在 `evidences/agent-runs/evidence-agent.json`。
- evidence-agent 状态为 `ok` 或可用的 `partial`。
- evidence-agent 已经产出 evidence files 和匹配的 `critical_evidence_hints`。
- case 类型属于当前明确支持的 capacity 或 expensive-query fast path。

触发后，runner 会基于已经产出的 evidence 写一个最小的 `diagnosis-analyst.json`，更新 `workspace_evidence_report.json` 中的 `analysis`，然后继续跑 judge。它不是代替真实诊断的通用捷径，而是避免“evidence 已经采完，但 analyst 子流程卡住”这种情况被算成 infra invalid。

今天用下午那轮 invalid workspace 做过模拟验证：

| Case | 原始结果 | salvage 后模拟 judge |
| --- | --- | --- |
| `cpu-spike-during-incident-window` | invalid, `agent_execution_timeout` | pass, score 83, critical evidence 30/30, diagnosis 33/50 |
| `expensive-query-spike-during-incident-window` | invalid, `agent_execution_timeout` | pass, score 83, critical evidence 30/30, diagnosis 33/50 |

这里的 salvage 结果是基于下午已生成 evidence 的模拟重判，不等价于一次新的完整 ACP end-to-end run。

第二类是修复 evaluation workspace `.env` 的 key 继承问题：runner 之前会把 `CLINIC_API_KEY=` 当成 managed prefix 清掉，但没有重新写回，导致新建 eval workspace 无法访问 Clinic API。这会让本来应该进入 evidence/diagnosis 的 case 直接退化成 `execution_contract_failed` 或 `agent_execution_timeout`。现在 `_write_eval_workspace_env()` 会从 `SKILL_CLINIC_API_KEY`、`CLINIC_API_KEY`、`INVESTIBOOK_CLINIC_API_KEY` 中读取可用值，并写入新 workspace 的 `.env`。对应测试也覆盖了这一点。

修复 key 继承后，针对下午两个 invalid case 又跑了一次真实 end-to-end evaluation：

| Case | 最新真实 run 结果 | Evidence 对比 | Diagnosis 状态 |
| --- | --- | --- | --- |
| `cpu-spike-during-incident-window` | fail, score 50, baseline 30, +20 | critical evidence 30/30, baseline 10/30, +20 | diagnosis 0/50，仍未识别 capacity / node-count shift 为主因 |
| `expensive-query-spike-during-incident-window` | fail, score 69, baseline 72, -3 | critical evidence 30/30, baseline 30/30, 0 | diagnosis 22/50，仍未明确命中 large scan / missing access path judging condition |

这说明今天最关键的修复已经生效：两个 case 不再是 invalid，evidence 层也都达到满分；剩下的问题已经明确转移到 diagnosis 层。

### 1.4 Case timeout 调整

涉及文件：

- `evaluation/regression/cases/sql-tail-latency-spike-during-incident-window/case.yaml`

把 `acp_bash_timeout_s` 从 `300` 调整到 `900`。这个 case 需要更多时间完成 heap / log / summary evidence 收集，300 秒容易在 collector 阶段制造非诊断质量问题。

## 2. 最新完整 Evaluation 结果集

最近一次完整 10 case evaluation 是下午这轮本地并发单 case run：

- 临时输出目录：`/tmp/investibook-full-regression-20260526-165033`
- Suite：`core-daily-demo @ 8c71a0d955f6`
- Registry revision：`25d29bdb37432c835b2f17120e03b9709d43d18a`
- Agent：`opencode`
- Model：`chat/claude-standard/high`
- Baseline：`baseline-evalrun-b92aea0782574c129e3a933d662adcdd`
- Baseline comparison 状态：`skipped / resolved_model_mismatch`

由于 promoted baseline 的 resolved model 是 `chat/claude-standard`，而本轮是 `chat/claude-standard/high`，系统自动 comparison 被跳过。下面的 baseline 对比来自各个 `report_detail.txt` 里的 bucket 分数。

本轮生成的 evaluation 数据文件如下：

| Case | Eval run | 主要数据文件 |
| --- | --- | --- |
| `component-restart-during-incident-window` | `evalrun-69826336c77b498090d5c939f3255415` | `backend/data/eval_audits/evalrun-69826336c77b498090d5c939f3255415/report_detail.txt` |
| `cpu-spike-during-incident-window` | `evalrun-ee3ecdfc921d4cb68f40139b56ce5a7a` | `backend/data/eval_audits/evalrun-ee3ecdfc921d4cb68f40139b56ce5a7a/report_detail.txt` |
| `expensive-query-alert-correlation-during-incident-window` | `evalrun-c1c1737a81df428582536ddc5ddbe1aa` | `backend/data/eval_audits/evalrun-c1c1737a81df428582536ddc5ddbe1aa/report_detail.txt` |
| `expensive-query-spike-during-incident-window` | `evalrun-ee3ecdfc921d4cb68f40139b56ce5a7a` | `backend/data/eval_audits/evalrun-ee3ecdfc921d4cb68f40139b56ce5a7a/report_detail.txt` |
| `panic-in-recoverable-goroutine` | `evalrun-aee3ea8346c34788903aa9cc883d9340` | `backend/data/eval_audits/evalrun-aee3ea8346c34788903aa9cc883d9340/report_detail.txt` |
| `service-degradation-during-incident-window` | `evalrun-94afbf6cf6214c51b28cc4f480f729c1` | `backend/data/eval_audits/evalrun-94afbf6cf6214c51b28cc4f480f729c1/report_detail.txt` |
| `sql-tail-latency-spike-during-incident-window` | `evalrun-edecfa8fdc5c4349b4d3da6eb8a09672` | `backend/data/eval_audits/evalrun-edecfa8fdc5c4349b4d3da6eb8a09672/report_detail.txt` |
| `sustained-cpu-pressure-during-incident-window` | `evalrun-1b7e50a241bc4b1ca173c5fedd1f59ad` | `backend/data/eval_audits/evalrun-1b7e50a241bc4b1ca173c5fedd1f59ad/report_detail.txt` |
| `system-unavailability-during-incident-window` | `evalrun-3f00d186112941c096e054f516dfd707` | `backend/data/eval_audits/evalrun-3f00d186112941c096e054f516dfd707/report_detail.txt` |
| `tail-latency-surge-during-incident-window` | `evalrun-45556dcfea1d46c3b626893de905d3fe` | `backend/data/eval_audits/evalrun-45556dcfea1d46c3b626893de905d3fe/report_detail.txt` |

每个 run 目录下还包含对应 case 的 workspace audit artifact，例如 `evidences/agent-runs/evidence-agent.json`、`evidences/workspace_evidence_report.json`、`semantic_context_v1.json`、`semantic_packet_v1.json` 等。

## 3. 最新 Evaluation 评分总结

下面是把下午完整 10 case run 与晚些时候针对两个 invalid case 的真实补跑结果合并后的最终口径。`cpu-spike` 和 `expensive-query-spike` 已用最新 `evalrun-ee3ecdfc921d4cb68f40139b56ce5a7a` 覆盖下午的 invalid 结果。

| Case | Verdict | Score | Critical evidence | Diagnosis | Failure |
| --- | --- | ---: | ---: | ---: | --- |
| `component-restart-during-incident-window` | pass | 71 | 30/30, baseline 0/30, +30 | 21/50, baseline 8/50, +13 | - |
| `cpu-spike-during-incident-window` | fail | 50 | 30/30, baseline 10/30, +20 | 0/50, baseline 0/50, 0 | `diagnosis_critical_gate_failed` |
| `expensive-query-alert-correlation-during-incident-window` | pass | 92 | 30/30, baseline 30/30, 0 | 42/50, baseline 17/50, +25 | - |
| `expensive-query-spike-during-incident-window` | fail | 69 | 30/30, baseline 30/30, 0 | 22/50, baseline 22/50, 0 | `diagnosis_critical_gate_failed` |
| `panic-in-recoverable-goroutine` | pass | 79 | 30/30, baseline 30/30, 0 | 29/50, baseline 17/50, +12 | - |
| `service-degradation-during-incident-window` | pass | 83 | 30/30, baseline 30/30, 0 | 33/50, baseline 25/50, +8 | - |
| `sql-tail-latency-spike-during-incident-window` | fail | 69 | 30/30, baseline 20/30, +10 | 19/50, baseline 8/50, +11 | `diagnosis_critical_gate_failed` |
| `sustained-cpu-pressure-during-incident-window` | pass | 88 | 30/30, baseline 16/30, +14 | 38/50, baseline 35/50, +3 | - |
| `system-unavailability-during-incident-window` | fail | 44 | 10/30, baseline 20/30, -10 | 14/50, baseline 14/50, 0 | `critical_evidence_partial` |
| `tail-latency-surge-during-incident-window` | pass | 100 | 30/30, baseline 30/30, 0 | 50/50, baseline 50/50, 0 | - |

汇总：

| 指标 | 结果 |
| --- | ---: |
| Pass / Fail / Invalid | 6 / 4 / 0 |
| 完整可评分 case 数 | 10 |
| Critical evidence 满分数 | 9 / 10 |
| Diagnosis 提升数 | 6 / 10 |
| 总分相对 baseline 提升数 | 7 / 10 |

只看 evidence 层，今天的最终结果已经非常清楚：

- `component-restart` 从 critical evidence 0/30 提升到 30/30。
- `cpu-spike` 从下午的 invalid 变成真实可评分结果，critical evidence 从 baseline 10/30 提升到 30/30。
- `expensive-query-spike` 从下午的 invalid 变成真实可评分结果，critical evidence 保持 baseline 30/30 的满分状态。
- `panic-in-recoverable-goroutine` 保持 critical evidence 30/30。
- `sql-tail-latency` critical evidence 从 20/30 提升到 30/30，但最终仍卡在 diagnosis critical gate。
- `sustained-cpu-pressure` critical evidence 从 16/30 提升到 30/30。
- `system-unavailability` 是本轮唯一真实 evidence 回退，critical evidence 从 20/30 降到 10/30。

因此，以 evidence 为主目标来看，10 个 case 里 9 个已经拿到 critical evidence 30/30。剩下唯一没满的是 `system-unavailability`。两个原本 invalid 的 case 已经证明不是 evidence 采不到，而是之前 workspace key 继承和后续 diagnosis 收敛的问题。

## 4. 当前判断

今天的代码改动可以总结成一句话：把 evidence collector 从“尽量多捞数据”推进到“把关键异常结构化交给后续 sub-agent”，同时给 evaluation runner 补上 evidence 已完成但 diagnosis 超时的窄口径救援。

按当前结果看：

- Evidence layer 已经接近目标状态。10 个 case 中，9 个 critical evidence 是 30/30。
- 真正还需要继续深挖的 evidence 缺口是 `system-unavailability`，它缺少“worker failure -> latency explosion / QPS collapse”和“coprocessor worker capacity drop”的直接证据。
- `sql-tail-latency` 已经从 evidence 问题转成 diagnosis 问题，需要专项诊断层更稳定地把 GC / heap profile / known issue path 连起来。
- `cpu-spike` 和 `expensive-query-spike` 已经完成真实 end-to-end 补跑，二者 evidence 都是 30/30；后续优化重点是 diagnosis：前者要稳定识别 capacity / node-count shift，后者要稳定识别 large scan / missing access path。

## 5. 验证

本地测试：

```bash
cd backend && uv run python -m pytest tests/test_diagnosis_orchestration_config.py tests/test_eval_runner.py
```

结果：

```text
45 passed in 6.87s
```

## 6. 建议下一步

短期可以按这个顺序继续：

1. 先提交当前 collector / prompt / eval runner 改动和这份总结文档。
2. 继续修 `system-unavailability` 的 evidence 缺口，目标是把它从 10/30 拉回 30/30。
3. 开始进入 diagnosis 优化：优先处理 `cpu-spike` 的 capacity shift 判断，以及 `expensive-query-spike` 的 large scan / missing access path 判断。
4. 再处理 `sql-tail-latency` 的 diagnosis 优化，把 heap / GC / known issue / fixed release 这条路径做成专项诊断规则，而不是继续堆在通用 collector 里。
