上個月在計劃為 AutoDev 添加多語言支援時候,發現 GitHub Copilot 的插件功能是語言無關的(通過 plugin.xml 分析),便想研究一下它是如何使用 TreeSitter 的。可惜的是,直到最近才有空,研究一下它是如何實作的。探索的過程中,發現:Copilot 圍繞上下文做了非常之多的工作,便想着寫一篇文章總結一下。
GitHub Copilot 的上下文建構
與 ChatGPT 相比,GitHub Copilot 的強大之處在于,它建構了足夠多的上下文,結合其對 LLM 的訓練(或微調),可以寫出非常精準的生産級代碼。
Copilot 的可見上下文
在肉眼可見的級别裡,即我們自身的使用感受,我們可以發現 Copilot 不僅是讀取目前檔案的源碼,而是一系列相關檔案的源碼,以建構更詳細的上下文。
簡單可以先劃分三個場景:
- 目前檔案。可以感覺某個類的屬性和方法,并做出自動填充。
- 相近檔案。如測試檔案,可以知道被測類的資訊,并自動編寫用例。
- 編輯曆史(疑似)。即當我們以某種方式修改多個代碼時,它也能識别出這個變化。
而在未來,相信它會擷取諸如項目上下文等資訊,如 Gradle 依賴、NPM 依賴等資訊,避免在打開的 tab 不夠用的情況下,引用不存在的依賴。
而針對于企業自身的 AI 程式設計工具而言,還可以結合服務上下文、業務上下文進行優化。
Copilot 的不可見過程
結合網上的逆向工程資料,以及自己對代碼的 debug 嘗試,最後梳理了一個大緻的 “四不像” (實在是懶得繼續畫)架構圖:
其作用如下:
- 監聽使用者操作(IDE API )。監聽使用者的 Run Action、快捷鍵、UI 操作、輸入等,以及最近的文檔操作曆史。
- IDE 膠水層(Plugin)。作為 IDE 與底層 Agent 的膠水層,處理輸入和輸出。
- 上下文建構(Agent)。JSON RPC Server,處理 IDE 的各種變化,對源碼進行分析,封裝為 “prompt” (疑似) 并發送給伺服器。
- 服務端(Server)。處理 prompt 請求,并交給 LLM 服務端處理。
而在整個過程中,最複雜的是在 Agent 部分,從上下文中建構出 prompt。
Copilot 的 Prompt 與上下文
在 “公開” 的 Copilot-Explorer 項目的研究資料裡,可以看到 Prompt 是如何建構出來的。如下是發送到的 prompt 請求:
- {
- "prefix": "# Path: codeviz\\app.py\n#....",
- "suffix": "if __name__ == '__main__':\r\n app.run(debug=True)",
- "isFimEnabled": true,
- "promptElementRanges": [
- { "kind": "PathMarker", "start": 0, "end": 23 },
- { "kind": "SimilarFile", "start": 23, "end": 2219 },
- { "kind": "BeforeCursor", "start": 2219, "end": 3142 }
- ]
- }
其中:
- 用于建構 prompt 的
部分,是由 promptElements 建構了,其中包含了:prefix
,BeforeCursor
,AfterCursor
,SimilarFile
,ImportedFile
,LanguageMarker
,PathMarker
等類型。從幾種RetrievalSnippet
的名稱,我們也可以看出其真正的含義。PromptElementKind
- 用于建構 prompt 的
部分,則是由光标所在的部分決定的,根據 tokens 的上限(2048 )去計算還有多少位置放下。而這裡的 Token 計算則是真正的 LLM 的 token 計算,在 Copilot 裡是通過 Cushman002 計算的,諸如于中文的字元的 token 長度是不一樣的,如:suffix
,其中 context 中的内容的 length 為 20,但是 tokenLength 是 30,中文字元共 5 個(包含{ context: "console.log('你好,世界')", lineCount: 1, tokenLength: 30 }
)的長度,單個字元占的 token 就是 3。,
到這裡,我算是解決我感興趣的部分,Agent 包裡的 TreeSitter 則用于分析源碼,生成
RetrievalSnippet
,其中支援語言是 Agent 自帶的
.wasm
相關的包,諸如:Go、JavaScript、Python、Ruby、TypeScript 語言。
LLM 的上下文工程
上下文工程是一種讓 LLM 更好地解決特定問題的方法。它的核心思想是,通過給 LLM 提供一些有關問題的背景資訊,比如指令、示例等,來激發它生成我們需要的答案或内容。上下文工程是一種與 LLM 有效溝通的技巧,它可以讓 LLM 更準确地把握我們的目的,并且提升它的輸出水準。
簡而言之,上下文工程是如何在有限的 token 空間内,傳遞最相關的上下文資訊。
是以,我們就需要定義什麼是該場景下的,最相關的上下文資訊。
基于場景與旅程的上下文設計
它的基本思想是,通過分析使用者在不同場景下的操作和行為,來擷取與目前任務相關的上下文資訊,進而指導 LLM 生成最佳的代碼提示。
Copilot 分析了使用者在不同場景下的操作和行為,如何使用 IDE 的旅程,以及與目前任務相關的指令和例子等資訊,進而擷取最相關的上下文資訊。這些上下文資訊可以幫助 LLM 更好地了解使用者的意圖,并生成更準确、更有用的代碼提示。
例如,在使用者編寫 JavaScript 代碼時,Copilot會分析使用者在編輯器中的光标位置、目前檔案的内容、變量、函數等資訊,以及使用者的輸入曆史和使用習慣等上下文資訊,來生成最相關的代碼提示。這些代碼提示不僅能夠提高使用者的編碼效率,還能夠幫助使用者避免常見的程式設計錯誤。
就地矢量化(Vector)與相似度比對
“衆知周知”,在 LLM 領域非常火的一個工具是 LangChain,它的處理過程類似于 langchain-ChatGLM 總結的:
加載檔案 -> 讀取文本 -> 文本分割 -> 文本向量化 -> 問句向量化 -> 在文本向量中比對出與問句向量最相似的個 -> 比對出的文本作為上下文和問題一起添加到
top k
中 -> 送出給
prompt
生成回答。
LLM
為了處理大規模的自然語言處理任務,Copilot 在用戶端使用了 Cushman + ONNX 模型處理。具體來說,Copilot 将 Cushman 模型的輸出轉換為向量表示,然後使用向量相似度計算來比對最相關的本地檔案。
除了就地矢量化(Vector)與相似度比對,Copilot 還使用了本地的相似計算與 token 處理來管理 token,以便更好地處理大規模自然語言處理任務。
有限上下文資訊的 Token 配置設定
而由于 LLM 的處理能力受到 token 數的限制,如何在有限的 token 範圍内提供最相關的上下文資訊,便是另外一個重要的問題。
諸如于如上所述的 Copilot 本地 prompt 分為了 prefix 和 suffix 兩部分,在 suffix 部分需要配置 suffixPercent,其用于指定在生成代碼提示時要用多少 prompt tokens 來建構字尾,預設值似乎是 15%。
通過增加 suffixPercent,可以讓 Copilot 更關注目前正在編寫的代碼片段的上下文資訊,進而生成更相關的代碼提示。而通過調整 fimSuffixLengthThreshold,可以控制 Fill-in-middle 的使用頻率,進而更好地控制生成的代碼提示的準确性。
Copilot 如何建構及時的 Token 響應
為了提供更好的程式設計體驗,代碼自動補全工具需要能夠快速響應使用者的輸入,并提供準确的建議。在 Copilot 中,建構了一個能夠在極短時間内生成有用的代碼提示的系統。
取消請求機制
為了及時響應使用者的輸入,IDE 需要向 Copilot 的後端服務發送大量的請求。然而,由于使用者的輸入速度很快,很可能會出現多個請求同時發送的情況。在這種情況下,如果不采取措施,後端服務會面臨很大的壓力,導緻響應變慢甚至崩潰。
為了避免這種情況,可以采用取消請求機制。具體來說,在 IDE 端 Copliot 使用
CancellableAsyncPromise
來及時取消請求,在 Agent 端結合 HelixFetcher 配置 abort 政策。這樣,當使用者删除或修改輸入時,之前發送的請求就會被及時取消,減輕後端服務的負擔。
多級緩存系統
為了加速 Token 的響應速度,我們可以采用多級緩存系統。具體來說,在 IDE 端可以使用 簡單的政策,如:SimpleCompletionCache,Agent 端使用 LRU 算法的 CopilotCompletionCache,Server 端也可以有自己的緩存系統。
多級緩存系統可以有效減少對後端服務的請求,提高響應速度。
LLM 的上下文工程的未來?
在網際網路上,我們常常能看到一些令人驚歎的視訊,展示了記憶體有限時代程式設計的奇妙創意,比如雅達利(Atari)時代、紅白機等等,它們見證了第一個 8-bit 音樂的誕生、Quake 的平方根算法等等。
而在當下,LLM 正在不斷地突破上下文能力的極限,比如 Claude 提供了 100K 的上下文能力,讓我們不禁思考,未來是否還需要像過去那樣節省 tokens 的使用。
那麼,我們還需要關注 LLM 的上下文嗎?
當記憶體有限時,程式員需要發揮想象力和創造力來實作目标。而至今我們的記憶體也一直不夠用,因為不合格的開發人員一直浪費我們的記憶體。是以吧,tokens 總是不夠用的,我們還是可以考慮關注于:
- 優化 token 配置設定政策:由于 token 數的限制,我們需要優化 token 配置設定政策,以便在有限的 token 範圍内提供最相關的上下文資訊,進而生成更準确、更有用的内容。
- 多樣化的上下文資訊:除了指令、示例等基本上下文資訊外,我們還可以探索更多樣化的上下文資訊,例如注釋、代碼結構等,進而提供更全面的上下文資訊,進一步提高 LLM 的輸出水準。
- 探索新的算法和技術:為了更好地利用有限的資源,我們需要探索新的算法和技術,以便在有限的 token 數限制下實作更準确、更有用的自然語言處理。
- ……
未來,一定也會有濫用 token 程式,諸如于 AutoGPT 就是一直非常好的例子。
結論
GitHub Copilot 可以在有限的 token 範圍内提供最相關的上下文資訊,進而生成更準确、更有用的代碼提示。這些政策提供了一定的靈活性,使用者可以根據自己的需要來調整 Copilot 的行為,進而獲得更好的代碼自動補全體驗。
我們跟進未來的路,依舊很長。
Copilot 逆向工程相關資料:
- https://github.com/thakkarparth007/copilot-explorer
- https://github.com/saschaschramm/github-copilot
其它相關資料:
- https://github.com/imClumsyPanda/langchain-ChatGLM