八小时里发生了什么:一条时间线
先看一个模拟场景。假设测试Agent对一款虚构的开放地图游戏做夜间长程运行,它按“接任务、探索、交付、换装、存档”的节奏循环,下面是这次运行的粗略时间线。
| 时间 | 大致操作 | 当时的状态 |
|---|---|---|
| 0:00–1:30 | 完成序章与前五个任务,第一次切换到北部荒原 | 一切正常,背包物品数稳定 |
| 1:30–3:00 | 反复往返两张地图,中途更换过三次护甲,做了两次手动存档 | 存档槽二写入的是旧任务标记,但没人发现 |
| 3:00–5:30 | 完成到第二十个任务,其间有一次在交付途中退出到主菜单 | “灰烬回廊守门人”的击败标记已置位 |
| 5:30–7:40 | 读存档槽二重进游戏,继续第二十一个任务 | 任务列表里出现一个已完成的任务再次处于“进行中” |
| 7:41 | 接取第二十一个任务时,任务系统抛出“状态非法” | 崩溃前最后一个动作:与NPC对话 |
时间线本身没有告诉我们答案,却说明了长程测试的难处:错误出现在7:41,根源可能早在1:30就埋下了。中间隔着数千步操作,其中绝大多数与这个问题无关。如果只有“7:41崩溃”这一条记录,开发只能重新跑八小时碰运气。所以,长程测试的核心,是把“发现”变成“可复现”。
轨迹里到底应该记什么
爱游戏测试AI把这类记录叫做行动轨迹(Action Trace,按时间顺序保存的操作与状态序列)。它不是屏幕录像,而是结构化的日志,至少应该包含下面这些字段。
- 操作:每一步的输入或游戏级动作,例如“接取任务”“更换装备”“存档到槽二”“读档”。
- 时间顺序:逻辑帧号与墙钟时间都要有,前者用于回放,后者用于排查环境问题。
- 地图与角色:所在场景、坐标、角色等级与职业。
- 任务状态:进行中、已完成、被放弃的任务集合,以及关键标记。
- 装备与资源:当前装备、背包摘要、货币等资源数值。
- 随机种子与环境:游戏使用的随机数种子、构建号、帧率设置、是否联网。
关键取舍在于粒度。每一帧都存完整状态,八小时的数据量很快变得无法处理;只存操作而不存状态,又无法判断哪一步之后出现了偏差。比较务实的做法是:操作逐步记录,状态每隔固定步数或在关键事件(存档、切图、任务变化)时记录摘要,并每隔一段时间保存一个可恢复的检查点。
状态覆盖:到底覆盖了什么
“覆盖率”在游戏里很容易变成一个空洞的数字,因为游戏的原始状态几乎是无限的:坐标是连续的,血量是连续的,物品组合是指数级的。爱游戏的做法是先做状态抽象,把状态压缩成少数几个对逻辑有意义的维度,例如“所在区域、最近完成的任务阶段、装备组合类别、是否处于存档读档之后”,然后统计这些抽象状态被访问过多少次、之间的转移有没有走过。
这样定义的覆盖有两个用处。一是指导探索:如果“任务阶段二十以后、读档之后、换过装备”这一类抽象状态一次都没有访问过,下一轮就应该主动去凑。二是评估一次运行的质量:八小时如果只是在同一片区域来回,抽象覆盖很低,运行再久也没有意义。抽象的粒度需要与开发一起定,过粗会把有区别的状态混在一起,过细则回到了原始状态爆炸的问题。
从八小时到二十步:缩减轨迹
回到那个7:41的崩溃。假设整条轨迹有几千步,开发需要的是能稳定触发问题的最短序列。缩减的思路类似delta debugging(通过反复删除输入的一部分并重新运行,找到仍能触发错误的最小输入):删掉一段操作,回放,看错误是否依旧出现;出现则说明这段删得对,否则要放回去。
直接逐步试太慢,实际会结合两种手段。一是二分:先删一半,再删四分之一,快速缩小范围。二是依赖分析:从崩溃时读取的任务标记出发,倒查是谁最后写过它,只保留与这个标记有因果关系的操作,比如存档写入、读档、任务交付,去掉大量无关的探索与战斗。缩减前后大致是这样:
缩减前(约数千步) 序章 -> 任务1..20 -> 多次切图 -> 三次换甲 -> 存档槽二 -> 交付途中退出 -> 读档槽二 -> 接取任务21 -> 状态非法 缩减后(约二十步) 1. 新档,完成任务19(保留守门人击败标记) 2. 接取任务20,中途退出到主菜单 3. 存档到槽二 4. 读取槽二 5. 接取任务21 -> 状态非法
缩减以后,开发一眼就能看出问题与“交付途中退出后的存档”有关,而不是被几千步的噪声淹没。这里还有个提醒:缩减后的序列必须重新回放确认,确实还能触发同样的错误,而不是触发了另一个相似的错误,否则报告会把开发带偏。
缩减时的两个陷阱
第一个陷阱是“看似无关的操作其实是前提”。假设在缩减时删掉了“三次换甲”,错误消失了,这不一定说明换甲无关,也可能是换甲改变了内存布局或加载顺序。遇到这种情况,应该把这些操作标为“影响复现的环境因素”,而不是直接判定为噪声。第二个陷阱是“缩减出的序列过于人工”。缩得过短的序列,可能包含玩家事实上无法做出的动作组合,比如在两帧之间完成存档和读档。报告里要区分“玩家可达”与“仅测试手段可达”,前者优先修复。
不确定性:为什么“偶尔能复现”最麻烦
不是每个BUG都听话。随机数、网络延迟、帧率波动、多线程加载顺序,都可能让同一段操作在这次触发问题,下次却没有。处理方式分几层。
- 固定随机种子,让战斗、掉落、AI行为在回放时尽量一致。
- 记录环境:构建号、帧率、是否联网、设备配置。同一个问题在低帧率下才出现,这是很有价值的线索。
- 多次回放,给出复现概率。假设同一序列回放十次,其中出现几次,报告里就写“偶发,约为若干分之几”,而不是含糊地写“可复现”。
- 标注疑似时序问题:如果概率随帧率或网络状况显著变化,说明更可能是竞态或异步加载问题,需要开发用专门工具排查。
诚实对待“不稳定”很重要。一份写着“百分之百复现”但其实只有三成的报告,会让开发浪费一下午。
一份可用的复现报告长什么样
整理成报告时,读报告的人可能是没有参与测试的开发。他需要的是简洁、有序、能对照。下面是一个示例(虚构游戏,数值均为示例)。
标题: 交付途中退出后存档,读档再接任务21时任务系统报状态非法 构建: 示例构建号 X 环境: 60帧,离线 复现概率: 回放10次中出现7次(固定种子) 前置状态: 新档;已完成任务1-19;守门人击败标记=是 步骤: 1. 接取任务20,前往北部荒原 2. 交付途中退出到主菜单 3. 存档到槽二 4. 读取槽二 5. 与NPC对话,接取任务21 实际: 任务20显示“进行中”,任务21接取时抛出状态非法 预期: 任务20保持“已交付”或“未接取”其一,任务21可正常接取 疑点: 退出时任务20的交付标记未写入存档 附件: 检查点文件、完整轨迹(约数千步)、崩溃前状态摘要
报告里“预期”一栏需要有依据,也就是我们在测试Oracle里讨论的问题:机器人怎么知道任务20不该是“进行中”。没有明确的预期,长程测试只会得到一堆“看上去怪怪的”状态。
一次长程运行之后,开发应该拿到什么
把一晚上的运行变成开发第二天早上能处理的东西,需要在输出上做取舍。我们倾向于不直接倒出所有异常,而是先按几个维度整理:是否可稳定复现、是否玩家可达、是否影响存档或主线进度、缩减后的步数多少。存档损坏、主线卡死、任务标记错乱这类会让玩家进度丢失的问题,即使复现概率不高,也应排在前面;而只在极端资源状态下出现的显示错位,则可以靠后。对同一类异常,还要做去重:如果二十次崩溃都源于同一个交付标记未写入,就应该合并成一条,并附上出现次数,而不是让开发面对二十份内容几乎一样的报告。
另外,任何由机器人整理出的结论,仍然需要人来复核。缩减以后的最短路径可能省略了真正的根因,报告里的“疑点”只是假设,不是定论。让开发看到完整轨迹作为附件,并且随时可以重新回放,是让这份结论可信的基础。
长程测试的成本怎么控制
长程运行昂贵,昂贵的原因有三点:时间长、状态多、每次回放都要重新跑。几个常见的省钱办法如下。
- 检查点:每隔一段保存可恢复的快照,缩减时从最近的检查点起跑,而不用从新档开始。
- 分段并行:多个实例从同一个检查点出发,采用不同的扰动策略。
- 增量记录:只有发生变化的状态字段才写入,减少存储。
- 优先级:根据抽象覆盖里“很少访问”的区域决定下一轮去哪,而不是平均用力。
测试Agent如何在多轮之间记住自己试过什么、并据此调整下一轮的策略,是另一个话题,可以参考测试Agent的长期记忆和反思。
边界:轨迹能做到什么,做不到什么
轨迹与缩减能把“发现”变成“可复现”,但它有明确的边界。它无法判断某个状态玩起来是否有趣,也无法替代真人对节奏和手感的判断;它只能复现被记录的输入所触发的问题,对依赖外部服务的偶发故障往往力不从心。爱游戏测试AI的定位,是把开发者最耗时的那部分工作,也就是“回想之前做了什么”,交给机器完成。