训练 Composer 处理更长周期任务
我们通过一种称为自我总结的强化学习方法,训练 Composer 处理长周期任务。通过将自我总结纳入 Composer 的训练,我们可以从远超模型最大上下文窗口的长轨迹中获取训练信号。这让 Composer 能够学会处理需要数百步操作的高难度编程任务。
压缩技术的局限性
在我们的内部基准测试套件 CursorBench 中,我们观察到,在高难度真实世界编程任务上,更好的表现与更充分的思考和更多的代码库探索直接相关。随着用户与智能体协作处理更困难、更具挑战性的任务,我们预计思考和探索带来的收益还会进一步提升。
不过,一个主要挑战在于,智能体轨迹的增长速度快于模型上下文长度的提升速度。许多智能体框架会尝试通过在智能体工作流中将压缩作为中间步骤来绕开这一问题。当智能体触及上下文限制时,框架会将上下文压缩到更短的长度,然后从中断处继续智能体的生成。
在实践中,压缩通常由框架通过两种方式之一来处理:要么在文本空间中借助提示驱动的摘要模型进行处理,要么通过滑动上下文窗口让模型丢弃较早的上下文。研究人员也开始探索在潜在 空间中的压缩方法,在这种方法中,模型将上下文记忆为向量而非文本,不过目前这些方法仍比基于文本的方法慢得多。
这些压缩方法的共同缺点在于,它们可能导致模型遗忘上下文中的关键信息,从而在推进长时间运行的任务时降低其效果。
作为一种训练出来的行为的自我总结


Composer 是一个专为智能体式编程设计的专用模型,并在 Cursor 的智能体 harness 中通过强化学习进行训练。这使它能够以“循环内压缩”的方式接受训练,提升其判断哪些信息最关键、需要总结并保留的能力。
当 Composer 处理任务时,接近固定的上下文长度触发点后,它会先暂停,对当前上下文进行总结,然后再继续。更准确地说,自我总结过程如下:
-
Composer 基于提示词持续生成,直到达到固定的 token 长度触发点。
-
我们插入一个合成查询,要求模型总结当前上下文。
-
给模型提供一定的草稿思考空间,让它构思最佳总结,然后生成压缩后的上下文。
-
Composer 使用压缩后的上下文回到步骤 1;该上下文包含总结以及对话状态 (规划状态、剩余任务、之前总结的次数等) 。
为了让 Composer 在推理时也能做好这一点,我们在训练中引入了相同的总结流程。每次训练 rollout 都可能包含多次由总结串联起来的生成,而不是单一的提示词—响应对。这意味着,自我总结本身也是奖励机制的一部分。
从技术角度看,这并不需要对训练做出重大改动。我们将最终奖励应用到这条链中模型生成的所有 token 上。这既会提高良好轨迹中智能体响应的权重,也会提高那些让这些轨迹得以奏效的自我总结的权重。与此同时,丢失关键信息的糟糕总结则会被降低权重。随着 Composer 训练不断推进,它会学会利用这一自我总结过程来构建更长的上下文。对于较难的示例,它通常会进行多次自我总结。
高 token 效率压缩
为了测试自我总结能力,我们将其与经过高度优化的、基于提示词的压缩基线进行比较。我们在一组高难度软件工程任务上研究了这个问题,同时改变压缩 trigger。
在基线压缩方法中,总结提示词长达数千个 token,包含近十个经过精心设计的部分,用于说明摘要中应保留哪些内容。压缩后的输出上下文平均也超过 5,000 个 token,并包含许多结构化部分,用于描述上下文中的关键信息。
相比之下,由于 Composer 接受过自我总结训练,它只需要一个非常短的提示词,内容几乎不过是“Please summarize the conversation”。它输出的摘要平均只有约 1,000 个 token,因为它能根据上下文判断哪些高价值信息值得保留。
为了衡量自我总结的影响,我们在两个受上下文约束的测试环境中测试了 Composer:一个使用 80k token trigger,另一个使用 40k trigger (这意味着会更频繁地生成摘要) 。在这两种场景下,自我总结都凭借这种高 token 效率的压缩方式,在 CursorBench 上取得了显著更好的结果。即便与这种有针对性的基线方法相比,它也能持续将压缩带来的错误降低 50%,同时只使用五分之一的 token,并复用 KV cache (即先前 token 的中间计算缓存结果) 。


解决难题
压缩技术更大的潜力在于,它能够让模型一次性解决那些需要长推理链的难题。在当前对 Composer 2 的训练中,我们经常看到这种情况发生。作为一个案例研究,我们来看一个来自 Terminal-Bench 2.0、名为 make-doom-for-mips 的问题。这个问题描述简短,但极具挑战性:
我已经提供了 /app/doomgeneric/,也就是 doom 的源代码。我还编写了一个特殊的 doomgeneric_img.c,希望您使用它;它会将绘制的每一帧写入 /tmp/frame.bmp。最后,我还提供了 vm.js,它会读取一个名为 doomgeneric_mips 的文件并运行它。其余部分请您自行解决……
虽然这个问题描述起来并不复杂,但它的难度很高,以至于一些强大的模型在官方报告结果中都无法正确解决它。
在测试 Composer 的一个早期研究检查点时,我们发现它能够正确解决这个问题。这个解法需要对大量代码进行工程实现和测试,同时还要探索一些替代实现。下面是一张在解决该问题过程中渲染出的图像:

总的来说,Composer 经过 170 轮交互后,找到了精确的解法,并在过程中以紧凑、人类可读且结构化的形式生成了自我总结。它将超过 100,000 个 token 总结压缩到 1,000 个,并保留它认为最有助于解决问题的信息:
## 对话摘要
### 用户目标
构建一个名为 doomgeneric_mips 的 MIPS 小端 ELF 文件,使 `node vm.js` 能够运行它:stdout 应正常工作,且帧数据应使用提供的 doomgeneric_img.c(以及 `/app/doomgeneric/` 下的 doomgeneric 源码)写入 `/tmp/frame.bmp`。
### 已实现内容
1. 为 JS VM 提供独立 libc(`/app/mips_freestanding/`)
- VM 实现了 Linux 风格的 open 标志,但系统调用号并非真实的 MIPS Linux 编号(0=read, 1=write, 2=open, 3=close, 8=lseek, 201=time, 38=rename, 39=mkdir)。普通的 mipsel-linux-gnu-gcc + glibc 会使用 4003+ 风格的编号,无法正常工作。
- `my_libc.c` + `vm_syscall.h`:包含上述系统调用,以及精简版的 `malloc`/`free`/`realloc`、`stdio`(`fopen`/`fread`/`fwrite`/…)、字符串处理、`printf` 子集、`qsort`、`sin`/`atan`/`fabs`、`strdup`、`abs`、`system` 桩函数等。
2. `doomgeneric_img.c`(位于 `/app/doomgeneric/doomgeneric/`)
- 使用 `my_stdlib.h`。
- `DG_DrawFrame`:将每一帧写入 `/tmp/frame.bmp`(32 位 BMP)。
- `main`:安全处理 `argc`/`argv`(若为 null 则使用备用 argv),然后执行 `doomgeneric_Create` → 无限 `doomgeneric_Tick()` 循环(与其他 doomgeneric 前端模式相同)。
3. 构建 — `/app/doomgeneric/doomgeneric/Makefile.mips`
- 使用 `mipsel-linux-gnu-gcc`、`-fno-pic -mno-abicalls`、`-static -nostdlib`、`-Wl,-e,main`,链接 `libgcc`。
- 必须禁用 PIC 以避免以下问题:在 `$t9` 未设置的情况下从 `main` 入口进入会破坏 GP 相对 GOT;禁用 PIC 解决了该问题。
- 目标文件:完整的 doom 文件列表,以 `doomgeneric_img.o` 替代 xlib;不含 `deh_str.o`(仅头文件)。
- `i_system.c`:`DEFAULT_RAM` / `MIN_RAM` 可通过 `-D` 覆盖;构建时使用 `-DDEFAULT_RAM=4 -DMIN_RAM=4` 以缩减 VM 的内存区域。
- 输出:`/app/doomgeneric_mips`。请勿 strip——VM 从 `.symtab` 解析 `main`。
4. `vm.js` 变更(对下一位助手而言很重要)
- OOM 问题:当 doom 访问大量内存字时,普通的 `{}` 内存对象会导致 V8 内存溢出。
- `runElf`:现在分配 `Uint32Array` 低地址 RAM(从 `0x400000` 起 96MB)+ 栈(从 `0x7f800000` 起约 8MB),将 `.text`/`.data`/`.rodata` 加载到低地址缓冲区,返回 `{ lowMem, stkMem, memLowBase, …, entry_point }` 而非一个巨大的 `memory` 对象。
- `MIPSVM`:`memGetWord` / `memSetWord` 通过上述缓冲区路由;稀疏的 `this.memory` 在写操作中实际上已被弃用(OOM 修复);`memGetWord` 越界时返回 0(可能掩盖缺陷)。
- 所有 `lw`/`sw`/字节辅助函数及 `runProgram` 指令获取均使用 `memGetWord`/`memSetWord`。脚本末尾将 `vm._lowMem` 等与 `runElf` 的返回值连接。
### 错误与修复(简述)
- 系统调用号错误 → 自定义 libc + VM 系统调用号。
- 链接错误 → 添加了 `strdup`、`strstr`、`abs`、`putchar`、`puts`、`atof`、`system`、`remove`/`rename`;移除了不存在的 `deh_str.o`。
- 入口处立即崩溃 → 使用 `-fno-pic -mno-abicalls`。
- Node 内存溢出 → 改用 ArrayBuffer 支持的 VM 内存;移除了无界的 `this.memory` 写操作。
- `r_segs.c`:`SHRT_MAX` → 已添加到 `limits.h`。
### 仍存在的问题 / 下一位助手待办事项
- `/tmp/frame.bmp` 在测试中从未生成:运行以 `Program terminated at PC=0x400151`、`Executed 626581578` 条指令结束。`0x400150` 是 `abort` 符号(自跳转死循环)。因此某处最终跳入了 `abort`,而非正常的游戏循环绘帧流程。
- 4MB 内存区域(`DEFAULT_RAM=4`)对于真实 IWAD 运行可能过小 → 导致 `Z_Malloc` / OOM 类失败或其他未定义行为;建议尝试 `DEFAULT_RAM=6`,在 `my_libc.c` 中增大 `HEAP_SIZE`(之前已缩减至 14MB),并确保 `memGetWord` 越界不会掩盖真实错误。
- 具体后续步骤:找出谁调用了 `abort`(例如 trap、libgcc 路径、内存分配失败);修复非 Windows 下的 I_Error 路径(当前可能调用 `ZenityErrorBox` 或走异常路径);确认 `doomgeneric_Create` → `D_DoomMain` → … → `doomgeneric_Tick`/`D_Display` → `DG_DrawFrame` 调用链;确认 VM 中 `fopen("/tmp/frame.bmp","wb")` + `SYS_close` 刷新正常;反复运行直到 `/tmp/frame.bmp` 成功生成且 stdout 输出正常。
### 路径
- ELF:`/app/doomgeneric_mips`
- 构建:`/app/doomgeneric/doomgeneric/Makefile.mips`
- 前端:`/app/doomgeneric/doomgeneric/doomgeneric_img.c`
- Libc:`/app/mips_freestanding/my_libc.c`、`/app/mips_freestanding/include/*`
- VM:`/app/vm.js`(已修补内存模型)
- 本地测试用 IWAD:`/app/doom1.wad`(用于测试)迈向长周期未来
通过将压缩整合进训练循环,Composer 学会了一种显式机制,能够高效地将关键信息向后传递,并在高难度任务上变得更有能力。我们在自我总结方面的工作,是朝着更广泛目标迈出的一步:让 Composer 在更长周期、更复杂的过程 (例如多智能体协同) 上进行训练。我们持续看到,更好的模型训练正在提升这些智能体系统的能力范围与智能水平。
我们也将很快分享更多关于 Composer 下一版本的信息。