天天看點

上下文工程:基于 Github Copilot 的實時能力分析與思考

作者:Phodal

上個月在計劃為 AutoDev 添加多語言支援時候,發現 GitHub Copilot 的插件功能是語言無關的(通過 plugin.xml 分析),便想研究一下它是如何使用 TreeSitter 的。可惜的是,直到最近才有空,研究一下它是如何實作的。探索的過程中,發現:Copilot 圍繞上下文做了非常之多的工作,便想着寫一篇文章總結一下。

GitHub Copilot 的上下文建構

與 ChatGPT 相比,GitHub Copilot 的強大之處在于,它建構了足夠多的上下文,結合其對 LLM 的訓練(或微調),可以寫出非常精準的生産級代碼。

Copilot 的可見上下文

在肉眼可見的級别裡,即我們自身的使用感受,我們可以發現 Copilot 不僅是讀取目前檔案的源碼,而是一系列相關檔案的源碼,以建構更詳細的上下文。

上下文工程:基于 Github Copilot 的實時能力分析與思考

簡單可以先劃分三個場景:

  • 目前檔案。可以感覺某個類的屬性和方法,并做出自動填充。
  • 相近檔案。如測試檔案,可以知道被測類的資訊,并自動編寫用例。
  • 編輯曆史(疑似)。即當我們以某種方式修改多個代碼時,它也能識别出這個變化。

而在未來,相信它會擷取諸如項目上下文等資訊,如 Gradle 依賴、NPM 依賴等資訊,避免在打開的 tab 不夠用的情況下,引用不存在的依賴。

而針對于企業自身的 AI 程式設計工具而言,還可以結合服務上下文、業務上下文進行優化。

Copilot 的不可見過程

結合網上的逆向工程資料,以及自己對代碼的 debug 嘗試,最後梳理了一個大緻的 “四不像” (實在是懶得繼續畫)架構圖:

上下文工程:基于 Github Copilot 的實時能力分析與思考

其作用如下:

  • 監聽使用者操作(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 請求:

  1. {
  2. "prefix": "# Path: codeviz\\app.py\n#....",
  3. "suffix": "if __name__ == '__main__':\r\n app.run(debug=True)",
  4. "isFimEnabled": true,
  5. "promptElementRanges": [
  6. { "kind": "PathMarker", "start": 0, "end": 23 },
  7. { "kind": "SimilarFile", "start": 23, "end": 2219 },
  8. { "kind": "BeforeCursor", "start": 2219, "end": 3142 }
  9. ]
  10. }

其中:

  • 用于建構 prompt 的

    prefix

    部分,是由 promptElements 建構了,其中包含了:

    BeforeCursor

    ,

    AfterCursor

    ,

    SimilarFile

    ,

    ImportedFile

    ,

    LanguageMarker

    ,

    PathMarker

    ,

    RetrievalSnippet

    等類型。從幾種

    PromptElementKind

    的名稱,我們也可以看出其真正的含義。
  • 用于建構 prompt 的

    suffix

    部分,則是由光标所在的部分決定的,根據 tokens 的上限(2048 )去計算還有多少位置放下。而這裡的 Token 計算則是真正的 LLM 的 token 計算,在 Copilot 裡是通過 Cushman002 計算的,諸如于中文的字元的 token 長度是不一樣的,如:

    { context: "console.log('你好,世界')", lineCount: 1, tokenLength: 30 }

    ,其中 context 中的内容的 length 為 20,但是 tokenLength 是 30,中文字元共 5 個(包含

    )的長度,單個字元占的 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 總結的:

上下文工程:基于 Github Copilot 的實時能力分析與思考
加載檔案 -> 讀取文本 -> 文本分割 -> 文本向量化 -> 問句向量化 -> 在文本向量中比對出與問句向量最相似的

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 總是不夠用的,我們還是可以考慮關注于:

  1. 優化 token 配置設定政策:由于 token 數的限制,我們需要優化 token 配置設定政策,以便在有限的 token 範圍内提供最相關的上下文資訊,進而生成更準确、更有用的内容。
  2. 多樣化的上下文資訊:除了指令、示例等基本上下文資訊外,我們還可以探索更多樣化的上下文資訊,例如注釋、代碼結構等,進而提供更全面的上下文資訊,進一步提高 LLM 的輸出水準。
  3. 探索新的算法和技術:為了更好地利用有限的資源,我們需要探索新的算法和技術,以便在有限的 token 數限制下實作更準确、更有用的自然語言處理。
  4. ……

未來,一定也會有濫用 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

繼續閱讀