V4 Layer: 分层自适应调查引擎(L3)#
目录:
app/investigation/,app/runbooks/,app/skills/上游: L2 预处理(IncidentContext) → 下游: L4 决策层 / L5 执行层
参考: Robusta playbooks (Tier 1);Microsoft RCACopilot (Tier 2);HolmesGPT + ReAct (Tier 3);Anthropic Orchestrator-Workers
3.1 核心设计:确定性三级递进,不是三个模式#
V3 最大的设计错误是让调用方手选 fast / deep 模式——让调用方替系统决定花多少算力,真实 OnCall 里这是错的。V4 的回答是:
调用方只提交告警,调查深度是系统的输出结果,不是输入参数。
告警(调用方只做这一步)
│
▼
L2 预处理 → IncidentContext
│
▼
Tier 1: 已知故障 Runbook 匹配 ──命中──► 确定性处置(零 LLM)
│ 未命中(继承证据)
▼
Tier 2: 相似故障向量检索 ──命中──► 复用历史 RCA
│ 未命中(继承证据)
▼
Tier 3: 假设驱动 ReAct Loop ──► 输出根因 + 证据链plaintext三点强调:
- 同一条链路上的三个确定性递进阶段,不是三个模式,没有入口分流开关
- 命中即止:Tier 1 命中则 Tier 2/3 一次不调——“省算力”是自然结果,不需要任何人预先选择
- 证据继承:未命中时已有证据传递给下一级,不重复取证
3.2 Tier 1:已知故障 Runbook 匹配#
零 LLM。告警指纹正则匹配 + 前置条件校验 + 冷却期检查。
class Runbook(BaseModel):
match_conditions: list[RunbookMatchCondition] # 正则 + 标签精确匹配
precondition_checks: list[RunbookStep] # 环境校验(可选)
action_steps: list[RunbookStep] # 资源域结构化动作
verify_steps: list[RunbookVerifyStep] # 三级验证门禁
rollback_steps: list[RunbookStep] # 回滚步骤
cooldown_sec: int = 300 # 冷却期
status: str = "active" # active / disabled / pending_review
source: str = "seed" # seed / auto / manualpythonRunbook vs Skill 的关系:
| Skill (保留) | Runbook (新增) |
|---|---|
| 给 LLM 看的”菜单卡”,自由 Markdown playbook | 确定性处置单元,结构化动作序列 |
| Tier 3 假设生成时注入排障经验 | Tier 1 确定性匹配 + 处置 |
triggers 启发式匹配 | match_conditions 正则 + 标签精确匹配 |
| 并存,不替换 |
种子 Runbook 冷启动#
data/runbooks/ 下 5 个种子 Runbook:container 重启、service 重启、process kill、deployment 回滚、imagestore prune(磁盘清理)。
前置条件校验的保守设计#
探测不到资源当前状态时判不通过,宁可递进 Tier 2 也不瞎跑处置。这意味着没有 MCP 工具连接的环境里 Tier 1 命中率恒为 0——这是刻意的。
候选逐个试#
一条”磁盘满”告警可能同时匹配三份 Runbook(悬空镜像/停止容器/构建缓存)。前置条件不过就试下一个,不是第一个失败就放弃。
3.3 Tier 2:相似故障向量检索 + 限定 3 轮验证#
检索 Top-3 相似历史 Incident,每条用便宜档小模型(Qwen-turbo)做适用性复核。
检索实现#
- 独立语料表
incident_vectors:RESOLVED 态的 Incident 向量化落库,与知识库kb_chunks分表——混表会让知识库检索被历史故障报告污染 - pgvector + pg_search(BM25) + RRF 融合:复用知识库的
rrf_fuse()纯函数 - RRF 只管排序,不当相似度用:门槛用向量 cosine;BM25-only 命中回落字面相似度
限定 3 轮验证#
class Tier2VerificationRound(BaseModel):
round_number: int # 1/2/3
retrieved_incident_id: str
similarity_score: float
applicability_verdict: str # applicable / not_applicable / uncertain
reasoning: str # 小模型的推理过程python任一轮 applicable 且相似度过阈值 → 命中。3 轮全部 not_applicable / uncertain → 递进 Tier 3,误命中记入负样本。
负样本闭环#
Tier2Result.negative_samples 回写 incident_vectors.negative_count,检索时按 sim / (1 + w·n) 乘性降权。误命中过的候选既排得更后也更难过门槛。
三档降级#
- embedding 挂 → 只走 BM25
- 没装 pg_search → 退纯向量
- 两路都空 → 回落字面相似 + Wiki
3.4 Tier 3:假设驱动两阶段推理#
Tier 3 不做无目标的 ReAct 循环,采用两阶段推理把发散收敛分离。
阶段一:假设生成#
LLM 结合 IncidentContext + 前两级继承的证据 + RAG 检索 + Skill 排障剧本,生成 3-5 条根因假设,按可能性排序。
class Hypothesis(BaseModel):
description: str # "Redis 内存溢出导致服务不可用"
target_component: str # 涉及组件
fault_domain: str # host/container/network/datastore/...
prior_rank: int # 可能性排序(1=最可能)
required_evidence_types: list[str] # log/metric/trace/config/...
status: HypothesisStatus # PENDING → INVESTIGATING → CONFIRMED/REFUTED
confidence: float # [0, 1]
evidence_chain: list[str] # evidence IDspython假设间应具有互斥性或至少不同故障域覆盖,避免同类假设堆叠。
阶段二:定向取证 ReAct Loop#
按可能性排序逐条假设调用工具定向取证(orchestrator-workers 模式)。
- 每轮输出结构化假设状态:
INVESTIGATING/CONFIRMED/REFUTED/INSUFFICIENT_EVIDENCE - 某假设达到
CONFIRMED+ 置信度达标 → 立即终止 - 所有假设遍历完无
CONFIRMED→ 取最高置信度者标记BEST_EFFORT,Incident 进入ESCALATED
置信度判定与 Impact 调门槛#
effective_threshold = tier3_confirm_threshold + impact_score.score * 0.15
# 默认: 0.8 + 0~0.15 = [0.8, 0.95]python影响面越大,置信度门槛越高——不敢便宜地停。
证据继承的实际落地#
Tier 1 产 resource_state_probe 证据(前置条件校验的副产品),Tier 2 产 similar_incident 证据(含不适用的——“不是它”也是有用的信息)。Tier 3 拿到前两级证据当既定事实,不重复取证,不提与之矛盾的假设。
3.5 Runbook 自增长飞轮#
Tier 3 调查完成 + root_cause.confidence ≥ 0.8
┐
人工确认 RCA 采纳 (POST /incidents/{id}/accept-rca)
├── 三个条件全中 → 触发 Runbook 生成
三级验证门禁通过 (RESOLVED 状态)
┘
│
▼
LLM 提炼 → Runbook 草稿 (source=auto, status=pending_review)
│
▼
审核队列 → 通过后 status=active, 入 Runbook Registry
│
▼
下次同类告警 → Tier 1 命中(调查深度从 Tier 3 前移至 Tier 1)plaintext防污染刹车:
- 草稿一律
pending_review,不参与 Tier 1 匹配 validate_draft拿资源域 REGISTRY 拒收 LLM 发明的动作- 门禁失败的 Runbook 自动打回
pending_review
两个触发点的到达顺序不确定(人可能在门禁前就点了采纳),靠 investigations.generated_runbook_id 做幂等——谁最后到,谁提炼。
3.6 优雅降级#
这是把规则层放在 LLM 前面的直接收益:
| LLM 状态 | Tier 1 | Tier 2 | Tier 3 | 系统行为 |
|---|---|---|---|---|
| 正常 | ✅ | ✅ | ✅ | 全功能 |
| RPM 限流 | ✅ | ✅(检索侧) | ❌ | 确定性自愈 + 相似案例推荐 |
| 完全不可用 | ✅ | 部分(BM25) | ❌ | 纯规则自愈 |
模拟面试问答#
🔥 热点拷问#
面试官:你说砍掉 fast/deep 换三级递进。但 fast/deep 是你自己设计的,现在又说它错了——你怎么确定三级递进不是下一个”被砍掉的设计”?
fast/deep 的问题不在于”只有两个模式”,而在于存在需要调用方选择的模式。我在中间确实考虑过”把 fast/deep 换成 Tier 1/2/3 但仍让调用方选”,这个方案被否掉了——复杂度不降反升。三级递进的关键改进是消除了选择:调用方只提交告警,系统内部一条确定性链路,走多深是链路的输出不是输入。如果要说三级递进可能被砍掉的场景,那就是 Tier 2 的价值被证伪(历史案例复用率太低 → 不如直接跳 Tier 3)——这需要实测数据。
追问:Tier 1 在没有 MCP 工具的环境命中率恒为 0——这不是说你的系统在 demo 环境下最核心的功能根本不工作吗?
是的,这是刻意的保守设计,面试时不会回避。Tier 1 的前置条件校验需要探测目标资源的当前状态——“容器是不是真的在 crash?磁盘是不是真的满了?“。探测不到就不处置,因为在无法确认现场的情况下执行处置动作是危险的。这不是 demo 环境的问题,而是任何没有可观测性基础设施的环境都不应该自动处置。Tier 1 的价值体现在”有完整 MCP 工具链”的生产环境,这正是目标场景。demo 环境里告警会一路走到 Tier 3,由 LLM 给出诊断和建议——这本身也是可用的降级路径。
面试官:Tier 3 说是”假设驱动”,但假设是 LLM 生成的——LLM 生成的假设靠谱吗?会不会所有假设都指向同一个方向?
这是假设驱动的核心风险。两个缓解措施:一是 prompt 里要求假设间”至少覆盖不同故障域”——如果告警是 API 超时,不能 5 条假设都说”网络问题”,必须覆盖 host/container/network/datastore/application 多个域。二是 Skill 排障剧本的注入——tier3._select_playbook 会把当前故障域的人工排查经验灌给 LLM,引导它沿着”人类运维工程师会检查什么”的方向生成假设。但确实有可能所有假设都指向错误的方向——这时候 Tier 3 会走到兜底:所有假设遍历完无 CONFIRMED → BEST_EFFORT + ESCALATED,不硬编结论。系统承认”我不知道”比编一个错误答案更有价值。
追问:假设生成 3-5 条,取证是逐条的——如果第一条假设就花了 5 分钟取证然后被证伪,剩下 4 条还来得及吗?总预算是多少?
每条假设的取证不是无限的,受 AgentHarness 的步骤上限(默认 15 步)和 token 上限约束。正常情况一条假设取证 2-4 轮工具调用就能 CONFIRMED 或 REFUTED。5 分钟是极端情况——取证的大部分时间花在 MCP 工具调用的 I/O 上(等 Docker inspect、等 metric query),不是 LLM 推理。总预算由调查引擎的迭代上限控制,所有假设共享同一个上限。更重要的是达标即终止:如果第一条假设就 CONFIRMED 了,后面 4 条根本不会开始。假设按可能性排序的意义在于:最可能的假设先验证,大概率一两条就命中了。
面试官:你的证据继承——Tier 1 的资源探测传给 Tier 2,Tier 2 的历史案例传给 Tier 3。但实现报告说这个之前是”空管道”,你怎么保证现在真的工作了?
之前确实是空管道——graph.py 里 tier2/tier3 节点硬编码 inherited_evidence=[],做了但没接通。修法分两步:第一步让 Tier 1/2 实际产出证据(前置条件校验的资源探测结果、相似案例检索结果),第二步在图节点间通过 state 传递这些证据。tests/test_evidence_inheritance.py 同时覆盖 LangGraph 节点和纯阶梯两条路径,验证前一级的证据确实出现在后一级的输入里。
深度追问链#
面试官:你说 Tier 2 用 pgvector + BM25 + RRF 混合检索历史 Incident——同一套检索逻辑你在知识库 RAG 里也用了。但历史 Incident 的文本结构和知识库文档完全不同,同一个检索管道真的适合两种语料?
检索管道复用的是 RRF 融合函数(rrf_fuse),不是整条管道。embedding 模型相同但语料表分开(incident_vectors vs kb_chunks),检索入口独立(HybridSimilarIncidentRetriever vs HybridRetriever)。分表是刻意的——历史 Incident 的摘要是结构化的(根因 + 故障域 + 组件),知识库文档是自由文本(SOP、架构说明),混在一起会让 BM25 的词频分布失真。RRF 本身是排序函数,与语料无关。真正需要调的是相似度门槛——知识库检索用 0.3 就够了(宁可多召回),但 Tier 2 的门槛要高得多(tier2_similarity_threshold = 0.75),因为误命中的代价不一样:知识库召回多一条无关文档只是多给 LLM 看几行字,Tier 2 误命中意味着直接复用一个错误的 RCA。
继续追问:那 incident_vectors 表一开始是空的——冷启动时 Tier 2 不是永远不会命中吗?
是的。冷启动时 Tier 2 的语料表为空,检索返回空,直接递进 Tier 3——这是预期行为。随着 Incident 被 RESOLVED 后向量化落库,Tier 2 逐渐有语料可搜。这和 Tier 1 的冷启动一样:种子 Runbook 只有 5 个,覆盖率很低,大部分告警一路落到 Tier 3。系统的设计预期是随着运行积累,Tier 1/2 的覆盖率逐渐上升,Tier 3 的调用率逐渐下降——这就是”越用越准”的飞轮。冷启动阶段的 Tier 3 调用量高是正常的,对百炼 RPM/TPM 的压力也是最大的——但这阶段 Incident 数量也少,整体可控。
再追问:Runbook 自增长闭环——三个条件(置信度 ≥ 0.8 + 人工确认 + 门禁通过)同时满足才触发。现实中人工确认的响应时间可能是几小时甚至几天,门禁可能在几分钟内完成——到达顺序完全不确定。你怎么保证不丢?
两个触发点各调一次 maybe_generate_runbook():API POST /incidents/{id}/accept-rca 和 remediation_worker 推 VERIFY_PASSED 之后。每次调用都检查三条件是否已全中,靠 investigations.generated_runbook_id 做幂等。谁先到,谁发现条件不全,什么也不做。谁后到,发现条件齐了,触发提炼。即使两者同时到达(理论上可能),幂等字段确保只生成一份草稿。GenerationRefused(LLM 拒绝或资源域校验失败)是正常路径不是异常——飞轮转不动不应该影响处置执行或 HTTP 响应。
常规问题#
面试官:为什么 Tier 2 限定 3 轮而不是 5 轮或 1 轮?
3 是成本和召回率的平衡。1 轮太少——检索排序第一的不一定适用,但第二三名可能适用。5 轮太多——适用性复核每轮消耗一次 LLM 调用(虽然是便宜档小模型),而且 Top-5 的相似度通常已经很低了,复核的价值递减。3 轮是经验值,如果实测 Top-3 的适用率都很低可以降到 1(直接跳 Tier 3),如果适用率高可以适当放宽。
面试官:BEST_EFFORT 的结论会写进诊断报告吗?用户怎么知道这不是一个确定的根因?
会写进报告,但会明确标注”BEST_EFFORT:以下结论基于有限证据,置信度未达标(X.XX / 阈值 X.XX),建议人工复核”。Incident 同时进入 ESCALATED 状态,表示自动化链路在这个问题上不收敛。系统不会假装它知道答案——这是和”即使不确定也给出结论”的系统最大的区别。
反思与改进#
面试官:分层自适应调查引擎是整个 V4 最核心的改动。如果重来你会改什么?
两个改进。第一,Tier 2 的适用性复核不应该用通用的 prompt,应该按故障域定制 prompt。磁盘满和网络超时的”适用性”判断标准完全不同——前者看”组件相同 + 告警指标相同”就够了,后者需要看”网络拓扑是否类似 + 链路节点是否匹配”。用同一个 prompt 做所有故障类型的适用性判断,准确率一定打折。第二,Tier 3 的假设数量 3-5 是固定的,更好的做法是根据 IncidentContext 的复杂度动态调整——单服务单指标的告警 2 条假设够了,跨域多服务的告警可能需要 7-8 条。但动态调整本身又引入了一个 LLM 决策,增加了不确定性。这是个 tradeoff,当前选择了简单的固定值。
面试官:从 fast/deep 到 Tier 1/2/3,你学到的最大教训是什么?
不要让用户做系统应该做的决定。fast/deep 的本质错误是”调查深度由调用方选择”——这等于把系统设计的责任转嫁给用户。OnCall 工程师凌晨三点被叫醒,不应该还要判断”这个告警该用 fast 还是 deep”。三级递进消除了这个选择,但更深层的教训是:如果你的系统需要一个模式开关,说明你的自适应逻辑没做好。 模式开关是工程师偷懒的产物——不知道什么情况用什么策略,就让用户选。正确的做法是系统内部根据信号自动决定。