解決真實GitHub Issue能力登頂,字節豆包MarsCode團隊分享背後工程實踐,踩過的坑也分享了
豆包MarsCode團隊 投稿
量子位 | 公眾號 QbitAI
解決真實GitHub Issue的基準測試,字節家的豆包MarsCode Agent悄悄登頂了。
SWE-Bench,一個由普林斯頓大學提出的極具挑戰性的Benchmark,近期受到工業界、學術界和創業團隊的廣泛關注。
在其子集SWE-Bench Lite排行榜上,豆包MarsCode Agent近期衝上第一。
雖然這是面向所有大模型解決方案的評測,但現在排名靠前的部分已基本被AI Agent佔領。
AI Agent即能夠感知外部環境、操作工具並具有一定自主決策能力的智能體,受到了越來越多的研究關注。
現在,豆包MarsCode Agent團隊分享了在軟件工程領域進行了一系列關於AI Agent應用的探索和嘗試:
通過構建Agent框架並為其提供代碼檢索、調試和編輯的交互接口和工具,使得Agent有可能接管部分軟件工程開發任務。
-
在Agent框架方面,豆包MarsCode開發了多Agent協作框架,根據所要解決軟工問題類型,分配靜態或動態求解管道,從而靈活適配多樣的軟件工程問題;
-
在代碼檢索能力方面,豆包MarsCode結合代碼知識圖譜和語言服務,為Agent提供全面的代碼實體召回、關係召回、定義與引用跳轉等能力,從而使Agent具備人類開發者類似的代碼瀏覽、分析過程;
-
在代碼編輯方面,豆包MarsCode採用Conflict形式的代碼編輯描述和靜態語法檢查,能夠準確生成格式正確的代碼編輯補丁;
-
在軟件動態調試方面,豆包MarsCode基於Docker的容器化沙箱環境,讓Agent具備了人類開發者的調試能力,比如缺陷複現、添加日誌和運行測試框架等。
01多Agent協作框架
開發者在日常的開發工作中常常會遇到各種問題,例如:
-
運行測試用例報錯,有錯誤或異常堆棧等,這可能是由於代碼邏輯錯誤或測試斷言失敗導致的問題;
-
代碼輸出結果不符合預期,沒有顯式的報錯信息,但有明確的輸出結果預期;
-
需要對現有功能進行擴展或增加新功能,有明確的開發需求及預期結果,但不知道如何實現和在哪實現;
-
需要進行簡單的缺陷修復,有大致的修復思路,但不熟悉語言特性需要協助。
上述多樣化的程序修復和開發任務通常無法用一種固定的模式來進行處理。
例如,一些簡單的缺陷修復或功能擴展僅需對代碼進行review即可完成;而較深的異常堆棧或複雜的邏輯錯誤僅憑閱讀代碼往往很難發現問題所在,需要通過動態執行代碼並追蹤相關變量才能暴露出相關缺陷,從而對其進行修復。
因此,團隊採用了多Agent協作的框架來適應不同的開發場景。如下圖所示,多Agent協作框架中包含以下7類角色:
-
Searcher:利用CKG、LSP等代碼檢索工具收集與當前問題相關的repo內代碼片段;
-
Manager:根據收集到的相關代碼片段對問題進行定性和分流,將問題場景分為動態調試修復和靜態修復兩類;
-
Reproducer:在動態調試修復場景下,根據相關代碼和問題描述編寫複現腳本,並在沙箱中對腳本進行動態調試以確認複現成功;
-
Programmer:根據問題描述和相關代碼進行編輯代碼,並結合Tester的測試結果進行多輪迭代修改;
-
Tester:根據問題複現腳本,對當前代碼版本進行動態驗證,檢查問題是否得到解決;
-
Planner:在靜態修復場景下,根據問題和相關代碼,製定求解計劃,規劃需要修改哪些代碼片段及其修改方式;
-
SymbolEditor:根據修改計劃,對所負責的代碼Symbol進行修改,返回修改補丁。
團隊為不同的Agent配備了相應的工具集以支撐其完成指定任務,各Agent配備的工具集如下表所示。
值得注意的是,團隊並沒有令每個Agent都擁有所有工具的使用權,而是嘗試限制各個Agent的能力和職責範圍,從而降低單個Agent解決當前環節問題的難度,以提高任務執行的穩定性和輸出結果的質量。
在動態調試修復場景下,各Agent的協作求解流程如下:
-
由Reproducer根據描述對問題進行複現,生成與問題描述相符的複現腳本;
-
將複現腳本提供給Tester進行驗證,將複現腳本運行得到的異常堆棧和其他輸出信息提供給Programmer進行修復;
-
Programmer完成修復後向Tester提出測試請求;
-
Tester再基於複現腳本進行驗證,並判斷問題是否解決:
-
若問題已解決,則通過diff工具獲取當前代碼變更作為該問題的修復方案,動態調試結束;
-
若仍未解決,則再次將複現過程中異常堆棧和其他輸出信息返回給Programmer;
-
由Planner根據檢索到的相關代碼片段,收集更多代碼上下文,製定修改方案,修改方案以代碼符號(Symbol)為單位組成,每條修改意見包含需要修改代碼所在的Symbol名(類、函數、Top-Level變量等)、修改處所在文件、該Symbol的代碼行號範圍、修改描述;
-
在生成修改方案的過程中,團隊採用了一些軌跡采樣和搜索的策略,使生成的plan有較高的準確性;
-
針對修改方案中的每一個Symbol,實例化出一個Symbol Editor,用於完成改Symbol的修改計劃,每個Symbol Editor完成後通過git commit提交修改;
-
完成所有Symbol的修改後,將當前代碼狀態與Base commit進行差異對照,生成最終補丁作為該問題的修復方案
Programmer可根據Tester的報錯信息,選擇繼續修改,或重置repo放棄過往編輯,重新進行修改。直至提交測試給Tester驗證通過為止。
在動態過程中,團隊通過在Docker容器中搭建一個運行環境沙箱,以實現動態調試的問題複現和運行驗證。
在靜態修復場景下,由於無法直接對問題進行複現和動態驗證,需要製定針對該問題的靜態修復方案。過程如下:
02代碼檢索
團隊為豆包MarsCode Agent提供了多種可跨語言適配的代碼檢索工具,以適應各種軟件工程開發場景下的代碼檢索需求。
代碼知識圖譜(Code Knowledge Graph)
代碼知識圖譜是將代碼元素、屬性以及元素之間的關係表示為圖結構,從而幫助Agent更好地理解和管理大規模的代碼庫。
在這種圖結構中,頂點代表代碼的實體(如函數、變量、類等),邊則代表實體之間的關係(如函數調用、變量引用、類的繼承關係等)。通過這種方式,代碼知識圖譜可以為代碼庫提供更豐富且結構化的信息。
團隊通過程序分析的技術,將倉庫中的代碼,文檔信息進行分析組織,生成一個以變量,函數,類,文件等代碼語義節點為實體,文件結構關係、函數調用關係,符號索引關係為邊的多向圖。構成一張融合了代碼,文檔,倉庫信息等多數據源的代碼知識圖譜。
在給定的代碼庫中,每個節點和邊都通過唯一標識符進行標記,確保每個代碼實體在整個代碼庫中都是唯一的。
在這種設計中,代碼知識圖譜將使用圖屬性來存儲代碼實體及其依賴關係。每個節點記錄其在代碼庫中的位置、類型和名稱。每條邊標識兩個節點之間的關係類型,以及關繫在代碼中的位置。
比如對下面這樣一段代碼:
// file: main/fileA.go
package main
import (
"ckg-prompt/main/cmd/pkg_b"
"fmt"
)
// StructA struct
type StructA struct{}
// FunctionA method for StructA
func (a *StructA) FunctionA() pkg_b.StructB {
x := pkg_b.NewStructB() // Instantiate StructB
return x
}
// XFunction function
func XFunction() {
x := pkg_b.NewStructB() // Instantiate StructB
x.FunctionB() // Calls FunctionB
}
它的代碼知識圖譜如下圖所示:
在構建完成代碼知識圖譜後,Agent的代碼檢索請求將通過下圖中的管道進行處理並實現召回。
-
將Agent的問題與代碼語句(如有),通過模型進行實體識別,得到實體mention+類型,然後在知識圖譜中進行SQL搜索查詢,查詢結果標記為候選實體列表1;
-
將Agent的問題與代碼語句(如有),進行embedding,在知識圖譜中進行embedding相似度匹配,取相似度最高的一批實體,為候選實體列表2;
-
將Agent的問題直接通過關鍵詞識別構造查詢語句,在知識圖譜中進行SQL搜索查詢,查詢結果標記為候選實體列表3。
最後將候選實體列表1 & 2 & 3進行合併,通過精排模型,得到最終實體列表X,返回給Agent,完成代碼檢索。
在豆包MarsCode Agent中,代碼知識圖譜工具能夠實現廣泛、全面的代碼檢索,為Agent提供了repo內知識問答的能力。目前,代碼知識圖譜支持包含C、C#、CPP、Java、Kotlin、Javascript、Typescript、TSX、Rust、Golang、Python、Lua在內的12種常用編程語言。
語言服務器協議(LanguageServerProtocol)
代碼知識圖譜能夠應對大部分目標工程下的類、函數定義與引用的檢索需求,但仍存在以下盲區:
-
代碼知識圖譜是對目標工程進行構建,而目標工程外(如標準庫、第三方庫等)的類、函數、變量的定義和引用是無法通過代碼知識圖譜被準確召回的;
-
對於庫中存在多個重名實體的情況,LSP相比代碼知識圖譜能夠更準確地跳轉到相關類或函數定義,避免召回和Rerank過程帶來的遺漏或冗餘;
為解決上述問題,豆包MarsCode Agent利用通用的語言服務協議(Language Server Protocol)實現用戶機器上全局、精確的代碼召回。
語言服務器協議是一種由Microsoft開發的協議,廣泛適配包含編程語言、標記語言、多種工具和框架在內的語法體系,在IDE場景下具有很好的通用性。豆包MarsCode Agent調用語言服務器協議實現代碼召回的過程如下圖所示:
Agent調用語言服務器進行代碼召回的過程與開發者在IDE中使用「Ctrl+左鍵」點擊某個標識符進行代碼跳轉的過程是一致的,但由於Agent的數字定位和計算能力較弱,團隊增加了模糊定位功能以進一步強化Agent使用LSP工具的能力:
-
根據Agent的提供的文件名和行號,在該行內尋找標識符並計算列號,構成LSP請求;
-
根據Agent的提供的文件名和行號,在該行附近尋找標識符並計算列號,構成LSP請求;
-
根據Agent的提供的標識符和行號,在Agent打開和瀏覽過的文件中尋找標識符,並構成LSP請求;
這三個服務的優先級自上而下由高到低,使用第一個成功得到響應的LSP請求結果作為工具的輸出。
其他通用檢索能力
除LSP和CKG外,團隊將通用的項目尼雲件檢索(find file)、項目或文件內標識符檢索(grep)等能力在豆包MarsCode Agent框架下進行統一的封裝,從而為Agent提供調用風格一致的代碼檢索工具庫。
03代碼編輯
代碼編輯描述
在團隊對研發域AI Agent的長期探索過程中,嘗試了各種使用LLM進行代碼編輯描述的方式,發現目前LLM的代碼修改能力普遍較弱。如下是團隊曾經探索(失敗)過的代碼編輯方案:
-
要求Agent生成Unified diff格式的代碼變更描述;
Unified diff格式的變更是將原始文件和修改後文件以一種統一的方式展示出來,比如:
Unified diff的代碼編輯描述有著非常嚴格的格式要求,而且LLM很難正確計算變更的代碼行號增量,從而導致生成的Unified diff無法成功apply。
-
要求Agent提供代碼變更的起始行號、終止行號和替換的代碼片段;
團隊在所有的代碼檢索的結果中都對代碼添加了行號,希望Agent能正確填寫修改範圍並完成修改,然而即便是GPT-4,也不能完全正確地提供需要修改的代碼範圍,時常會出現1~2行的偏移,從而導致修改後出現重覆行或誤刪;
-對整個文件進行重寫;
為LLM提供整個文件的內容和修改描述,要求LLM輸出修改後的完整文件內容。這種方式能夠避免LLM進行行號的計算,但顯然每次代碼編輯都使用閉源模型生成整個文件是非常不經濟的,且在一些長文件中幾乎不可用。團隊也正在嘗試通過SFT獲取一個專門的代碼編輯模型實現全文重寫的能力,但這是一個長期計劃。
經過大量的探索和嘗試,團隊認為LLM的代碼編輯描述需要具備以下特點:
-
不需要嚴格的格式校驗,編輯描述在經過處理和解析後能夠穩定apply;
-
不需要提供行號範圍,或進行行號計算,LLM在這方面的能力很不穩定;
-
出於token成本和時間成本的考慮,編輯描述要簡明,不包含冗餘信息。
基於此,團隊注意到Aider的代碼變更方式,受此啟發並開發了團隊目前認為相對穩定的代碼編輯:豆包MarsCode AutoDiff。
(https://aider.chat/docs/benchmarks.html#the-benchmark)
AutoDiff的代碼編輯描述與git衝突的表現方式是類似的,Agent需要在Conflict標識柵欄中提供需要編輯的文件路徑、代碼原文、替換代碼。
AutoDiff會對該代碼編輯塊進行解析,嘗試在給定文件中匹配與LLM提供代碼原文片段相似度最高的片段,並用LLM提供的替換代碼進行替換操作,隨後結合修改文件的上下文對替換代碼的縮進進行自動調整。
最後修改前後的文件進行差異對照從而獲取Unified diff格式的變更文件。上述修改是模擬進行的,並未實際在用戶設備上落盤,只是為了獲取Unified diff格式的變更文件,最終代碼修改需要經過後續的靜態代碼診斷。
```diffs
/playground/swe_ws/testbed/matplotlib__matplotlib__3.6/lib/matplotlib/figure.py
<<<<<<< SEARCH
elif constrained_layout is not None:
=======
elif constrained_layout in [None, False]:
>>>>>>> REPLACE
<<<<<<< SEARCH
else:
=======
elif constrained_layout in [None, False]:
self.set_layout_engine(layout='none')
>>>>>>> REPLACE
靜態代碼診斷
儘管AutoDiff能夠正確完成大部分的代碼編輯請求,但仍然會存在諸如類型錯誤、變量未定義、縮進調整錯誤、括號沒有正確閉合等LLM常見的代碼編輯語法問題,針對這些問題,團隊使用語言服務器協議對AutoDiff修改前後的文件進行靜態代碼診斷,過程如下:
-
對AutoDiff生成的Unified diff格式的代碼編輯補丁在原文件上進行apply,得到修改後的文件內容;
-
對原文件內容進行LSP靜態代碼診斷,保存診斷結果;
-
對修改後文件內容進行LSP靜態代碼診斷,保存診斷結果;
-
對兩次代碼診斷結果進行對照,檢查Agent的診斷結果是否引入新的靜態錯誤(只關注Fatal和Error級別的診斷結果);
-
如果未引入新錯誤,則完成修改並向Agent返回修改成功的消息和相應的Unified diff描述;
-
如果引入了新錯誤,則將相關診斷信息返回給Agent進行修改和調整。
04 實驗結果分析
團隊在SWE-bench Lite數據集上對豆包MarsCode Agent的性能進行了詳細評測。
SWE-bench是由普林斯頓大學提出了一個極具挑戰性的針對LLM解決程序邏輯bug的benchmark。該數據集整理了真實的來自Github的12個工業級Python代碼大倉中的2294個issue問題。
給定一個代碼庫以及對要解決issue問題描述,Agent需要從代碼倉中檢索並編輯代碼,最終提交能解決相關問題的代碼補丁。
在SWE-bench中解決問題通常需要同時理解和協調多個函數、類甚至文件之間的更改,要求模型與執行環境交互,處理極長的上下文並執行比傳統代碼生成更複雜的推理。作者評估表明,Claude 2和GPT-4僅能解決其中4.8%和1.7%的實例。
由於SWE-bench的難度很大,在後續的研究中,開發者們發現在SWE-bench的2294個實例上進行評測是一個時間與Token投入巨大且令人沮喪的過程,無法驗證短期內的進展。
所以SWE-bench作者從原本的數據集中抽取了300個Issue描述完整、求解邏輯清晰、相對易於解決的300個實例,組成SWE-bench Lite數據集。目前,SWE-bench Lite數據集已經成為Agent解決軟件工程問題水平的評測標杆,已有20多家企業與研究組織參與了SWE-bench Lite評測和提交中。
豆包MarsCode Agent在最新的SWE-bench Lite評測實驗中,成功求解了其中的118個實例,求解率達到39.33%。
下表展示了評測實驗結果的詳細分析:
豆包MarsCode Agent的錯誤定位能力
團隊對目前SWE-bench Lite排行榜上排名前三的工具(CodeStory Aide,Honeycomb和AbanteAI MentaBot)進行了錯誤定位能力評估。對於一個patch,如果它修改的文件和gold patch所修改的文件相同,則認為錯誤定位正確,否則錯誤。分析結果如圖6所示:
豆包MarsCode Agent通過代碼知識圖譜、語言服務等代碼檢索工具,在300個實例中成功定位到了245個實例的待修改文件(成功率81.7%),而CodeStory Aide為221(成功率73.7%),Honeycomb為213(成功率71%),AbanteAIMentatBot為211(成功率70.3%)。從代碼檢索和錯誤定位能力看,豆包MarsCode Agent處於領先地位。
豆包MarsCode Agent動態靜態求解的實例分佈
團隊分析了實驗中靜態和動態求解的實例分佈,如下圖所示,在所有實例中,有104/300=34.67%的實例被PlannerAgent認為適合動態求解,196/300=65.3%的實例被認為適合進行靜態求解,通過動態方式成功求解53個實例,求解率為51%,靜態方式成功求解65個實例,求解率為33%。由於動態調試有缺陷複現和和驗證的過程,表現在求解率上略高於靜態修復。在被求解的118個實例中,有44.9%的實例是由動態方式修復,55.1%的實例由靜態方式修復。
05未來展望
豆包MarsCode Agent團隊致力於AI Agent方法在軟件工程領域的落地和應用。在未來將持續關注以下優化方向:
-降低大語言模型調用成本,推廣豆包MarsCode Agent在更多場景的落地,為更多用戶提供高質量智能軟件開發服務;
-加強用戶與Agent的協作和交互,提升用戶對Agent作業過程的掌控感;
-支持Agent對用戶工作區的動態調試,同時避免用戶環境汙染等問題;
-進一步提升文件錯誤定位準確率和代碼修改正確率。
歡迎大家通過論文瞭解更多信息:
https://arxiv.org/abs/2409.00899