
1. 从游戏AI到测试Agent一个反直觉的起点第一次看到用打DOOM的思路做自动化测试这个说法我脑子里蹦出来的第一个念头是这俩东西八竿子打不着吧一个是1993年的第一人称射击游戏一个是软件工程里最枯燥的环节之一。但仔细琢磨之后我发现这个类比背后藏着一套非常扎实的决策模型设计思路而且它解决的是自动化测试领域一个长期被忽视的核心问题测试Agent在不确定环境下的决策能力。传统自动化测试脚本是什么本质上就是一条一条写死的指令序列——点这个按钮等那个元素出现输入这段文本断言结果等于预期值。这种模式在稳定环境下跑得挺好但一旦页面结构变了、接口返回慢了、弹窗顺序不一样了脚本立刻挂掉。你可能会说那就加等待、加重试、加异常捕获呗。没错这些手段确实能缓解问题但它们本质上是在用更多的硬编码去对抗环境的不确定性治标不治本。Jev这个项目标题里提到的AI Agent决策模型核心思路完全不同。它不是在脚本层面打补丁而是把测试执行过程建模成一个序贯决策问题——Agent在每一步观察当前环境状态然后从可行动作空间中选择一个最优动作执行后环境状态发生变化Agent再根据新的状态做下一步决策。这个循环和DOOM里AI敌人看到玩家→选择行为→执行→观察结果→再决策的循环在数学结构上是一模一样的。这篇文章我会从决策模型的设计原理讲起拆解Jev这类测试Agent的核心架构然后给出可落地的实现方案和实操中踩过的坑。不管你是做自动化测试的工程师还是对AI Agent决策机制感兴趣的技术人应该都能从中拿到一些能直接用的东西。2. 为什么测试脚本需要DOOM式的决策模型2.1 传统测试脚本的根本瓶颈在哪里先把这个事情说透。大部分自动化测试框架不管是Web端的还是移动端的遵循的都是录制-回放或者手写步骤的模式。这种模式有一个隐含假设环境是可预测的。脚本作者在写脚本的时候脑子里已经预设了一条固定的执行路径脚本就是这条路径的忠实记录。问题在于真实环境从来都不是完全可预测的。我做过一个统计在一个中等复杂度的Web应用里一个包含50个步骤的端到端测试脚本在不加任何容错处理的情况下连续跑10次的通过率大概只有60%到70%。失败的原因五花八门元素加载顺序变了、异步请求返回时间波动、第三方组件初始化延迟、浏览器渲染差异等等。传统的应对方式是防御性编程——到处加显式等待、加try-catch、加重试逻辑。这么做不是不行但代码会变得极其臃肿而且每加一层防御就多一层维护成本。更关键的是这种模式下的测试脚本没有真正的决策能力。它不会根据当前页面的实际状态来判断我应该先处理弹窗还是先填表单它只会按照预设顺序执行遇到意外就报错。2.2 DOOM AI给了什么启发DOOM里的AI敌人比如Imp和Demon行为逻辑其实很简单但它体现了一个重要的设计模式基于状态的行为选择。AI不会预先编排好先走三步再左转再攻击这样的固定序列而是每帧根据当前状态与玩家的距离、自身血量、视线是否被遮挡等从一组候选行为中选择一个执行。这个模式套到测试Agent上对应的设计就是Agent不预先编排固定的操作序列而是每一步根据当前页面状态DOM结构、可见元素、网络请求状态等从候选动作空间中选择最合适的动作。这样做的好处是当环境发生变化时Agent不需要修改脚本逻辑它会自动看到新的状态并做出适应性的决策。举个具体的例子。假设你要测试一个登录流程传统脚本写的是找到用户名输入框→输入→找到密码框→输入→点击登录按钮。但如果页面上突然弹出了一个Cookie同意横幅传统脚本会直接失败因为它找不到用户名输入框被横幅挡住了。而一个基于决策模型的Agent会先看到横幅的存在判断当前最优先的动作是关闭横幅执行关闭操作后再继续登录流程。2.3 决策模型的核心要素拆解要把这个思路落地需要明确定义几个核心要素。我用一个表格来梳理要素在DOOM AI中的对应在测试Agent中的对应状态空间玩家位置、血量、武器状态DOM树、可见元素集合、网络状态动作空间移动、攻击、躲避点击、输入、滚动、等待、断言状态转移游戏引擎物理模拟页面响应操作后的新状态奖励函数击杀玩家、存活时间测试步骤完成度、发现缺陷数策略行为选择规则动作选择策略规则/学习这个映射关系看起来简单但真正落地的时候每个要素都有大量的工程细节需要处理。状态空间怎么表示动作空间怎么定义粒度奖励函数怎么设计才能引导Agent走向完成测试而不是乱点一气这些问题的答案直接决定了Agent的实际效果。3. Jev决策模型的状态表示与动作空间设计3.1 页面状态怎么喂给Agent状态表示是整个决策模型的地基。如果状态表示得不好后面的策略再精妙也没用。在测试Agent的场景下页面状态需要被编码成一种Agent能理解、能比较、能泛化的形式。最直接的方式是用DOM树的原始HTML作为状态表示。但这样做有几个问题第一HTML文本太长一个中等页面的DOM序列化后动辄几十万字符直接作为状态输入计算量太大第二HTML里包含大量与决策无关的信息比如样式定义、注释、隐藏元素会引入噪声第三不同页面的HTML结构差异很大Agent很难泛化。我在实践中总结出一套分层状态表示方案效果比较稳定第一层可见元素摘要。只提取当前视口内可见的交互元素按钮、输入框、链接、下拉框等每个元素用一组特征向量表示元素类型、文本内容、位置坐标、尺寸、是否可点击、是否可输入。这一层的信息量大概在几百到几千个token的量级适合作为Agent的主要状态输入。第二层页面语义标签。对页面进行语义级别的理解比如这是一个登录表单、这是一个导航栏、这是一个模态弹窗。这一层可以通过规则匹配或者轻量级模型来实现目的是给Agent提供更高层次的上下文信息。第三层历史动作序列。Agent之前执行了哪些动作、产生了什么结果这些信息对于决策非常重要。比如Agent已经点击了登录按钮但页面没有跳转那它下一步就不应该再点一次登录按钮而应该检查是否有错误提示。这三层信息组合起来形成一个结构化的状态表示。在实际实现中我通常会把第一层和第二层编码成文本描述第三层用一个固定长度的向量表示然后一起输入给决策模块。3.2 动作空间的粒度控制动作空间的设计有一个核心矛盾粒度太粗Agent的灵活性不够粒度太细决策空间爆炸Agent很难学到有效策略。举个例子点击登录按钮这个动作如果粒度粗到完成登录这个级别那Agent就失去了处理中间异常的能力比如弹窗拦截。如果粒度细到移动鼠标到坐标(x,y)→按下左键→松开左键那动作空间会变得巨大而且大部分动作序列是无效的。我的经验是采用两级动作空间高层动作语义级别的操作比如点击元素A、在元素B中输入文本C、滚动到元素D、等待X秒、断言元素E存在。这些是Agent实际决策的单位。底层执行每个高层动作对应一组底层操作序列由执行引擎负责展开。比如点击元素A会被展开为滚动到元素A可见→获取元素A坐标→模拟鼠标移动到坐标→模拟点击。这样做的好处是Agent只需要在高层动作空间做决策动作数量可控通常几十到几百个同时底层执行的复杂性被封装起来了。还有一个细节值得注意动作空间应该是动态的。也就是说Agent在当前状态下可用的动作集合应该根据当前页面状态动态生成。比如当前页面没有输入框那在输入框中输入文本这个动作就不应该出现在候选集合里。这样做可以大幅缩小搜索空间提高决策效率。3.3 状态转移的建模与处理状态转移是决策模型里最容易被忽视的环节。在DOOM里状态转移由游戏引擎的物理模拟负责是确定性的、即时的。但在测试Agent的场景下状态转移有三个显著特点异步性。点击一个按钮后页面可能不会立即变化而是等待一个异步请求返回后才更新。Agent需要知道等待这个动作的存在并且能够判断等待多久是合理的。不确定性。同样的操作在不同环境下可能产生不同的结果。比如点击提交按钮有时候成功跳转有时候弹出验证码有时候报网络错误。Agent需要能够处理这种不确定性。部分可观测。Agent只能看到当前视口内的内容页面下方的内容需要滚动才能看到。这意味着Agent的状态观测是不完整的它需要主动探索滚动页面来获取更多信息。针对这些特点我在实现中采用了几个策略。对于异步性引入一个状态稳定检测机制——每次动作执行后持续监测DOM变化直到连续N毫秒没有显著变化才认为状态转移完成。对于不确定性在决策模块中加入随机探索机制避免Agent陷入固定模式。对于部分可观测在状态表示中加入已探索区域的标记引导Agent优先探索未查看的区域。4. 策略设计规则引擎、学习模型还是混合方案4.1 纯规则方案的适用边界说到Agent决策很多人第一反应是上强化学习。但我在实际项目中的体会是在测试Agent这个场景下纯规则方案能解决70%以上的问题而且开发成本低、可解释性强、调试方便。规则方案的核心是一组优先级排序的条件-动作规则。比如如果存在模态弹窗且弹窗有关闭按钮优先关闭弹窗如果存在表单且表单有未填写的必填字段优先填写字段如果存在错误提示元素记录错误信息并尝试恢复如果当前测试步骤的预期元素已出现执行该步骤的断言这些规则的优先级顺序很重要。弹窗处理必须排在最高优先级因为弹窗会遮挡其他元素不处理弹窗后面的操作都无法进行。错误处理排在第二优先级因为出现错误时需要先恢复环境再继续。正常的测试步骤执行排在最后。规则方案的优点是显而易见的每一条规则都可以单独测试、单独调试出问题的时候很容易定位是哪条规则触发了。而且规则可以用自然语言描述测试人员不需要懂机器学习就能理解和修改。但规则方案也有明显的边界。当页面结构非常复杂、状态组合爆炸的时候手写规则会变得不可维护。比如一个有几十种弹窗类型、每种弹窗有不同的关闭方式的页面规则数量会急剧膨胀。这时候就需要引入学习模型。4.2 学习模型能补足什么学习模型这里主要指强化学习在测试Agent场景下的价值主要体现在两个方面泛化能力。规则方案遇到没见过的页面结构就歇菜了而学习模型可以通过训练学会看到类似的结构就知道该怎么做。比如Agent在训练中见过各种形式的关闭按钮X图标、文字关闭、取消按钮它就能泛化到新的关闭按钮样式上。长期规划能力。规则方案通常是短视的它只考虑当前状态下最优的动作不考虑这个动作对后续步骤的影响。学习模型可以通过奖励函数的设计让Agent学会为了长期目标牺牲短期利益。比如Agent可能需要先滚动页面加载更多内容才能找到目标元素规则方案可能不会主动做这个动作但学习模型可以通过训练学会这个策略。不过学习模型在测试Agent场景下也面临很大的挑战。最大的问题是奖励函数设计困难。在DOOM里奖励很明确击杀敌人加分被击杀扣分。但在测试场景下完成测试步骤这个目标很难被分解成每一步的即时奖励。而且测试环境的状态空间太大训练一个可用的策略需要大量的交互数据成本很高。4.3 混合架构的工程实践综合来看我在实际项目中采用的是规则引擎为主、学习模型为辅的混合架构。具体来说规则引擎负责处理高频、确定性强的决策场景比如弹窗处理、表单填写、元素定位。这些场景的规则相对稳定手写规则的性价比最高。学习模型负责处理规则覆盖不到的边缘场景比如页面布局异常时的恢复策略、多步骤操作的顺序优化。这些场景规则很难穷举但学习模型可以通过少量样本学会合理的策略。两者之间的切换由一个置信度评估模块控制。当规则引擎对当前决策的置信度高于阈值时采用规则引擎的输出当置信度低于阈值时交给学习模型决策。置信度的计算综合考虑了当前状态与规则匹配的紧密度、历史决策的成功率等因素。这种混合架构的好处是它既保留了规则方案的可解释性和可控性又通过学习模型补足了泛化能力。而且在实际运行中如果学习模型给出了不合理的决策规则引擎可以作为兜底避免Agent完全失控。5. 从零搭建一个最小可用的测试Agent5.1 环境准备与技术选型要搭建一个最小可用的测试Agent你需要准备以下环境浏览器自动化工具Playwright或Puppeteer。我推荐Playwright因为它对多浏览器支持更好API设计也更现代。安装命令npm install playwright。编程语言Node.js或Python。Node.js与Playwright的集成更自然Python则在后续接入学习模型时更方便。我这里用Node.js做示例。决策引擎初期用纯规则方案即可不需要额外依赖。后续如果要接入学习模型可以用ONNX Runtime加载预训练模型。状态存储用一个简单的内存对象存储当前状态和历史动作序列即可不需要数据库。整个项目的目录结构建议这样组织test-agent/ ├── src/ │ ├── observer/ # 状态观测模块 │ │ ├── dom-parser.js │ │ └── state-builder.js │ ├── decision/ # 决策模块 │ │ ├── rule-engine.js │ │ └── action-selector.js │ ├── executor/ # 动作执行模块 │ │ └── action-runner.js │ └── agent.js # Agent主循环 ├── rules/ # 规则配置文件 │ └── default-rules.json └── package.json5.2 状态观测模块的实现状态观测模块负责把页面状态转换成Agent能理解的结构化表示。核心代码如下// src/observer/state-builder.js async function buildState(page) { // 提取可见交互元素 const elements await page.evaluate(() { const interactables []; const selectors button, input, a, select, textarea, [rolebutton]; document.querySelectorAll(selectors).forEach((el, index) { const rect el.getBoundingClientRect(); const style window.getComputedStyle(el); // 只保留可见元素 if (rect.width 0 rect.height 0 style.display ! none style.visibility ! hidden) { interactables.push({ id: index, tag: el.tagName.toLowerCase(), text: (el.innerText || el.value || ).slice(0, 50), type: el.type || , x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height), clickable: true, editable: [input, textarea].includes(el.tagName.toLowerCase()) }); } }); return interactables; }); // 检测弹窗 const hasModal await page.evaluate(() { const modals document.querySelectorAll([roledialog], .modal, .popup); return Array.from(modals).some(m { const style window.getComputedStyle(m); return style.display ! none style.visibility ! hidden; }); }); return { url: page.url(), title: await page.title(), elements, hasModal, timestamp: Date.now() }; }这段代码做了几件事遍历页面上所有可能的交互元素过滤掉不可见的提取每个元素的关键特征同时检测是否存在模态弹窗。返回的状态对象包含了Agent决策所需的核心信息。5.3 规则引擎与动作选择规则引擎的核心是一个优先级排序的规则列表。每条规则包含匹配条件和对应的动作生成逻辑// src/decision/rule-engine.js const rules [ { name: close-modal, priority: 100, match: (state) state.hasModal, action: (state) { // 寻找弹窗中的关闭按钮 const closeBtn state.elements.find(el el.text.includes(关闭) || el.text.includes(取消) || el.text × || el.text X ); if (closeBtn) { return { type: click, targetId: closeBtn.id, reason: 关闭弹窗 }; } return { type: press, key: Escape, reason: 按ESC关闭弹窗 }; } }, { name: fill-required-field, priority: 80, match: (state) { return state.elements.some(el el.editable !el.value); }, action: (state) { const emptyField state.elements.find(el el.editable !el.value); return { type: fill, targetId: emptyField.id, value: generateTestValue(emptyField), reason: 填写必填字段 }; } }, { name: click-primary-action, priority: 50, match: (state) { return state.elements.some(el el.clickable /提交|登录|确认|下一步/.test(el.text) ); }, action: (state) { const btn state.elements.find(el el.clickable /提交|登录|确认|下一步/.test(el.text) ); return { type: click, targetId: btn.id, reason: 执行主操作 }; } } ]; function selectAction(state) { const sortedRules rules.sort((a, b) b.priority - a.priority); for (const rule of sortedRules) { if (rule.match(state)) { return rule.action(state); } } return { type: wait, duration: 1000, reason: 无匹配规则等待 }; }这个规则引擎的设计要点是规则按优先级从高到低排列第一个匹配成功的规则决定当前动作。弹窗处理优先级最高100表单填写次之80主操作执行再次之50。如果没有规则匹配Agent选择等待给页面更多时间加载。5.4 Agent主循环的组装把观测、决策、执行三个模块串起来就形成了Agent的主循环// src/agent.js async function runAgent(page, maxSteps 100) { const history []; for (let step 0; step maxSteps; step) { // 1. 观测 const state await buildState(page); // 2. 决策 const action selectAction(state); // 3. 执行 console.log([Step ${step}] 动作: ${action.type}, 原因: ${action.reason}); await executeAction(page, state, action); // 4. 记录历史 history.push({ state, action, timestamp: Date.now() }); // 5. 检查终止条件 if (await isTestComplete(page)) { console.log(测试完成); break; } // 6. 等待状态稳定 await waitForStable(page); } return history; }这个主循环的逻辑非常清晰观测→决策→执行→记录→检查终止→等待稳定→下一轮。每一轮循环对应Agent的一个决策步。waitForStable函数负责检测页面是否已经稳定DOM不再变化避免在页面还在加载时就做下一步决策。6. 实测中暴露的问题与调优经验6.1 元素定位的稳定性问题在实际跑测试的时候我发现最大的坑是元素定位不稳定。同一个按钮在不同次加载中可能因为页面布局的微小变化而位置不同或者因为动态生成的ID不同而无法用选择器定位。我的解决方案是多特征融合定位。不依赖单一的选择器而是综合元素的文本内容、类型、相对位置、尺寸等多个特征来定位。具体实现是给每个元素计算一个特征向量然后在定位时找特征向量最匹配的元素。这样做的好处是即使某个特征发生变化比如ID变了其他特征仍然能帮助正确定位。还有一个实用技巧在状态表示中给每个元素分配一个稳定的临时ID基于元素在DOM中的路径和特征生成这样在同一个页面状态下Agent可以用这个临时ID来引用元素避免了选择器不稳定的问题。6.2 等待策略的优化等待策略是另一个影响测试稳定性的关键因素。我试过几种方案固定等待简单粗暴每次操作后等固定时间比如2秒。问题是等待时间不好设定太短了页面没加载完太长了测试效率低。显式等待等待特定元素出现或特定条件满足。比固定等待好但需要预先知道要等什么元素不够灵活。状态稳定检测监测DOM变化直到连续一段时间没有变化才继续。这个方案最灵活但实现复杂度也最高。我最终采用的是状态稳定检测为主、显式等待为辅的方案。状态稳定检测负责处理大部分场景对于已知的慢加载场景比如文件上传后的处理额外加显式等待。状态稳定检测的实现要点是用MutationObserver监听DOM变化每次变化时重置计时器当计时器超过阈值我用的500毫秒时认为状态稳定。这个阈值需要根据实际页面的动态程度来调整动态内容多的页面可以适当调大。6.3 死循环与无效动作的防范Agent在运行过程中很容易陷入死循环。比如它一直点击同一个按钮但页面没有任何变化它就再点一次无限循环下去。防范死循环有几个策略。第一动作去重如果连续N次选择了相同的动作且状态没有变化强制切换到其他动作。第二状态变化检测每次动作执行后比较前后状态如果状态完全没变给这个动作打一个负分降低它下次被选中的概率。第三最大步数限制设置一个硬性的步数上限超过就终止并报告异常。无效动作的另一个来源是Agent选择了当前状态下不可执行的动作。比如它试图点击一个已经被移除的元素。防范措施是在动作执行前做一次可用性检查如果目标元素不存在或不可交互跳过这个动作重新决策。6.4 规则冲突与优先级调整随着规则数量的增加规则冲突变得越来越频繁。比如关闭弹窗和填写表单两条规则可能同时匹配弹窗里有一个表单这时候优先级决定了哪条规则生效。我的经验是优先级的设定需要遵循几个原则恢复性动作优先于推进性动作先处理异常再继续正常流程全局性动作优先于局部性动作先处理影响整个页面的弹窗再处理局部表单确定性高的动作优先于确定性低的动作先做有把握的操作。另外规则优先级不应该是静态的而应该根据运行时的情况动态调整。比如某条规则连续多次执行失败应该临时降低它的优先级让其他规则有机会被尝试。7. 这套决策模型还能用在哪些场景7.1 跨平台兼容性测试同一套测试逻辑需要在Web、移动端、桌面端等多个平台上运行每个平台的UI结构和交互方式都不同。用决策模型来做这件事的优势在于Agent的状态表示和动作空间是平台无关的只需要为每个平台实现对应的观测模块和执行模块决策逻辑可以复用。比如关闭弹窗这个决策在Web端对应的是点击关闭按钮或按ESC键在移动端对应的是点击遮罩层或滑动关闭在桌面端可能是点击标题栏的X。Agent的决策逻辑是一样的检测到弹窗→选择关闭动作只是执行层不同。7.2 探索性测试与缺陷发现传统的自动化测试是验证性的——验证已知的功能是否正常工作。而探索性测试是发现性的——在没有预设脚本的情况下自由探索应用寻找潜在的缺陷。决策模型天然适合探索性测试。Agent可以在没有明确测试目标的情况下根据当前页面状态选择最有信息量的动作。比如优先点击那些还没被点击过的元素优先探索那些还没被访问过的页面区域。通过这种方式Agent可以发现一些人工测试容易遗漏的边缘情况。7.3 用户行为模拟与性能测试在做性能测试的时候需要模拟真实用户的行为模式。传统的做法是用脚本录制用户操作然后回放但这种方式生成的行为模式很机械不够真实。用决策模型来模拟用户行为可以生成更加多样化和自然的操作序列。Agent可以根据页面状态做出像人一样的决策比如看到表单先填必填项、看到弹窗先关闭、看到加载中的提示先等待。这样生成的负载更接近真实用户的行为性能测试的结果也更有参考价值。7.4 与CI/CD流水线的集成把测试Agent集成到CI/CD流水线中需要解决几个工程问题。第一是执行速度Agent的决策过程需要足够快不能成为流水线的瓶颈。第二是结果报告Agent需要输出结构化的测试报告包括执行了哪些动作、发现了什么问题、在哪个步骤失败等。第三是环境隔离每次CI运行都需要一个干净的环境Agent需要能够处理环境的初始化。我的做法是把Agent打包成一个独立的命令行工具接受测试目标URL和配置文件作为输入输出JSON格式的测试报告。在CI流水线中这个工具作为一个步骤被调用测试报告被上传到制品库供后续分析。8. 一些踩坑之后的个人体会做这个方向有一段时间了踩过的坑比预想的多得多。有几个体会特别深分享出来给后来人省点时间。第一个体会是不要一上来就追求智能。我最初的想法是直接上强化学习让Agent自己学会怎么测试。结果发现训练成本极高而且训练出来的策略很难调试——Agent做了个奇怪的操作你根本不知道为什么。后来退回到规则引擎先把基础流程跑通再逐步引入学习模型处理边缘场景效果好得多。规则引擎虽然笨但它可控、可解释、可调试这在工程实践中比智能重要得多。第二个体会是状态表示的质量决定了Agent的上限。我花了很多时间在优化状态表示上从最初的原始HTML到后来的分层结构化表示每一步优化都带来了明显的效果提升。状态表示做得好Agent能看到关键信息决策自然就准状态表示做得差再好的策略也是巧妇难为无米之炊。第三个体会是测试Agent的评估比训练更难。怎么判断一个测试Agent好不好通过率高就代表好吗不一定。一个Agent可能通过率很高但它只是学会了绕过困难——遇到复杂场景就跳过只做简单的操作。真正好的Agent应该能够覆盖更多的测试场景发现更多的潜在问题。所以评估指标不能只看通过率还要看场景覆盖率、缺陷发现率、决策多样性等多个维度。第四个体会是这个方向还在早期很多问题没有标准答案。状态怎么表示、动作空间怎么设计、规则和学习模型怎么融合这些问题在不同的项目中有不同的最优解。我的经验只能作为参考具体怎么做还需要根据实际场景来调整。保持开放的心态多尝试不同的方案比固守某一种最佳实践更重要。