上个月我准备为 AutoDev 增加多语言支持的时候,偶然发现 GitHub Copilot 的插件功能其实跟语言无关(通过 plugin.xml 分析得出的),这让我对它如何利用 TreeSitter 的机制产生了兴趣。遗憾的是,直到最近我才抽出时间来深入研究这一点。在探索的过程中,我发现 Copilot 在构建上下文方面做了很多工作,心里想着不如写篇文章来总结一下这些发现。
GitHub Copilot 的上下文构建
相比于 ChatGPT,GitHub Copilot 之所以更强,是因为它能够构建出丰富的上下文,并结合对 LLM 的训练(或者说是微调),从而生成非常精准的生产级代码。
Copilot 的可见上下文
在我们实际使用中,可以明显感受到 Copilot 不仅仅是获取当前文件的代码,而是能够读取一系列相关文件的内容,以此来构建更为详细的上下文。

简单来说,可以分为三个主要场景:
-
当前文件。Copilot 能够识别某个类的属性和方法,并进行自动补全。
-
相近文件。像测试文件这样的内容,它能够获取被测试类的信息,并自动生成测试用例。
-
编辑历史(可能)。当我们对多个代码片段进行修改时,它也能捕捉到这些变化。
展望未来,我相信它还会获取更多的项目上下文信息,比如 Gradle 和 NPM 的依赖,这样在打开的标签页不够用时,就能避免引用那些不存在的依赖。
而针对一些企业自家的 AI 编程工具,结合服务上下文和业务背景进行优化,将会是一个不错的方向。
Copilot 的不可见过程
通过一些逆向工程的资料和我对代码调试的尝试,我最终梳理出一个大概的 “四不像” (实在不想继续画了)架构图:

这个架构的功能大致如下:
-
监听用户操作(IDE API)。它能够监听用户的运行操作、快捷键、UI 交互、输入等,还能记录最近的文档操作历史。
-
IDE 胶水层(Plugin)。作为 IDE 和底层代理之间的桥梁,负责处理输入和输出。
-
上下文构建(Agent)。JSON RPC 服务器,负责处理 IDE 的各种变化,分析源码,并将其封装为 “prompt”(可能)后发送给服务器。
-
服务端(Server)。处理 prompt 请求,并将其交给 LLM 服务端进行处理。
在整个过程中,最复杂的部分无疑是在 Agent 阶段,从上下文中构建出合适的 prompt。
Copilot 的 Prompt 与上下文
在 “公开”的 Copilot-Explorer 项目研究资料中,可以看到 Prompt 是如何被构建的。以下是发送的 prompt 请求示例:
{
"prefix": "# Path: codeviz\app.pyn#....",
"suffix": "if __name__ == '__main__':rn app.run(debug=True)",
"isFimEnabled": true,
"promptElementRanges": [
{ "kind": "PathMarker", "start": 0, "end":23},
{ "kind": "SimilarFile", "start": 23, "end":2219},轻松理解 LLM 的上下文工程与 Copilot 的智能响应机制 在这段内容中,我们聊聊 prompt 的构成。首先,构建 prompt 的
prefix部分是由 promptElements 生成的,里面有些东西像BeforeCursor、AfterCursor、SimilarFile、ImportedFile、LanguageMarker、PathMarker和RetrievalSnippet等。这些名称其实透露了它们的具体功能。再说说 prompt 的
suffix部分,这个部分其实是由光标所在位置决定的,得看当前的 tokens 上限(2048)还有多少位置可以放内容。而这里的 Token 计算是 LLM 真实的 token 计算。在 Copilot 中,它是通过 Cushman002 进行计算的。需要注意的是,不同语言的字符 token 长度是不同的,举个例子:{ context: "console.log('你好,世界')", lineCount: 1, tokenLength: 30 },这里 context 的实际长度是 20,但 tokenLength 却是 30,因为中文字符的 token 占用情况是比较复杂的。到这里,我差不多搞定我关心的内容。Agent 包里的 TreeSitter 用于分析源码,生成
RetrievalSnippet,支持的语言主要是 Agent 内置的.wasm相关包,像 Go、JavaScript、Python、Ruby 和 TypeScript 都能用。深入 LLM 的上下文工程
上下文工程其实是帮助 LLM 更好地解决特定问题的一个方法。简单来说,就是通过给 LLM 提供一些背景信息,比如指令和示例,来引导它生成我们想要的答案或内容。这是一种有效与 LLM 沟通的技巧,能让它更清楚我们的需求,从而输出更精准的结果。
归根结底,上下文工程就是在有限的 token 空间中,传递最相关的上下文信息。
所以,我们得先明确,在特定场景下,什么算是最相关的上下文信息。
以场景与旅程为基础的上下文设计
这个思路主要是通过分析用户在各种场景下的操作和行为,来获取与当前任务相关的上下文信息,从而优化 LLM 生成的代码提示。
Copilot 会仔细分析用户在不同场景中的操作、使用 IDE 的旅程,以及与当前任务相关的指令和示例等,进而获取最相关的上下文信息。这些信息能帮助 LLM 更好地理解用户的意图,生成更准确且实用的代码提示。
比如,当用户在编写 JavaScript 代码时,Copilot 会分析用户在编辑器中的光标位置、当前文件的内容、变量和函数等信息,还有用户的输入历史和使用习惯,来生成最贴合的代码提示。这些提示不仅能提高用户的编码效率,还能帮助他们避免常见的编程错误。
就地矢量化与相似度匹配
大家可能都知道,LLM 领域一个流行的工具是 LangChain,它的处理流程类似于 langchain-ChatGLM 的总结:
加载文件 -> 读取文本 -> 文本分割 -> 文本向量化 -> 问句向量化 -> 在文本向量中匹配出与问句向量最相似的
top k个 -> 匹配出的文本作为上下文和问题一起添加到prompt中 -> 提交给LLM生成回答。在处理大规模自然语言处理任务时,Copilot 在客户端使用了 Cushman + ONNX 模型。具体来说,Copilot 将 Cushman 模型的输出转化为向量表示,然后使用向量相似度计算来找到最相关的本地文件。
除了就地矢量化与相似度匹配,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 端,Copilot 利用
CancellableAsyncPromise来及时取消请求,而在 Agent 端则结合 HelixFetcher 配置 abort 策略。当用户删除或修改输入时,之前发送的请求就会及时取消,从而减轻后端的负担。多级缓存系统
为了加快 Token 的响应速度,可以采用多级缓存系统。在 IDE 端,可以使用一些简单的策略,比如 SimpleCompletionCache;在 Agent 端则可以使用 LRU 算法的 CopilotCompletionCache,Server 端也可以有自己的一套缓存系统。
多级缓存系统的优势
多级缓存系统真的是个好东西,能大大减少后端服务的请求,让我们的响应速度飞起来。
LLM 的上下文工程未来会如何发展?
你有没有注意到,网上有一些让人惊叹不已的视频,回顾了内存有限的时代,像雅达利(Atari)和红白机这些经典,它们记录了第一段8-bit音乐和《Quake》中平方根算法的诞生,真的是个编程的奇妙世界。
而如今,LLM(大语言模型)正在不断突破上下文能力的极限,比如 Claude 就能处理高达 100K 的上下文,这让我们不禁思考,未来还需要像以前那样节约 tokens 吗?
我们还需不需要关注 LLM 的上下文呢?
当内存有限时,程序员们的创造力和想象力就显得尤为重要。但现在的内存依然不够用,因为总有一些不合格的开发者在浪费资源。因此,tokens 总是显得不够,我们或许应该关注以下几点:
优化 token 分配策略:由于 tokens 的数量有限,我们得好好优化分配策略,这样才能在有限的 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

GitHub Copilot 在上下文构建方面的能力确实令人印象深刻,尤其是在处理多语言支持时,它的自动补全和测试用例生成功能大大提高了开发效率。期待未来能看到更多的优化!
对 GitHub Copilot 的上下文构建能力感到惊讶,它不仅能识别当前文件的内容,还能参考相近文件和编辑历史,真是提升了编程效率。期待它未来能加入更多项目上下文信息!
GitHub Copilot 在上下文构建方面的表现确实出色,能够综合考虑多个文件和编辑历史,提升了代码生成的准确性。这样的能力对于开发者来说无疑是一个巨大的助力。