让 Agent 自己长记忆:用代码覆盖率当奖励,把"探索"和"记住"绑在一起训

一句话说清楚

GUI Agent 在长轨迹里有两个老大难:历史塞不下、又不知道哪些该记。这篇论文的做法很取巧——把浏览器跑出来的 JavaScript 代码覆盖率 当成新奇性奖励,让记忆模块和探索策略绑在一起一起训。模型用了 9B(2B 压历史 + 7B 出动作),在 ScaleWoB 测试集上拿到 20.7 的累计奖励,几乎追平 Gemini 3.1 Flash-Lite 的 ReAct-vision(20.9),但输入 token 只有它的 1/22

arXiv ID:2606.01528


这论文为什么值得聊

先说我读完的第一感受:"用 V8 覆盖率当 reward"这件事真的挺有想法的

GUI Agent 这一年大家都在卷"长轨迹"。卷到最后,方法基本分两派: - ReAct 派:什么都塞 prompt,截图、a11y 树、思考过程一起堆,跑 50 步轻松上百万 token; - 滑动窗口派:只留最近 N 步,再加个 Notetaker 之类的"摘要器"硬撑,老步骤的关键信息会丢。

两派都有自己的痛。第一派费钱、第二派记不住。最近开始有人想"潜在记忆"——把历史压成几个 latent token 喂回去。听起来很美,但训练这种 latent memory 的监督信号从哪来? 这是个真问题。普通的 SFT 只能学"下一步动作做什么",没法直接监督"这段历史里哪些信息值得压进 memory"。

JAMEL 这篇的回答是:让探索本身成为监督。具体讲:

浏览器执行 JS 时 V8 引擎会生成代码覆盖率报告。Agent 每点一下、每填一次表单,如果触发了之前没跑过的 JS 行/分支/语句/函数,就给 +1 奖励,否则 0。

这个设计的妙处在于——新奇性是持久的(persistent novelty)。已经覆盖过的代码就永远不会再产生奖励,不像很多 RL 探索奖励会因为"老路又新鲜了"反复给分。这种持久性自带课程:浅层交互被吃完之后,奖励自然变稀疏,逼策略去走更深的多步路径。

记忆和探索在这个 loop 里是互相喂饭的:探索去触发新覆盖率 → 产生奖励 → 这条 trace 是"有用的历史" → 用它来监督 latent memory → memory 让 Agent 知道哪些行为已经试过了 → 探索得更深。

我看到这套设计的时候是真愣了一下。用代码覆盖率当 intrinsic reward——这事在传统软件测试 fuzzing 里早就是标配(AFL、libFuzzer 全靠它),但搬到 LLM Agent 上当 RL 信号还没怎么见人这么干过。挺漂亮的迁移。


论文信息

  • 标题:Joint Agent Memory and Exploration Learning via Novelty Signals
  • 作者:Shizuo Tian, Xiaohong Weng, Rui Kong, Yuxuan Chen, Guohong Liu, Yuebing Song, Jiacheng Liu, Yuchen Li, Dawei Yin, Ting Cao, Yunxin Liu, Yuanchun Li
  • arXiv2606.01528
  • 日期:2026 年 6 月 1 日

框架长什么样

图1:JAMEL 整体架构

图1:JAMEL 框架架构。左边是冻结的 VLM 历史压缩器,把每一步的 (观察, 动作) 编成一个 memory token;右边是 7B 策略网络,吃当前观察 + 历史 memory token 序列出下一步动作。中间橙色的线性对齐层 W 是为数不多需要训的部分。

拆开看是三块:

1. 历史压缩器(冻结)

每一步的观察 \(o_i\) 和动作 \(a_i\) 拼起来叫 \(x_i\),丢给一个冻结的 Qwen3-VL-2B,取 EOS token 的最后一层隐状态当压缩结果:

\[h_i = F_\phi(x_i)_{\text{EOS}} \in \mathbb{R}^{2048}\]

注意是冻结的——这个选择很关键。如果压缩器也跟着训,整个 pipeline 计算量翻倍,且压缩出来的语义会随训练漂移,不稳。冻结之后,历史 token 是固定的特征提取,只让下游学怎么用。

到第 \(t\) 步,记忆状态就是 \(\mathbf{M}_t = [\mathbf{h}_1, \ldots, \mathbf{h}_{t-1}]\),一个 \((t-1) \times 2048\) 的矩阵。

2. 线性对齐器(要训)

记忆 token 的维度是 2048,策略 LM 的 embedding 维度是 3584,对不上。中间架一个学习的线性投影 \(\mathbf{W} \in \mathbb{R}^{2048 \times 3584}\) 把 memory token 映到策略空间。然后跟当前观察的 embedding 拼起来:

\[\mathbf{e}_t = [\mathbf{M}_t \mathbf{W}\;|\;\text{Embed}(o_t)]\]

3. 策略网络(要训)

Qwen2.5-VL-7B-Instruct,吃 \(\mathbf{e}_t\) 出动作 \(a_t\)

总参数

策略 7B + 压缩器 2B = JAMEL-9B。但实际只有策略 + 那个 W 在更新,压缩器全程冻结。


用 V8 覆盖率监督 memory,到底怎么训

新奇性奖励的形式化

\[r_t = \mathbf{1}[\text{Novelty}(o_t, a_t, \mathcal{H}_{<t}) > 0]\]

具体到 GUI 场景:

\[\mathcal{C}(t) = \text{cov}_{\text{lines}}(t) + \text{cov}_{\text{branches}}(t) + \text{cov}_{\text{statements}}(t) + \text{cov}_{\text{functions}}(t)\]

四种覆盖率累加起来。每一步如果 \(\mathcal{C}(t) > \mathcal{C}(t-1)\) 就给 +1,否则 0。

实现上靠 V8 的覆盖率报告 + Istanbul 报告器。覆盖率 baseline 在整个 session 维护,浏览器回到首页也不重置——这点很重要,否则刷首页这种 trivial 操作会反复触发奖励。

数据怎么收

整体是个先 rollout 再筛轨迹的 SFT 范式,不是端到端 RL(这是论文的一个保留——明确把 RL 留给 future work 了):

  1. 用一个通用 LLM 作为 rollout policy,去 86 个训练应用上跑探索,每步带 CoT;
  2. 一个 session 由多个 episode 组成,每个 episode 从应用首页开始,最多跑 N 步;
  3. 对每个 episode,找到最后一个新奇步 \(t^*\)只保留 \(1\)\(t^*\) 这段前缀——后面那些没拿到奖励的步骤当成噪声砍掉;
  4. 对每个保留的 \(t \leq t^*\),预算 \(\mathbf{M}_t\),组成训练样本 \((o_t, \mathbf{M}_t, a_t)\)
  5. SFT 目标:最大化 \(\log \pi(a_t | o_t, \mathbf{M}_t)\)

最终拿到 24k 训练样本、覆盖 86 个 web app,留 10 个未见过的 app 做测试。

我的判断:这个监督信号到底有多硬核

说实话,这个 reward 信号有它聪明的地方,也有它的限制。

聪明的地方:覆盖率是确定性的、客观的、无标注的。不像 LLM-as-Judge 给一个模糊的 0~10 分,覆盖率就是要么涨要么不涨,没什么 reward hacking 空间——你想骗高分,就必须真的去触发新代码路径。

限制:代码覆盖率 ≠ 任务完成度。Agent 可能走了一堆乱七八糟的弹窗,覆盖率涨得很欢,但跟用户真实任务一点关系没有。论文里其实也承认了——这个方法目前只是 reward exploration,没有跟下游任务对齐。要做"先探索后执行"还得加一层。

但话说回来,对于 pretraining 阶段的"通用 GUI 探索能力",这个信号已经够用了。就像人学用一个 App,前期就是各种乱点,把功能摸清楚,后期再针对具体目标用。


实验结果

主表:累计覆盖率奖励

50 步会话内,10 个未见 app 上的平均奖励:

类型 方法 模型 内存机制 平均奖励
闭源 ReAct-text Gemini 3.1 Flash-Lite 完整历史 19.9
闭源 ReAct-vision Gemini 3.1 Flash-Lite 完整历史 20.9
开源 MAI-UI MAI-UI-8B 设备/云路由 8.4
开源 Mobile-Agent-v3.5 GUI-Owl-1.5-8B 滑窗 + Notetaker 5.9
开源 JAMEL JAMEL-9B 潜在记忆 20.7

把这张表读一下:

  • JAMEL 干翻同体量开源基线:20.7 vs 8.4、5.9。这两个 baseline 是 8B GUI Owl 系列和 MAI-UI,都不是弱鸡,常规任务上表现都不错。但在"50 步开放式探索"这种长轨迹场景下,滑窗 + Notetaker 的方案直接打脸了,5.9 这个数字很惨——意味着它在前几步之后就基本陷入循环了。
  • 跟闭源云模型几乎打平:差 Gemini 3.1 Flash-Lite ReAct-vision 仅 0.2 分,超 ReAct-text 0.8 分。考虑到 JAMEL 是 9B 的开源模型,这个对比是真的能打。

Token 消耗对比

方法 总输入 token 平均每步 相对 JAMEL
Mobile-Agent-v3.5 2,931,946 5,863.9 2.76×
MAI-UI 2,980,061 5,960.1 2.81×
ReAct-Text 18,938,833 37,877.7 17.85×
ReAct-Vision 23,260,296 46,520.6 21.92×
JAMEL 1,061,103 2,122.2 1.00×

这张表才是 JAMEL 真正的杀手锏。

ReAct-vision 那个 4.6 万 token/step 的数字,平均每步要塞 4.6 万 token——一会儿就奔着 Gemini 上下文限制去了。50 步累计 2300 万 token,这个成本如果按 API 报价算下来不便宜。

JAMEL 平均每步 2122 token,直接砍到 1/22。原因也好理解:所有历史都压成 \(h_i \in \mathbb{R}^{2048}\) 这一个向量了,每步就一个 memory token 进去。

效果几乎追平、token 砍到 5%——这是真能打的工程优势


探索曲线长什么样

图2:测试 app 上的累计奖励曲线

图2:10 个测试 app 上 50 步会话的平均累计奖励,阴影是跨 app 的标准误。蓝色 JAMEL 全程稳步上涨,跟绿色的 Gemini ReAct 几乎贴着走;橙色和红色的开源 baseline 在大概第 10-15 步就开始平台化,再也涨不动了。

这张图我盯着看了挺久。MAI-UI 和 Mobile-Agent-v3.5 在第 15 步左右几乎完全停滞——这就是滑窗截断的典型表现:丢掉的早期历史信息让 Agent 反复尝试已经试过的操作,新奇性奖励再也触发不了。

JAMEL 跟 ReAct 系列贴着 Gemini 的曲线一路涨上去,没有出现明显平台。这个差异其实就证明了 latent memory 的核心价值——用 9B 的潜在压缩,留住了相当于完整历史的信息密度

单个 app 上的差异

图3:每个测试 app 的累计奖励轨迹

图3:10 个未见 app 上各自的累计奖励曲线。深结构应用(Vipshop、Expedia、Temu)上 JAMEL 持续上涨;媒体应用(Youku、Keep)上所有方法都早期平台化;阿里巴巴、淘宝这种复杂界面上 JAMEL 出现阶梯式跳跃,能跳出局部最优。

读出来的几个有意思的点:

  • Vipshop / Expedia / Temu:电商和旅游平台结构深,多层级导航,JAMEL 持续找到新路径——这种 app 是 latent memory 优势最大的场景;
  • Youku / Keep:媒体类 app 上所有方法都早期平台化。论文坦诚说了:可能本身交互深度就有限,跟方法没多大关系;
  • 拼多多JAMEL 偶尔也会卡住——超密集的界面对压缩 memory 仍然是挑战。

最后一点是论文挺实诚的地方。没有藏着掖着说"我们方法在所有场景都吊打",而是承认密集模态覆盖(弹窗、广告、悬浮层)目前对 JAMEL 仍然是难点


案例分析里的失败模式

论文里提到一个很具体的失败模式——持久模态覆盖(persistent modal overlay)

Agent 频繁尝试与那些视觉上看起来可点击、但实际被前景弹窗禁用的背景元素交互。

我读到这里挺有共鸣。这其实是个多模态对齐问题:a11y tree 给的可访问元素列表里这些元素还在,但 V8 那边它们对应的 onclick handler 被弹窗拦截了。Agent 的视觉感知和实际交互结果对不上

这跟 latent memory 没多少关系,所有 GUI Agent 都会碰到。但确实是论文方法的短板——记忆压缩之后,"这个按钮我刚才点了三次都没反应"这种细粒度信息可能就糊在向量里了。


几个值得思考的点

1. RL 留给未来——这事其实是个保留

JAMEL 现在是用 SFT 在 rollout 数据上训的,rollout policy 是另一个通用 LLM。也就是说这套监督信号还没跟训练做端到端 RL。论文 Future Work 里点了一下要做 RL,但这一版没做。这个设计选择我能理解(端到端 RL on 9B + 浏览器环境,工程量爆炸),但也意味着目前的探索 ceiling 受限于初始 rollout policy 的能力——它没见过的路径,JAMEL 大概也学不到。

2. 代码覆盖率作为 reward,能迁移到非 web 场景吗?

论文里一直是 web/JS 环境。如果是 native app(Android/iOS)、桌面应用,没有 V8 这种现成的覆盖率工具,这套方法就吃力了。当然可以换其他持久新奇性信号(比如 UI 状态哈希),但代码覆盖率这种细粒度、确定性、客观的信号是真不容易找替代的

3. 跟 World Model 路线的对比

最近还有一些工作走"先训环境模型、再用 model-based RL 探索"的路线。JAMEL 这个 latent memory 其实就是一种隐式的环境模型——它不显式预测下一步状态,但压缩了历史,间接支持策略做反事实推理。两条路各有取舍:显式 world model 表达力强但训练不稳,latent memory 训练稳但容量受限。


工程上能拿走什么

如果你也在做 GUI Agent 或者长轨迹 Agent 的工作,几个我觉得值得借鉴的点:

  1. 找客观的、持久的、无标注的 reward 信号。代码覆盖率是个很好的范例。游戏类 Agent 可以是地图覆盖、技能解锁;爬虫类可以是新发现的 URL;科研类可以是新触达的 API endpoint——不一定要用 LLM Judge。

  2. 冻结大压缩器、只训对齐层。这种轻量化训练范式很值得借鉴,尤其是在算力有限的情况下做长上下文压缩任务。

  3. rollout 数据用"最后一个新奇步前缀"截断。这个细节挺工程化的——把没拿到奖励的尾巴砍掉,避免学到"无效 idle 行为"。简单但有效。

  4. 效率指标要单独看。20.7 vs 20.9 这个数字差距很小,但 token 砍到 1/22 是质变。部署一个能在端侧跑 50 步的 Agent,跟非要调 Gemini 跑 50 步,用户体验完全不是一回事


觉得有启发的话,欢迎点赞、在看、转发。跟进最新 AI 前沿,关注我。