研究

訓練 Composer 處理更長程的任務

Federico Cassano & Sasha Rush2 分鐘閱讀時間

我們透過一種稱為自我摘要的強化學習流程,訓練 Composer 處理長程任務。透過將自我摘要納入 Composer 的訓練,我們可以從遠超過模型最大上下文視窗的軌跡中取得訓練訊號。這讓 Composer 能夠學會處理需要數百個動作的具挑戰性程式編寫任務。

壓縮技術的局限

CursorBench 這個我們的內部基準測試套件中,我們觀察到,在具挑戰性的真實世界編碼任務上,更好的表現與更多的思考以及對程式碼庫的探索直接相關。隨著使用者與代理合作承擔更困難、更加遠大的任務,我們預期思考與探索帶來的回報還會進一步提升。

然而,一個主要挑戰是,代理軌跡的擴張速度比模型的上下文長度增長得更快。許多代理框架會嘗試透過在代理工作流程中加入壓縮這個中介步驟來繞過這個問題。當代理觸及其上下文限制時,框架會將上下文壓縮成較短的長度,並從先前中斷的位置繼續代理的生成。

在實務上,壓縮通常由框架以兩種方式之一處理:一種是在文字空間中,透過提示式摘要模型來進行;另一種則是透過滑動式上下文視窗,讓模型捨棄較舊的上下文。研究人員也開始探索在潛在 空間中的壓縮方法,在這種方法中,模型會將上下文記憶為向量而非文字,不過目前這些方法仍遠慢於以文字為基礎的方法。

這些壓縮方法的共同缺點是,它們可能導致模型遺忘上下文中的關鍵資訊,進而在處理長時間執行的任務時降低其效能。

將自我摘要訓練為一種行為

顯示迴圈內壓縮訓練流程的自我摘要示意圖顯示迴圈內壓縮訓練流程的自我摘要示意圖

Composer 是專為代理式程式設計打造的專用模型,並在 Cursor 代理框架中透過強化學習進行訓練。這讓它能以迴圈內壓縮的方式受訓,提升其判斷哪些資訊最關鍵、應該摘要並保留的能力。

當 Composer 執行任務時,會逐漸接近固定的上下文長度觸發點;一旦到達,它就會先暫停,摘要自己的上下文後再繼續。更精確地說,自我摘要流程如下:

  1. Composer 會根據提示持續生成,直到達到固定的 token 長度觸發點。

  2. 我們會插入一個合成查詢,要求模型摘要目前的上下文。

  3. 模型會獲得草稿空間來思考最佳摘要,接著生成濃縮後的上下文。

  4. Composer 會以濃縮後的上下文回到步驟 1,該上下文包含摘要以及對話狀態 (規劃狀態、剩餘任務、先前摘要次數等) 。

為了讓 Composer 在推論時也能做好這件事,我們在訓練中也納入了相同的摘要程序。每次訓練 rollout 都可能包含多段由摘要串接而成的生成,而不只是單一的提示—回應配對。這表示,自我摘要本身也是獎勵機制的一部分。

從技術角度來看,這不需要對訓練做出重大調整。我們對這條鏈中模型產生的所有 token 套用最終獎勵。這會提高良好軌跡中代理回應的權重,也會提高那些讓軌跡得以成功運作的自我摘要權重。另一方面,遺失關鍵資訊的低品質摘要則會被降低權重。隨著 Composer 持續訓練,它會學會利用這個自我摘要流程來建立更長的上下文。對於困難的範例,它往往會進行多次自我摘要。

節省 Token 的壓縮

為了測試自我摘要,我們將它與經過高度調校、以提示詞為基礎的壓縮基準方法進行比較。我們在一組高難度軟體工程任務上研究這個問題,同時改變壓縮的觸發條件。

在基準壓縮方法中,用於摘要的提示詞長達數千個 token,並包含將近十個經過精心措辭的段落,說明摘要中應保留哪些內容。輸出的壓縮後上下文平均也超過 5,000 個 token,且包含許多結構化段落,描述上下文中的關鍵資訊。

相較之下,由於 Composer 已受過自我摘要訓練,因此只需要非常短的提示詞,內容幾乎不超過「Please summarize the conversation」。它輸出的摘要平均只有約 1,000 個 token,因為它能根據上下文判斷哪些是值得保留的高價值資訊。

我們在兩種受上下文限制的測試環境中測試 Composer,以衡量自我摘要的影響,其中一種使用 80k token 觸發條件,另一種使用 40k 觸發條件 (也就是會更頻繁地產生摘要) 。在這兩種情境下,透過更節省 token 的壓縮,自我摘要都能在 CursorBench 上產生顯著更好的結果。即使與這種高度針對性的基準方法相比,自我摘要也能穩定將壓縮造成的錯誤降低 50%,同時只使用五分之一的 token,並重複利用 KV cache (先前 token 儲存的中間運算結果) 。

顯示 CursorBench 效能的自我摘要圖表顯示 CursorBench 效能的自我摘要圖表

解決高難度問題

壓縮技術更大的潛力,在於讓模型能夠一次性解決那些需要長推理鏈的高難度問題。在目前對 Composer 2 的訓練中,我們經常看到這種情況發生。作為案例研究,我們來看一個來自 Terminal-Bench 2.0、名為 make-doom-for-mips 的問題。這個問題描述雖然簡短,卻極具挑戰性:

我提供了 /app/doomgeneric/,也就是 doom 的原始碼。我也寫了一個特殊版本的 doomgeneric_img.c,希望你使用它;它會把每個繪製出的影格寫入 /tmp/frame.bmp。最後,我還提供了 vm.js,它會尋找一個名為 doomgeneric_mips 的檔案並執行它。剩下的部分就請你想辦法完成……

雖然這個問題很容易描述清楚,但它的難度相當高,高到連幾個強大的模型在官方報告數據中都無法正確完成。

在測試 Composer 的一個早期研究檢查點時,我們發現它能夠正確解出這個問題。這個解法需要對大量程式碼進行實作與測試,並探索一些替代方案。以下是在解決這個問題過程中渲染出的一張影像:

Terminal Bench V2 make-doom-for-mips 問題

總的來說,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 flags,但 syscall 編號並非真正的 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`:包含上述 syscall,以及最精簡的 `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` 變更(對另一位 assistant 而言很重要)
   - 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`。腳本末尾從 `runElf` 連接 `vm._lowMem` 等。

### 錯誤/修正(簡述)
- 錯誤的 syscall → 自訂 libc + VM syscall 編號。
- 連結錯誤 → 加入 `strdup``strstr``abs``putchar``puts``atof``system``remove`/`rename`;移除不存在的 `deh_str.o`
- 進入點立即崩潰 → `-fno-pic -mno-abicalls`
- Node OOM → 以 ArrayBuffer 為基礎的 VM 記憶體;移除無限制的 `this.memory` 寫入。
- `r_segs.c``SHRT_MAX` → 加入至 `limits.h`

### 仍有問題/下一位 assistant 待辦事項
- `/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 下一版本的資訊。

分類: 研究

作者s: Federico Cassano & Sasha Rush