“Jev”是我给我做的一个内部工具起的代号。它最早的任务是在 DOOM 里学会怎么从危机四伏的 3D 迷宫里存活下来,后来我却把它那套决策框架原封不动地搬到了自动化测试里,让它决定每次代码提交之后到底该跑哪些测试、跑多久、跑挂了该先救谁。整个迁移过程比我想象中顺畅,踩过的坑也很有意思。
这篇分享不是讲 DOOM 复刻,也不是介绍某个测试框架的用法,而是要讲清楚一件事:一个在游戏环境里被反复验证的 AI Agent 决策模型,怎么迁移到软件测试场景里用,以及迁移过程中会遇到哪些坑。如果你在做自动化测试平台、CI 流水线优化,或者对游戏 AI 和工程工具的结合感兴趣,可以把这篇文章当成一份真实项目复盘来读。
我尽量把设计思路、代码骨架、参数初始化和问题排查都写透,方便你直接在自己的项目里复刻。
1. 为什么选 DOOM 当试验场:决策模型的通用性
游戏圈里有个共识,DOOM 这种经典 FPS 很适合做 AI 决策实验,因为它的核心不是“手速”,而是“选择”。什么时候冲,什么时候撤,什么时候该捡血包,什么时候该换一把弹药更少但性价比更高的武器,这些选择在自动化测试里全都能找到对应物。
1.1 一场游戏和一版软件发布的相似度
我第一次意识到有迁移空间,是在梳理测试决策的痛点时。测试团队最常问的问题无非是这几个:
- 这次提交改动很大,要不要跑全量回归?
- 某个模块最近老挂,是环境问题还是代码问题?
- 队列里排着 10 个任务,先跑谁、后跑谁?
- 新用例还没稳定,要不要纳入每日回归?
这些问题放到 DOOM 里,几乎是同一句话:
- 这个区域敌人很多,要不要绕路?
- 刚才那枪没命中,是枪不行还是我操作不行?
- 前方有 10 个怪,先打谁、后打谁?
- 新捡到的武器还不会用,要不要现在换?
两者都需要在信息不全、预算有限的情况下,实时做取舍。DOOM 有战争迷雾,你看不清所有敌人;测试也一样,你永远不可能完全预测哪段代码会出问题。
更重要的是,两者的失效模式非常相似。游戏里一次鲁莽冲锋可能导致满盘皆输;测试里一次不合理的“全量回归”可能把 CI 卡死,耽误真正关键的验证。反过来,太保守导致漏测一个改造模块,可能让缺陷直接溜到线上。
所以我把这个决策模型命名为 Jev,核心决策框架完全复用,只是换掉了环境接口。对于游戏 AI,环境是引擎渲染出来的 3D 场景;对于测试调度,环境是 CI 流水线、用例库和历史报告。
1.2 为什么游戏 AI 的经验能“搬到”测试里
很多人觉得游戏 AI 和工程工具是两条路,我始终认为这是误解。
游戏 AI 面对的环境复杂度,实际上比大多数业务系统还要高。DOOM 里的每一帧状态都在变化,敌人 AI 会包抄、会躲在掩体后,玩家的动作空间包括移动、转向、开火、换枪、交互等,组合起来有几十上百种。要在这种环境下做出“看起来聪明”的决定,模型必须处理状态感知、风险评估、资源管理和策略切换这些通用能力,而这些能力测试调度也全都要。
反过来说,软件测试的调度问题一直缺一套比较好的决策理论。很多团队用静态规则,比如“每晚全量回归”“每次提交跑冒烟”,可一旦环境和业务变化,这些规则就僵硬了。把游戏里那套“观察-判断-行动-反馈”的循环借鉴过来,是让测试执行变得更聪明的一条捷径,而不是什么天马行空的事。
很多研究组也早就用游戏环境验证过通用决策算法,只是这些成果大多停留在实验论文里,真正落到 CI 调度、用例选择场景的案例反而不多。Jev 这个项目算是我自己的一次实践。
1.3 一个容易被忽略的点:止损能力
玩 DOOM 时间久的人都有一个直觉:残血的时候不能贪。你手里拿着大枪、弹药充足,但血量只剩 10,这时候冲进怪堆就是送命。一个好的 AI 决策模型必须学会“放弃当前目标,先找安全位置回血”。
把这个直觉翻译到测试决策里,就是著名的“止损问题”。CI 全量回归跑到一半,失败用例突然猛增,是继续跑完还是立刻停掉?停掉看似浪费,但可能帮团队节省半小时的无效等待。Jev 里专门设计了放弃策略(Abort/Stop)这个动作,在游戏里对应“撤出战斗”,在 CI 里对应“终止当前回归,优先保留现场并通知负责人”。
这个点常被自动化测试平台忽略,因为做平台的人喜欢“一键全跑”,很少考虑早停。但它其实是决策模型里价值最高的部分之一。一个能早停的调度器,比你多跑三小时的调度器更重要。
2. Jev 的核心决策架构:三层结构与反馈闭环
Jev 没有堆一堆神经网络。它的核心是一个三层决策架构,加上一前一后两条通道。这个结构在处理游戏和测试时完全一致,换的只是输入输出接口。
2.1 认知层:把环境压缩成一个特征向量
认知层的任务是回答“我现在处于什么状态”。DOOM 版 Jev 从引擎里拉出这些指标:
- 当前血量、护甲、弹药量
- 视线内可见敌人数量、距离
- 当前武器类型与剩余备弹
- 附近是否有血包/护甲包
- 听到枪声来源方向(这是部分可观测环境的典型输出)
测试版 Jev 的认知层同样从 CI、版本库、测试报告里拉出指标:
- 本次提交涉及的变更文件、变更行数
- 变更影响到的功能模块和风险系数
- 最近 N 次构建中该模块的失败率
- 任务队列当前深度、运行中的并发数
- 测试环境健康度,比如容器创建成功率、网络延迟
这里有一条很重要的经验:特征数量宁少勿多,每个特征都要能实时拿到。我最早做测试版 Jev 时,想把“测试用例作者最近活跃度”也塞进认知层,结果这个数据只有周报系统里才有,跑一次要半小时,直接拖慢了决策周期。后来把它删掉,整个决策反而更稳定了。所有需要人工填表或跨系统异步同步的特征,都不适合出现在实时决策循环里。
2.2 策略层:不是哪一个策略最好,而是要对策略打分
策略层是 Jev 的核心,也是最容易写成“静态规则”的一层。在 DOOM 版里,策略包括“突击”“后撤找补给”“绕路探索”等。在测试版里,我定义并实现了五类策略:
- FULL:执行完整回归集
- TARGETED:只执行变更影响模块的定向用例集
- SMOKE:只执行冒烟集
- HOLD:暂不执行,等待队列或环境恢复
- PROBE:执行一组随机小样本作为探针
每个策略不是简单的开关,而是一个收益估算函数。Jev 为每个候选策略算出一个“期望收益分”和“预计成本”,再做选择。比如在 DOOM 里,面对一个低血量的敌人,突击策略收益很高、成本很低;但如果你自己也残血,突击优先级就会被下调。同理,在测试里,变更模块风险值很高时,定向回归 TARGETED 的收益高;但如果你此刻的 CI 队列已经堵到无法执行,那么 HOLD 的成本优势就会凸显。
打分公式可以被总结为:
score(strategy) = expected_finding_value / estimated_cost × priority_factor
这里:
- expected_finding_value 代表“这个策略能帮我逮到缺陷的期望价值”
- estimated_cost 是这个策略预计占用的执行时间和资源
- priority_factor 代表该策略是否符合当前环境限制,比如环境健康度低时,HOLD 会被放大
最终选择得分最高的策略,这就是一次“决策”。相比传统规则表,打分制的好处是可以动态权衡多个因素,而不是命中一个条件就无条件触发。
2.3 行动层:把策略拆成可执行的动作序列
DOOM 里,“突击”策略不能只是一条意图,它会被翻译成具体的动作帧:转向目标方位、长按开火键、在掩体间移动。测试版 Jev 也一样。选择 TARGETED 之后,行动层要生成实际执行计划:
- 从用例库中选出受影响模块的用例集合,比如 300 条
- 按“最近失败率”从高到低排序
- 分成 3 个并行任务,每个任务不超过 15 分钟预算
- 设置早停条件:同一模块失败率超过 20% 就整组停止
- 失败用例自动重跑 1 次并保留产物
这样整个决策才能落到 CI 里真实跑起来,而不只是停留在理论层面。这也是我特别强调行动层要设计的原因:很多 AI 决策模型在策略层做得花哨,但到执行层却没有完整的落地计划,最后无法真正接入流水线。
2.4 前向与反向双通道
Jev 的整个决策循环可以简化为:
目标定义 → 状态感知 → 策略打分 → 动作执行 → 结果反馈 → 权重更新 → 再回到状态感知
前向通道是“目标驱动”。Jev 知道当前迭代的核心目标是什么,比如“保证发布质量”“缩短反馈时间”“降低资源成本”,然后把它逐层分解到策略上。目标变了,策略的 priority_factor 就会变。
反向通道是“数据驱动”。每次动作执行完之后,Jev 会对比实际结果与决策时的预期。比如预期这个模块的定向回归能发现 2 个缺陷,结果只发现 0 个,那“模块风险系数”的权重就要稍微下调;反之,如果预期的失败模块没有失败,而另一个没覆盖到的模块却出了问题,说明特征选择不合理,要扩展影响范围计算。
反向通道是决策模型区别于静态规则的分水岭。静态规则只会一板一眼地按配置执行,而 Jev 会在每次执行后微调自己,让下一次判断更贴近真实环境。这个机制同样是从 DOOM 里学到的:Agent 发现“这个敌人用某把武器才划算”,在测试里就是“这个模块跑特定规模的回归才划算”。
3. 从 DOOM 到自动化测试:四组映射与动作设计
这个迁移不是凭空的比喻,而是四个层面的对应关系。我整理了一张对照表,方便大家理解 Jev 的决策变量是怎么切换的。
3.1 游戏机制与测试机制的映射
| DOOM 中的机制 | 自动化测试中的机制 | 共性 |
|---|---|---|
| 敌人出现与强度 | 变更模块的风险系数 | 威胁等级需要评估 |
| 血量/护甲值 | CI 流水线的剩余预算/时间 | 决定性资源,归零就结束 |
| 弹药量 | 测试执行预算 | 有限,需要合理分配 |
| 地图的探索区域 | 被测系统的模块/路径空间 | 不知道哪条路上有陷阱 |
| 捡到补给 | 扩展环境容量/打开并行 | 资源可用性影响策略 |
| 死亡惩罚 | 回归跑挂/漏测事故 | 一次失误代价高昂 |
| 游戏得分 | 缺陷发现数 + 反馈耗时 | 最终评估目标 |
这张表的价值在于,如果你未来想把任何决策问题迁移进这个模型,都可以用同样的方式画表,找到“环境资源”和“失败代价”的对应物。一旦找到对应物,决策框架就能直接套用。
3.2 动作空间:Jev 在 CI 里能做的 5 件事
游戏里 Agent 的动作是离散的,开火、换枪、移动、跳跃。测试里我把动作收敛成 5 类,避免动作空间过大导致决策难收敛:
- 执行全量回归(FULL)
- 执行定向回归(TARGETED)
- 执行冒烟集(SMOKE)
- 暂停并发执行(HOLD)
- 发送告警并保留现场(ALERT)
这五类动作组合起来已经能覆盖绝大多数发布场景。我没有开放“编造自定义测试用例”的动作,因为它超出了当前模型的能力边界。做 AI Agent 决策,动作越可控,反馈越干净。先确保每个动作可执行、可记录,再考虑增加更强的动作。
3.3 一个完整决策片段:从提交到反馈
我用一个通用场景描述整条链路,避免涉及具体业务。
事件:开发者 A 向某系统提交了一个网关模块的改动分支,提交信息里带关联单号。
触发:Jev 的感知器捕获到 commit 事件,拉取变更文件列表,发现改动涉及用户校验、账务同步两个模块,分别算出风险系数 0.8 和 0.6。
策略打分:TARGETED 得分最高,因为这两个模块最近 48 小时的失败率一度达到 15%。FULL 代价太高,因为当前队列上还排着 6 个任务,预计排队时间已经超过 20 分钟。HOLD 的 priority_factor 又不够高,因为这次变更量其实不大。
动作生成:Jev 从模块索引里捞出 200 条关联用例,按最近失败率排序,切成 2 个并行任务,并把失败重跑次数设为 1,早停阈值设为同模块失败率 20%。
执行:12 分钟后跑完。失败 3 条,全是环境连接超时。Jev 判定并非代码缺陷,按“环境类失败”计入反馈库,同时调低了该模块最近的失败率特征,避免下次误报。
反馈:这次没发现缺陷,但 Jev 记录了“覆盖过一次高风险变更但无缺陷”的样本。之后两周里,网关模块的风险系数被微调下降,为下一次决策提供了修正后的先验。
这一段跟 DOOM 里“Agent 探索了一个房间,发现没有敌人,于是记录该区域敌对度降低”的逻辑完全一样。决策模型要做的不是每次都完美,而是随着经验积累越用越稳。
4. 实操复盘:从零搭一个 Jev 决策模型
讲完设计,我来走一遍在真实 CI 环境里搭建 Jev 的完整过程。代码不是生产级,但足够让你在自己的项目里复刻整个思路。
4.1 第一步:搭状态感知器
先做一个状态采集模块,统一输出结构化 JSON,比如:
{ "commit_id": "abc123", "changed_files": ["gateway.py", "user_check.py"], "impact_modules": ["gateway", "account"], "module_risk": {"gateway": 0.8, "account": 0.6}, "recent_failure_rate": {"gateway": 0.15, "account": 0.10}, "queue_depth": 6, "env_health": 0.98 }最关键的是要有一张“模块影响关系表”,把代码文件映射到测试模块。没有这个映射,Jev 根本不知道改了哪个文件该跑哪些用例。我一开始用了最简单的路径匹配,比如文件名前缀包含 gateway 就映射到网关模块,后来改成扫描依赖关系再做映射,精确度明显提升。
建议先人工维护一张粗糙的映射表,不要一上来就追求自动依赖分析。粗糙的映射表也能跑出 80% 效果,后续再逐步优化。很多项目死在第一步,就是因为他们想一次做到完美。
4.2 第二步:定义策略打分函数
核心代码如下,我用 Python 伪代码写在决策脚本里:
STRATEGIES = { "full": {"cost": 300, "priority": 1.0}, "targeted": {"cost": 80, "priority": 1.0}, "smoke": {"cost": 10, "priority": 1.0}, "hold": {"cost": 0, "priority": 2.0}, } def score(strategy, state): if strategy == "full": expected = 0.0005 * len(all_cases(state)) * impact_score(state) cost = STRATEGIES["full"]["cost"] elif strategy == "targeted": expected = sum(state["module_risk"].values()) * 0.4 cost = STRATEGIES["targeted"]["cost"] elif strategy == "smoke": expected = 0.05 cost = STRATEGIES["smoke"]["cost"] else: # hold expected = 0 cost = STRATEGIES["hold"]["cost"] priority = STRATEGIES[strategy]["priority"] if state["env_health"] < 0.8: priority *= 0.5 return expected / max(cost, 1) * priority def decide(state): best = None best_score = -1 for name in STRATEGIES: s = score(name, state) if s > best_score: best_score = s best = name return best有三个细节需要特别注意:
- 分数只用于排序,绝对值无意义。所以不用把“分数”做得很精确,只要相对高低符合直觉就行。
- cost 用执行时间估算,单位是分钟。初始值可以拍脑袋,但之后必须用真实执行数据回填。
- priority 参数相当于环境政策的权重。比如在发布冻结期,可以人工把 full 的 priority 调高,让它更倾向保守。
4.3 第三步:接反馈回流
决策完不是结束,还要让执行结果影响下一次决策。我加了一个非常简单的在线更新逻辑:
def update_weights(feedback): expected_defects = feedback["expected_defects"] actual_defects = feedback["actual_defects"] delta = (expected_defects - actual_defects) / (expected_defects + 1) weights = load("module_risk_weights.json") weights[feedback["module"]] *= (1 + delta * 0.02) save("module_risk_weights.json", weights)这里的思想是“温和调整”,每次最多微调 2%,防止一个偶然的失败把权重推飞。我还给权重加了上下界,限制在 0.5 到 1.5 之间,保证长期稳定。如果你想用更复杂的在线学习算法,当然可以,但对调度类场景来说,温和调整已经足够了。
4.4 第四步:沙盒对比裁判
上线前最怕的不是模型效果差,而是“效果差但你不知道”。所以我设计并实践了一个 Shadow Mode:
- 让 Jev 在影子模式里陪着旧逻辑一起跑
- 旧逻辑照常执行,比如每次全量跑
- Jev 同时输出自己的判断,但不实际控制测试
- 一个对比脚本会记录两种方案的覆盖用例数、执行时长、发现缺陷数
- 连续跑两周后对比,再决定是否让 Jev 真正接管
这个成本不高,但能避免很多自信翻车。我见过太多团队把新方案直接上生产,结果一天后匆匆回滚的。决策模型是底层调度器,影响面太广,务必要用影子模式过渡。
5. 实测数据与效果对比
在某个模拟项目的 CI 流水线上,我把 Jev 接入了 4 周,得到了一批初步数据。取其中典型的一周结果看:
| 指标 | 旧逻辑(全量回归) | Jev 决策模型 |
|---|---|---|
| 日均执行用例数 | 约 3200 | 约 980 |
| 平均单次构建耗时 | 约 137 分钟 | 约 48 分钟 |
| 日均捕获缺陷数 | 平均 2.1 | 平均 2.6 |
| 环境类误报 | 较多 | 明显下降 |
| 变更后首个反馈速度 | 提交后约 2 小时 | 提交后约 11 分钟 |
需要明确说明,这里的数字不能直接搬到自己团队,因为每个团队的用例质量、环境健康度、模块划分都不一样。但它至少说明一件事:削减测试执行量不一定会损失缺陷发现率,反而可能因为反馈更快、精力更聚焦,让真正有价值的测试被跑得更透。
5.1 结果解读:少跑不等于漏测
有意思的现象是,Jev 把“全量回归”变成低频操作,把“定向回归”变成高频操作之后,测试团队开始愿意为模块风险表更新数据,形成正向循环。以前大家觉得“反正每天都要全量跑,不需要优先级”,现在会更主动地标记高风险模块。
少跑 60% 的用例并没有带来缺陷暴涨,原因在于 Jev 把预算花在了最可能翻车的模块上。这就跟 DOOM 里高手不会在空旷区域闲逛一样,他会把弹药留给最可能的交火点。
5.2 一个典型的坑:脏特征让模型变“抖”
第一次上线时,我直接把“测试环境健康度”作为权重特征。结果容器的偶发创建失败被当成健康度低,Jev 两步一停、三步一 HOLD,整条流水线的执行量反而降到了历史最低。后来我把环境健康度的采样窗口从 5 分钟拉长到 30 分钟,并过滤掉凌晨无人使用的时段,情况才恢复正常。
这说明决策模型也怕脏特征。环境不稳定导致的特征抖动,会被它当成真实风险,这个偏差比静态规则的误配置更隐蔽,排查起来也更费时间。任何进入决策循环的指标,都要先做过平滑和异常剔除。
6. 常见问题与排查速查表
最后整理一下我在实际调试 Jev 时遇到的高频问题,做成速查表,方便你在复刻时对照。
6.1 Jev 变得过于保守,什么都不想做
最典型的表现是 HOLD 策略频繁被选中,执行量掉到正常水平的 30% 以下。
可能原因:
- priority 表被训练数据带偏,hold 的 priority 设置过大
- 环境健康度特征一直偏低,导致所有策略的 priority 被压
- 收益估算里的 expected 值长期低于成本,模型认为跑什么都亏
排查办法:查看最近 100 次决策日志,把每个策略的 expected 和 cost 曲线拉出来,看是不是某个特征被长期低估。决策日志必须包含特征向量、策略得分、实际结果,这三样缺一不可。
6.2 反馈延迟导致权重更新飘忽不定
第二次踩坑是结果入库延迟导致的。测试结果库是批处理同步,每次决策完拿到的反馈数据其实是 30 分钟前的,所以权重一直在“追”昨天的数据。
解决办法:把反馈改成流式读取,并给权重更新加时间衰减机制,越旧的反馈对当前权重影响越小。如果你没有流式基建,可以把更新频率限制在每 15 分钟一次,宁慢勿乱。
6.3 新模块没有历史数据,Jev 不敢碰
新模块没有任何失败率,权重默认是 0.5。看起来是中性,但模型会因为“没有战绩”而不敢对它做定向执行。这在 DOOM 里相当于一个新地图,AI 不敢探索。
我加了两个机制:
- 默认风险不设成最低,而是设成 0.6 的“未知风险”,因为未知的往往比已知的更危险
- 对新模块强制每 3 次提交至少执行一次探测性用例(PROBE)
6.4 沙盒对比裁判怎么落地
推荐的影子模式方案是:旧逻辑跑,新逻辑记,但新逻辑不接管。同时专门写一个对比断言,关注三个数字:
- 覆盖率差异:Jev 少跑了哪些用例,这些用例在旧逻辑下是否成功或失败
- 耗时差异:Jev 判断下总执行时间是更短还是更长
- 误判率:Jev 的 HOLD 和 TARGETED 判断事后是否被认为合理
两周之后出一份简单报告。如果 Jev 方案在少跑多数用例的同时缺陷发现率没有下降,就可以灰度切换。这是最保守也最稳妥的方式。
6.5 决策模型对人工经验不友好怎么办
一开始团队里的资深测试人员会质疑,凭什么一个 AI 决定我该跑哪些用例。我用的策略是给决策加一层可解释面板。每次 Jev 决策后显示“因为网关模块风险系数 0.8,最近失败率 15%,所以选择了定向回归”。
一旦把原因展示出来,大家发现 Jev 的逻辑其实跟自己多年的直觉是一致的,就不再抵触了。这个可解释面板甚至反过来帮助团队校准自己的风险判断。可解释性不是可选加分项,而是这类决策模型能落地的必备条件。
7. 一个小技巧:把“决策日志”当第一优先级
最后分享一个也许最不起眼、但影响最大的习惯:从一开始就做好决策日志。
在 DOOM 版 Jev 时期,我并没有认真记录“为什么选了这个动作”,只是开了几千局游戏,靠胜率笼统判断。等到迁移到测试场景时,评审的人第一句话就会问“你这个决策依据是什么”,如果没有日志,根本没法回答。
在 CI 落地后,我立刻加了一个决策日志库,每次决策都持久化:
{ "timestamp": "...", "state": {...}, "strategy_scores": {"full": 0.23, "targeted": 2.15}, "selected": "targeted", "post_result": {"defects": 0, "cost": 12, "env_fail": 3} }这保证了两件事:第一,任何一次线上事故都可以追溯当时的决策依据;第二,后续的权重更新和优化都有数据支撑。没有这个日志,Jev 就像一台没有黑匣子的飞机,你不知道它起飞前凭什么做了那些决定。
根据我个人经验,一个决策模型的失败,八成不是模型本身的问题,而是缺少可观测性。游戏环境里你还能用回放看它怎么死的,工程环境里如果连日志都没有,就只能靠猜。
Jev 从 DOOM 搬到自动化测试,整个过程让我验证了一件事:AI Agent 的决策框架天然适合跨场景迁移,关键是你得先弄清楚环境和决策变量的映射,而不是一上来就堆算法。想复现的人,建议也从最小的决策循环开始,先把“感知-决策-执行-反馈”这四步跑通,再考虑引入更复杂的模型。