面试知识库

面试 Q&A — 简历 5 个技术点的 Benchmark 构建方法论#

背景:简历上 5 个技术点(Fast 工具收窄、Deep 并行取证、Fast/Deep 动态切换、意图-执行分离处置、 告警风暴三层去重)原本只有设计文档,没有任何脚本能产出量化数字。本文档记录这 5 个 benchmark 从”设计怎么测” → “写脚本” → “真实跑通” → “中途发现并修复真实 bug” 的完整过程,以及每一步 背后的方法论取舍。所有数据均来自 2026-07-05 在本机真实服务栈(Postgres + Redis + 5 个 MCP 工具服务 + DashScope 真实 LLM)上的实测,脚本在 scripts/eval_*.py,数据集在 benchmark/*.jsonl,报告在 benchmark/reports/


零、为什么要做这轮 Benchmark#

Q1: 简历上这些数字之前是怎么来的?为什么突然要重新做 benchmark?#

诚实回答:这几个技术点(Skill 路由收窄工具、Deep 模式并行取证、动态切换、处置护栏、告警去重) 在代码里都是真实实现的功能,但项目原有的评测脚本(scripts/eval_router.pybenchmark/run_benchmark.py)只覆盖了 RAG 检索和 Router 路由准确率,没有一个脚本测过 “误调用率下降多少”、“收敛轮数少几轮”、“75:1 的收敛比”这些简历上写的具体数字。也就是说, 这些数字之前更多是”设计意图的合理推算”,不是”跑出来的”。这轮工作就是把 5 个点逐一补上 可复现的量化实验,跑出真实数字,如果数字和简历原话对不上,就诚实调整措辞,而不是先射箭 再画靶。

Q2: 5 个点分别对应项目里的什么模块?测试思路怎么定的?#

#简历技术点代码位置测什么
1Fast 模式工具收窄app/runtime/tool_filter.py误调用率、收敛轮数
2Deep 模式并行取证app/diagnosis_graphs/deep_diagnosis_graph.py证据覆盖率、RCA 命中率、并行 vs 串行耗时
3Fast/Deep 动态切换app/runtime/escalation.py + app/api/v1/webhook.py::_diagnosis_mode_for三组性价比对比、L0/L1 信号精确率召回率
4意图-执行分离处置app/runtime/command_guard.py + app/runtime/approvals.py护栏红队拦截率、CAS 并发正确性
5告警风暴三层去重app/incidents/repository.py三层消融、收敛比

设计思路的共同点:每个点都要有一个”对照组”——要么是”关掉这个机制会怎样”(点1/5 的消融), 要么是”三种模式横向对比”(点3),要么是”红队攻击 vs 合法请求”(点4)。没有对照组的 “绝对数字”(比如单独一个”覆盖率 0.67”)说服力是不够的,这也是为什么点2专门拼了一个 “串行版对比图”去测并行的边际收益,而不是只报一个绝对耗时。

Q3: 怎么决定每个 benchmark 该跑多大规模?#

这是面试官很可能会追问的一点,因为处理不好容易被质疑”为什么这几个数字量级差这么多”。 关键洞察是:“生产量级”这个词对 5 个点的含义完全不同,不能一刀切

  • **点5(去重)**是唯一”原始告警量级”直接适用的场景——它测的正是”去重发生之前”那一层, 所以直接照简历数字跑:--n 2400 --distinct 32,纯 DB 操作不调 LLM,几乎零成本。
  • 点1/2/3(诊断时刻的行为)测的不是原始告警量级,因为生产里几千条告警在真正进入 Router/Planner/Executor 之前,已经被点5的三层去重收敛掉了(2400→50)。这三个脚本 该扩大的是”知识库覆盖的故障模式种类数”,这个数字有自然上界——9 个故障域、每域 5-15 种 典型现象,全量大概 50-100 条,不是可以无限放大的原始流量。当前用的是 24/12/20 题(覆盖 6-9 个域),量级合理但受限于真实 LLM 调用的时间和 token 成本,用户明确表示”不用追求 和简历数字精确对齐,流程跑通即可”,所以没有进一步扩到 60/20/40 题的推荐规模。
  • 点4两部分成本不同:Part A(红队)免费可以随便扩,Part B(CAS 并发)测的是”并发抢 同一条审批”这个瞬时压力场景,用 concurrency=50/repeats=10 比生产实际部署的 3-worker 规模更狠,专门用来验证”就算并发量比现在部署规模大得多,CAS 语义依然成立”。

一、点1:Fast 模式工具收窄(scripts/eval_fast_tool_scoping.py#

Q4: 这个实验的对照组是怎么设计的?#

同一批 24 道故障描述(benchmark/fast_tool_scoping_24.jsonl,覆盖 6 个故障域,每题标了 golden_tools——人工认为这题真正该用的工具),只切 Executor 侧的工具过滤方式:

  • scoped 组:生产现状,app/runtime/tool_filter.py::filter_tools_for_skill 正常跑。
  • unscoped 组monkeypatch app.agents.executor.filter_tools_for_skill,让它忽略 Skill 白名单,把 get_all_tools() 拿到的全量工具原样暴露——模拟”收窄前”的基线。

两组跑完之后互相 monkeypatch/还原,中间会清空 _agent_cache(否则会复用上一组 patch 后建好的 Agent 对象)。

Q5: “误调用率”具体怎么算?为什么不是简单数一下调了几个工具?#

misuse_rate = 调用了 golden_tools ∪ ALWAYS_OK_TOOLS 之外的工具次数 / 总工具调用次数。 关键设计是 golden_tools 是”这题诊断上真正该用的工具”,不是”允许用的工具”——如果拿 “收窄后的白名单”当基线来验证收窄效果,那是循环论证(永远 100% 达标)。必须用独立于两组 配置之外的人工标注做基线,才能量化”选择空间变大导致的误调用”。ALWAYS_OK_TOOLS(如 search_knowledge_base/get_current_time)两组都天然可见,不计入误调用判定。

Q6: 第一次全量跑出来两组几乎没差异(scoped 49.3% vs unscoped 48.4%),当时怎么判断这是#

真实结论还是脚本 bug?

这是这轮工作里最有价值的一次排查。做法是免费的事后归因分析,不用再跑 LLM:把 unscoped 组实际调用的全部工具,拿 scoped 组的 filter_tools_for_skill 重新过一遍,算 “这些调用里有多少次原本会被拦”——结果是 0 次(436 次调用,0 次会被拦)。这说明不是 数据噪声,而是当前代码里”收窄”这个机制本来就不会让只读工具产生任何调用差异tool_filter.py 的”共享只读工具自动放行”策略(read_only=True & scoped=False)把 docker_ps/docker_stats/docker_logs/http_check/prom_query 等几乎所有只读工具 都自动暴露给每个 Skill,不管 Skill 的 allowed_tools 里有没有声明。真正被硬挡的只有 写/危险工具,纯诊断场景根本不会调这些。

这个发现直接对应项目 tests/test_scoped_tool_tiering.py 的一个盲点:这份单测验证的 “组件专属只读工具(scoped=True)不享受共享豁免”的场景,用的是一个虚构的测试工具 redis_slowlog_probe(注释写的是”模拟未来的 redis_slowlog / mysql_processlist / kubectl_get”)——也就是说,这一层机制在测试里验证过”如果用会怎样”,但在当时的真实工具 注册表(app/tools/meta.py)里,scoped=True 从未被应用到任何一个真实工具上。

Q7: 后来这个问题是怎么解决的?#

不是我改的代码——把这个发现汇报给项目负责人后,对方直接改了 app/tools/meta.py:把 docker_ps/docker_stats/docker_logs/docker_inspect/query_windows_event 标成 scoped=True,并新增了 redis_info/redis_slowlog/redis_client_list + mysql_processlist/mysql_status/mysql_innodb_status 6 个 scoped=True 的中间件专属 诊断工具(对应新增的 mcp_servers/redis_server.py/mysql_server.py)。改完之后我先用 同一份”事后归因分析”手法(不跑 LLM,直接把改动前 unscoped 组的历史调用记录,拿改动后的 新规则重放)验证效果——反事实拦截率从 0% 变成 14.9%(65/436),证明修复有真实效果, 然后才重新跑全量 benchmark 花真钱验证。这个”先免费复算、确认有效果再花钱重跑”的顺序, 是控制成本的关键。

Q8: 修复后全量重跑,最终数字是多少?和简历原话对得上吗?#

scoped     工具数(avg)=17.54   误调用率(avg)=47.8%   收敛轮数(avg)=3.92
unscoped   工具数(avg)=24.0    误调用率(avg)=54.1%   收敛轮数(avg)=4.25

误调用率下降 6.4pt (54.1%→47.8%,相对下降约12%)
收敛轮数下降 0.33 轮 (相对下降约8%)
工具数从 24 收窄到 17.54
plaintext

方向终于对了,且工具数收窄是实打实的(24→17.5)。但相对下降 12% 离简历原话”下降约 30%“有距离,诚实的解释:数据集里落到 generic_oncall(兜底域,本身允许几乎所有只读 工具)的 case 误调用率高达 70-78%,且两组差异不大——因为它本来就是”什么都能调”的域, 拉低了整体的组间差异。要是真想验证到 30%,需要把这类”路由本身就落到宽泛兜底域”的噪声 单独摘出来看,或者扩大数据集摊薄这类 case 的权重。用户明确表示这个精度不重要,“流程 跑通、数字方向对即可”。

Q9: 用什么方式给这个脚本提速的?#

组内 case 用 asyncio.Semaphore 限流的 asyncio.gather 并发跑(默认并发 4,实测用 5),组间仍然串行——因为 unscoped 组靠 monkeypatch 一个模块级全局函数区分行为, 和 scoped 组同时跑会互相污染。全量 24×2 从串行的 44 分钟降到并发后的 12 分 24 秒,约 3.5 倍提速。没有开到更高并发的原因是 DashScope 经百炼调用的真实瓶颈是 RPM/TPM(项目 memory 里记过这个坑),并发太猛会导致个别 case 因限流报错,反而拉低数据可信度。

Q10: 跑的过程中还发现了什么值得说的细节?#

  • 一个 case(s19network_diagnosis)反复 reroute 到 8 轮,最终撞到了 LangGraph 的 recursion_limit=20 硬限制而不是 agent_max_steps 这个软限制,_run_single_graph 把这个异常干净地转成了 type=error 事件,没有让整个 benchmark 崩掉——这本身也是一个 值得关注的真实现象(有些复杂 case 确实会顶到图递归上限)。
  • 日志里有一条 docker_ps 调用因为 LLM 多传了个不存在的 show_all 参数被 pydantic 校验拒绝,被 tool_runner.py::_safe_invoke_tool 优雅降级捕获(记一条失败,不炸整个 执行)——这是一个小的 tool schema/prompt 不匹配,和收窄机制无关,但也说明生产代码的 容错设计在真实跑批量测试时确实起作用了。

二、点2:Deep 模式并行取证(scripts/eval_deep_evidence.py#

Q11: 这个脚本测两件事,为什么要分开测?#

  • coverage 实验:跑生产真实 Deep 图(get_deep_diagnosis_graph(),带 checkpointer, 和线上完全一致),测证据覆盖率(产出证据的 source 集合命中人工标注 golden_evidence_sources 的比例)和 RCA 关键词命中率。
  • parallel-benefit 实验:测”4 个专业 subagent 并行取证”本身的耗时收益。

分开的原因是生产图里 remediation 阶段会触发人工审批等待,这部分耗时和”并行取证” 毫无关系,混在一起测耗时会引入噪声,所以 parallel-benefit 实验特意跳过 remediation/ verify 节点,只隔离”证据阶段”的耗时。

Q12: 生产代码里没有”并行改串行”的开关,你是怎么测并行收益的?#

调研确认了这一点:4 个专业 subagent(Metric/Log/Infra/Runbook)的并行完全靠 LangGraph 静态图的 fan-out(同一个上游节点挂 4 条出边),不是 asyncio.gather,也没有配置项能 退化成串行;app/config.py 里唯一相关的 executor_parallel_enabled 管的是 fast 图 Executor 内部工具并行,对 deep 图 subagent 并行毫无作用。

解法是在 benchmark 脚本里 _build_comparison_graph(parallel: bool)复用生产的真实 节点函数incident_manager_node/evidence_plan_node/_resolve_specialist_node_fn/ evidence_reducer_node/rca_judge_node/report_node,全部从 deep_diagnosis_graph.py 直接 import),只把 fan-out 边改成链式(log_agent → metric_agent → infra_agent → runbook_agent),其余逻辑完全不变。这样保证”并行 vs 串行”是唯一变量,节点内部行为 没有任何改动。

Q13: 跑这个 benchmark 时发现了什么安全隐患?怎么处理的?#

这个环境 REMEDIATION_EXECUTE_ENABLED=true,如果某个 case 的 RCA 判定提出了可执行的 处置动作(比如”重启 payment-svc 容器”),remediation_executor_node 会真的调 execute_command_with_approval——这是内联同步等审批,默认超时 300 秒,而合成的 benchmark case 永远不会有真人去点批准,会导致每个提了处置方案的 case 卡 5 分钟,理论上 如果真被批准还会调用真实 docker 工具改到宿主机的容器状态。修复方式是在 coverage 实验里 临时把 settings.remediation_execute_enabled 设为 False,跑完自动还原——这不影响 evidences/rca 的采集(那些在 remediation 节点之前就已经产出)。这是跑 benchmark 之前 必须做的风险评估,不是事后才想到的补丁。

Q14: 实测数据是多少?#

单 case 冒烟测试(d01:payment-svc OOM 场景):coverage=0.667(命中 log/ mcp_tool_result,未命中 metric),RCA 关键词命中率 0.75,RCA 产出的根因文本是”疑似 payment-svc 容器因内存不足 (OOM) 被 Kill 导致重启”——语义合理。parallel-benefit 实验: 并行 15.53s vs 串行 32.92s,加速比 2.12x,数字符合直觉(4 个 subagent 里如果有 2-3 个被 EvidencePlan 实际派遣,并行应该有明显收益)。这两个数字都只是单 case 冒烟验证, 12 题全量跑到一半被用户中止过,重新起了并发支持(--concurrency 3,deep 图开销比 fast 图大,并发不敢开太高)但尚未产出完整报告——这是诚实边界,不能拿单 case 结果当作最终结论。


三、点3:Fast/Deep 动态切换性价比(scripts/eval_escalation_tradeoff.py#

Q15: 三组对照是怎么设计的?#

同一批 20 道标注 case(benchmark/escalation_labeled_20.jsonl,每题人工标 labelfast_sufficientneeds_deep):

  • fast_only:直接用 _run_single_graph + get_diagnosis_graph()(fast 图),完全 不走升级判断,等价于”强制只用 fast”。
  • deep_only:走公开 run_diagnosis_graph(diagnosis_mode="deep"),跳过 fast 直达 deep。
  • dynamic:生产默认行为——run_diagnosis_graph(diagnosis_mode="fast"),fast 跑完 用 app.runtime.escalation.should_escalate_to_deep() 判断是否升级。

每组记录成本(tokens、tool_calls、耗时)和质量(报告是否命中人工标注的 golden_root_cause_keywords)。核心结论不是看某一组的绝对值,而是”dynamic 组质量 接近 deep_only、成本显著低于 deep_only”——这才是动态切换的价值所在。

Q16: L1 信号的精确率/召回率是怎么算的?为什么能”顺便”算出来不用多跑一次?#

fast_only 组本来就要跑一次真实 fast 图,这次运行的内部 meta dict(transitions/ selected_skill/skill_confidence/reroute_count,这些字段确认不会经 SSE 事件透出, 只能直接 import _run_single_graph 拿内部状态)刚好可以重建 FastRunSignals 并调用 should_escalate_to_deep(),和人工标注的 labelneeds_deep 是否为真)对比:

  • precision = TP / (TP + FP)(脚本判定该升级的里面,真的该升级的比例)
  • recall = TP / (TP + FN)(真正该升级的里面,被正确识别出来的比例)

这样 fast_only 组的一次真实运行同时服务了”三组性价比对比”和”L1 信号精确率/召回率”两个 实验,不用重复花钱。小样本冒烟测试时(只抽了 2 题,恰好都是 fast_sufficient 标签) TP=FP=FN=0,精确率/召回率算不出来(除零保护返回 None,没有崩),这是样本量问题,不是 脚本 bug——真正有意义的数字需要跑满 20 题保证正负样本都有。

Q17: L0 入口预判是怎么测的?为什么说它”几乎零成本”?#

_diagnosis_mode_for(payload, alert) 是纯函数(不调 LLM),依据三个信号判断是否直达 deep:severity ∈ {critical,page,p0}、告警条数 ≥ alert_storm_deep_threshold (默认10)、跨故障域数 ≥ alert_cross_domain_deep_threshold(默认2,用 app/incidents/fault_domain.py::is_cross_domain 的静态关键字查表,不调 LLM)。测试 构造了 5 条合成 AlertmanagerPayload(单条 warning / critical / 告警风暴 12 条 / 跨域 host+network / 同域多条),验证跨域判断时特意选了能对上真实关键字表的 alertnameHostCpuHigh 命中 cpu 关键字 → host 域,Http5xxRate 命中 http/5xx → application 域),确保测的是真实分类逻辑,不是巧合。结果 5/5 全对。

Q18: 这个脚本里也有一个”跑 benchmark 前先做风险评估”的例子?#

有。deep_only/dynamic 两组走公开 run_diagnosis_graph(),跑完会 best-effort 把诊断报告 ingest 进真实的 data/wiki/ 知识库(settings.wiki_enabled 默认 True)——benchmark 用的是合成故障描述,不能真的写进生产 wiki 污染真实 RAG 召回内容。修复方式和点2一样: 跑之前临时把 settings.wiki_enabledFalse,跑完还原,并且额外用 find data/wiki -newer <某个跑之前的文件> 实际核对过确实没有新文件写入。


四、点4:意图-执行分离处置(scripts/eval_remediation_safety.py#

重构更新(资源建模):处置层已从”命令建模(command_guard 拼 argv)“顶层重构为 “资源建模 + 事务化工具”(详见 03-Q21.6)。系统里不再有命令 字符串——LLM 只声明意图 {resource, operation, params},唯一收敛点变成 app/remediation/model.validate_intent + policy.check_protected。红队 benchmark 相应 从”测两条命令护栏路径”改成”测意图校验”:22 条对抗意图(注入 URI / 白名单外操作 / 保护资源 / 越界参数)+ 混入合法意图,block_rate 100%、false_positive_rate 0% (报告 benchmark/reports/remediation_safety_20260707-*.json)。CAS 并发正确性那一半 (Part B)不受影响,只把测试用的 tool_namerun_docker_command 换成 container_restart下面 Q19~Q21 保留旧命令护栏的叙事——它记录了”字符串解析 vs 意图-执行分离”的对比实验和一个真实发现的 gap(多目标注入),是重构动机的实证来源, 面试时可以讲成”为什么我最后把整套命令护栏都删了,改成资源建模”的演进故事。

📊 处置安全数字卡(2026-07-07 实测,remediation_safety_20260707-010133.json

指标数值说明
红队意图拦截率17/17 = 100%注入 URI / 白名单外操作 / 保护资源 / 越界参数
合法操作误杀率0/5 = 0%合法意图全部放行
典型攻击样本container://web-api; rm -rf /URI 解析层字符集校验直接拒

同日完成闭环接线(Saga 补偿 / 资源锁+幂等 / 回归验证 / 飞书审批升级,详见 07-处置事务与闭环),配套 28 个离线单测 覆盖逆序补偿、占位拒补、锁互斥、幂等复见、告警消退验证等关键路径( tests/test_remediation_saga.py / test_closed_loop_execution.py / test_remediation_locks.py / test_remediation_regression.py)。

Q19(旧): 红队测试的两条路径分别测什么?为什么要分开测?#

(下述 command_guard.py 已在资源建模重构中删除,内容作为演进背景保留。)

app/runtime/command_guard.py(已删除)曾有两条入口:

  • typed 主路径build_docker_action/build_service_action/build_kill_action): LLM 只填结构化字段(verb 是枚举、target 过正则、pid 是 int、signal 是枚举),命令 argv 由代码固定拼装。
  • string 防御路径check_docker_command 等,遗留/纵深):接收整条命令字符串, 靠 shlex + 元字符拦截 + flag 黑名单校验。

分开测是为了对比”意图-执行分离”相对”传统 LLM 直接生成 shell 命令”架构的真实收益——如果 两条路径拦截率一样,那”意图-执行分离”这个卖点就没有实证支撑。

Q20: 红队测试构造了哪些攻击用例?结果如何?#

typed 路径 13 条攻击(非法枚举 verb、越界 PID、伪装成 target 的 shell 元字符/命令替换/ flag、KILL(-9) 信号)+ 4 条合法指令对照;string 路径 11 条攻击(shell 元字符拼接、危险 flag、命令替换、多目标、kill -9)+ 4 条合法指令对照。

结果:typed 路径 block_rate=100%,false_positive_rate=0%string 路径 block_rate=90.9%,false_positive_rate=0%。差的那一个是 "docker restart a b c"—— check_docker_command 只挡了元字符/危险 flag/非法 verb,没有限制目标数量,导致 这条多目标字符串命令被判定为合法(allowed=True),而 typed 路径因为 target 是单个 字符串字段,天然堵死了这个口子(“a b c”整体过不了正则)。这是红队测试真实发现的一个 生产代码 gap,不是编出来凑数据的,也直接印证了”意图-执行分离比字符串解析更安全”这个 论点——不是理论上更安全,是实测出来更安全。

Q21: 一开始这个红队测试的结果是错的,是怎么发现并修复的?#

第一版 STRING_CASES 里给 check_action 传的 action_type 用了 "service"/"kill", 但 check_action 的分发逻辑只认 "docker"/"host_service"/"host_process" 三个值—— 写错的这 4 条用例全部走到了”未知类型 → 直接拒绝”的兜底分支,根本没有真正测到 check_service_command/check_kill_command 的逻辑,只是侥幸因为兜底也是拒绝而 凑对了攻击用例的期望值,而 2 条合法指令因此被误判成”假阳性”。这是代码审查阶段(写完 脚本先自己过一遍逻辑,不是先跑)发现的,改成正确的 action_type 字符串后才是现在的 真实结果。这个经历本身也是一个很好的”如何做代码审查”的例子——光看脚本能跑通、能出数字 是不够的,还要核对每个分支是不是真的测到了想测的代码路径。

Q22: CAS 并发正确性为什么一定要用真实 Postgres 测,in-memory mock 测不出来吗?#

ApprovalRepository.claim_for_execution/claim_resumeUPDATE ... WHERE status='approved' RETURNING id 做乐观 CAS,真正的原子性保证在 Postgres 单条 UPDATE 语句本身(行级锁 + MVCC),不是应用层加的锁。in-memory 的 Python 对象(哪怕加 asyncio.Lock)测的是”我自己写的并发控制代码对不对”,测不出 “数据库引擎级别的原子性是否真的生效”——这恰恰是这个设计最核心的假设。所以脚本对同一条 真实插入的 approval_request,用 asyncio.gather 发起 N 个真正走连接池的并发调用, 断言”恰好 1 个成功”。跑的时候(concurrency=50, repeats=10,两个场景各跑 10 轮)通过率 100%,每次都精确 1 个成功。

Q23: 为什么 Part B 要跑到 concurrency=50 这么高,生产实际部署才 3 个 worker?#

生产现在部署的是 3 个 diagnosis worker,正常情况下并发抢同一条审批的场景不会超过个位数。 但压测的目的是验证”这个 CAS 语义在远超当前部署规模的并发下依然成立”,这样即使未来扩容到 数十个 worker、或者审批超时扫描器和正常 worker 同时触发恢复,也有实测数据支撑”不会双重 执行”这个结论,而不是只能说”3 个 worker 下测过没问题”。


五、点5:告警风暴三层去重消融(scripts/eval_dedup_layers.py#

Q24: 三层去重机制分别是什么?消融实验怎么设计的?#

  • L1 fingerprint 幂等_normalize_alert()idempotency_key(receiver:groupKey: fingerprint:时间桶:status),_upsert_alert()ON CONFLICT (idempotency_key) 防止同一条告警的重复投递在 alerts 表里增殖。
  • L2 事件聚合分组_correlation_key()correlation_key(优先 alertmanager:{receiver}:{groupKey}),_upsert_incident_group()ON CONFLICT (correlation_key) 把同一根因的多条告警归到同一个 incident_group
  • L3 诊断任务级去重_create_or_get_task()dedup_key=incident_group_id, Postgres 部分唯一索引 ON CONFLICT (dedup_key) WHERE status IN ('pending','running'), 这是唯一处理并发竞态的原子操作(改造前是 SELECT-then-INSERT,有竞态)。

代码里没有任何配置开关能单独关掉某一层(搜过 app/config.pydedup/ correlation/alert_storm 等关键词,都不是”关闭某层”的开关,后两个是 L0 fast/deep 路由阈值,跟去重是两回事)。消融只能靠 monkeypatch app.incidents.repository 里的 _normalize_alert/_correlation_key/IncidentRepository._create_or_get_task,强迫 某一层的 key 每次都唯一(附加一个递增 nonce),绕开它的 ON CONFLICT 命中,模拟”这层 不存在”。

Q25: 为什么”关掉 L1”对收敛比没有影响,这是不是说明 L1 没用?#

不是没用,是L1 和 L2/L3 保护的是不同的东西。L1 的 idempotency_key 只影响 alerts 原始表的行数增殖(同一条告警重复投递不会在表里堆出多行),不影响 incident_group_id/dedup_key 的计算(那两个是从 receiver/groupKey/labels 算出来的,跟 alert.id 无关)。所以关掉 L1,diagnosis_tasks 的收敛比完全不变——这是 实测验证出来的,不是靠代码读出来的猜测:n=2400/distinct=32 时,baseline 收敛比 75.0, disable_l1 收敛比依然是 75.0,disable_l2/disable_l3 都直接崩到 1.0。这组对比恰好说明了 “L1 保护的是写入放大,L2/L3 保护的是诊断触发频率”——是两个独立的、都必要但作用层面不同 的机制。

Q26: 这个脚本是怎么在真实数据库上跑又不破坏真实数据的?#

_run_burst() 每轮生成一批带 BenchStorm-{run_tag}- 前缀的合成 alertname,跑完 _cleanup() 会精确删除本轮制造的数据——但不是简单粗暴地按前缀 DELETE,而是先 SELECT 收集 alert_ids/group_ids 集合,再按 diagnosis_tasks → incident_group_alerts → incidents/incident_groups → alerts 的依赖顺序删除。这里有一个我自己写的时候踩过的 坑:最初的版本在删完子表之后又用子查询反查父表(比如”删 incident_groups WHERE id IN (SELECT incident_group_id FROM incident_group_alerts)”),但这时候 incident_group_alerts 已经被删空了,子查询会查到无关的行,有误删真实生产数据的风险—— 后来改成先收集 ID 快照再按顺序删,彻底消除了这个隐患。每次跑完都用 docker exec ... psql -c "SELECT count(*) FROM alerts WHERE alertname LIKE 'BenchStorm-%'" 之类的命令实际核对过清理干净(结果是 0)。

Q27: 最终跑出来的数字是多少?#

n=2400/distinct=32:baseline 收敛比 75.0——和简历”2400 条收敛至 ~50 个诊断 (75:1)“精确对上(2400/32=75)。disable_l1 不变(75.0),disable_l2/disable_l3 都崩到 1.0。吞吐 300-350 条/秒(纯 DB upsert,不含 LLM/HTTP)。这是 5 个点里唯一一个真实数字 精确复现了简历原话的,因为这是纯 DB 逻辑消融,没有 LLM 随机性和路由噪声干扰。


六、横向的工程方法论#

Q28: 5 个脚本跑起来花了不少真实成本,你是怎么控制这个成本的?#

几个原则:

  1. 免费分析优先于花钱重跑——点1 的反事实归因分析(用历史调用记录 + 新规则重放,不 调 LLM)是核心手法,能先低成本验证”改动有没有效果”,再决定要不要花钱重新跑全量。
  2. 量级分层决策——纯 DB/纯函数的实验(点4 Part A、点5)敢往生产真实量级冲,真实 调 LLM 的实验(点1/2/3)保守,优先保证流程能跑通、方向正确,不盲目堆样本量。
  3. 组内并发、组间串行——用 asyncio.Semaphore 限流的 asyncio.gather,在不破坏 “同组内需要共享的全局 monkeypatch/settings 状态”这个约束的前提下,把可以并行的部分 并行掉。点1 实测并发 5 比串行快 3.5 倍。
  4. 小样本冒烟测试先行——每个脚本写完先跑 --limit 1--limit 2 验证端到端能 通、没有明显 bug,再决定要不要跑全量,避免脚本有 bug 但要等 40 分钟才发现。

Q29: 跑 benchmark 之前你具体做了哪些”安全评估”?能举全部例子吗?#

四个真实发现并修复的风险点:

风险触发场景修复
真实审批等待卡 5 分钟点2 coverage 实验,RCA 提出可执行处置动作临时 remediation_execute_enabled=False
污染真实 wiki 知识库点3 deep_only/dynamic 走公开 API 会触发 ingest_diagnosis临时 wiki_enabled=False,跑后用 find -newer 核对无变化
误删生产 DB 数据点5 cleanup 逻辑如果用”删除后反查子表”会误删无关行改成先快照 ID 再按依赖顺序删
真实执行危险命令点4 红队测试如果传的是”合法样式”的写命令红队用例本身设计上就不含真实可执行目标(如 payment-svc 是虚构服务名),且 command_guard 只做静态校验,不真的调 subprocess

这几个风险不是写完脚本才想到的,是写脚本时就先读一遍相关生产代码的副作用(比如 读 remediation_executor_node 的完整实现,确认它在什么条件下会真的调工具)才发现的。

Q30: 后台跑这些脚本时踩过什么坑?#

有一次在已经用工具的 run_in_background: true 机制的前提下,又手动在 shell 命令里加了 nohup ... &,导致工具只跟踪到”外层 shell 脚本启动完子进程就退出”这个事件,几十秒后就 误报”任务完成”,但真正的 benchmark 进程其实是用 nohup 分离出去独立跑的,完全没被工具 的完成通知机制捕获。发现的方式是看到”进度日志内容明显只到中途”就觉得不对,去 ps -p <pid> 一查发现进程真的还在跑。后来的做法是:要么直接把长耗时命令原样交给 run_in_background: true(不再叠加 nohup/&),要么如果已经手动 backgroud 了,就再 包一层”轮询 ps -p <pid> 直到进程退出再 tail 日志”的等待脚本重新交给 run_in_background: true,让工具跟踪这个”等待壳”而不是原始命令。

Q31: 5 个脚本各自的核心指标计算逻辑,能不能各用一句话概括?#

一句话
1同一批 case 跑两遍 fast 图,只切 Executor 能看到的工具集合,比工具调用是否落在人工标注的”该用工具”之外
2跑真实 Deep 图测证据/RCA 质量;另外拼一个”节点函数不变、只把 4 个 subagent 从并行改成链式”的对比图测并行的边际耗时收益
3同一批 case 分别跑”只 fast/只 deep/生产默认(fast-first+条件升级)“三条路径,比成本和质量;顺便拿 fast 图这次运行的内部信号回放升级判断逻辑算精确率召回率
4拿真实攻击字符串/参数喂护栏纯函数算拦截率;拿真实 Postgres 并发抢同一条审批算 CAS 是否恰好一个赢
5monkeypatch 掉某一层的 key 生成逻辑让它「失效」,比消融前后诊断任务的收敛比

七、简历数字诚实性#

Q32: 面试官问”你简历上写的数字是不是编的”,你怎么回答?#

如实说:这轮工作本身就是”把设计意图变成可复现的量化实验”,跑出来的结果有的和简历原话 对得上(点5 的 75:1 精确复现),有的方向对但幅度没到(点1 的 12% vs 简历说的 “约30%”),也有的因为 LLM 随机性/样本量不够暂时不能下结论(点2/3 的全量数据还没跑完)。 我不会用”数字都对得上”这种一刀切的说法——那反而不可信。我会说:能拿出脚本和真实 报告 JSON,任何一个数字都可以现场重新跑一遍复现,跑不出来的地方我会直接说”这个还需要 更大样本/更严谨的实验设计才能下结论”。

Q33: “12% 相对下降”离”简历约 30%“有差距,你会怎么处理这个差距?#

两个方向都合理,面试时可以说清楚权衡:一是调整简历措辞,把”误调用率下降约30%“改成 更贴近实测且依然有说服力的表述(比如强调”写工具硬门禁 + 组件专属只读工具跨域隔离”这个 真实生效的机制,附上”12%相对下降”这个实测数字,而不是笼统喊一个更大的百分比);二是 重新设计实验剔除噪声,比如把落到 generic_oncall 兜底域的 case 单独摘出来看,因为 这类 case 两组配置本来就很接近,会拉低组间差异的绝对值。用户在这个项目上明确表示不追求 数字精确对齐,优先级是”流程全部跑通”,所以这次没有深入做第二个方向,但如果要在其他场合 汇报这个数字,我会补一次这个拆分实验再下结论。

Q34: 如果要在简历/面试里体现”你懂怎么严谨地做 benchmark”,这轮工作里最有说服力的是#

哪个环节?

点1 从”0% 差异 → 排查出真实设计缺陷 → 推动修复 → 用免费复算验证修复有效 → 重新花钱 跑出真实且方向正确的数字”这一整条链路,比任何一个单独的最终数字都更有说服力——它证明 了”发现数据不符合预期时,我会去找根因而不是调整评测方法凑数字”,而且根因排查用的是 零成本的事后归因分析(拿历史数据重放新规则),这本身也是一个值得展开讲的方法论 亮点。另外点4 红队测试发现的 check_docker_command 多目标绕过 gap,和自己发现的 STRING_CASES 参数写错的 bug,都是”过程中真实发现问题、真实修复”的例子,比一次性 跑出漂亮数字更能体现工程严谨性。


八、设计取舍与反思#

Q35: 如果重新设计这 5 个 benchmark,你会改什么?#

  1. 点1 的 golden_tools 标注应该分层:现在是单一”该用工具”集合,应该按”核心 必须工具”和”合理探索工具”分两档,避免 LLM 合理的探索性调用(比如”先查一下是不是 容器问题”)被一刀切算作误用。
  2. 点2/3 应该有一个统一的”标注置信度”字段:现在 golden_evidence_sources/ golden_root_cause_keywords 都是我个人标注的,没有第二个人交叉验证,样本量也不够 大,理想情况下应该让真实 SRE 标一批,或者至少多轮自评一致性检查。
  3. 点1 应该把”路由是否选对域”和”选对域之后误调用率如何”拆成两个独立指标——现在 两者混在一起,路由随机性会稀释 scoping 本身的信号,拆开看会更干净。
  4. 应该有一个统一的成本追踪——5 个脚本各自打点 token/耗时,但没有汇总一个”这轮 benchmark 总共花了多少钱多少时间”的报告,方便下次决定要不要扩大规模时有参考。
  5. 点5 的 monkeypatch 消融手法应该反向推动一个真实的配置开关——发现”没有办法单独 关闭某层去重”这件事本身,也是一个可以推给项目侧的产品建议:加一组 dedup_layer_l1/l2/l3_enabled 开关,不仅方便下次消融测试,生产上遇到某层出问题 需要临时降级时也有用。

Q36: 这轮 benchmark 工作和你平时做单元测试有什么本质区别?#

单元测试(tests/test_*.py)验证的是”这段代码的逻辑对不对”,大量用 mock/monkeypatch 隔离外部依赖,跑得快、可重复、不需要真实 LLM。这轮 benchmark 验证的是”这个机制在真实 分布/真实成本下,效果究竟有多大”——必须用真实 LLM、真实数据库、真实并发,甚至像点5 一样跑到真实生产量级,因为这些问题(“收窄能省多少误调用”、“并行能快多少”、“75:1 的 比例是不是真的”)本质上是经验性问题,光看代码逻辑推不出答案,必须实测。两者互补:单测 保证机制”没写错”,benchmark 回答机制”有没有用、值不值”。