研究

持續改進我們的 代理 工具鏈

Stefan Heule & Jediah Katz閱讀時間 2 分鐘

我們打造 Cursor 代理工具鏈的方式,就像打造任何一款有雄心的軟體產品一樣。其中很大一部分工作由願景驅動:我們先對理想的代理體驗應該是什麼樣貌形成看法。

接著,我們提出如何更接近這個願景的假設,執行實驗來測試這些假設,並根據來自 eval 與實際使用的量化和質化訊號持續迭代。這個過程仰賴合適的線上與離線監測機制,這樣我們才能判斷某項變更是否真的讓工具鏈變得更好。

當我們搶先體驗新模型時,這些方法都會匯聚在一起。我們會花上數週,針對模型的優勢與怪癖客製化工具鏈,直到同一個模型在我們特別調校過的工具鏈中,明顯變得更快、更聰明,也更有效率。

我們偶爾會發現能帶來躍進式提升的改進。但更常見的是,改進工具鏈是一個近乎執著地堆疊許多微小最佳化的過程,而這些最佳化加總起來,能讓代理更擅長打造軟體。

持續演進的上下文視窗

與大型語言模型互動的核心,就是上下文視窗。當要求代理打造某個東西時,上下文視窗會先包含系統提示詞和工具說明,接著是目前的對話狀態,最後才是使用者的請求。

Cursor 一路走來,我們填入及管理這個視窗的方式已經有了很大的演變。

當我們在 2024 年底首次開發 程式設計代理 時,模型還很不擅長自行選擇所需的上下文,因此我們投入了大量上下文工程工作來建立各種防護措施——例如,每次編輯後都將 lint 和型別錯誤提供給代理、當它要求讀取的行數太少時改寫其檔案讀取請求,甚至限制它在單一回合中可呼叫的工具數量上限。

我們也提供了大量靜態上下文,讓代理在每個工作階段一開始就能使用。在不同時期,這些內容包括程式碼庫的資料夾結構、在語意上與查詢相符的程式碼片段,以及使用者手動附加檔案的壓縮版本。

如今,這些做法大多已經消失了。

我們仍會包含一些實用的靜態上下文 (例如作業系統、git 狀態、目前和最近檢視過的檔案) 。但隨著模型能力提升,我們已逐步移除這些防護措施,並提供更多動態上下文,讓代理能在工作過程中自行擷取。在先前的一篇文章中,我們曾對部分動態上下文背後的技術做過深入解析,其中許多方法後來也被其他程式設計代理採用。我們現在的許多工作,都聚焦於提供更多方式,讓代理能動態擷取上下文並與外界互動。

有了動態上下文,模型就能自行決定何時將過往對話、使用中的終端機工作階段或相關工具等額外資訊納入上下文視窗。有了動態上下文,模型就能自行決定何時將過往對話、使用中的終端機工作階段或相關工具等額外資訊納入上下文視窗。

評估工具鏈變更的兩種方式

工具鏈和模型會共同決定代理的表現好壞,但「好」其實很難精準定義。為了掌握這件事,我們建立了多層次的衡量方式。

除了自有的 eval 套件之外,我們也維護公開基準測試 CursorBench,讓我們能快速且標準化地掌握品質,並比較不同時間點的表現。但即使是最佳的基準測試,也只能近似真實用量,這表示如果完全依賴它們,我們就會錯過重要訊號。

因此,我們也會進行線上實驗,同時部署兩種或更多工具鏈變體,並在真實使用情境中對它們做 A/B 測試。在這些測試中,我們會透過各種指標來衡量代理品質。有些指標很直接,例如延遲、token 效率、工具呼叫次數,以及快取命中率。這些指標對判斷方向很有幫助,但仍無法回答更模糊卻更重要的問題:代理是否真的把事情做好了。我們用兩種方式衡量這些問題。

第一種是代理產生程式碼的「Keep Rate」。對於代理提出的一組程式碼變更,我們會追蹤在固定時間間隔後,這些變更有多少比例仍保留在使用者的程式碼庫中。這讓我們能了解使用者何時必須手動調整代理輸出,或需要反覆嘗試並讓代理修正問題,這些都表示代理最初的回應品質較低。

第二,我們會使用語言模型讀取使用者對代理初始輸出的回應,從語意層面判斷使用者是否滿意。當使用者繼續進行下一個功能時,這是代理已完成任務的強烈訊號;而當使用者貼上堆疊追蹤時,則是代理未能完成任務的可靠訊號。

有時,這些線上測試會告訴我們該擱置某個看似很有潛力的想法。在一項實驗中,我們嘗試用成本更高的模型來做上下文摘要,結果發現它對代理品質的提升微乎其微,不值得增加這些成本。

追蹤並修正退化

隨著我們加入更多模型與能力,工具鏈就像任何軟體一樣,隨著潛在狀態增加而變得更複雜。這也意味著會有更多地方可能冒出錯誤,而其中許多只有在大規模運行時才偵測得到。

代理的工具是最容易出現錯誤的環節之一,而工具呼叫錯誤對 Cursor 中的工作階段可能造成極大影響。雖然代理通常能自行修正,但錯誤仍會留在上下文中,浪費 tokens,並造成「上下文腐化」 (context rot) ,也就是累積的錯誤會降低模型後續決策的品質。

有時候,代理甚至可能在工具呼叫失敗後卡住,或徹底偏離正軌。雖然工具呼叫量和錯誤率這類指標,並不能直接衡量代理是否表現良好,但它們可以作為訊號,幫助我們發現更廣泛的問題。

任何未知錯誤都代表工具鏈中存在 錯誤,我們也會據此處理。但許多錯誤是「可預期的」,例如模型偶爾提出不正確的編輯,或嘗試讀取不存在的檔案。我們會依成因對這些可預期錯誤進行分類。InvalidArgumentsUnexpectedEnvironment 用來標記模型失誤與上下文視窗中的矛盾,而 ProviderError 則用來標記來自 GenerateImageWebSearch 這類工具供應商的服務中斷。

我們還有其他幾種分類,例如 UserAbortedTimeout,合計涵蓋了大多數可預期錯誤。

在今年稍早一次聚焦衝刺中,我們將所有工具呼叫的可靠性提升到至少 2 個 9,而且通常可達 3 個 9。在今年稍早一次聚焦衝刺中,我們將所有工具呼叫的可靠性提升到至少 2 個 9,而且通常可達 3 個 9。

我們根據這些指標定義警示,以便捕捉進入正式環境的重大退化。由於未知錯誤永遠都是 錯誤,因此只要任何工具的未知錯誤率超過固定門檻,我們就會發出警示。但要判斷可預期錯誤究竟代表工具鏈中的 錯誤,還是可預期的行為,往往並不容易。

舉例來說,一次 grep 搜尋逾時,可能是工具的效能問題,也可能只是程式碼庫太大,導致模型形成了效率不佳的查詢。為了解決這個問題,我們設有異常偵測警示,當可預期錯誤明顯高於基準線時就會觸發。我們會依工具和模型分別計算基準線,因為不同模型在工具呼叫上出錯的比率可能不同。

我們也會每週執行一個配備技能的 Automation,教模型如何搜尋我們的日誌、找出新出現或近期激增的議題,並在待辦清單中建立或更新工單,附上調查結果。我們大量依賴雲端代理,同時啟動多個問題的修正,甚至可以直接從 Linear 觸發它們

這個流程是我們將代理工具鏈落實為自動化「軟體工廠」的一部分。在今年稍早一次聚焦衝刺期間,我們將非預期的工具呼叫錯誤降低了一個數量級。

為不同模型自訂工具鏈

我們所有的工具鏈抽象層都與模型無關,並且可依據我們支援的每個模型進行高度自訂。舉例來說,OpenAI 的模型在訓練時,是使用以 patch 為基礎的格式來編輯檔案;而 Anthropic 的模型則是以字串取代為基礎進行訓練。兩種模型其實都能使用任一種工具,但如果提供它不熟悉的工具,就會消耗額外的推理 token,並產生更多錯誤。因此,在我們的工具鏈中,我們會為每個模型提供它在訓練期間所使用的工具格式。

這種自訂的層次非常深入,包含針對不同 provider,甚至不同模型版本的自訂提示詞。OpenAI 的模型在遵循指示時,通常更傾向於照字面理解且更精確;而 Claude 則更直覺一些,也更能容忍不夠精確的指示。

當我們在正式發布前搶先體驗某個新模型時,會先從最接近的現有模型工具鏈著手,然後開始反覆迭代。我們會執行離線 eval,找出模型容易混淆的地方,讓團隊成員實際使用它並回報發現的問題,然後據此調整工具鏈。我們會持續以這種方式迭代,直到得到我們有信心推出的模型與工具鏈組合。

這個調校流程有很大一部分,是根據新模型的優勢來自訂工具鏈;但有時我們也會遇到模型本身確實存在的一些怪癖,而這些問題可以透過工具鏈來緩解。舉例來說,我們觀察到其中一個模型出現了我們後來稱為上下文焦慮的現象:當它的上下文視窗逐漸被填滿時,它就會開始拒絕處理工作,並保守地表示這個任務看起來太龐大。我們透過調整提示詞,成功減輕了這種行為。

讓使用者在聊天中途切換模型

要把工具鏈設計成支援使用者在對話中途切換模型特別棘手,因為不同模型有不同的行為、提示詞和工具介面。

當使用者切換模型時,Cursor 會自動切換到對應的工具鏈,並載入該模型專屬的一組提示詞和工具。不過,模型仍然必須把這些工具套用到由另一個模型產生的對話歷史,而這些內容也超出了它訓練時的分佈。

為了解決這個問題,我們加入了自訂指示,告訴模型它何時是在聊天中途接手另一個模型。這些指示也會引導它避免呼叫那些出現在對話歷史中、但不屬於自己工具集的工具。

防止模型呼叫不屬於其工具集的工具防止模型呼叫不屬於其工具集的工具

第二個挑戰是,快取是依 provider 和模型而定,因此切換代表會發生快取未命中,導致首次回應更慢、成本也更高。為了減輕這個問題,我們會在切換時為對話產生摘要,讓模型取得一份乾淨的摘要,以降低快取懲罰。但如果使用者已經深入處理複雜任務,摘要可能會遺漏重要資訊,這也是為什麼除非你有切換的理由,否則我們通常建議在整段對話期間維持使用同一個模型。

另一種避開對話中途切換模型所帶來挑戰的方法,是改用子代理,因為它會從全新的上下文視窗開始。我們最近也在工具鏈中加入了一項能力,讓使用者可以直接要求以特定模型執行子代理

工具鏈與軟體開發的未來

AI (人工智慧) 輔助的軟體工程未來將走向多代理模式。系統不會再讓單一代理處理每個子任務,而是會學會將工作委派給專門化的代理與子代理:一個負責規劃,另一個負責快速編輯,第三個負責除錯,各自聚焦於自己最擅長的事。

要把這件事做好,根本上是工具鏈的挑戰。系統需要知道該派出哪個代理、如何依照該代理的強項來設定任務,以及如何將結果串接成一致的工作流程。編排這種協作的能力,將存在於工具鏈中,而不是任何單一代理身上。這也意味著,雖然工具鏈工程一直是代理成功的重要因素,但未來只會變得更加關鍵。

分類於: 研究

作者s: Stefan Heule & Jediah Katz