最近在实际的项目中接触到帮助程序员写代码的 Coding Copilot ,其中业界翘楚是 GitHub Copilot 。这个领域仍在快速发展中,有非常多的追赶者,包括而不仅限于:
Cursor - The AI-first Code Editor
AI assistant for software developers | Tabnine
AI 代码生成器 - Amazon CodeWhisperer - AWS
Ghostwriter - Code faster with AI - Replit
StarCoder: A State-of-the-Art LLM for Code
要做好一个 Coding Copilot ,除了后端的代码生成模型(比如 GitHub Copilot 用到的 Codex )至关重要外,客户端(比如 vscode 中的插件)的 Prompt Engineering 同样重要。受 Copilot 官网示例的影响,你可能和我一样,认为只需要将我们的编码需求以注释的方式或者直接将其函数定义直接发送给 Codex 模型,它就能生成我们想要的代码了。这种简化的 Prompt 也被很多项目(比如 StarCoder 、Tabnine )等采用,但写过程序的同学都知道,在实际的编码过程中,一个程序的上下文绝不仅仅只有本文件光标之前的函数定义和注释部分,还会包括光标之后的部分、引用文件、相关文件(比如 python 中同一模块下的其他文件、C 语言中的头文件)等等。如何更好地利用这些上下文,在有限的 Prompt 长度内给模型以更多的、更丰富的信息输入,这些都是 Coding Copilot 工程化要特别关注的重要议题。
作为行业领头羊的 GitHub Copilot 是商业产品,没有开源,因此无法看到它是如何工作的。不过,世上总有好奇、聪明且乐于分享的头脑,会想方设法搞懂新生事物的内部机制,然后公之于众。很有幸,在 GitHub Copilot 上,我在网上邂逅了 thakkarparth007 这位老兄。他对 GitHub Copilot 的工作机制非常好奇,同时他有非常强的 Hacking 能力,通过对 GitHub Copilot 的 VSCode 插件进行逆向工程,从而破解了 GitHub Copilot 客户端的工作秘密。乐于分享的他对此写了专门的文章,开发了一个相关的可视化代码的库:
文章:copilot-explorer
https://thakkarparth007.github.io/copilot-explorer/posts/copilot-internals
代码:Copilot-Explorer
后来发现国内的工程师也做了类似的工作:
花了大半个月,我终于逆向分析了 Github Copilot
https://zhuanlan.zhihu.com/p/639993637
它用更长的篇幅更细致地介绍了逆向工程的过程以及 Prompt 的生成细节。
在上面两篇文章的基础上,梳理一下 GitHub Copilot 在客户端是如何工作的。
为了让大家更好地理解 GitHub Copilot 的工作,先简要介绍一下其工作场景。程序员一般会在 IDE (集成开发环境)中编写代码,比如上图中,程序员老 A 正在编写一个 python 程序,用来解析日常记录的收支情况。于是他打开了一个名叫 parse_expenses.py 的文件,准备实现一个叫做 parse_expenses 的函数。这个函数包含:
函数签名。def parse_expenses(expenses_string)
部分,用来标识一个具有一定功能的模块。
注释。文中用"""包括起来的部分,用来说明这个函数是做什么、怎么做的,并举例说明。
函数体。绿色矩形框标识的部分,是函数的具体实现。
如果没有 Copilot ,老 A 需要一个字母一个字母地敲出上面的所有内容,俗称“编程”;在 Copilot 的加持下,当老 A 敲完第二个"""后,神奇的事情开始发生:函数体自动生成了,一开始以灰色字体出现;老 A 对 AI 生成的代码做一遍检查,如果没有问题(很大概率),他只需按一下 Tab 键,这段代码就被接受,变成图中靓丽的颜色,正式成为代码库的一部分,此为“AI 辅助编程”。
下面就展开说说具体的工作原理,包括:
提示词工程:Prompt Engineering
模型触发:Model Invocation
远程监测:Telemetry
最终发送给 Codex 模型的 Prompt 如下:
包括:
isFimEnable:是否是 Fill-in-middle 模式,如果 suffix 后缀部分非空,就是 Fill-in-middle 模式,即 OpenAI 官方文档中所谓的“Inserting code”:
https://platform.openai.com/docs/guides/code/inserting-code 。
suffix:Prompt 的后缀部分,在插入代码的场景中指当前光标位置后面的部分。因为 Prompt 长度有限,suffix 的长度亦会限制,由系统参数 suffixPercent 设定,默认为 15%。也就是后缀部分不能超过 Prompt 最多输入 Tokens 的 15%。
prefix:前缀部分。这个前缀不仅是指当前文件当前光标之前的部分,还包括其他相关文件的相似代码片段等内容。prefix 的计算是 Copilot Prompt 的核心部分,它提供 AI 给当前编码位置最重要的上下文。prefix 默认占整个 Prompt 长度的 85%。上面截图中 prefix 展开后的详情见:
https://thakkarparth007.github.io/copilot-explorer/posts/prompt-full
promptElementRanges:Prompt 中各种要素的范围。它说明了 prefix 的构成:
PathMarker:当前文档的路径,prefix 字符串下标 0~23 部分。即:# Path: codeviz\\app.py
。
SimilarFile:相识文件的代码片段,prefix 字符串下标 23~2219 字符串部分,以#Compare this snippet from
开始的注释部分。
BeforeCursor:顾名思义,本文件光标之前的部分,prefix 字符串下标 2219~3142 部分,剩下的、没有注释的部分。
总结为下图
其中主要内容是 Prompt 的前缀部分 prefix 的计算。它包括:
BeforeCursor
,光标前的内容,是最直观、也最容易想到的上下文
SimilarFile
,与当前文件相似度较高的内容
ImportedFile
,Import 依赖文件(现在只对 TypeScript 语言做了实现)
LanguageMarker
,文件语言标记,标识使用的编程语言,比如 Python 、PHP 、Java 等
PathMarker
,文件的路径信息
其中最为代码着力最多的部分是SimilarFile
,它获取与当前文件相似度较高的内容,作为BeforeCursor
强用力的补充,是重要的上下文信息。其计算方法如下:
查询最近访问的与当前语言相同的(最多) 20 个文件,称作备选文件,以用于提取相似代码片段
相似代码片段提取的逻辑为:
对当前文件分词,并过滤掉编程中常见的关键词(比如 if, for 等),得到当前文件 token 集合 A
用一个滑动窗口(大小为 60 行,每次滑动一行)遍历备选文件,在滑窗上做分词与过滤,得到 token 集合 B
对集合 A 与集合 B 做 Jaccard 相似度计算:J(A, B) = AB 交集的 token 数/AB 并集的 token 数
,也就是计算两个集合的重合度
取相似度最高的前 N 个滑窗对应的代码片段作为相似代码片段
然后按照下面的优先级(从高到低)做文本处理,然后组合拼装成 prefix 前缀部分:
beforeCursor
importedFile
Snippet
pathMarker
languageMarker
进而组装成上文所示的完整 Prompt 发送给后端的大模型做代码生成。
以上就是整个 Copilot 的 Prompt 生成过程。正如
硬核 Prompt 赏析:HuggingGPT 告诉你 Prompt 可以有多“工程”
中对 Prompt 的理解一样,这是一个动态过程:每次敲击键盘,都需要重新去计算对应的 Prompt ;而如何计算,反映的是研发团队对用户使用产品场景的理解;具体到编程 Copilot 中,对应的是产品对程序员写代码这件事情的了解程度。
现在的 Prompt 对应的上下文,还仅限于当前文件和最近打开的文件,随着研发的深入,其搜索的范围会逐渐扩大到当前的 codebase 、其他相关 codebase ,甚至是企业的知识库。而代码相似性的计算方法,会从现在基于文本的相似性计算,逐渐扩展到基于语义的相似性计算。这种搜索空间和相似性计算的扩展,会带来更加丰富、也更加精准的上下文信息,值得期待。
每次大模型的调用,比起传统的应用 API ,都是一个耗时费力的过程。因此,系统在满足功能实现的基础上,应尽可能减少对大模型的触发。那么,GitHub Copilot 在这方面做了哪些工作呢?
首先,在 UI 前端:
用户在一行代码的中间位置(光标后面还有其他字符)输入时,只有当光标右侧的字符是空格、括号等特定字符时,才会发送请求
用户输入速度非常快时,会有 debounce 防抖处理,允许一定时间段(默认 75ms )内只发送一次请求。
接着,在生成 Prompt 之后:
当.copilotignore 中设置了 copilotNotAvailable 时,不发送请求
当 Prompt 太短,不发送请求
当用户主动取消请求,不发送
如果 Prompt 缓存命中,直接取缓存结果,不发送请求。这里的有两层:首先是判断本次请求的 prefix 和 suffix 和上次是否一样,如果一样,直接返回上次的请求结果;如果不一样,查找系统维护的一个大小为 100 的 LRU 缓存,看看是否命中。
如果 Prompt 缓存不命中,还会通过一个机器学习模型,通过线性回归,进一步判断该 Prompt 的质量。该模型考量的因素包括前一次建议是否被采纳、和上次采纳之间的时间间隔、Prompt 最后一行的长度、光标前一个字母等等。这个模型评估的得分超过一定的阈值,Prompt 才会真正被发送给模型。
如何通过收集用户的行为,从而评估模型的效果,进而不断改进模型,是 AI 产品持续迭代的基础。MidJourney 让用户 4 选 1 ; ChatGPT 通过用户对回答赞或踩; GitHub Copilot 是通过用户对生成代码的接受或者拒绝。这些产品通过各自不同的用户交互实现了梦寐以求的用户数据飞轮。
那 GitHub Copilot 是如何评估用户对其代码的接受度的呢? GitHub 团队在 2022 年 6 月的博客中提到:有 40%的代码都是 Copilot 写成的,这个 40%是怎么得来的呢?这就涉及到 Copilot 的另一个模块:远程检测或者叫做遥测,英文是 Telemetry ,是采集远端数据,进行系统性能监控的意思。在这里是指采集用户在 vscode 、neovim 这类代码编辑器中对 Copilot 产生的代码的后续行为,从而判断其接受程度。
如何判断一段建议代码是否被接受?这不是简单计算用户通过按 Tab 键表示接受的比例,因为即使被接受,后续也可能被修改。因此为了精确计算接受率,GitHub 采取的方法是建议代码生成后,在一定的时间点( 15s, 30s, 2min, 5min and 10min )上去检测建议代码是否还保留在代码库中。具体的算法是计算原始的建议代码和代码库中插入点之后一定窗口范围内的代码之间的编辑距离,如果这个编辑距离与建议代码的长度比例小于 50%,就认为代码被接受,保留在代码库中。这个计算过程有点像计算用户的留存率,会有当日、次日、3 日、7 日、15 日、30 日等留存率指标。
这种接受率等指标的计算,往往不是在客户端进行的,因为计算指标必须在大规模用户数据的基础上才有效。因此客户端只负责收集数据,汇集到后台,离线计算指标。这里的客户端数据收集,会采集用户的代码片段,可能会涉及隐私、安全和知识产权。因此 GitHub 在用户协议中,会有 opt-out 选项,在获得许可的基础上才会回传代码片段。
在 Copilot Internals 的文章中,还多次提到 AB 测试,在 Copilot 中有拉取微软 AB 实验平台的代码。诸如后缀比例、Jaccard 计算等因素在 AB 测试中。AB 测试会在不同的用户上运行不同版本的产品,从而比较哪些参数的设定比较合理,哪些 feature 有点改进。这也是产品数据化运营的重要部分。
上面阐述的远程监控,是一个典型的数据闭环过程的基础:采集数据、计算指标以及 AB 测试用以评估产品的效用,为下一步的迭代提供决策依据。这个过程也叫做数据化运营,其核心是设计合理的核心指标来测试并改进产品的有效性。Copilot 通过用户对代码的接受或者拒绝,自带一种天然的数据收集机制,能非常好地建立数据的闭环。
除去后端的大模型训练和部署,通过对 GitHub Copilot 客户端的 Prompt Engineering (提示词工程)、Model Invocation (模型触发)和 Telemetry (远程监测)的系统了解,可以一窥 AI 产品研发的轮廓面貌:
贴近业务场景的 Prompt 是提升产品性能的关键
想办法减少模型触发可以有效降低成本
依靠远程监测,收集用户行为数据,评估产品效用,进而改善模型和系统,形成数据飞轮可构造竞争壁垒
除此之外,对比 ChatGPT 和 GitHub Copilot ,我们可以看到大模型应用的两条不同道路:
一是通用的个人助手。ChatGPT 通过聊天帮助用户快速获取知识、调研方案、撰写文案、学习新知等,是全能型的个人助理。这是 OpenAI 这样的头号玩家的赛道。
一是专业领域的 Copilot 。GitHub Copilot 专注在给程序员提供编程辅助,在这个专业领域上,用比 GPT-3.5 更弱一些的 Codex 模型( GPT-3 的一个分支),嵌入到 vscode 这样的专业工具上,大幅度提升程序员的效率。这是众多在专业领域深耕的玩家的机会。
[完]
往期相关
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.