
第一次看到这个项目名字时我确实愣了一下Jev从 DOOM 到动化测试的 AI Agent 决策模型。DOOM 在游戏圈里几乎就是“怪物 AI 教科书”的代名词而动化测试说白了就是让自动化测试不再老老实实按固定脚本跑而是像游戏里的敌人那样根据当前局势动态决定下一步动作。这个项目把这两件看起来毫无关系的事拉到了一起做了一个叫做 Jev 的决策代理它负责在每次测试任务到来时判断这一轮到底该跑哪些用例、用多大并发、按什么顺序执行哪些失败了要立即重试哪些稳定用例可以直接跳过。它要解决的是测试圈里最头疼的调度问题用例数量越来越多回归时间越来越长但真正能抓住问题的永远只是其中一小撮用例。如果你正在做测试平台、CI 流水线优化或者对 AI Agent 实际落地感兴趣这篇内容应该能给你一套可以照着搭的决策模型。我会从设计动机、决策链路、DOOM 行为到测试调度的映射关系、最小落地实现和踩坑实录五个角度展开。这里不说大词只讲我实际推演和试验过的方案。1. 为什么好端端的测试要借鉴一只老游戏里的怪物AI1.1 传统自动化测试里的“死板”困境先聊一个每天都在发生的场景某个项目里积累了三千多个自动化用例单个用例平均跑三十秒全量跑完要二十五个小时。CI 窗口只有四个小时所以“全量回归”在物理上就是不成立的。于是团队开始拍脑袋要么按最近修改的模块筛要么按上次失败过的用例筛要么干脆按用例名称排序截取前几百条。这些做法不能说完全没用但本质上都是静态规则根本不管这次提交到底改了什么、风险集中在哪、哪些用例最近已经连续验证过很多次了。我见过更极端的情况某个核心模块的用例因为长期稳定被规则直接跳过结果一次改动恰好破坏了它依赖的环境配置上线后直接炸了。这就是传统自动化测试的死穴——它把“执行用例”当成固定动作而不是“根据证据做决策”。用例本身只是承载验证逻辑的工具真正值钱的信息是这次改变影响什么、哪些测试最近处于失稳状态、哪些用例历史上抓到过真 bug。传统方案把这些信息全扔了。1.2 DOOM 的 AI 到底强在哪现在说说 DOOM。这款老游戏里的敌人 AI 放到今天看依然非常值得学习但它的强不在于“聪明”而在于用极少的资源实现了有效的威胁反馈。小怪平时在走廊里巡逻玩家一进入视线范围立刻切换到攻击模式近战怪会拼命拉近距离远程怪会保持安全距离持续放弹幕Boss 血量掉到一定程度后会进入狂暴状态攻击频率和移动速度双双提升。这些行为在当年只有几十 KB 内存的机器上跑得很流畅靠的不是神经网络而是非常扎实的状态机加感知切换。用生活类比来解释这就像一个不会思考但策略极稳的安保他的行动完全由一套规则驱动——“我看到了陌生人感知距离从远到近状态变化所以我要做出相应反应动作切换”。他不是真的“懂”发生了什么但他表现出的行为足够有攻击性、有策略性。DOOM 的怪物 AI 最核心的一点就是每一个决策背后都有明确的环境信号触发而且决策结果会实时影响后续环境形成闭环。1.3 游戏 AI 和测试执行的同构关系为什么游戏里的怪物决策可以映射到测试调度上因为它们本质上都在处理同一个问题资源有限、目标不确定、环境动态变化。我做了个对照表越看越觉得两者结构完全一致。DOOM 里的元素测试系统里的对应物玩家位置、血量、距离代码变更范围、CI 剩余时间、当前风险水位怪物察觉范围用例关注度阈值命中变更点才触发攻击模式切换执行策略切换冒烟、定向、全量、重放弹幕密度控制并发度与资源配额限制怪物死亡重生用例跳过、失败重试、下个周期重新纳入仇恨值记忆历史失败率、失败新鲜度权重这种同构不是巧合。两边都需要在信息不完整的情况下用有限预算做出尽量优的决策而且决策本身又要能根据结果不断修正。DOOM 的敌人会一直盯着玩家曾经出现的位置看一眼再走开测试调度也应该记住哪些用例曾经出过事、最近又在蠢蠢欲动。这就是 Jev 整个模型的地基。2. Jev 的整体决策链路感知、判断、行动、记忆2.1 第一层感知层——把测试信号变成决策特征Jev 不看单个用例里的断言内容它只看围绕用例产生的元信号。这有点像 DOOM 怪物不需要理解玩家拿什么枪只需要感知“距离近了”“我被攻击了”这几个信号就够了。我把特征归成六类用例历史状态通过、失败、超时、跳过按时间序列记录。失败新鲜度最近五次执行里失败分布在哪越是刚发生过的失败权重越高采用时间衰减窗口。代码影响面用例关联的模块、文件在本次提交中的命中次数命中越多说明越该被验证。执行成本单条用例耗时、占用的资源、外部服务依赖数。历史信息量这条用例从上线到现在抓到过多少个真实缺陷这是它的“战绩”。环境状态CI 队列压力、当前剩余时间预算、被测服务版本健康度。感知层要做的一件事就是把原始数据统一成可计算的数值。比如失败新鲜度不是简单记“失败几次”而是用一个指数衰减窗口计算最近一次失败记当前值 1.0前一次失败乘以 0.7再前一次再乘以 0.7最后加起来。连续两次刚发生的失败会得到接近 1.7 的分数而一个月前连续的失败基本衰减到可以忽略。这么做是为了让模型对“正在发生的问题”敏感而不是对“曾经的历史”念念不忘。2.2 第二层决策层——状态机驱动下的行动选择特征算出来后进入决策层。Jev 的核心调度逻辑是一套有限状态机它不试图在所有时刻做出完美决策而是保证自己在任何时刻都有一个明确的执行模式。状态包括待命、定向追踪、冒烟压制、全量清扫、失败重放和隔离观察。每个状态之间的切换条件都是可解释的规则表达式直接由感知层的特征触发。比如“定向追踪”状态要求高风险用例集合的数量超过预设阈值并且当前剩余时间还够执行其中一半“全量清扫”则需要剩余时间预算超过估算的全量执行时长的一点五倍同时变更影响范围扩散到三个以上模块“失败重放”只在刚跑完一轮且失败率突然升高到百分之十五以上时触发。这套状态机解决了传统方案里最尴尬的“策略打架”问题。你不可能同时既想跑冒烟又想跑全量就像 DOOM 的怪物不可能同时处于潜行和狂暴状态。状态机逼迫决策者在每个时刻只有一个明确的主目标所有资源都围绕这个主目标分配避免了两边讨好的混乱。2.3 第三层行动层——四类执行动作的触达方式行动层把决策结果翻译成实际执行的指令我把它归结为四种动作跳过当用例稳定性极高且不命中任何变更点时直接标记“跳过并保留理由”不进执行队列。冒烟压制选取冒烟集合并叠加当前风险分最高的用例用最快速度跑完一轮验证主链路没断。定向追踪围绕变更命中模块展开相关用例按风险分降序执行不再依赖人工维护的“影响用例清单”。全量清扫在时间预算充足时全量执行但顺序仍然由打分决定而不是按名称或创建时间。失败重放对刚失败的用例立即用相同环境重跑一次区分稳定性和真实缺陷降低误报率。这四类动作对应 DOOM 里的怪物行为逻辑平时巡逻、看见人冲锋、被逼急了放弹幕、脱战了回到角落回血。每一类动作都不是拍脑袋定的而是对应一种典型测试风险场景。2.4 记忆与反馈让决策模型越用越准Jev 和普通规则调度最大的区别在最后一层执行结束后结果会回写到记忆单元用于修正后续的判断。逻辑很简单如果一个用例连续五次被跳过后来在某次变更时被选中执行并发现了缺陷那么“跳过”这个动作会得到一个负反馈决策模型会把这个用例的稳定性基线往下调后续不再轻易跳过它。反过来一个用例连续十次通过且从未命中过缺陷那它的执行价值就在下降资源应该让给更有信息量的用例。反馈更新我用了指数加权移动平均公式是new_estimate alpha * latest_result (1 - alpha) * old_estimatealpha 取 0.3 左右。这个值不能太大太大模型会对单次随机抖动过度反应也不能太小太小反馈要很久才能影响决策等于没有记忆。为了让冷启动阶段更稳定我还在估计值里加了拉普拉斯平滑避免某个用例因为历史样本太少出现极端分数。这一环节做好了Jev 才算是一个真正的 Agent而不是一个静态评分器。3. 从 DOOM 怪物行为到测试调度算法的逐条映射3.1 威胁等级评估测试用例的“仇恨值”DOOM 里的怪物会对玩家维持一个威胁感知距离近的威胁高、正在攻击自己的威胁高、血量低的玩家在部分怪物眼里反而威胁更高因为它“更容易被击杀”。Jev 给每条用例算一个对应的“仇恨值”我习惯叫风险分。综合公式是线性加权score w1 * fail_freshness w2 * code_hit w3 * bug_rate w4 * cost_penalty - w5 * stability exploration_noise默认权重建议是失败新鲜度 0.4、代码命中 0.35、历史发现缺陷率 0.15、执行成本 0.05、稳定性负权重 0.05。这个权重分配的逻辑很直白正在失败的和被变更命中的用例最值得跑历史有价值但别迷信成本只是一个温和的惩罚项。举个例子某条用例最近两次执行连续失败失败新鲜度算出来 0.8它关联的模块这次提交命中了一个文件code_hit 记 1.0历史上它抓到过三个真实缺陷bug_rate 归一化后 0.6执行时长中等成本项 0.3稳定性评估 0.1。代入公式算出来是0.4*0.8 0.35*1.0 0.15*0.6 0.05*0.3 - 0.05*0.1 0.77这个分数足够让它在当前状态里进入定向追踪的执行列表头部。我刻意没有一上来就用复杂模型原因只有一个线性加权可以解释。当测试owner看到某条用例被调上来了他可以指着权重说“因为最近失败得频繁而且这次改到它的模块了”这个解释在工程协作里值千金。黑盒模型分数高但说不清原因根本没人敢在生产环境用。3.2 攻击模式切换按风险状态切换执行策略DOOM 的怪物最精彩的设计就是模式切换玩家距离远就巡逻距离近了就冲锋血量健康就放弹幕玩家残血反而狂暴追击。Jev 的执行策略切换也是同样的逻辑只不过感知信号从“距离”变成了“风险聚合度”和“时间预算”。伪代码可以这样描述def decide_state(features): if features.time_left estimated_full_suite_time * 0.3: return State.SMOKE if features.high_risk_case_count 20 or features.risk_spread 3: return State.TARGET if features.time_left estimated_full_suite_time * 1.5: return State.SWEEP if features.recent_fail_rate 0.15: return State.REPLAY return State.STANDBY注意判断顺序是有讲究的。时间不足是第一优先级因为物理约束必须先满足其次是风险集中度再其次是全量可行性失败率触发重放要放在最后因为重放依赖前面已经产生了执行结果。这个顺序就是 DOOM 怪物面对玩家时“先保命再攻击最后追击”的行为层级换到测试场景就是“先保证出结果再提高覆盖率再追求缺陷捕获率”。3.3 资源控制调度密度与并发窗口DOOM 里所有怪物不会在同一帧内同时开枪因为那样玩家根本没有操作空间游戏就失去了平衡。测试调度面对同样的约束不可能让所有高风险用例同一时刻并发执行数据库连接池会爆、测试数据会互相污染、缓存服务会被冲垮。Jev 的资源控制借鉴了令牌桶的原理。执行器每开始一个用例就消耗一个令牌用例结束令牌归还每个状态对应不同的令牌上限。冒烟状态的并发控制在 8 到 12 个因为冒烟用例简单快速单位时间令牌消耗大但单占用小定向追踪控制并发在 4 到 6 个因为要跟踪失败现场并在执行时额外开启日志采集和网络抓包成本更高全量清扫时反而把并发压到 10 个以内防止长尾用例把共享环境拖垮。我踩过一次很痛的坑一开始没做资源控制高风险用例一次性全部并发出去结果某个共用测试库的无主数据把整个 CI 环境搞乱了所有用例都在失败失败率百分之百。决策模型立刻进入重放状态重放又继续并发雪上加霜。后来加上令牌桶和单环境并发上限这个问题才彻底消失。DOOM 教给测试调度最重要的一课就在这里攻击力再强的怪也要受弹幕密度约束否则只是自杀式冲锋。4. 一个可直接复制的落地改法最小可行实现4.1 定义用例画像与历史基线要复现 Jev第一步是把用例信息结构化。这里假设测试平台已经能记录每次执行的结果我们需要为每条用例整理一张画像表至少包含用例标识、关联模块、平均耗时、历史失败记录、发现缺陷记录、稳定基线。我用下面的结构来组织{ case_id: case_10087, modules: [order, payment], avg_duration_sec: 42, history: [ {ts: 1742000000, status: fail, bug_id: BUG-324}, {ts: 1742080000, status: pass}, {ts: 1742160000, status: pass}, {ts: 1742240000, status: fail, bug_id: NULL} ], bugs_found: 3, stability: 0.75 }这个画像的积累成本很低所有数据都是执行过程中自然产生的。冷启动阶段没有历史数据我的建议是不要假装有经验把bugs_found设为 0stability设为 0.5fail_freshness全零让模型在前两周进入“探索模式”也就是每次都在得分排序的尾部随机补充百分之十的用例专门用来收集基线数据。4.2 特征采集与打分特征采集函数可以从版本管理系统、测试平台数据库、CI 系统三个来源拉数据。打分部分我用一个可配置的加权函数实现方便随时调参FEATURE_KEYS [fail_freshness, code_hit, bug_rate, cost, stability] DEFAULT_WEIGHTS { fail_freshness: 0.40, code_hit: 0.35, bug_rate: 0.15, cost: 0.05, stability: -0.05, } def compute_score(features: dict, weights: dict DEFAULT_WEIGHTS) - float: score 0.0 for key in FEATURE_KEYS: score weights[key] * features.get(key, 0.0) return round(score, 3)这里features里的每一项都必须是已经归一化到 0 到 1 区间的值。归一化的方法不复杂但很重要fail_freshness用衰减窗口算完再除以窗口内最大可能值code_hit用命中文件数除以该用例关联文件总数bug_rate用发现缺陷数除以历史执行次数然后做一次上限截断cost用执行时长除以项目最慢用例时长stability直接用历史通过率。归一化做不到完美但至少让不同量纲的特征能安全相加。4.3 状态机的实际数据结构状态机不需要引入框架一个枚举加一个转移表就够了。我用 Python 伪代码实现过一版from enum import Enum class State(str, Enum): STANDBY STANDBY TARGET TARGET SMOKE SMOKE SWEEP SWEEP REPLAY REPLAY TRANSITIONS { State.STANDBY: [ (risk_level 0.6 and high_risk_cases 20, State.TARGET), (time_left full_estimate * 0.3, State.SMOKE), (time_left full_estimate * 1.5, State.SWEEP), ], State.TARGET: [ (time_left full_estimate * 0.3, State.SMOKE), (risk_level 0.2, State.STANDBY), ], State.SMOKE: [ (recent_fail_rate 0.15, State.REPLAY), (time_left full_estimate * 1.5, State.SWEEP), (smoke_all_passed, State.STANDBY), ], }有一个关键细节状态转移表是顺序检查的不是优先级最高匹配。这意味着条件顺序本身就是策略的一部分调整顺序比调整权重更方便观察效果。DOOM 里每个怪物也是按固定优先级检查距离、血量、攻击状态顺序错了 AI 就会变得很蠢。4.4 执行、回写与再训练闭环调度主循环很短真正的复杂度都在前面几步def run_round(job): features extract_features(job) state decide_state(features) cases select_cases(state, features) results execute_with_budget(cases, max_concurrencycompute_budget(state)) writeback_to_store(results) update_weights(results) # 每一轮做增量更新回写不能只存“通过或失败”至少要带上环境版本、用例版本、执行时长、失败类型、关联的缺陷号。这些字段决定了后续反馈更新的质量。再训练我建议放在每天凌晨跑一次离线任务用前一天的全部结果重新拟合稳定性基线和权重而不是在每轮 CI 实时更新。实时更新会让高频调度的用例获得不成比例的影响力离线批量更新能抹平这种时间上的不公平。最后所有决策记录都要落库。哪轮跑了哪些用例、哪些被跳过、跳过理由是什么、当时的状态是什么全都可以回溯。这是 Jev 模型让人敢用的底气——它不是黑盒它每一次行动都有据可查。5. 踩坑实录Jev 在实战场合常见的六个问题5.1 稳定用例被反复误杀现象某条用例标记为高稳定连续好几轮都被跳过结果一次线上事故恰恰由它所在模块引起。根因稳定性权重只用了通过率没有考虑“上次出问题距今多久”。一条用例即使整体通过率很高只要它在最近一次被选中执行时暴露过隐患就应该长时间保留在关注列表里。解决把fail_freshness的衰减窗口从纯数值衰减改成“离散事件衰减”也就是一旦命中缺陷即使分数衰减到零仍然保留一个持续三十天的“观察标记”在这段时间内禁止进入跳过集合。这个标记相当于 DOOM 里怪物失去玩家视野后仍然会走向最后看到的位置看一眼是一种合理的“持久仇恨”。5.2 策略震荡导致执行队列反复跳变现象连续几个 commit 之间同一批用例一会进入定向追踪一会退回待命执行队列每次都剧烈变化测试 owner 根本看不懂。根因失败新鲜度对单次随机失败太敏感。一条偶发超时的用例瞬间把风险分拉高下一轮它通过后分数又跌回去模型就跟着反复横跳。解决给状态转移加最短驻留时间。比如进入定向追踪后至少要停留在该状态两轮 CI 才能切换出去除非时间预算出现硬约束。这个改动和 DOOM 怪物的攻击后摇机制很像——怪物挥出一刀后必须等动作结束才能切状态否则 AI 会变成每秒切换动作的抽搐怪。5.3 反馈循环固化了错误偏见现象模型越来越偏重某几个历史上有战功的用例频繁调度它们导致它们的失败次数上升模型为进一步偏向它们其他用例被系统性饿死。根因这是经典的利用与探索失衡。反馈循环里“发现过缺陷”的权重被反复强化模型没有机制去尝试那些没被选中的用例。解决把 epsilon 探索机制写进选择函数每次在选择执行集时以一定概率我建议百分之十从低分区域随机抽取用例进入队列。这个思路直接来自 DOOM 怪物不会永远朝同一个方向看它会偶尔回头巡逻。实测下来探索率百分之十对整体回归效率的影响几乎可以忽略但对模型盲区的覆盖帮助巨大。5.4 冷启动阶段采样偏差现象刚上线 Jev 的团队发现模型过于激进地选择新提交的用例而历史稳定的核心用例全被晾在一边。根因历史数据为空时模型只能依赖 code_hit 和新用例的默认分值本质上变成了“最近改哪里就跑哪里”回到了拍脑袋筛选的老路。解决冷启动前两周不做任何跳过执行全量候选集的冒烟加定向子集同时把探索率临时提高到百分之二十五尽量在两周内收集足够多的基线数据。前两周牺牲一点效率是值得的否则模型从一开始就被带偏后面纠正成本更高。5.5 时间预算硬约束下的饥饿现象现象剩余时间只有四十分钟高分用例却有两千个队列永远跑不完后面大部分用例一直饥饿。根因按照分数降序排列时没有考虑用例的执行成本低价值但五分钟的长用例会霸占队列高价值但三十秒的用例反而排在后面。解决排序键从score改成score / avg_duration这本质上是单位时间收益排序。这样调整后长时低价值用例会让位给短时高价值用例同样的预算下能覆盖的模块数量明显增加。这个思路在第一轮实验后就被证实有效冒烟阶段覆盖模块数提升了约百分之四十。5.6 数据漂移环境变化让历史特征失效现象测试环境升级了数据库版本所有联调用例集体超时失败Jev 把它们全部标记为高风险开始疯狂重放但重放结果还是失败。根因特征没有归一化到环境版本上。执行结果和测试环境强相关环境本身出了问题时所有历史特征都失真了。解决执行库里给每条结果加上环境版本和被测服务版本反馈更新的时候只使用同环境版本的数据。更进一步的做法是加一个“环境健康度”信号当环境下整体失败率超过平时百分之二十时把失败新鲜度特征整体打五折避免模型被环境波动误导。这条经验让我之后再也不敢裸奔部署任何决策调度器环境信息必须是你模型的亲爹。下面把这些问题整理成一个速查表方便实际排查时对照。问题典型现象根因解决建议稳定用例误杀跳过用例所在模块出现线上问题只看通过率、忽略上次缺陷时间增加离散事件观察标记策略震荡执行队列每轮大变对单次随机失败过度敏感状态最短驻留时间反馈偏见固化少数用例被反复调度利用与探索失衡增加 10% 探索率冷启动偏差新用例占比过高历史基线缺失前两周全量收集基线时间预算饥饿长时长低价值用例霸占队列排序键未考虑成本改用单位时间收益排序环境数据漂移环境故障导致特征失真特征未按环境版本归一化按环境版本分组更新6. 模型边界、演进方向与个人体会Jev 目前这套方案能直接解决“跑哪些用例、怎么跑、跑完怎么记住教训”的问题但它不是万能的。它不负责生成新的测试用例也不负责自动定位 bug 根因。它更像一个战场指挥官而不是侦察兵或军医。在实际落地中我见过团队在这个模型基础上往三个方向延伸效果都还不错。第一个方向是把 Jev 的输出从“执行队列”升级成“发布门禁意见”。每次提交时Jev 给出的不再只是用例列表而是一个置信度数值这批变更在当前测试覆盖下的风险评级。评级低于阈值就阻止发布高于阈值则放行这比固定跑完多少用例才算合格要科学得多。因为有些变更就是只改了一个注释全量回归显然是浪费有些变更表面上很小但精确命中了核心支付链路必须全量确认。第二个方向是让 Jev 和缺陷定位模块协作。以前失败用例只会写一句“用例失败”定位缺陷还是靠人肉翻日志。Jev 在定向追踪模式下执行用例时会额外采集失败现场附近的关联特征比如最近变更的代码行、依赖服务的错误码、以及同模块其他用例的状态然后把这些特征打包交给下一层的分析代理。这本质上延续了 DOOM 怪物感知玩家位置的方式——不只是看见“敌人存在”还要感知“敌人从哪来、往哪走”。第三个方向是给决策过程增加自然语言解释层。Jev 每次决定跳过或重跑某条用例时都产出一段结构化理由比如“因为近期失败新鲜度 0.8且本次变更命中了支付模块所以进入定向追踪队列”再由生成式模型把它翻译成自然语言通知推送给开发者。这个功能解决了一个很现实的问题测试智能调度刚上线时开发者最大的质疑就是“你凭什么跳过我的测试”。有了可读的解释信任度提升得非常快。但我要提醒一句解释层不要做太重理由的每个关键数字都必须来自真实的特征值不能由语言模型自由发挥。回到开头那个问题为什么好端端的测试要借鉴 DOOM我个人在实际落地中的体会是Jev 真正改变的不是那套打分公式而是测试工程里的一种习惯——测试不再是一个执行动作而是一个持续观察、判断、行动的闭环。DOOM 里的怪物再强也是靠每一帧的感知反馈才活下来的。测试决策代理也一样它不需要一开始就很聪明但它必须每一步都看着真实的结果调整自己。如果你也想做类似的调度系统我最后只有一个建议先别碰复杂算法把你现在拍脑袋定的那些筛选规则全部实现成一行一行可回看的数据和规则跑上两周你会看清楚真正的问题在哪里。然后你会发现从 DOOM 到测试自动化差的只是一个 Jev。