Gaby - Golang 社區機器人
Gaby 是一個實驗性的新機器人, 在 Go 問題跟蹤器中以 @gabyhelp[1] 的身份運行, 試圖自動化各種機器可以合理完成的瑣碎事務, 同時也試圖發現機器可以合理完成的新事物。
gaby 這個名字是 "Go AI Bot" 的縮寫, 因爲這個實驗的目的之一是瞭解 LLM 可以有效用於哪些方面, 包括識別它們不應該用於哪些方面。一些 gaby 功能將涉及 LLM; 其他功能則不會。指導原則是創造一些能幫助維護者並且維護者喜歡的東西, 這意味着在 LLM 有意義和有幫助時使用它們, 而在不適合時則不使用。
從長遠來看, 我們打算讓這個代碼庫或後續版本接管當前 "gopherbot" 的功能, 成爲 @gopherbot[2] , 屆時 @gabyhelp 賬號將被停用。
目前我們不接受新的代碼貢獻或 PR。我們希望很快將這些代碼移至更正式的地方, 屆時我們將接受貢獻。
GitHub 討論 [3] 是留下關於 @gabyhelp 反饋的好地方。
代碼概述
機器人功能在子目錄的內部包中實現。這個註釋簡要介紹了結構。
Gaby 代碼庫的一個明確目標是能夠在許多不同的環境中良好運行, 從維護者的家庭服務器甚至樹莓派一直到託管雲。(目前, Gaby 運行在我地下室的 Linux 服務器上。) 由於這種對可移植性的強調, Gaby 爲它需要的所有來自周圍環境的功能定義了自己的接口, 然後還定義了這些接口的各種實現。
Gaby 代碼庫的另一個明確目標是經過非常充分的測試。(關於爲什麼這如此重要, 請參閱我的 [Go 測試演講]。) 將各種外部功能抽象爲接口也有助於簡化測試, 一些包還提供了明確的測試支持。
這兩個目標的結果是, Gaby 爲自己定義了一些基本功能, 如時間排序索引, 而不是依賴於某些特定的其他實現。從大局來看, 這些只是少量需要維護的代碼, 而對可移植性和可測試性的好處卻很顯著。
測試
與 GitHub 等服務交互的代碼和在雲服務器上運行的代碼通常難以測試, 因此測試不足。這個倉庫的一個明確要求是測試所有代碼, 即使 (尤其是) 在測試困難的情況下。
在處理代碼時, 一個有用的命令是 rsc.io/uncover[4] , 它會打印出未被單元測試覆蓋的包源代碼行。一個有用的調用是:
% go install rsc.io/uncover@latest
% go test && go test -coverprofile=/tmp/c.out && uncover /tmp/c.out
PASS
ok rsc.io/gaby/internal/related 0.239s
PASS
coverage: 92.2% of statements
ok rsc.io/gaby/internal/related 0.197s
related.go:180,181
p.slog.Error("triage parse createdat", "CreatedAt", issue.CreatedAt, "err", err)
continue
related.go:203,204
p.slog.Error("triage lookup failed", "url", u)
continue
related.go:250,251
p.slog.Error("PostIssueComment", "issue", e.Issue, "err", err)
continue
%
第一個 "go test" 命令檢查測試是否通過。第二個重複測試並啓用覆蓋率。這樣運行測試兩次可以確保編譯器報告的任何語法或類型錯誤都在沒有覆蓋率的情況下報告, 因爲覆蓋率可能會扭曲錯誤輸出。在兩個測試都通過並且第二個寫入覆蓋率配置文件後, 運行 "uncover /tmp/c.out" 會打印未覆蓋的行。
在這個輸出中, 有三個未經測試的錯誤路徑。通常, 應該測試錯誤路徑, 所以應該編寫測試來覆蓋這些代碼行。在有限的情況下, 測試某個部分可能不切實際, 例如當代碼不可達但爲了防止將來的更改或錯誤假設而保留時。該部分代碼可以用以 "// Unreachable" 或 "// unreachable" 開頭的註釋標記 (通常後面跟有解釋性文本), 然後 uncover 就不會報告它。如果某個代碼部分應該被測試但測試被推遲到以後, 那麼該部分可以用 "// Untested" 或 "// untested" 標記。
這個 rsc.io/gaby/internal/testutil[5] 包提供了一些其他的測試輔助工具。
代碼概述現在從底向上進行,從存儲開始, 一直到實際的機器人。
密鑰存儲
Gaby 需要管理一些用於訪問服務的祕密密鑰。 rsc.io/gaby/internal/secret[6] 包定義了獲取這些祕密的接口。目前唯一的實現是一個內存映射和一個基於磁盤的實現, 後者讀取 $HOME/.netrc。未來的實現可能包括其他文件格式以及基於雲的祕密存儲服務。
祕密存儲有意與下面描述的主數據庫存儲分開。主數據庫應該保存公共數據, 而不是祕密。
大型語言模型
Gaby 定義了它期望從大型語言模型獲得的接口。
llm.Embedder[7] 接口抽象了一個可以接收一組文檔並返回它們的向量嵌入的 LLM, 每個嵌入的類型是 llm.Vector[8] 。迄今爲止唯一的真正實現是 rsc.io/gaby/internal/gemini[9] 。添加一個使用 Ollama 的離線實現也會很好。
對於需要嵌入器但不關心嵌入質量的測試, llm.QuoteEmbedder[10] 以確定性的方式將文本的前綴複製到向量中 (保持向量單位長度)。這對於測試向量搜索等功能已經足夠好, 並且通過避免依賴真實的 LLM 來簡化測試。
目前, 只定義了嵌入接口。將來我們預計會添加更多圍繞文本生成和工具使用的接口。
存儲
如上所述, Gaby 爲其外部環境所需的所有功能定義了接口, 以允許各種執行和測試的實現。最低級的接口是存儲, 定義在 rsc.io/gaby/internal/storage[11] 中。
Gaby 需要一個支持鍵範圍有序遍歷和適度大小限制 (至少幾兆字節) 的原子批量寫入的鍵值存儲。基本接口是 storage.DB[12] 。 storage.MemDB[13] 返回一個對測試有用的內存實現。其他實現可以使用 storage.TestDB[14] 進行測試。
唯一真正的 storage.DB[11] 實現是 rsc.io/gaby/internal/pebble[15] , 它是一個源自 LevelDB[16] 的磁盤鍵值存儲, 作爲 CockroachDB[17] 的一部分開發和使用。它是一個生產質量的本地存儲實現, 並將數據庫維護爲一個文件目錄。
將來我們計劃添加一個使用 Google Cloud Firestore[18] 的實現, 它提供了一個生產質量的鍵值查找作爲雲服務, 沒有固定的基線服務器成本。(Firestore 是 Google Cloud Datastore 的繼任者。)
storage.DB[11] 做了一個簡化的假設, 即存儲永遠不會失敗, 或者更確切地說, 如果存儲已經失敗, 那麼你寧願崩潰程序也不願通過通常未經測試的代碼路徑繼續執行。因此, Get 和 Set 等方法不返回錯誤。它們在失敗時會引發 panic,DB 的客戶端可以調用 DB 的 Panic 方法來引發相同類型的 panic, 如果他們注意到任何損壞。這個決定是否保留還有待觀察。
除了常規的 Get、Set 和 Delete 方法外, storage.DB[11] 還定義了 Lock 和 Unlock 方法,用於獲取和釋放由數據庫層管理的命名互斥鎖。這些方法的目的是在無服務器雲執行平臺上運行多個 Gaby 程序實例時實現協調。到目前爲止,Gaby 只在地下室服務器(與雲相反)上運行,因此這些方法還沒有得到充分的測試,API 可能會發生變化。
除了常規數據庫外,storage 包還定義了 storage.VectorDB[19] ,這是一個用於 LLM 嵌入的向量數據庫。基本操作包括 Set、Get 和 Search。 storage.MemVectorDB[20] 返回一個內存實現,它將實際向量存儲在 storage.DB[11] 中以實現持久化,但同時在內存中保留副本並通過比較所有向量進行搜索。當由 storage.MemDB[12] 支持時,這種實現對於測試很有用,但當由持久化數據庫支持時,該實現足以滿足小規模生產使用(比如,最多一百萬個文檔,需要 3 GB 的向量)。
這裏的包排序可能是錯誤的,VectorDB 應該定義在 llm 包中,建立在 storage 之上,而不是當前的 "storage 建立在 llm 之上"。
有序鍵
由於 Gaby 對其存儲層的要求很少,我們想要施加的任何結構都必須在其之上實現。Gaby 使用 rsc.io/ordered[21] 編碼格式來生成以有用方式排序的數據庫鍵。
例如,ordered.Encode("issue", 123) < ordered.Encode("issue", 1001),因此這種形式的鍵可以用來按數字順序掃描問題。相比之下,使用類似 fmt.Sprintf("issue%d", n) 的方式會在訪問 issue 123 之前訪問 issue 1001,因爲 "1001" < "123"。
在使用 NoSQL 鍵值存儲時,使用這種編碼是很常見的。有關特定編碼的詳細信息,請參閱 rsc.io/ordered[20] 包。
時間戳存儲
Gaby 的一個隱含任務是收集開源項目的所有相關信息:問題、代碼變更、文檔等。這些源始終在變化,因此像爲文檔添加嵌入這樣的派生操作需要能夠識別哪些是新的,哪些已經處理過。爲了實現這一點,Gaby 實現了帶時間戳的存儲(簡稱 "timed" 存儲),其中一組鍵值對還有一個 "按時間" 索引,包含 ((timestamp, key), no-value) 對,使得只掃描上次掃描後修改的鍵值對成爲可能。這種增量掃描只需要記住上次處理的時間戳,然後從該時間戳之後開始有序鍵範圍掃描。
這個約定由 rsc.io/gaby/internal/timed[22] 實現,同時還有一個 [timed.Watcher],它將增量掃描模式形式化。
文檔存儲
各種包負責從問題跟蹤器等下載狀態,但隨後所有這些狀態都需要統一到一個可以索引和搜索的通用文檔格式中。該文檔格式由 rsc.io/gaby/internal/docs[23] 定義。文檔由 ID(通常是 URL)、文檔標題和文檔文本組成。文檔使用時間戳存儲,實現新添加文檔的增量處理。
文檔嵌入
任何新文檔的下一站是將其嵌入到向量中並將該向量存儲在向量數據庫中。 rsc.io/gaby/internal/embeddocs[24] 包完成了這項工作,考慮到其他包提供的具有增量掃描功能的文檔存儲、LLM 嵌入器和向量數據庫的抽象,這個包的內容非常少。
HTTP 記錄和重放
到目前爲止提到的包都不涉及網絡操作,但接下來的幾個包會涉及。測試這些包很重要,但同樣重要的是不要在測試中依賴外部網絡服務。相反, rsc.io/gaby/internal/httprr[25] 包提供了一個專門設計用於幫助測試的 HTTP 記錄 / 重放系統。它可以在一種使用外部網絡服務器並記錄 HTTP 交換的模式下運行一次,但默認情況下,測試會在先前記錄的日誌中查找預期的響應,重放這些響應。
結果是,可以對發出 HTTP 請求的代碼進行一次真實服務器流量的測試,然後使用該流量的記錄進行重新測試。這避免了必須編寫整個服務的假版本,同時也避免了爲了通過測試而需要服務保持可用。它通常也使測試比使用真實服務器快得多。
GitHub 交互
Gaby 主要以兩種方式使用 GitHub。首先,它將問題跟蹤器狀態的整個副本下載到定時存儲中,並進行增量更新。其次,它在問題跟蹤器中執行操作,如編輯問題或評論、應用標籤或發佈新評論。這些操作由 rsc.io/gaby/internal/github[26] 提供。
Gaby 使用 GitHub 的 REST API 下載問題跟蹤器狀態,這使得增量更新變得非常容易,但不提供對一些較新功能的訪問,如項目板和討論,這些功能只在 GraphQL API 中可用。使用 GraphQL API 進行同步留待將來工作:REST API 提供了足夠的數據,因此現在我們可以專注於如何使用這些數據,而不是缺少一些較新的 GitHub 功能。
github 包爲測試提供了兩個重要的輔助工具。對於問題跟蹤器狀態,它還允許從簡單的基於文本的問題描述中加載問題數據,完全避免了實際使用 GitHub,並使修改測試數據變得更容易。對於問題跟蹤器操作,github 包在測試中默認不實際進行更改,而是將編輯轉移到內存日誌中。然後測試可以檢查日誌以查看是否請求了正確的編輯。
rsc.io/gaby/internal/githubdocs[27] 包負責將下載的 GitHub 狀態中的內容添加到通用文檔存儲中。目前,唯一的 GitHub 衍生文檔是每個問題對應一個文檔,包含問題標題和正文。可能值得嘗試以某種方式合併問題評論,儘管它們帶來了大量潛在的噪音。
Gerrit 交互
Gaby 需要下載並將 Gerrit 狀態存儲到數據庫中,然後從中派生文檔。這部分代碼尚未編寫,儘管 rsc.io/gerrit/reviewdb[28] 提供了一個可以適配的基本版本。
網絡爬蟲
Gaby 還需要下載並將項目文檔存儲到數據庫中,並從中派生出對應於每個標題處切割頁面的文檔。這部分代碼已經編寫,但尚未經過充分測試,無法提交。它將在稍後添加。
Gaby 最簡單的工作是修復新評論,包括問題描述(看起來像評論但是不同類型的 GitHub 數據)。 rsc.io/gaby/internal/commentfix[29] 包實現了這一功能,增量監視 GitHub 狀態並對每個新評論或問題正文應用幾種重寫規則。commentfix 包允許自動編輯文本、自動編輯 URL 和自動超鏈接文本。
查找相關問題和文檔
Gaby 的下一個工作是用相關問題和文檔迴應新問題。 rsc.io/gaby/internal/related[30] 包實現了這一功能,增量監視 GitHub 狀態以查找新問題,過濾掉應該忽略的問題,然後查找相關問題和文檔併發佈列表。
這個包最初旨在識別並自動關閉重複問題,但對於 LLM 來說,區分重複問題和非常相似或尚未完全修復的問題是一個太困難的判斷。即便如此,提供可能被遺忘或從未被閱讀問題的人知道的相關上下文的行爲已被證明是非常有幫助的。
主循環
所有這些部分都被整合到主程序中,即這個包 rsc.io/gaby[31] 。實際的 main 包目前還沒有測試,但也非常簡單直觀。它確實需要測試,但我們還需要找出將包中硬編碼的策略提取爲數據的方法,以便自然語言接口可以操作。例如,當前 main 包中的策略選擇大致如下:
cf := commentfix.New(lg, gh, "gerritlinks")
cf.EnableProject("golang/go")
cf.EnableEdits()
cf.AutoLink(`\bCL ([0-9]+)\b`, "https://go.dev/cl/$1")
cf.ReplaceURL(`\Qhttps://go-review.git.corp.google.com/\E`, "https://go-review.googlesource.com/")
rp := related.New(lg, db, gh, vdb, dc, "related")
rp.EnableProject("golang/go")
rp.EnablePosts()
rp.SkipBodyContains("— [watchflakes](https://go.dev/wiki/Watchflakes)")
rp.SkipTitlePrefix("x/tools/gopls: release version v")
rp.SkipTitleSuffix(" backport]")
這些可以作爲數據存儲在某處,並由 LLM 根據維護者的提示進行操作和添加。其他功能也可以以類似的方式添加和配置。確切如何做到這一點是未來實驗中需要學習的重要內容。
未來工作和結構
如上所述,Gaby 已經完成的兩項工作都相當簡單直接。一個看起來行之有效的通用方法是編寫良好、測試充分的確定性傳統功能,如評論修復器和相關文檔發佈器,由 LLM 根據項目維護者指定的具體指示或最終更高層次的目標進行配置。其他值得探索的功能包括自動標記問題的規則、識別需要提醒的問題或 CL 的規則、識別需要維護者注意或需要提交的 CL 的規則等。另一個延伸目標可能是識別何時需要更多信息的問題並請求這些信息。當然,重要的是不要詢問已經存在或無關的信息,所以做到這一點將是一個非常高的門檻。目前還不能保證今天的 LLM 能夠很好地構建這種功能的有用版本。
未來工作的另一個重要領域將是在雲數據庫之上運行 Gaby,然後將 Gaby 自身的執行移至雲端。爲其提供一個帶有 URL 的服務器將啓用 GitHub 回調,而不是當前的 2 分鐘輪詢循環,這將實現與 Gaby 的交互式對話。
總的來說,我們相信有一些好的想法可以讓基於 LLM 的機器人幫助簡化項目維護者的工作並減少單調性,這些想法有待發現。同時也存在許多糟糕的想法,必須將它們過濾掉。理解其中的區別需要相當的謹慎、思考和實驗。我們還有工作要做。
參考鏈接
- @gabyhelp: https://github.com/gabyhelp
- @gopherbot: https://github.com/gopherbot
- GitHub 討論: https://github.com/golang/go/discussions/67901
- rsc.io/uncover: https://pkg.go.dev/rsc.io/uncover
- rsc.io/gaby/internal/testutil: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/testutil
- rsc.io/gaby/internal/secret: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/secret
- llm.Embedder: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/llm#Embedder
- llm.Vector: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/llm#Vector
- rsc.io/gaby/internal/gemini: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/gemini
- llm.QuoteEmbedder: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/llm#QuoteEmbedder
- rsc.io/gaby/internal/storage: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/storage
- storage.DB: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/storage#DB
- storage.MemDB: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/storage#MemDB
- storage.TestDB: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/storage#TestDB
- rsc.io/gaby/internal/pebble: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/pebble
- LevelDB: https://github.com/google/leveldb
- CockroachDB: https://github.com/cockroachdb/cockroach
- Google Cloud Firestore: https://cloud.google.com/firestore
- storage.VectorDB: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/storage#VectorDB
- storage.MemVectorDB: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/storage#MemVectorDB
- rsc.io/ordered: https://pkg.go.dev/rsc.io/ordered
- rsc.io/gaby/internal/timed: https://pkg.go.dev/rsc.io/gaby/internal/timed
- rsc.io/gaby/internal/docs: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/docs
- rsc.io/gaby/internal/embeddocs: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/embeddocs
- rsc.io/gaby/internal/httprr: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/httprr
- rsc.io/gaby/internal/github: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/github
- rsc.io/gaby/internal/githubdocs: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/githubdocs
- rsc.io/gerrit/reviewdb: https://pkg.go.dev/rsc.io/gerrit/reviewdb
- rsc.io/gaby/internal/commentfix: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/commentfix
- rsc.io/gaby/internal/related: https://pkg.go.dev/rsc.io/gaby@v0.0.2/internal/related
- rsc.io/gaby: https://pkg.go.dev/rsc.io/gaby@v0.0.2
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/KnfuJNWm_EhWPkVL1VqlZg