邁向自動駕駛的程式碼庫

Wilson Lin研究
邁向自動駕駛的程式碼庫

社群對我們在擴展長時間執行的自主編碼研究上的回應,讓我們感到非常振奮。

這項工作一開始是作為內部研究,用來推進當前模型的極限。作為研究的一部分,我們建立了一個新的代理框架,用來協調成千上萬個代理並觀察它們的行為。到了上個月,我們的系統已經足夠穩定,可以連續執行一週,並完成我們研究專案(是一個網頁瀏覽器)中絕大多數的程式碼提交。這個瀏覽器原本並非打算對外提供使用,我們也預期程式碼會有一些不完美之處。

然而,即使存在一些小問題,成千上萬個代理能夠協同工作,產出幾乎完全無需人為介入即可執行的成果,本身就是一個值得分享的里程碑。從那之後,我們持續推進這項研究,也希望能更深入介紹這個框架是如何建構出來的。

同時,我們也會將這項研究的一部分開放給部分使用者試用。

背景

我們的研究專案一開始是我個人的 side project。

瀏覽器看起來是一個有趣的基準測試目標。它的複雜度足以暴露前沿模型的各種限制,而且有許多不同的子系統必須協同運作。

我一開始的計畫是支援在沒有 JavaScript 的情況下渲染網頁。我先用提示請求 Opus 4.5,讓它寫出一份建構瀏覽器引擎的詳細計畫。我會一再催促它「繼續」,想看看它在這份計畫上可以走多遠。

這很快就失敗了。模型會失去對自己正在做什麼的掌握,常常中途停下來宣稱任務已完成,實際上還差得遠,而且會卡在複雜的實作細節上。不過它也展現出深厚的知識和智慧,可以在小範圍內寫出不錯的程式碼。

關鍵問題在於,瀏覽器這個任務本身太龐大,必須拆解成子任務。接著,我讓代理規劃一份主要工作的相依圖,讓多個代理可以並行處理。代理會被手動啟動去執行任務,當它們停下來時再被推動繼續。這確實提高了吞吐量,但結果並沒有好太多。代理之間無法互相溝通,也無法對整體專案提供回饋。系統需要變得更加動態。

同時,GPT-5.1(以及之後的 GPT-5.2)在精準遵循指令的能力上開始展現更好的結果。這看起來很適合長時間執行的代理,因此我們根據這些實驗更新了控制框架,改用 OpenAI 的模型。

在這個時間點上,控制框架已經能建出一個不支援 JavaScript 的簡單版網頁瀏覽器,但若只用一個代理來打造完整的瀏覽器引擎,速度會慢到難以接受。

這也開啟了我們下一輪的研究:如果我們多花 10 倍運算資源,能不能換來 10 倍更有意義的吞吐量?

從單一代理到多代理

我們從一個新的程式碼儲存庫開始,裡面只有一個以 Rust 實作的簡單測試框架(harness)。

與其一開始就處理分散式系統的複雜度,我們選擇在一台擁有大量資源的大型 Linux VM(虛擬機器)上執行這個測試框架。為了控制它,我們會透過 SSH 登入該 VM,並使用簡單的終端機介面。

我們一開始就投入更多時間,確保系統具有完善的可觀測性。我們記錄所有代理訊息、系統動作和指令輸出,並加入時間戳記,方便我們分析與重播各次工作階段。這不僅有助於我們手動檢閱,也能將這些資料回送到 Cursor,來篩選大量資料並快速找出模式。

自我協調

我們第一個多代理構想非常簡單:讓彼此地位對等的代理使用一個共享狀態檔案,來查看其他代理正在處理什麼、決定自己要做什麼,並更新該檔案。

顯示代理連接到共享協調檔案的自我協調示意圖顯示代理連接到共享協調檔案的自我協調示意圖

我們原本打算盡量少下指令該怎麼做,而是讓這些代理自己想辦法自我協調。這很快就失敗了。

協調檔很快就製造了更多問題。代理會把鎖握在手上太久、忘記釋放鎖、在不允許的情況下嘗試加鎖或解鎖,而且整體來說並不理解在協調檔上持有鎖的意義。鎖一不小心就很容易用錯,就算勉強用對,也只是非常狹隘地剛好正確,多給提示也沒有幫上忙。

鎖定也造成了過多的資源爭用。20 個代理的吞吐量會被拖慢到只剩 1–3 個的水準,大部分時間都花在等待鎖上。我們曾嘗試提供一個工具,讓代理可以明確地等待另一個代理完成工作,但它們很少使用。我們也嘗試了無鎖的樂觀併發控制方法,雖然降低了額外負擔,但並沒有消除混亂。

代理之間缺乏結構,代表沒有任何一個代理會承擔大型、複雜的任務。它們會避開爭用和衝突,選擇較小且安全的變更,而不是為整個專案負起整體責任。

新增結構與角色分工

接著,我們進一步將角色拆分,讓各個代理在各自負責的領域擁有明確的責任與主導權:

顯示 Planner、Executor、Workers 與 Judge 在管線中依序流程的結構化角色示意圖顯示 Planner、Executor、Workers 與 Judge 在管線中依序流程的結構化角色示意圖

一位規劃者會先擬定出明確的執行方式與交付項目,以便在使用者指示的方向上持續推進。這份規劃會交給執行者,由執行者作為唯一的主導代理,負責確保整個計畫被完整落實。執行者可以為工作者分派任務,藉此達到線性擴展處理量。

為了持續推進並建立責任機制,在執行者完成後,會再由一名獨立的評審者進行檢查,以判斷是否已真正完成,以及是否需要再執行一輪。這解決了許多協調上的問題。由單一角色專責負責並監督執行,讓工作者可以專注在各自的任務上,同時仍確保整體系統能持續交付成果。

觀察與爬山式優化

達成這個設計需要對整個系統進行細緻的觀察。

如果出現重大問題,往往會在許多代理與工具呼叫中反覆發生。舉例來說,我們注意到因為許多代理同時執行 git restore,產生了過多的資源競爭。我們使用 Cursor 分析日誌,並將其與提示詞對照,來理解為什麼實際行為與預期不符。

最終,我們發現這個系統被最慢的工作者拖慢,彈性不足。

在一開始就把所有規劃做完,也會讓系統在發現新問題時很難動態調整。一些代理最後會往適得其反的方向前進,直到下一次迴圈才有機會自我修正。

連續執行器

下一版移除了獨立的規劃器。

這個執行器現在除了負責啟動任務之外,也會規劃如何達成目標。由於它是唯一的代理,因此不需要把計畫寫到任何地方、不必拘泥於一個固定不變的計畫,也不必硬等所有工作者。

確保最新狀態

為了確保各個角色的代理不會在長時間執行中逐漸偏離原本設定,我們引入了「新鮮度」機制:

  1. 應該經常重寫 scratchpad.md,而不是不斷往後附加內容。
  2. 個別代理在達到上下文限制時應自動進行摘要。
  3. 我們在系統提示詞中加入了自我反思與對齊目標的提醒。
  4. 鼓勵代理隨時調整方向並質疑既有假設。

整個系統因此變得高度動態且具彈性:它可以主動探索程式碼、重新審視決策、管理 workers、交錯處理任務,並持續反映最新資訊。我們發現代理在依照指示執行至完成方面表現相當不錯,於是移除了 judge,以保持系統簡潔。

連續 Executor 示意圖,顯示帶有 Workers 的無限 Executor 迴圈連續 Executor 示意圖,顯示帶有 Workers 的無限 Executor 迴圈

病態行為

儘管做了這些改進,continuous executor 仍開始出現病態行為。它會隨機休眠、停止執行代理、把工作自己做掉、拒絕規劃,只產生少數幾個過於狹隘的任務、無法正確合併 worker 的變更,還會過早宣稱任務已完成。

我們發現它同時被賦予太多角色與目標,包括:規劃、探索、研究、產生任務、檢查 worker 狀態、審查程式碼、進行編輯、合併輸出,以及判斷循環是否已結束。事後想想,它會不堪負荷也就不難理解了。

最終系統設計

最終設計融合了我們所有的心得與經驗:

  1. 一個 root planner 掌握使用者指示的整體範圍。它負責理解目前狀態,並產出明確、具體的任務,讓系統能朝目標推進。它本身不會寫任何程式碼,也不會知道自己的任務是否被接手,或是由誰來執行。
  2. 當某個 planner 覺得自己的範圍可以再細分時,它會產生 subplanner,讓它們完全負責被委派的較小範圍,並以類似的方式對該範圍負全責。這個過程是遞迴的。
  3. Worker 會接下任務,並全權負責將任務推進到完成。他們不了解更大的整體系統,也不會與其他任何 planner 或 worker 溝通。他們在自己的一份 repo 副本上工作,完成後會寫出一份交接說明,系統會將其提交給請求該任務的 planner。

有趣的是,這的確反映了某些現代軟體團隊的實際運作方式。

顯示遞迴規劃器、次級規劃器、Worker 和 git 的最終系統設計顯示遞迴規劃器、次級規劃器、Worker 和 git 的最終系統設計

次級規劃器透過快速擴散 Worker 來提升吞吐量,同時確保整個系統仍然由某個代理全權擁有並負責。這也特別有助於處理大型專案和任務,否則單一規劃器很容易不堪負荷並產生視野狹隘的問題。

交接內容不僅包含已完成的事項,還包括重要的備註、疑慮、偏差、發現、想法和回饋。規劃器會將這些作為後續訊息接收。這讓系統保持持續運轉:即使某個規劃器「完成」了,它仍然會持續收到更新、拉取最新的程式庫,並可以持續規劃與做出後續決策。

所有代理都有這套機制,這讓系統能夠保持高度動態並自我收斂,將資訊沿著鏈路向上傳遞給視野愈來愈全局的負責代理,而不需要承擔全域同步或橫向溝通的額外負擔。

移除整合器

我們一開始加入一個整合器,是為了進行集中式、具全域視野的品質控管,並避免太多 worker 同時嘗試 push、rebase、解決衝突與 merge 時產生的資源爭用。

但它很快就成了明顯的瓶頸:有數百個 worker,卻只有一個所有工作都必須通過的閘口(也就是所謂的「官僚手續」)。我們曾嘗試修改提示詞,但最終判斷這個整合器其實是多餘的,可以移除來簡化整體系統。

吞吐量與取捨

系統在一週期間的峰值約為每小時 1,000 次 commit,橫跨 1,000 萬次工具呼叫。系統一旦啟動後,就不再需要我們的任何干預。

為了達成這樣的吞吐量,我們在設計上做出了刻意的取捨。

提交正確性

當我們要求每一次提交之前都必須達到 100% 的正確性時,會導致嚴重的串行化,並大幅拖慢有效吞吐量。即使只是一個很小的錯誤,例如 API 變更或拼字錯誤,也會讓整個系統幾乎停擺。工作程序會超出自己的職責範圍,開始修補不相干的東西。許多代理會一窩蜂湧上來、互相踩踏,試圖修正同一個問題。

這種行為既沒幫助也非必要。允許一點彈性,表示代理可以信任其他問題會很快被其他代理修好——在系統對整個程式碼庫有有效的所有權與委派機制時,這確實成立。錯誤會出現,但也會很快被修復。錯誤率維持在小而穩定的水準,或許很少能完全「零錯誤」,但仍然平穩且可管理,不會飆升或持續惡化。

這可能說明,理想且高效的系統會接受某種錯誤率,但仍需要一個最終的「綠色」分支,由某個代理定期建立快照,並在發佈前進行快速修補檢查。

同步開銷

有時候多個代理會同時修改同一個檔案或重構同一段程式碼。與其試圖完全消除這種情況或為了解決它而過度設計,我們選擇接受短暫的波動,讓系統在短時間內自然收斂並穩定下來。

這會多花一些額外的 Token,並在局部造成資源競爭,但能讓整體系統保持更簡單:更容易讓模型保持一致且不被壓垮、更容易管理與觀察、更少摩擦、更高的整體生產力。同時也避免了過於複雜的做法。

基礎設施經驗

每次多代理執行都在一台具備充足系統資源的大型主機上進行,以避免過早引入分散式系統的複雜度。這樣的做法相當合適,因為大多數執行在高峰時會有數百個代理,通常會吃滿但不會過度超載這些機器。這種架構也讓觀察系統指標,以及在必要時共享和複製狀態變得更容易。

在限制代理的 RAM 使用後,磁碟成了新的瓶頸。特別是在單體式專案中,數百個代理同時進行編譯,會對建置產物產生數 GB/s 的讀寫量。這對整個測試框架的吞吐量產生了顯著影響,也帶來了一個有趣的教訓:專案結構、架構決策與開發者體驗都會影響 token 與提交的吞吐量,因為實際上是與程式碼庫互動(例如編譯)主導了時間,而不是理想中的思考與撰寫程式。

一般開發環境本身也存在一些限制與效率問題:對單一使用者工作空間來說合理或不明顯的事情,當在同一台機器上有數百個代理做著相同的事時,就會變得格外突出。一個看似簡單的解法是讓每個代理都擁有自己的機器。但只要重新思考與重新設計其中一些基礎元件與工具,就有機會以相對簡單的方式獲得非常可觀的效率提升。

例如,許多像 Git 和 Cargo 這樣的工具使用共享鎖,主要作為簡單的併發控制機制。若引入來自資料庫等併發系統中成熟的機制,是否能讓這些工具在多代理系統中同樣運作良好?所有代理各自持有一份 repo,但大多數檔案與產物其實是相同的;若加入簡單的寫時複製(copy-on-write)與重複資料刪除(deduplication)等功能——這些在更先進的生產級儲存系統中已很常見——是否能在不另建一套基礎設施的情況下,為典型的「單一使用者」系統帶來同樣容易取得的效益?

向代理清楚說明意圖

給這個多代理系統的指令非常重要。

一開始,我們並沒有把設計指令當成首要目標,而是優先確保 harness 穩定且有效。但指令本身的重要性很快就變得明顯。我們本質上是在與一個典型的程式設計代理互動,只是擁有多出數個數量級的時間與運算資源。這會放大一切,包括不夠理想或不清楚的指令。

在初始指令上投入更多時間和心力是合理的。代理終究是代理(agents):被訓練成嚴格遵守你的指令,沿著那些路徑走,不會變更或推翻它們,就算那些指令很糟也一樣。

我們希望在研究專案中看到成功,因此隨著專案和 harness 的演進,我們也調整了初始指令。我們一邊學習如何打造瀏覽器,一邊學習如何操作這個新的多代理系統,並且可以看到規格若很差或定義不清,會直接反映在輸出品質上,而這並不是 harness 本身的問題。harness 只是精確地遵照我們的指令。

以下是瀏覽器專案中的一些例子:

  • 一開始,指令著重在實作規格和修 bug。像「spec implementation」這種指令太模糊,導致代理會深挖一些冷門、幾乎不會用到的功能,而不是聰明地排定優先順序。
  • 我們在心裡假定效能會保持在對使用者友善的範圍內。但實際上必須加入明確的指令並強制 timeout,才能迫使代理在效能與其他目標之間取得平衡。
  • 對於系統中較複雜的部分,代理可能會寫出有記憶體洩漏或造成死鎖的程式碼。人類會發現這些問題,但對代理來說就不一定顯而易見。必須引入明確的、以流程為基礎的資源管理工具,才能讓系統在故障時優雅復原,並更具防禦性。

我們第一版不支援 JavaScript 的簡易瀏覽器,最後收斂出的架構並不適合進一步演進成一個完整的瀏覽器。這是初始規格的失敗。

同樣地,儘管代理被告知這個專案是從零開始做一個瀏覽器,它們仍然拉入了一些原本可以自己實作的相依套件,或是本可以在正式實作進行時只當作暫時支架使用的套件。這是指令上的疏忽。後來的一次執行中,我們明確說明了相依性的原則,以及哪些函式庫絕對不能使用,才修正了這點。

那次後來的執行也對程式結構做了大規模重構,拆成許多自包含的 crates,而不是維持單體架構。當時整個 repo 處於高度破碎的狀態,但多代理系統在幾天內就收斂出可運作的程式碼。這顯示出系統具有很強的能力,能夠協作且具智慧地工作,面對完全破碎的狀態仍能往好的方向收斂,而不是持續惡化或卡死。那次執行在等待編譯上的時間也少了許多,實際吞吐量是先前的數倍。

架構和指令都很重要。代理具備極強的工程能力,但會一路忠實地執行指令,不論好壞。要在過度狹隘的評估指標和完全無結構的自由之間取得平衡並不容易,同樣難的是判斷哪些事情是「顯而易見」,哪些則必須明確寫進指令裡。

所有這些都指出了一件事:在這個規模下,引出、具體化並理解意圖變得更加重要。可引導性(steerability)和可觀測性(observability)將會是值得持續探索的有趣研究領域。

最佳化提示詞

在整個演進過程中,提示詞是非常關鍵的一部分。

我們發現,與其去指示模型已經會做的事情,不如只交代它不熟悉的部分(例如多代理協作 multi-agent collaboration),或是與當前領域高度相關的細節(例如如何執行測試、你的部署 pipeline)。把模型當成一位聰明絕頂的新進工程師:它懂工程,但不熟悉你特定的程式碼庫與流程。

限制條件通常比指令更有效。「不得留下 TODO、不得有部分實作」會比「記得完成所有實作」來得好用。模型在預設情況下通常會做對的事情,而限制條件是在幫你劃出它們的邊界。

在較高層次或更深入的任務上,避免「打勾清單」式的心態。你可以詳細說明你的意圖,但要記得,列出太多具體要做的事項,會讓模型把重心放在完成那些條目,而忽略更大的整體範圍。你同時也會隱性地把沒寫出來的事情往後排。多數情況下,讓模型發揮自己的判斷力和自主性會比較好。

我們也發現,在談到工作範圍的大小或數量時,給出具體數字或範圍很有幫助。像是「產生很多任務」這種指令,往往只會得到少量的輸出:保守的預設值、求穩、技術上依然符合指示。「產生 20–100 個任務」則能傳達你的意圖是更大的範圍、要有企圖心,我們也觀察到明顯不同、更加廣泛的行為。

系統設計心得

我們從研究中建立了一些原則:

  1. 系統必須具備反脆弱性。 當我們擴大同時執行的代理數量時,失敗的機率也會提高。我們的系統需要能承受單一代理失敗,並允許其他代理協助復原或嘗試替代方案。
  2. 以實證為主,而不是以假設驅動。 我們希望透過數據與觀察來做調整,而不是一開始就帶著「它應該如何運作」的假設,去模仿人類組織或既有的系統設計。
  3. 明確為吞吐量而設計。 這表示在程式開發的其他面向上做取捨,例如接受一個小但穩定的錯誤率,並在最後再做一次統一核對,而不是追求 100% 完美運作的程式碼,因為那會大幅拖慢整個系統。

這類系統在設計得當時,往往會非常簡潔優雅,但在我們探索了許多不同方法之前,並不清楚哪一種簡單方法真正有效。目前的系統設計在極低額外開銷下穩定運行,並能以實用的方式在 Token 吞吐量上達到線性擴充。在這個 harness 上,之後不太需要再做重大迭代。

結論

雖然品味、判斷與方向來自人類,但 AI(人工智慧)在快速迭代與推進這項研究上,是極大的效能倍增器。

這與所謂的「良性」AI 循環有些相似:使用 AI 來開發 AI,而隨著模型、代理與測試框架不斷精進,它會反饋到自身,使迭代加速得愈來愈快。我們塑造工具,而工具也塑造我們。

這項研究在某種程度上,也以一種頗具詩意的方式呼應了部分現代軟體團隊的運作模式。這些模型並未以這種方式明確訓練,這暗示這是一種湧現行為,並且或許確實是組織軟體專案的正確方式。

我們會持續研究可極長時間運行的代理,並讓研究成果回饋到未來產品的設計中。

歸檔於: 研究

作者: Wilson Lin

邁向自動駕駛的程式碼庫 · Cursor