本文介紹 Onyx,我們用於可程式化 Agent 編排的虛擬機。進一步來說,這是一個將編排轉化為軟體工程的執行環境。閱讀完本文後,你將了解建構此虛擬機所涉及的約束與設計決策,以及如何建立你自己的程式並架構你的 Agent 系統。
簡介
Agent 本質上是不確定的。這正是其重點所在。如果你想要確定性,你應該直接寫軟體。
但在這過程中,每個使用 Agent 的人都希望將它們推向極致。我們學到,將執行過程分解為結構化步驟有助於提升效能:規劃、實作、審查、品質保證等。接著,我們似乎達成共識,要編寫腳本、工具和技能來引導每個 Agent,在它們之間共享上下文,並為它們設置護欄。然後,我們透過在 Agent 之間傳遞文字來拼湊這些腳本,而因為我們只是傳遞文字,所以某種程度上是可行的。
如果你在這個問題上投入足夠時間且特別聰明,你可能已經想出如何從系統中獲得保證,以便能根據給定狀態進行條件式執行。而且你可能會將該狀態儲存在一個或多個可解析的標記檔案中,以引導你的 bash 腳本。你甚至可能為你的 Agent 建立了一個自訂的 CLI。
作為工程師,我們對此很熟悉,我們在做軟體工程時會使用腳本。然而,現代軟體並非透過串聯 bash 腳本和 CLI 工具來建構。相反地,我們有程式語言、執行環境和工具鏈來幫助我們工程化我們的系統。我們使用程式語言來編寫軟體,因為它們附帶標準函式庫、清晰的語意,以及我們可以信賴的執行模型。它們擁有豐富的生態系統,配備滿足我們所有需求的工具鏈。
它們為我們的系統提供的保證,讓我們能夠在更高的抽象層次上進行推理。
但對於工程化 Agent 系統,卻沒有相對應的方案。為了建構系統,Agent 編排需要像現代軟體一樣可程式化。
今天,我們將介紹 PROGRAMS(*.program.ts)的規範,以及 Onyx,我們專為確定性 Agent 編排打造的虛擬機。這篇文章探討了 Agent 編排的歷史、能夠執行程式的虛擬機的靜態與執行時期語意,以及這對該領域未來發展的影響。
聽起來很昂貴,但實際上並非如此。我將在文章後半部分解釋。
對於好奇的人來說,這是 Andrej Karpathy 的 Autoresearch 作為一個程式的樣子:

Agent 編排中未解決的問題
要理解 Agent 編排的執行環境應包含什麼,我們需要先了解 Agent 的限制。
一個 LLM Agent 可以被視為一個 JSON 串流產生器,饋入解析器,然後在一個循環中將工具呼叫分派給環境。
每個工具呼叫都有完全相同的外部結構,但這個輸出串流的內容是不確定的。
確定性與不確定性的結合,正是 Agent 如此有價值的原因。它們足夠靈活,能以獨特的方式串聯一系列動作,但又足夠確定,能透過工具呼叫與電腦互動。
如果你願意放棄該串流內容必須是型別化的要求,那麼可組合性幾乎是免費的。模型已經足夠好,可以在我們提供的軌道(提示詞、訊息和工具呼叫)內進行文字的輸入與輸出。
這暴露了一個非常可組合的介面:文字
文字是一個通用介面。電腦上的所有東西都可以序列化為文字,即使只是機器碼。如果你能讓 LLM 透過這個通用介面輸入和輸出文字,你就能獲得文字串流上的可組合性。
這意味著你的Agent 行為的可靠性,直接與模型輸出的一致性相關。高輸出變異性意味著更不穩定的 Agent 行為。
一旦你有了一個介面來組合各個部分,下一個你需要關心的約束就是可引導性:
你希望 Agent 做什麼,以及你如何持續地讓它做你想做的事
我們透過改變模型取樣的分布來引導 Agent,換句話說,就是提示工程。
2022 年,ReAct 問世,從根本上開創了 Agent 的可引導性。事實上,我們可以說它讓我們所知的 Agent 成為現實。在採取下一步之前,對工具輸出進行思考和推理,是保持循環連貫性的關鍵。

我們仍然需要更聰明的 Agent。測試時間計算擴展的應用,由 @OpenAI 的 O 系列模型實現量產,讓模型實驗室能夠內建更好的 Agent 行為 [[11]](http://localhost:5173/blog/onyx#ref-11)。在呼叫工具之前輸出更多 token,允許模型逃離如果其推理輸出長度受限時會陷入的輸出分布。你可以選擇訓練模型如何在其輸出分布景觀中移動,因此可以自由地在你關心的任務上訓練更清晰的 Agent 行為。
隨著上下文長度無限制增長,引導 Agent 變得困難,任務完成的可能性也降低。即使使用推理模型,也無法保證能恢復,Agent 可能會就此終止。Agent 可能達到其上下文限制、提前宣告完成、陷入循環等。
從非確定性系統中提取保證
對此的解決方案多種多樣,但有一個特別突出:Ralph 循環,由 @GeoffreyHuntley 提出 [[3]](http://localhost:5173/blog/onyx#ref-3)。
他引入了這樣的想法:你可以限制 Agent 的執行,然後利用這些限制來推理任務完成情況。這使得 Ralph 循環能夠做到神奇的事情:它在一個非確定性系統中提供了你可以信賴的東西。
一線確定性的曙光。
與其再拉一次吃角子老虎機的拉桿,不如保證失敗並逐步趨向正確。這個定義好的邊界給了你具體的東西來推理,一旦你能推理某事物的邊界,你就可以用它來建立一個系統。
對抗上下文長度的限制
但有一個問題:一個全新的 Agent 會在多次運行之間失去連貫性,但一個單一的 Agent 在足夠時間後又會耗盡上下文。
RLM 由 @lateinteraction 和 @a1zhang 提出。RLM 為我們提供了一個概念,說明如何以結構化方式與長上下文(即 Agent 運行)互動 [[4]](http://localhost:5173/blog/onyx#ref-4)。RLM 的靈感來自 CodeAct,這是一篇 2024 年的論文,展示了使用程式碼來編排操作 [[5]](http://localhost:5173/blog/onyx#ref-5)。Agent 編寫腳本,在 REPL 中編排操作以取得輸出。RLM 以相同方式運作,但額外使用變數來儲存上下文並對該上下文進行操作。它還允許在 REPL 中進行遞迴的 LLM 呼叫。你可能會失去其他循環所具有的反應性,但你獲得了以程式化方式處理上下文的能力。這裡的關鍵是 REPL 中的腳本是短暫的。你得到了一個腳本執行環境和上下文管理,但沒有可重用性或可組合性。只需編寫腳本、執行它,然後它就消失了。就建構系統而言,這嚴格來說比僅僅使用 bash 腳本將 Agent 和 Markdown 檔案串聯起來更差,因為你失去了持久性和有界執行。
從單一循環到擴展編排
OpenAI 的 Deep Research[[6]](http://localhost:5173/blog/onyx#ref-6) 是最早的確定性工作流程範例之一,它有一個通用的執行形狀或結構,每次運行的變異性很小。它的運作方式是規劃一批查詢,在網路上執行它們,審查結果,然後規劃下一批查詢。每一批都更深入地探入問題空間。

Cursor 將確定性的概念推得更遠,當時 @wilsonzlin 展示了一個用於編排 Agent 以建構瀏覽器的框架。他建立了一個客製化的框架,使用並行規劃 Agent 和任務 Agent 來協調大量工作 [[7]](http://localhost:5173/blog/onyx#ref-7)。這裡相關的是,框架各部分之間的關係是固定的。有規劃者,負責探索當前系統狀態並產生任務;有執行者,負責接收任務並並行實作。Agent 之間有固定的護欄,以及固定的資訊溝通管道。要做好協調,你需要在介面上有保證。
使用終止條件進行有界執行
五月,Codex 引入了目標(goal)的概念,它使用一個驗證器循環,針對某個期望的最終狀態進行爬山,直到任務完成。你可以將其視為 Ralph 循環的生產就緒版本,內建於 Codex 中。它允許你指定一個目標,並有一個自動化的循環來執行和審查。

Karpathy 的 Autoresearch[[9]](http://localhost:5173/blog/onyx#ref-9) 類似於 Codex 的 /goal 和 Ralph 循環。它將目標的可驗證終止條件與 Ralph 循環在迭代上的執行邊界結合起來,使其能夠持續朝目標前進。它透過搜尋想法空間,隨著時間迭代改進來取得進展。

到目前為止,所有將編排外部化到 Agent 之外的解決方案,其執行圖形形狀都是固定的。它們使用手寫的模式運行,並且對其允許運作的形狀有某種結構。它們不會根據任務進行調整,或者根本無法對執行圖形形狀提供強有力的保證。
讓編排變得靈活
今年三月,我們推出了 Slate,這是第一個使用程式碼以 RLM 風格進行即時子 Agent 編排的程式碼 Agent。它仍然是唯一一個廣泛使用、利用程式碼進行即時 Agent 編排的程式碼 Agent。在 Slate 中,執行緒可以即時地被產生、暫停、恢復和引導。主 Agent 深刻理解如何編排所有正在運行的子 Agent,因此你無需操心。然而,與 RLM 類似,我們仍然面臨著在子 Agent 之間共享狀態以及短暫腳本編寫的挑戰,這是使用 bash 腳本和 Markdown 檔案時不會遇到的問題。
即便如此,如果模型負責編排,你該如何引導它?你要告訴它以特定方式編寫其編排程式碼嗎?你要怎麼做?
我們最初的解決方案(在我們發布 Onyx 執行環境之前的一個修補程式)被稱為編排技能 [[13]](http://localhost:5173/blog/onyx#ref-13)。想法很簡單:允許使用者提供一個技能來引導 Agent 如何處理其編排。就這樣。它運作得還可以,但有很多問題。
也就是說,技能不是一個具有約束力的行為合約。你無法從文字中獲得保證。
這意味著編排器不必遵循期望的執行模式,因為沒有真正的方法來強制執行它。Onyx 執行環境最大的好處之一就是我們解決了這個問題。
上述提到的系統中,沒有一個具有具有約束力的行為合約。
那麼,如果 Agent 可以為每個任務將其編排程式碼寫入一個腳本,從而使執行圖形固定下來呢?這就是 Claude 動態工作流程 的做法。[[10]]([http://localhost:5173/blog/onyx#ref-10)[[12]](http://localhost:5173/blog/onyx#ref-12](http://localhost:5173/blog/onyx#ref-10)[[12]](http://localhost:5173/blog/onyx#ref-12) 與 RLM 和 Slate 類似,透過編寫程式碼來編排子 Agent,動態工作流程允許 Claude 編寫和儲存工作流程形狀。這與 /loop 結合,能夠循環處理特定模式。它為一組 Agent 的行為提供了一個宣告式合約。這仍然不同於編寫軟體,因為它缺乏功能組合之類的東西,但你獲得了持久性,以及對任務將如何執行的強有力保證。它們是針對特定任務動態編寫的臨時工作流程腳本。[[12]](http://localhost:5173/blog/onyx#ref-12) 而且由於它們被持久化到磁碟,它們還有一個額外的好處:它們可以重新運行,並用編排膠水(如 /loop)包裹起來。

如果你注意到了,所有上述解決方案都在追求同一件事:一種確定性的方式來控制 Agent 如何隨著時間執行。
這是我們在軟體工程領域已經見證過的故事。我們從將不同的系統和腳本任務黏合在一起開始,然後我們的語言變得更加靈活和強大。我們在工程過程中獲得了越來越多的槓桿作用,更強大的生態系統使我們能夠在更高的抽象層次上建構更可靠的系統。
現在,Agent 正處於相同的軌跡上,今天,我們將發布這條軌跡上的下一步,讓你能夠工程化運行你 Agent 的系統。程式語言通常使用直譯器或虛擬機來自動排程資源。這就是你作為使用該語言的工程師獲得槓桿作用的方式。
如果要讓一個虛擬機對 Agent 編排有意義,你需要以下幾點:
- 持久化狀態管理:我們應該能夠定義狀態、按名稱引用它、持久化它,並以程式化方式操作它。
- 型別保證:我們應該尊重定義好的輸入和輸出形狀並遵循它們,並且能夠信賴它們。
- 控制流程原語:最好是 LLM 能理解的、廣為人知的原語。
- 清晰的錯誤處理結構(例如 try-catch)。
- 資源管理:對資源(如 Agent 並行度、成本、正在運行的模型等)的定義控制。
- 執行隔離:除非明確共享狀態,否則一個正在運行的 Agent 或程式應與另一個隔離。
- 生命週期控制:Agent 程式的樣貌,以及運行、取消和引導的語意。沒有這個,你就沒有清晰的清理路徑,也無法控制生命週期管理。
- 可組合性:程式應該能夠相互組合,並且應該能夠使用定義好的輸入和輸出型別進行呼叫。
- 可視性:我們應該能夠知道什麼時候運行了什麼,並且應該能夠追溯原始碼中的執行失敗。
- 持久性:我們應該有一個清晰的模型來說明如何從崩潰中恢復和恢復執行。
所有這些都是程式語言在幾十年前就已經解決的問題。Agent 編排只是第一次再次遇到它們。
要真正能夠為此編寫軟體,一個 "program.ts" 程式必須在支援上述所有功能的執行環境中編寫,這樣我們才能推理當程式不工作時會發生什麼,並圍繞失敗進行工程設計。
這就是我們建構 Onyx 的原因。它是一個 Agent 編排虛擬機,專門設計用於同時支援持久化的可組合程式和一個直譯的腳本層。以下是它的運作方式,以及一個 "program.ts" 相容的執行環境需要支援什麼。
設計執行環境
當我們為一種語言及其執行環境進行設計時,我們需要思考我們希望能夠推理的約束,以及我們關心什麼是易於表達的。然後我們可以將產生的語意分為兩類:靜態語意和執行時期語意。
靜態語意是指僅通過查看程式就能推斷出的所有事物。編譯器或型別檢查器對給定程式所了解的事物。
執行時期語意定義了程式碼的實際含義以及程式實際運行的方式。這包括底層的資源分配和排程機制。
我們為 Agent 建立執行環境的目標是將編排控制流程轉化為程式碼,並且我們希望使執行狀態持久化且型別化,以便我們能夠可靠地使用它來引導編排。
一些虛擬機需求
除了像正常的 TypeScript 執行之外,我們還關心 3 個虛擬機特定的事項。
- 作為一個 Agent 編排執行環境,它需要能夠編排 Agent。這意味著建立它們、追蹤它們的生命週期等。我們希望執行環境能夠以阻塞或非阻塞的方式運行它們,並正確地排程它們。
- 我們希望控制 Agent 的輸出形狀,並希望有嚴格的輸出合約執行。
- 我們希望對外部資源(如模型和成本)擁有執行時期控制。
運行 Agent 和程式
為了運行一個 Agent,我們選擇了兩個基本動詞:run 和 spawn。Run 在前台運行一個阻塞的 Agent。Spawn 在後台運行一個 Agent。這與對 spawn(如 posix_spawn)的普遍理解一致,使得模型易於理解我們的新動詞,因為它們在概念上存在於訓練資料中。Spawn 和 run 允許你直接調用從磁碟讀取的 Agent 和程式,並返回足夠的資訊作為執行控制代碼。
Run 還支援一些功能。它透過 zod @colinhacks 支援直接強制執行輸出型別,並且支援直接模型覆蓋,使得編寫和運行需要分散到多個不同模型以獲得不同解決方案或任務不同步驟的程式變得容易。
1function run<S extends z.ZodType>(2 name: string,3 options: ...4): Promise<z.infer<S>>
Run 允許你直接內聯串聯子 Agent。
1// 簡單的 Agent 運行2const out = await run({ type: "read", prompt: () => "Reply with: ok" })3// 命名運行(字串 = 子工作流程 ID)4const review = await run("reviewer", {5 type: "general",6 prompt: () => "Review the diff",7})8// 結構化輸出(型別化結果)9const Verdict = z.object({ risk: z.enum(["low", "high"]), why: z.string() })10const v = await run({11 type: "general",12 prompt: () => "Assess risk",13 output: Verdict,14})
Spawn 與 run 類似,但在後台建立一個 Agent。被 Spawn 的子 Agent 不會被 await,控制流程會直接繼續。Spawn 對於啟動多個非阻塞執行 Agent 非常有用。
1// 後台 Agent2const h = await spawn("worker", { type: "general", prompt: "Long task" })
與正在運行的 Agent 互動
我們希望能夠對正在運行的 Agent 執行兩種類型的操作:引導和停止。
引導訊息是在 Agent 運行時發送給它的訊息,LLM 會接收該訊息以將其推向某個方向。這對於更新 Agent 的任務上下文很有用,而無需拆除工作進程。
取消也很重要,我們希望能夠在子 Agent 不應該運行時主動將其拆除。
能夠從即時 REPL 和預先編寫的程式中運行這些操作,賦予了 Slate 即時編排一切的能力。它可以在執行時期動態定義編排的形狀,或者它可以編寫並迭代真正的軟體來進行編排。
Slate 能夠將程式寫入 \.program.ts 檔案。一個程式檔案包含以下內容:其名稱(Slate 透過此名稱知道它是什麼)、一個 JSDoc 描述,以及實際的程式主體*。一個程式宣告看起來像這樣:
1program(async (ctx) => {2 // 用於搜尋的廉價模型 — 它只需要找到檔案3 const findings = await run("search", {4 type: "read",5 prompt: "Find all authentication-related files",6 model: "codex/gpt-4.1-mini", // 使用你內建的 codex 金鑰7 })8})
程式遵循相同的非同步執行模型,這允許我們在前台和後台運行一個程式,並在它運行時與之互動。
1// 後台 Agent2const h = await spawn("worker", { type: "general", prompt: "Long task" })3await h.notify("focus on the parser first") // 向正在運行的 Agent 發送引導訊息4const result = await h.result() // 稍後等待完成5// 分散,然後匯聚6const a = await spawn({ prompt: "task A" })7const b = await spawn({ prompt: "task B" })8const [ra, rb] = [await a.result(), await b.result()]9// 在後台運行一個程式10import Audit from "deep-audit"11const ah = await spawn(Audit, { input: { pr: 42 } })12const auditResult = await ah.result()
結構化輸出與狀態
這是迄今為止所有其他系統的主要限制。在所有的其他系統中,狀態的外部化很差,並且沒有安全地隔離。如果它是系統上的一個檔案,你無法保證沒有損壞。即使你能保證,你也無法保證可解析性。你無法訂閱狀態變更來驅動操作,也無法保證型別一致性。
還記得我們希望擁有持久化、結構化且可引用的狀態嗎?
在 Onyx 中,狀態是不同的。狀態命名空間是宣告的,直接命名並隨時間持久化。這意味著一個狀態儲存可以一次又一次地被重用,允許你使用真實資料建構長時間運行的 Agent 系統。
Agent 和程式碼都會讀取狀態,我們從執行環境中期望的確定性也因此產生。Agent 透過一個專用工具讀取狀態,該工具允許它們始終以安全結構化的方式與狀態互動。Agent 和程式都是可以被引導以修改狀態的消費者,這使得執行環境能夠依賴狀態物件來驅動編排。
狀態和結構遵循,是子 Agent 完成的閘門。因此,狀態為引導整個程式提供了一個統一的表面。
狀態物件也可以作為執行時期變數傳遞到與主 Agent 共享的子會話中。這種在整個 Agent 層級結構中透過引用傳遞的存取方式(這本身就是首創),允許透過共享狀態通道進行跨 Agent 通訊。

長時間運行的循環
有些程式需要更像運行中的系統那樣運作。以 Openclaw 為例。給定正確的原語,你實際上可以將 Openclaw 表示為一個程式。為此,我們使用兩個原語:sleep 和 checkpoint。
Sleep 如其名,就是休眠。
現在問題來了,假設你想要在後台進行一些長時間運行的任務管理。一個預先定義的執行圖形可能會卡住或中斷,因此主 Agent 能夠感知程式的狀態非常重要。
為了支援這一點,我們引入了 checkpoint 原語。
一個 checkpoint 可以是任何東西,但它被命名為 checkpoint 的原因是它會用一個固定形狀的物件通知主 Agent。這允許主 Agent 追蹤任務進度等事項,並直接收到程式狀態變更的通知。反過來,主 Agent 可以更有效地管理正在運行的程式。
Onyx 支援製作像 Openclaw 一樣的 Agent 循環,即一個具有心跳的持久化 Agent。
這其實真的很酷,你可以只透過 while 迴圈、睡眠和檢查點,將這些原語組合成一個完全不同類型的 Agent。
Openclaw 可以簡單地表示為一個程式檔案!
1// 一個用於執行長時間自動研究風格迴圈的程式2for (let i = 0; i < maxExperiments; i++) {3 const idea = await run("propose", { ... })4 const result = await run("train", { ... })5 checkpoint({ message: `實驗 ${i}`, data: { idea, result } })6 await sleep(30_000) // 實驗之間冷卻7}89// 一個用於執行 openclaw 風格持久化 Agent 的程式10while(true) {11 const status = await run("status_check", { ...insert cheap model here... })if(status.pending_tasks) {checkpoint({ tasks: status.pending_tasks }) // 回傳重要狀態並喚醒主 Agent}12 await sleep(30_000) // 實驗之間冷卻13}
組合能力
透過 Onyx,Slate 可以為你撰寫一個 *.program.ts。這個檔案是持久化的,並且可以(也應該)像一般程式碼一樣被對待。它內建了型別系統,在一個移除了執行時期全域變數的環境中運行,而且它本質上就是 TypeScript,所以它的組合模型就是匯入並呼叫另一個程式。
正因為它只是 TypeScript,你可以免費獲得像是並行處理(Promise.all)和迴圈這樣的功能。
以下是你如何將一個程式匯入並在另一個程式中使用:
1import Audit from "deep-audit"program (() => {const ah = await spawn(Audit, { input: { pr: 42 } })2 const auditResult = await ah.result()3 const fixer = await run("fixer", ... audit output) // 這會執行並修復 audit 程式的輸出。4})
錯誤語意
錯誤,在理想的 VM 中,會大聲地拋出。這些錯誤應該發生在執行時期語法問題、Agent 失敗、崩潰等情況。
具體來說,我們將編排錯誤定義為:
- Agent 被任務阻塞
- Agent 未能完成任務
- Agent 在任務上耗盡了步驟或預算
- 程式在執行期間耗盡了預算
- 編排模型未能寫出語法正確的程式碼
- 進行了非法的狀態修改
所有這些特定的錯誤情況定義了執行時期的語意。它們的意思是:「你可以預期這個執行時期會拋出錯誤,因為我們看待 Agent 執行失敗的方式,與看待程式碼錯誤的方式相同」。一開始這可能看起來很煩人,但這種大聲的失敗機制會回報你一件事:一個明確的方式來準備並圍繞失敗進行程式設計。所以實際上,它賦予你更多的控制權,而非更少。
1// 錯誤是 try/catch — 與任何 TypeScript 程式相同2program(async (ctx) => {3 try {4 const result = await run("risky-refactor", {5 type: "general",6 prompt: "重構認證模組",7 model: "claude-sonnet",8 maxSteps: 20,9 })10 } catch (err) {11 // Agent 失敗了 — 但我們確切知道原因。12 // 追蹤記錄中有導致此結果的每一個工具呼叫、每一個模型請求、13 // 每一次狀態寫入。1415 // 用不同的模型重試16 const result = await run("risky-refactor-retry", {17 type: "general",18 prompt: `先前的嘗試失敗了:${err.message}。嘗試不同的方法。`,19 model: "claude-opus",20 maxSteps: 30,21 })22 }23})
模型選擇、預算執行與 BYOK
內建的模型選擇功能讓你擁有更精準的控制權。/models 技能賦予 Slate 完整存取可用模型清單的能力,讓 Slate 能夠編寫使用多種不同模型執行不同工作的程式。想要讓 Fable 擔任規劃者,但讓 GLM 5.2 在確定性框架內實作?沒問題。想要將一個問題分散給 Gemini、GPT 5.5 和 DeepSeek?也行得通。
此外,執行時期支援兩種程式設定覆蓋:
- 用於 Agent 執行的預設全域模型
- 運行程式的預算
你可以直接設定執行預算來限制特定迴圈的支出。
此外,執行時期支援使用你既有的 OpenAI 和 Github Copilot 訂閱。
1program(async (ctx) => {2 // 用於搜尋的便宜模型 — 它只需要找到檔案3 const findings = await run("search", {4 type: "read",5 prompt: "找出所有與認證相關的檔案",6 model: "codex/gpt-4.1-mini", // 使用你內建的 codex 金鑰7 })89 // 用於困難部分的推理模型 — 它需要思考10 const plan = await run("architect", {11 type: "general",12 prompt: `根據這些內容設計一個修復方案:${findings.output}`,13 model: "openai/o3", // 最終會使用 API 點數14 output: z.object({15 approach: z.string(),16 files: z.array(z.string()),17 risk: z.enum(["low", "medium", "high"]),18 }),19 })2021 // 用於實作的中階模型 — 它只需要編輯22 const handles = await Promise.all(23 plan.files.map(f => spawn("fix-" + f, {24 type: "general",25 prompt: `將此修復套用至 ${f}:${plan.approach}`,26 model: "anthropic/claude-sonnet-5",27 maxSteps: 15,28 }))29 )30 await Promise.all(handles.map(h => h.result()))31})
定義創作介面
在設計程式的創作介面時,有兩個主要考量:Agent 理解它的容易度,以及人類閱讀它的容易度。我們選擇了相對簡單、讀起來像英文的動詞,並明確決定要以程序式而非宣告式的方式來建模編排。
選擇 TypeScript 作為語言也很重要。市面上有太多程序式的 TypeScript 程式碼,使得模型即使在沒有後期訓練的情況下,也能隱含地理解 TypeScript 語意。

我們軟體工廠的工程元件
接下來要回答的問題是:這一切能為你帶來什麼好處?
它讓你得以為你的 Agent 編寫真正的軟體。你現在可以從頭到尾自行設計你的 Agent 編排流程。
你可以為這個工廠設計工程流程。
例如,你可以建立一個程式來持續監控 Github,以及另一個獨立的程式來運行一個實作 Agent 和一個 QA Agent 進行審查。這兩個都是你在實際應用中可能遇到的、各自有用的模式。然後,你可以將它們組合起來,形成一個能監聽 PR 留言、啟動實作者來處理這些留言、然後再啟動 QA Agent 來確保修復有效的系統。
接著,你可以將這個程式連接到任務佇列,來委派並監控你程式碼庫中的工作,並讓它自動回應 PR 留言。
而且,這一切都可使用快速、開放權重的模型來完成。因為它只是程式碼,在首次編寫完成後,你不需要一個強大的 LLM 來思考編排邏輯。
現在進入有趣的部分,是時候分享一些我們用來大幅提升產出的程式了。
深度程式碼庫研究
我們使用這個程式來幫助界定任務範圍。它對我們的單一儲存庫狀態進行深入研究,並為實作者準備一份研究資料包以供參考。我們經常使用它。聽起來可能很昂貴,但實際上並非如此。你可以在 Slate 中使用 DeepSeek V4 Flash 來運行這個程式,研究過程既徹底又極其便宜。

目標-審查-PR
這個程式是我們在研究完成後用來實作任務的。幸運的是,當研究結果送達目標程式時,大部分的任務模糊性都已被釐清,這使得執行任務更加迅速。使用輕量級的 OSS 模型預先進行研究,讓我們可以輕鬆地將昂貴的模型(如 Opus)用在最重要的事情上:撰寫真正優秀的程式碼並驗證系統狀態。你甚至可以修改程式,使用 GPT 5.5 來對抗性地審查 Opus 4.8 的工作成果。

自動研究作為一個程式
自動研究[[9]](http://localhost:5173/blog/onyx#ref-9) 最初完全是 LLM 驅動的。將一個 Agent 指向 program.md 提示,它就會決定要嘗試什麼以及如何推進。
毫不意外地,自動研究其實就是一個程式。
Agent 程式讓你反轉這個過程,將控制流放入執行時期。程式擁有控制流,而 Agent 則執行具有副作用的操作(編輯程式碼、執行 git、SSH 到遠端 GPU、訓練)。對於自動研究程式,保留/還原的決策是確定性的程式碼:
1kept = status === "ok" && valBpb != null && valBpb < best
在我們的案例中,這個程式會運行一個設定 Agent 來準備一個全新的儲存庫,並驗證遠端的 A100 是可連線的。如果設定失敗,它會根據一個型別化的值提前返回並乾淨地退出。否則,它會進入實驗迴圈。
每個實驗都會啟動一個全新的 Agent。這個 Agent 會被給予當前最佳配置以及先前想法和結果的歷史記錄,這樣它就不會重複自己,並且可以在保留的基礎上繼續發展。它會提出變更、編輯 train.py、提交、rsync 到遠端機器、進行訓練,並對結果進行分類。
Agent 和程式共享狀態。Agent 將資料寫入狀態,而程式則評估狀態以進行控制流。根據結果,一個記錄 Agent 會更新 results.tsv,並在程式決定丟棄該實驗時選擇性地重置運行。這使得 git HEAD 始終指向實驗樹當前最佳分支。
有兩個核心差異值得注意:1) 這在程式中執行,所以我們可以為每個實驗啟動一個全新的 Agent;2) 我們可以根據即時的程式狀態來決定 Agent 應該執行什麼任務。


它在程式碼中看起來像這樣:
1// ---------- 程式 ----------23program(async (ctx) => {4 const c = cfg(ctx.input)5 const total = ctx.input?.maxExperiments ?? 2067 const setup = await run("ar-setup", {8 prompt: setupPrompt(c),9 type: "general",10 maxSteps: 40,11 output: SetupResult,12 })13 if (!setup.ready) {14 return { aborted: true, reason: `設定失敗:${setup.note}`, setup }15 }1617 let best = c.baselineValBpb18 let bestCommit = setup.baselineCommit19 const history = []2021 for (let i = 1; i <= total; i++) {22 let exp23 try {24 exp = await run(`ar-exp-${i}`, {25 prompt: experimentPrompt(c, i, total, best, historyText(history)),26 type: "general",27 maxSteps: 80,28 output: ExperimentResult,29 })30 } catch (err) {31 // Agent 錯誤/阻塞 — 視為崩潰,將儲存庫恢復至最佳狀態,繼續執行。32 exp = {33 description: `實驗 ${i} Agent 錯誤`,34 commit: "error",35 status: "crash",36 valBpb: null,37 peakVramMb: null,38 numSteps: null,39 exitCode: -1,40 retries: 0,41 note: String(err?.message ?? err).slice(0, 200),42 }43 }4445 const kept = exp.status === "ok" && exp.valBpb != null && exp.valBpb < best4647 await run(`ar-record-${i}`, {48 prompt: recordPrompt(c, exp, kept, bestCommit),49 type: "general",50 maxSteps: 20,51 output: RecordResult,52 })5354 if (kept) {55 best = exp.valBpb56 bestCommit = exp.commit57 }5859 history.push({60 idx: i,61 description: exp.description,62 status: exp.status,63 valBpb: exp.valBpb,64 kept,65 commit: exp.commit,66 retries: exp.retries,67 })6869 await checkpoint({70 name: `experiment-${i}`,71 message: `exp ${i}/${total}: ${exp.status}${kept ? " 保留" : ""} val_bpb=${exp.valBpb ?? "n/a"} (最佳=${best})`,72 data: { i, total, status: exp.status, valBpb: exp.valBpb, kept, best, bestCommit },73 })74 }7576 const kepts = history.filter((h) => h.kept)77 return {78 baselineValBpb: c.baselineValBpb,79 bestValBpb: best,80 bestCommit,81 improvement: c.baselineValBpb - best,82 experimentsRun: history.length,83 kept: kepts.length,84 crashes: history.filter((h) => h.status === "crash").length,85 infraFails: history.filter((h) => h.status === "infra_fail").length,86 localRepo: c.localRepo,87 branch: c.branch,88 history,89 }90})
未來工作
我們尚未定義的唯一剩餘 VM 需求是程式的持久性模型。目前尚不清楚恢復和處理程式生命週期的正確模型是什麼,以及應該在執行時期上暴露多少控制權。
除此之外,我們將添加許多令人興奮的功能來支援不同的工作負載和任務型態,以便我們能夠編寫真正的軟體來更好地編排 Agent。我們確信,許多模式將來自人們以創造性的方式自行使用程式。
我們真誠地迫不及待想看到你們會做出什麼。
- RL 團隊
參考文獻
- Yao 等人, "ReAct: Synergizing Reasoning and Acting in Language Models," 2022
- Geoffrey Huntley, "The Ralph Loop"
- Geoffrey Huntley, "everything is a ralph loop," January 2026
- Zhang, Kraska, Khattab, "Recursive Language Models," December 2025
- Wang 等人, "Executable Code Actions Elicit Better LLM Agents," ICML 2024
- OpenAI, "Introducing Deep Research," February 2025
- Cursor, "Scaling Agents," January 2026
- OpenAI, "Using Goals in Codex"
- Andrej Karpathy, "autoresearch"
- Anthropic, "Introducing Dynamic Workflows in Claude Code"
- OpenAI, "Learning to Reason with LLMs," September 2024
- Anthropic, "A harness for every task: dynamic workflows in Claude Code"
- Random Labs, "Skill Chaining"





