面试知识库

P0 实施报告 —— Incident 状态机 + 分层自适应调查引擎#

日期:2026-07-09 状态:已实现,pytest 472 passed + 真机验证通过 对应设计:docs/REDESIGN_CLOSEDLOOP_20260708.md(总纲)+ docs/layers-v4/P0-incident-statemachine-and-adaptive-investigation.md(深挖) 本文按”做了什么 / 没做什么 / 为什么”三段写,已知缺口单列一节,不粉饰


1. 一句话结论#

fast/deep 双模式已从代码里彻底删除。现在只有一条链路:告警 → L2 纯规则预处理 → Tier 1 → Tier 2 → Tier 3(命中即止,未命中自动递进并继承证据)→ 处置审批 → 三级验证门禁 → 报告。调查深度是链路的输出,不是调用方的输入。

真机验证(连的是本地 Postgres 5432 + Redis,不是 mock)#

验证项结果
DDL 平滑升级incidents 新增 5 列 + incident_transitions / runbooks 建表;遗留 open 行已迁移为 detected
状态机detected → triaging → investigating → rca_ready 逐级落库 + 3 行审计日志;非法迁移 rca_ready --verify_passed--> 被拒
L2 降噪连推 5 条同 fingerprint 告警 → 只有 1 条进入调查链路,降噪率 80%,其余 4 条从未触碰 LLM
Runbook 飞轮刹车source=auto 草稿落 pending_review不参与 Tier 1 匹配;审核通过后才进 active 列表
卡审批不僵死处置 interrupt 时 Incident 仍推进到 rca_ready(否则会僵在 investigating,见 §5 的坑)
处置侧状态机committed+门禁过 → resolved;committed+门禁挂 → escalated;rolled_back → escalated;CAS 漂移 → escalated

E2E 图冒烟(LLM/subagent 打桩)跑通两条路径:未知故障(Tier1 miss → Tier2 miss → Tier3 confirmed,出完整假设树报告)与 L2 抑制短路(不进任何 Tier,直接出降噪说明)。


2. 新增模块#

模块职责关键文件
app/incidents/state_machine.py8 态生命周期 + 迁移矩阵(默认拒绝)+ 拉回上限纯函数,零 DB 依赖,可单测
app/preprocessing/L2 纯规则:去重/抑制/时间窗聚合/抖动/拓扑/Impactrules.py topology.py(适配层) impact.py engine.py store.py
app/topology/资源图 + 级联关联(SCC 缩点 / 多跳上溯 / 因果时序门 / 静默根因)graph.py correlate.py resolver.py registry.py store.py providers/ discovery/
app/runbooks/Tier 1 剧本库 + 匹配 + 冷却 + 自增长 + 审核models/registry/matcher/generator/prompts/reviewer.py
app/investigation/L3 三级递进引擎 + 唯一诊断图engine.py graph.py tier1/2/3.py hypothesis.py retriever.py state.py
app/api/v1/runbooks.py自增长闭环的出口(审核队列 API)
data/runbooks/*.yaml5 个种子 Runbook(container/service/process/deployment)冷启动
data/topology_rules.yaml静态拓扑声明:主机/分区/外部依赖(默认关闭)服务间调用交给 compose/nginx provider 自动解析
scripts/discover_topology.py拓扑自动发现 CLI:--sniff / --mine / --expire / 审核队列产出一律 pending_review

3. 删除的东西(doc 明确要求)#

删除理由
app/agents/graph.py(fast 图装配)fast/deep 双模式归零
app/agents/planner.py / replanner.pyPlan-Execute-Replan 随 fast 图一起删
app/agents/remediation_fast.pyfast 图专属处置衔接节点
app/runtime/escalation.pyFastRunSignals / should_escalate_to_deepTier 递进是链路内部行为,不再有”升级”概念
DiagnosisMode 枚举 + 全链路 diagnosis_mode 参数调用方不再选模式
settings.deep_diagnosis_enabled / fast_first_escalation_enabled / fast_escalation_min_skill_confidence / inspection_mode / fast_remediation_plan_enabled同上
tests/test_fast_escalation.py / tests/test_fast_remediation.py被测对象已删除
scripts/eval_escalation_tradeoff.py度量的是 fast→deep 升级权衡,概念已消失
scripts/eval_fast_tool_scoping.py见 §6 缺口 ①(这条要留意

4. 保留并”重新安家”的东西#

  • skills/ + skill_router:doc §3.1 明确”Skill 与 Runbook 并存,不替换”。现在的活口是 Tier 3 假设生成前选一本排障剧本注入 prompt(tier3._select_playbook)——Skill 的自由 Markdown playbook 恰好是”人怎么排查这类故障”的经验,正是假设生成需要的东西。
  • fault_domain.is_cross_domain:原先用来”跨域 → 直达 deep”。V4 没有模式可跳,它改成 L2 Impact 的 blast_radius 因子(跨域 = 波及面按定义就大 → 抬高各 Tier 置信门槛)。同理 alert_storm_threshold
  • deep 图的处置后段节点remediation_planner → guard → [interrupt 等审批] → apply_decision → verify):这条链路已经真机跑通(飞书一键审批 + 超时降级 + CAS 漂移检测 + 补偿回滚),本次重构一行没动,主图直接 import 复用。app/diagnosis_graphs/build_deep_graph 仅供评测脚本回放,不在生产路径。

4.5 实现过程中发现并修掉的一个真 bug#

处置命中人工审批(interrupt())时,tier_completed 事件永远不会发出来。 我最初把 INVESTIGATING → RCA_READY 的状态机迁移挂在 tier_completed 上——那是 runner 在图跑完之后才发的事件。可一旦处置卡在审批 interrupt,astream 提前返回,这个事件根本不会出现,Incident 会永远僵在 INVESTIGATING,处置链路接管时找不到合法的前驱状态。

修法:把 RCA_CONFIRMED 迁移挂到 rca 事件(由 tier 节点在图内发出,interrupt 之前一定已经发过),并让 runner 在 escalate=True不发 rca 事件——那是个 BEST_EFFORT 的”最像的猜测”,不是确认的根因,绝不能据此推进 RCA_READYescalatetier_completedRCA_INSUFFICIENT

另外补了 _ensure_investigating():手动诊断 / 聊天升级 / 定时巡检建的 Incident 停在 DETECTED,不先推一步的话后续 RCA_CONFIRMED 就是非法迁移。

4.6 处置侧状态机(P0 §1.2 的后半张表)#

app/remediation_worker.py 现在把执行结果翻译成 Incident 迁移:

审批通过 (claim_for_execution)  → AUTONOMY_APPROVAL   → REMEDIATING
committed + 三级门禁通过        → EXECUTION_DONE → VERIFY_PASSED → RESOLVED
committed + 门禁未过            → EXECUTION_DONE → VERIFY_FAILED → ESCALATED
rolled_back (已自动回滚)        → EXECUTION_DONE → VERIFY_FAILED → ESCALATED
state_drift/guard_rejected/...  → EXECUTION_FAILED               → ESCALATED
plaintext

“resolved ≠ 根治”这句话的唯一落点就在这里:只有三级门禁真的过了才允许 RESOLVED;回滚成功只说明处置流程妥善收尾,故障并没有解决,照样交人工。

同时闭合了 L6 防污染的最后一环:Tier 1 的提案携带 runbook_id(新增 approval_requests.runbook_id 列)流到 worker,门禁没过就自动把那份 Runbook 打回 pending_review —— 一份会把事情做错的剧本,在人工看它之前不该再命中第二次。

5. 三级验证门禁(P0 §4 的裁决落地)#

新增第一级 command_success(此前”隐含在事务引擎里”,没有显式门禁对象,报告看不到、统计不到、Runbook 里也声明不了):

  • 判失败:apply 步 ok=False,或 post_state.state ≠ effect命令返回 0 但什么也没发生,这正是这一级要抓的);
  • 判 inconclusive(不失败):journal 无 apply 步、post_state.predicted=True(探针不可用时的引擎预测值)——拿不到证据 ≠ 证据说它失败了;
  • 统一入口 build_standard_gates()STANDARD_GATE_ORDER = (command_success, service_health, metric_recovery)。Runbook 的 verify_steps 与 closed_loop 的回归验证共用同一份实现,否则”跑完 ≠ 解决”只是口号。
  • baseline_comparison / impact_assessment 仍是 P2 backlog。

6. 已知缺口(诚实清单,明早请重点看这一节)#

eval_fast_tool_scoping.py 已删除,对应的工具收窄度量失去测量脚本。 它度量的是 fast Executor 的 filter_tools_for_skill(“每轮可用工具 30+ → 7-9 个”)。fast 图删除后,工具收窄在 V4 里由 Tier 3 各专业 subagent 的硬编码工具白名单承担(metric_agent._load_metric_tools 等),不再经过 filter_tools_for_skill。 → 后果:需要给 Tier 3 subagent 补一个等价的工具可见性 benchmark。

Tier 2 的检索不是 pgvector + pg_search(BM25) + RRF。已闭合(2026-07-09) Incident 现在会向量化落库:新表 incident_vectorsapp/incidents/vector_index.py),HNSW/cosine + pg_search BM25 双索引,写入时机是 IncidentRepository.transition() 迁到 RESOLVED 之后(事务外 fire-and-forget,索引失败不影响状态机)。存量回填走 scripts/reindex_incidents.pyHybridSimilarIncidentRetriever 用与知识库同一份 RRF 实现 —— 为此把 hybrid_retriever.py 里的 RRF 抽成了通用纯函数 rrf_fuse(ranked_lists, key_fn, k)hybrid_search 改为调它,两处不再各写一遍。 两个设计要点:

  • RRF 只管排序,不当相似度用。 tier2_similarity_threshold 的语义是”像不像”,而 RRF 分数没有 [0,1] 量纲。门槛用向量 cosine;BM25-only 命中(向量路没召回)回落字面相似度,同样 [0,1] 且更保守。融合时向量路排在前面,rrf_fuse 对同一 key 保留首次出现的对象,于是两路都命中时留下的是带 cosine 的那个。
  • 负样本闭环接活了。 Tier2Result.negative_samples 此前是收集了但无人消费的死字段。现在 Tier 2 的复核结论回写 incident_vectors.negative_count/positive_count,检索时按 sim / (1 + w·n) 乘性降权(tier2_negative_penalty,默认 0.3),误命中过的候选既排得更后也更难过门槛。 三档降级仍在:embedding 挂 → 该行只进 BM25;没装 pg_search → 退纯向量;两路都空 → 回落字面相似 + Wiki。所以 tier2_retriever=hybrid(默认)在语料表还是空表时行为与 Day 1 完全一致,不需要手动切。

Tier 1 在没有 MCP 只读工具的环境里命中率恒为 0。 前置条件校验要求资源当前态 ∈ 该操作的 preconditions;探测不到状态时判不通过(宁可递进 Tier 2,也不瞎跑剧本处置生产环境)。这是刻意的保守设计,但意味着”Tier 1+2 消化率 > 60%“这个目标必须在 MCP 工具连上的环境里测。

2026-07-09 修:_snapshot_resource_state 此前只认 dict 返回值,而 FastMCP 会把 dict 序列化成 JSON 文本 block 再经 LangChain 回来。于是所有返回 dict 的探针(deployment_status / service_status / 新增的 imagestore_usage)实际拿到的 state 恒为空,对应 Runbook 永远 miss 且不报错。现在会先试着把文本解回 dict。

磁盘清理没有种子 Runbook。已闭合(2026-07-09),但没有引入 disk.clean。 通用的 disk.clean(path) 一旦存在,LLM 就能把任意路径塞进参数,等价于把 rm -rf 交还给它 —— 白名单工具就白做了。所以新增的是一个粒度写死的资源类型 imagestoreapp/remediation/model.py):

  • 资源 URI imagestore://docker/<scope>回收范围写在 URI 里做闭集校验,只有 dangling_images / stopped_containers / build_cache 三个值。volumesall、任意路径一律 ValueError。写操作 imagestore.prune 不接受任何 params(scope 只能来自 URI,否则调用方就能绕开 URI 校验)。
  • prune 的 argv 是常量表,没有字符串拼接,也就没有注入面。绝不带 -a、绝不 --volumes、绝不走 docker system prune
  • 状态机是关键reclaimable(该范围实际可回收字节 ≥ 阈值)/ clean。于是”没什么可清”天然是前置条件失败 → 自动递进 Tier 2,不会空跑一次不可回滚的处置;审批期间垃圾被别的进程清掉 → CAS 检出 reclaimable → clean 漂移 → 零变更中止。
  • 不可回滚是如实建模,不是妥协:删掉的镜像层回不来,磁盘已满时”先备份再删”物理上也不成立。所以 imagestore.prune 是 IRREVERSIBLE,三份种子 Runbook(data/runbooks/seed_disk.yaml)都没有 rollback_stepsreversible=False → 按 §5 D4 自动降级人工审批。Tier 1 在这里贡献的是零 LLM 的确定性识别与提案,不是无人值守的自动执行。
  • imagestore_prune 已登记进 app/tools/meta.pyrestricted=True。这不是可选项:RESTRICTED_TOOLSTOOL_META 推导,未登记的工具走保守默认 restricted=Falsetool_filter 就不会把它剔出诊断工具集,Tier 3 的 subagent 能直接调它删镜像。登记后诊断阶段一律 deny(连 BYPASS 直通都挡),只有 remediation 执行上下文才放行到审批。只读的 imagestore_usage 不 restricted —— 否则 Tier 1 前置校验探不到状态。
  • 覆盖率的代价照付:这三份 Runbook 只能救”docker 垃圾把盘吃满”,救不了”某个业务日志目录涨爆了”,后者一路落到 Tier 3 由人来看。宁可 Tier 1 覆盖率低,不为覆盖率退回自由命令。

顺带修了 Tier 1 的一个既有局限:候选 Runbook 前置条件不过就直接判 miss,不会试下一个。一条”磁盘满”告警会同时匹配三份 Runbook,“悬空镜像没得清”不代表”构建缓存也没得清” —— 卡在第一个候选上会白白放过能自愈的那份。现在逐个候选试,命中第一个真正有东西可回收的。(真机验证:前两份因 clean 前置失败,第三份 build_cache 3.86GB 通过。)

三个 docker JSON 的坑,踩过并已钉进测试(tests/test_imagestore_reclaim.py):

  • docker system df -v 把布尔序列化成字符串 'false'not x.get('InUse') 恒为 False → build_cache 可回收量恒 0,Runbook 永不命中且不报错;
  • 时间串有三种格式(带时区秒 / 纳秒 UTC / 无时区),解析错会让年龄过滤失效;
  • docker builder prune --filter until= 过滤的是未使用时长LastUsedAt),不是创建时间;用 CreatedAt 会把”上周建、今早刚用过”的缓存算进可回收量,而 prune 不删它 → verify 永远假失败。
  • 同理,顶层 docker system dfImages.Reclaimable有 tag 但未被容器使用的镜像,而 image prune(不带 -a)根本不删它们。真机实测:顶层报 13.04GB,实际 dangling 只有 0.907GB,差 14 倍。可回收量的口径必须与 prune 的过滤条件一一对应。

拓扑关联默认关闭,Day 1 只有静态 YAML。已重做(2026-07-09,新增 app/topology/

原实现有一个 bug 级的硬伤和三个建模缺陷。bugactive_services 只取同一个 webhook POST 里的告警(webhook.py),但 Alertmanager 按 group_by 分批投递,上游(mysql)与下游(web-api)的告警几乎必然落在两个 POST 里 —— 级联在真实流量下从来没有命中过。现在改从 PreprocessStore.firing_services() 这个跨请求的窗口视图读,并带每个服务的最早告警时间(因果时序判定要用)。

三个建模缺陷及其修法:

  • 只有 depends_on 一种边,是把 K8s/微服务的世界观直接搬过来了。小企业 90% 的级联是垂直的(磁盘写满 → MySQL 拒写 → 业务 502)。现在是 typed 资源图:depends_on(调用)+ runs_on(承载),方向统一指向”因”,于是同一套遍历既能上溯调用链又能上溯承载链。same_node 这种”共因”关系被物化成合成主机节点providers/static_yaml.py)——“共用一台主机”的准确表达就是”都 runs_on 这台主机”。
  • 根因节点常常自己不告警(那块盘没配监控)。旧算法只在”正在告警的节点”里挑根因,这类故障必然挑错。新增静默根因判定:在残差集(互相独立的直接根因)上找共同祖先,两个指标同时达标才成立 —— explains = |残差∩辖区|/|残差|(它解释了多少个本该互相独立的根因)、precision = |告警∩辖区|/|辖区|(它的辖区烧得有多彻底)。两个分子刻意不同口径:只有 explains 会让”整个机房”永远满分,只有 precision 会让任何叶子的父节点都是 1.0。残差集是关键 —— 用全部告警的话,“根因所在的那台主机”永远抢走根因位(它的辖区当然全在烧,但那是根因烧的)。
  • 一跳。现在是 Tarjan SCC 缩点(环里分不出先后,报成一个整体)+ 多跳反向可达 + 置信度衰减(Π边置信度 × decay^跳数,最大乘积路径用 Dijkstra 式最优先搜索)+ 因果时序门(上游晚于下游超过 causality_skew_sec 就不认它是因 —— “MySQL 在 web-api 挂了三分钟后才报警”,它多半是被打死的受害者)。同分时层级低的赢:基础设施 > 中间件 > 应用 > 业务指标。

还有一个隐性的、实战里占 80% 工作量的东西:身份解析resolver.py)。同一个 MySQL 在不同告警里可能是 service=mysql-master / instance=10.0.0.5:9104 / container_name=prod-mysql-1。解析不到就返回 None,让该告警退化成”自己是根因”,绝不猜 —— 猜错的拓扑归因比没有拓扑归因更有害。

拓扑数据不再靠人手写topology_sources,按性价比排序):① compose / nginx provider 从既有配置解析(depends_on/links/DB_HOST 环境变量/volumes→宿主机分区/upstream),零人工;② sniffss -tnp 连接表自动发现,无侵入;③ mined 从告警历史挖共现(同时看条件概率和 lift —— 只看前者会被”B 本来就一直在告警”骗过去);④ 手写 YAML 降级为”补充与覆盖”,只声明推不出来的东西(主机/分区/DNS/证书/第三方 API)。自动发现的边落 topology_edges 表,带 observations/last_seen_at/TTL —— 服务下线后边会自己过期消失,静态拓扑腐烂的问题由此自愈

降噪的闸门(会吞告警,三道锁)topology_cascade_suppress 默认关闭;归因链上每条边必须 status=active(人审过);静默根因永不触发抑制(没人会去调查一个没有告警的节点,把它的受害者全吞了等于让整场故障静默)。自动发现的边一律 pending_review 落库,只参与根因打分 —— 与 Runbook 自增长飞轮同一个刹车。

真机验证(本地 Postgres):topology_edges DDL、upsert 幂等(observations 累加、置信度只升不降)、人审的 active 不会被下一轮嗅探降级回 pending_review、TTL 过期只删 pending 不删 active、refresh_graph() 跨 provider 别名解析(10.0.0.5:9104service:mysql-master)与悬挂边丢弃,全部跑通。 单测:tests/test_topology_correlation.py(17) / test_topology_providers.py(15) / test_topology_discovery.py(12)。 仍是 backlog:K8s API / OTel Service Graph provider(接口就是 providers/*.py 那个形状,链路不变);pending 边的审核队列目前只有 CLI(scripts/discover_topology.py --list/--approve),没有前端。

Runbook 自增长闭环的提炼环节从未被触发。已闭合(2026-07-09) generate_runbook() 此前零生产调用方(只有 tests 在调),rca_accepted / verification_passed 两个参数在 app/ 里除了函数签名再无第二处出现,也没有任何”人工确认 RCA”的入口。后果是:Tier 3 查明未知故障、门禁全过、Incident 走到 RESOLVED 之后什么也不会发生,/runbooks/pending 永远返回空,Tier 1 覆盖率永远只有手写种子那几份 —— 飞轮装好了,没人踩第一脚。 补上的是那一脚:

  • 新表 investigationsapp/investigation/repository.py)。提炼需要当时那次调查的完整假设树 + 证据链 + 根因,而这些此前只活在 LangGraph 的内存 state 里,图一跑完就没了。audit.py 消费 runner 新发的 investigation 事件落库;一次诊断恰好落一行(只有最终停下的那一级会返回 investigation)。Tier 1/2 的结论也落 —— 它们提炼不出 Runbook,但”停在哪一级、当时看到了什么”是可审计的事实。
  • 两个触发点app/runbooks/growth.py::maybe_generate_runbook):POST /incidents/{id}/accept-rca(人工确认)与 remediation_worker 推 VERIFY_PASSED 之后(门禁通过)。两者到达顺序不确定(人可能在门禁跑完前就点了采纳),所以不能”谁先到谁负责”,只能各调一次、各自检查三条件是否已全中,靠 investigations.generated_runbook_id 做幂等 —— 谁最后到,谁提炼。
  • 判据不重复造verification_passed 的唯一依据是 Incident 已被状态机推到 RESOLVED,不另读 regression 结果。“resolved ≠ 跑完”这条规则的裁决者只有 _advance_after_execution,在别处再写一份迟早漂移。
  • 刹车全在:草稿仍是 source=auto + status=pending_reviewvalidate_draft 仍拿资源域 REGISTRY 拒收 LLM 发明的动作。GenerationRefused 是正常路径不是异常,一律吞掉 —— 飞轮转不动不该影响处置执行或 HTTP 响应。

真机验证:investigations 表 DDL + 仓储 SQL(含 mark_rca_accepted 的子查询 UPDATE)+ 三条件 + 幂等已在本地 Postgres 跑通;单测见 tests/test_runbook_growth.py

“未命中自动递进并继承证据”里的证据继承是空管道。已闭合(2026-07-09) 此前有两层脱节:(a) 做了证据继承的 engine.py::run_ladder 不在生产路径上(调用方只有 tests 和 __init__ 导出),线上跑的 graph.py 里 tier2/tier3 节点硬编码 inherited_evidence=[];(b) 就算修好也继承不到东西 —— Tier 1/2 压根不产 Evidenceevidence_ids 的赋值全项目只出现在 tier3.py 一处,而那里因为 subagent 的 Evidence 没有 id 字段,[ev["id"] for ev in ...] 过滤下来也恒为空。跨级真正传递的只有 excluded_directions(排除方向)。 现在:

  • Tier 1 产 resource_state_probe 证据:前置条件校验本来就对目标资源做了一轮只读探测,未命中时这些观测(容器还在 running / 镜像没什么可回收)恰恰是 Tier 3 生成假设时最想要的事实。丢掉它等于让 Tier 3 再派一次 subagent 去问同一个问题。
  • Tier 2 产 similar_incident 证据(含被判 not_applicable 的),并把 Tier 1 的证据注入适用性复核的可信上下文段(与 UNTRUSTED 的历史报告分列)——历史案例说”容器 OOM 被 kill”而 Tier 1 刚探到容器仍 running,这条根因就该判不适用。
  • Tier 3 的假设生成拿到前两级的证据当既定事实(“不要提出与之矛盾的假设,也不要重复取证”),判定每条假设时继承证据与本轮定向取证一起呈给 LLM,报告里两者分段渲染。
  • 顺带修掉 investigation.evidence_ids / hypothesis.evidence_chain 恒空的 bug:给 subagent 的 Evidence 补本地引用 id(它们尚未落库,没有 DB id)。
  • engine.py 顶部已注明它不是生产路径,两处的 Tier 顺序 / 命中即止 / 证据继承必须保持一致;tests/test_evidence_inheritance.py 同时覆盖 LangGraph 节点与纯阶梯两条路径。

§12 的指标一个都还没实测。 降噪率 / Tier 命中分布 / token 节省 / 飞轮前移率的采集埋点都已就位(suppressed/dedup_count/investigation_tier/tier_trace 都落库),但没有跑过告警风暴回放。基线数字要用 scripts/loadtest.py + scripts/eval_dedup_layers.py 实测后再写。

docs/diagnosis-modes.html 仍描述已删除的 fast/deep,需要重写或标注为历史文档(已在 CLAUDE.md 里标注为历史留存)。

“命中后 verify 失败的 Runbook 自动标记待审查”只做了一半已闭合:给 approval_requests 加了 runbook_id 列,Tier 1 的提案带着它一路流到 worker;门禁没过就自动 flag_for_review() 打回 pending_review(并记进 hit_count/success_count 分母)。LLM 规划的处置没有 runbook_id,不会被误记。


7. 数据库变更(老库平滑升级,全部 IF NOT EXISTS / 幂等 UPDATE)#

-- incidents: 5 个新列 + 状态值迁移 + 默认值改 detected
ALTER TABLE incidents ADD COLUMN IF NOT EXISTS investigation_tier / investigation_id
                                             / rca_confidence / resolved_count / escalation_reason;
UPDATE incidents SET status = 'detected' WHERE status = 'open';   -- 及 mitigated/closed/suppressed
-- 新表
CREATE TABLE incident_transitions (...);   -- 状态迁移审计
CREATE TABLE runbooks (...);               -- 自增长产物 + 人工创建
CREATE TABLE incident_vectors (...);       -- Tier 2 检索语料 (HNSW/cosine + pg_search BM25)
-- diagnosis_tasks
ALTER TABLE diagnosis_tasks ADD COLUMN IF NOT EXISTS investigation_tier;
ALTER TABLE diagnosis_tasks ALTER COLUMN diagnosis_mode DROP NOT NULL;  -- 列保留停写
sql

incident_vectors 建表在 app/incidents/vector_index.py::init_incident_vector_schema(),API 与 diagnosis worker 启动时各调一次(advisory lock 990003 串行化 DDL)。建表失败不阻塞启动 —— 检索侧会回落字面相似。embedding可空:embedding provider 挂掉时该行仍进 BM25 索引,向量检索侧 WHERE embedding IS NOT NULL 跳过它。

app/incidents/state_machine.py::LEGACY_STATUS_MAP 兜住”DDL 跑之前就被读到的旧行”。

8. 测试#

新增测试覆盖
tests/test_incident_state_machine.py (11)迁移矩阵默认拒绝、失败路径全收敛 ESCALATED、拉回上限、遗留状态归一
tests/test_preprocessing_rules.py (13)去重/状态变化不去重/抑制/自抑制护栏/抖动折减/拓扑指上游/impact 只调门槛
tests/test_runbooks.py (23)写不出 shell 命令、fullmatch 严格匹配、冷却、种子静态校验、自增长三条件、LLM 发明动作被拒收、草稿恒 pending_review
tests/test_investigation_ladder.py (13)命中即止(Tier1 命中则 Tier2/3 一次不调)、限 3 轮、负样本、达标即终止、BEST_EFFORT 不硬编、impact 抬门槛
tests/test_regression_gates.py (+7)command_success 三态(pass/fail/inconclusive)+ 三级顺序 + 缺探针跳过
tests/test_remediation_lifecycle.py (10)执行结果 → 状态机迁移映射;committed 但门禁挂 → ESCALATED;回滚 ≠ 解决;门禁失败的 Runbook 自动打回待审查
tests/test_diagnosis_runner_thread_id.py (重写)单图 thread_id 透传 + IncidentContext 透传 + diagnosis_mode 参数不存在的回归护栏
tests/test_tier2_hybrid_retrieval.py (16)RRF 只管排序不当相似度用、两路命中保留带 cosine 的对象、负样本降权能把候选压到门槛下、三档降级链(embedding 挂 / BM25 挂 / 语料空)
tests/test_imagestore_reclaim.py (24)回收范围是闭集(volumes/-a/路径穿越全拒)、prune 不接受 params、argv 无用户可控片段、recovery_plan 恒 None、docker JSON 的三个坑(字符串布尔 / 三种时间格式 / LastUsedAt vs CreatedAt)、探针解 JSON 文本

全量:472 passed

9. 下一步(按 ROI)#

  1. 补 §6 ① 的工具可见性 benchmark;
  2. 跑一次告警风暴回放,实测降噪率 / Tier 命中分布(§12 的数字);
  3. Incident 向量化落库 → Tier 2 换成真正的 pgvector + BM25 + RRF ✅ 已完成(§6 ②)。尚未实测检索质量incident_vectors 需要积累到一定量级才有意义,且换 embedding 模型后要 reindex_incidents.py --force 重算。Tier 2 命中率 / 误命中率的基线要等风暴回放一起测;
  4. layers-v4/L4-decision-autonomy.md(自治分级矩阵 + 变更冻结窗口 + “无回滚即降级”规则的显式实现);
  5. layers-v4/security-defense-in-depth.md + 命令安全闸(§5 D3,翻写 commandFilter)。