背景
GitHub Copilot 是 GitHub 和 OpenAI 提供的一项新服务,被描述为“您的 AI 结对程序员”。它是 Visual Studio Code 的一个插件,可以根据当前文件的内容和您的光标位置自动为您生成代码。
使用起来感觉相当神奇。例如,这里我输入了一个函数的名称和文档字符串,它应该“将文本写入文件 fname”
函数灰色的函数体完全由 Copilot 为我写好了!我只需按下键盘上的 Tab 键,建议就会被接受并插入到我的代码中。
这当然不是第一个“AI 驱动”的程序合成工具。GitHub 于 2018 年推出的 自然语言语义代码搜索 演示了如何使用简单的英语描述查找代码示例。Tabnine 提供“AI 驱动”的代码补全功能已有几年了。Copilot 的不同之处在于,它可以基于代码文件的完整上下文生成完整的、包含多行的函数,甚至文档和测试。
这对于我们 fast.ai 来说尤其令人兴奋,因为它有望降低编程门槛,这将极大地帮助我们实现 我们的使命。因此,我特别热衷于深入研究 Copilot。然而,正如我们将看到的,我尚未确信 Copilot 实际上是一种祝福。它甚至可能最终成为一种诅咒。
Copilot 由一个名为 Codex 的深度神经网络语言模型提供支持,该模型在 GitHub 上的公共代码仓库上进行了训练。这对我来说特别有意义,因为早在 2017 年,我是第一个证明通用语言模型可以通过微调在各种自然语言处理(NLP)问题上获得最先进结果的人。我开发并在 fast.ai 课程中展示了这一点。Sebastian Ruder 和我随后完善了该方法并撰写了一篇论文,该论文于 2018 年由计算语言学协会(ACL)发表。OpenAI 的 Alec Radford 告诉我,这篇论文启发了他创建了 GPT,而 Codex 就是基于 GPT 的。以下是该课程中的一个片段,我首次展示了语言模型微调在分类 IMDB 情感方面取得了最先进的结果:
语言模型被训练用于猜测文本中缺失的词语。前几年使用的传统“ngram”方法在这方面表现不佳,因为正确猜测需要上下文。例如,思考一下你将如何填写以下每个示例中缺失的词语:
知道在一种情况下“hot day”(热天)是正确的,而在另一种情况下“hot dog”(热狗)是正确的,这需要阅读并(在某种程度上)理解整个句子。Codex 语言模型学习猜测编程代码中缺失的符号,因此它必须学习大量关于计算机代码的结构和含义的知识。正如我们稍后将讨论的,语言模型确实存在一些显著的局限性,这些局限性根本上源于它们的创建方式。
Copilot 基于各种许可下的公共代码进行训练的事实,引发了关于其道德和法律影响的许多讨论。由于这一点已广泛讨论,我在此不再深入阐述,仅指出 IP 律师 Kate Downing 讨论的一个明确的法律问题,即在某些情况下,使用 Copilot 的建议可能构成违反许可协议(或要求将您自己的作品在 GPL 兼容许可下重新授权)
“建议越复杂、越冗长,就越有可能包含某种受版权保护的表达。”
使用示例
在我们更深入地探讨 Copilot 之前,先来看几个它在实际应用中的示例。
为了知道那个自动生成的 write_text
函数是否真的有效,我们需要一个测试。让 Copilot 也来写测试吧!在这种情况下,我只需要输入测试函数的名称,Copilot 就为我填写了文档字符串:
接受该建议后,Copilot 有点困惑,并建议了一个无意义的函数,其中包含许多行几乎重复的代码:
没问题 – Copilot 可以通过按下 Ctrl-Enter 向我们显示其他建议选项。列表中第一个看起来相当合理(除了第一行中有一个奇怪的额外 Tab 字符外)
在这里,Copilot 假定有一个名为 read_text
的可用函数,但它实际上并不存在。不过,我们也可以很容易地请 Copilot 为我们编写并测试这个函数。
我还请 Copilot 为我创建一个函数,用于“将目录 dir
打包(tar)并可选地压缩到 dest
,格式为 bz2、xz 或 gzip”,结果生成了
def tar_dir(dir, dest):
"Tar and optionally compress directory `dir` to `dest` in bz2, xz, or gzip format"
if dest.endswith('.bz2'):
with tarfile.open(dest,'w:bz2') as f: f.add(dir, arcname=os.path.basename(dir))
elif dest.endswith('.xz'):
with tarfile.open(dest,'w:xz') as f: f.add(dir, arcname=os.path.basename(dir))
elif dest.endswith('.gz'):
with tarfile.open(dest,'w:gz') as f: f.add(dir, arcname=os.path.basename(dir))
else: raise ValueError('bad dest: {}'.format(dest))
我还让 Copilot 使用与上述相同基本方法创建了一个测试,它写下了
with TemporaryDirectory() as tmp:
'a.txt'),'hello')
write_text(os.path.join(tmp,'b.txt'),'world')
write_text(os.path.join(tmp,'out.tar.bz2')
tar_dir(tmp,assert os.path.exists('out.tar.bz2')
with tarfile.open('out.tar.bz2') as f:
assert f.getnames()==['a.txt','b.txt']
该测试实际上并未通过,因为最后一行中的 getnames
包含了父目录,但这很容易修复。Copilot 甚至巧妙地决定使用我之前创建的 write_text
函数,这是我没有预料到的。
你甚至可以用 Copilot 来写散文。我现在正在 vscode 中写这篇博文,刚刚点击了“启用 Copilot”按钮。在我输入上一句话后,以下是 Copilot 推荐的补全内容:
“我现在可以用一行文本写我的博文,Copilot 将为我生成博文的其余部分”
显然,Copilot 对自己的散文生成能力有点过于高估了!
代码问题
Copilot 生成的代码质量不高。例如,考虑上面的 tar_dir
函数。其中有很多重复的代码,这意味着将来需要维护的代码更多,读者理解起来也更费劲。此外,文档字符串说“可选压缩”,但生成的代码却总是进行压缩。我们可以通过改为这样写来解决这些问题:
def tar_dir(dir, dest):
"Tar and optionally compress directory `dir` to `dest` in bz2, xz, or gzip format"
= ':' + Path(dest).suffix[1:]
suf if suf==':tar': suf=''
with tarfile.open(dest,f'w{suf}') as f: f.add(dir, arcname=dir)
一个更大的问题是,write_text
和 tar_dir
本来就不应该自己写,因为 Python 的标准库已经提供了这两种功能(分别是 pathlib 的 write_text
和 shutil 的 make_archive
)。标准库版本也更好,pathlib 的 write_text
提供了额外的错误检查并支持文本编码和错误处理,而 make_archive
支持 zip 文件以及您注册的任何其他存档格式。
为什么 Copilot 生成的代码不好
根据 OpenAI 的论文,Codex 只有 29% 的时间能给出正确的答案。而且,正如我们所见,它生成的代码通常重构得很差,并且未能充分利用现有解决方案(即使这些解决方案就在 Python 标准库中)。
Copilot 读取了 GitHub 的全部公共代码存档,其中包含数千万个仓库,包括许多世界顶尖程序员的代码。鉴于此,为什么 Copilot 生成的代码如此糟糕?
原因是语言模型的工作方式。它们展示了大多数人平均如何编写代码。它们没有正确或好的概念。GitHub 上的大多数代码(按软件标准衡量)相当陈旧,并且(根据定义)由普通程序员编写。Copilot 吐出其最佳猜测,推测这些程序员如果编写与您相同的文件可能会写什么。OpenAI 在其 Codex 论文中讨论了这一点
“与其他基于下一词元预测目标训练的大型语言模型一样,Codex 将生成与训练分布尽可能相似的代码。这样做的结果之一是,此类模型可能会做一些对用户没有帮助的事情”
Copilot 比那些普通程序员更差的一个重要方面是,它甚至不会尝试编译代码或检查其是否有效,也懒得考虑它是否真正实现了文档中说明的功能。此外,Codex 没有针对近一两年的代码进行训练,因此完全缺少最新的版本、库和语言特性。例如,提示它生成 fastai 代码,结果只会给出使用 v1 API 的建议,而不是大约一年前发布的 v2 API。
抱怨 Copilot 生成的代码质量,有点像遇到一只会说话的狗,然后抱怨它的发音。它能说话本身就已经足够令人印象深刻了!
明确一点:Copilot(和 Codex)能够生成看起来合理的代码,这本身就是一项了不起的成就。从机器学习和语言合成研究的角度来看,这是一个巨大的进步。
但我们也需要清楚地认识到,那些看起来合理但不起作用、不检查边缘情况、使用过时方法、冗长且产生技术债务的代码,可能会成为一个大问题。
自动生成代码的问题
代码创建工具的历史几乎和代码本身一样悠久。而且它们在其整个历史中一直备受争议。
大多数编程时间并非花在编写代码上,而是花在设计、调试和维护代码上。当代码自动生成时,很容易生成大量代码。如果您只需要修改代码自动生成的源文件(例如使用代码模板工具时)就可以维护或调试它,那么这不一定是问题。即使如此,调试时也可能会变得令人困惑,因为调试器和堆栈跟踪通常会指向冗长的生成代码,而不是模板源。
而使用 Copilot,我们并没有这些优势。我们几乎总是不得不修改它生成的代码,而且如果想改变其工作方式,也不能简单地回去修改提示词。我们必须直接调试生成的代码。
经验法则告诉我们,代码越少,维护和理解的工作量就越少。Copilot 生成的代码很冗长,而且太容易生成大量代码,很可能最终会得到很多代码!
Python 拥有丰富的动态和元编程特性,极大地减少了对代码生成的需求。我听到不少程序员说他们喜欢 Copilot 为他们编写大量的样板代码(boilerplate)。然而,我几乎从不编写任何样板代码——过去任何时候我发现自己需要样板代码时,我都会使用动态 Python 将样板代码重构掉,这样我就不再需要编写或生成它了。例如,在 ghapi 中,我使用动态 Python 创建了一个完整的 GitHub API 接口包,大小仅 40kB(相比之下,用 Go 编写的类似包包含超过 100,000 行代码,其中大部分是自动生成的)。
一个非常有启发性的例子是当我用以下提示词引导 Copilot 时发生的事情:
def finetune(folder, model):
"""fine tune pytorch model using images from folder and report results on validation set"""
只需少量额外输入,它就几乎完全自动地生成了这 89 行代码!从某种意义上说,这确实令人印象深刻。它确实基本上完成了所要求的任务——微调一个 PyTorch 模型。
然而,它微调模型的方式很差。这个模型训练会很慢,并且导致精度不高。正确微调模型需要考虑诸如处理 batchnorm 层统计信息、在微调主体之前微调模型头部、正确选择学习率、使用适当的退火调度等。此外,我们可能希望在过去几年创建的任何 CUDA GPU 上使用混合精度训练,并且很可能希望添加更好的数据增强方法,例如 MixUp。修改代码以添加这些功能需要额外数百行代码和大量深度学习专业知识,或者使用更高级别的 API,例如 fastai,它可以在 4 行代码中微调 PyTorch 模型,从而获得更高精度、更快且更具扩展性的结果。)
我不太确定 Copilot 在这种情况下最好怎么做。我不认为它目前的功能在实践中真正有用,尽管它是一个看起来令人印象深刻的演示。
使用正则表达式解析 Python
我向 fast.ai 社区询问了 Copilot 在为他们编写代码方面提供帮助的示例。有人告诉我,当他们编写正则表达式从包含 python 代码的字符串中提取注释时(因为他们想将函数中的每个参数名称映射到其注释),Copilot 帮了大忙。我决定自己试试。以下是给 Copilot 的提示词:
= """def connect(
code_str host:str, # host to connect to
port:int=80, # port to connect to
ssl:bool=True, # whether to use SSL
) -> socket.socket: # the connected socket
"""
# regex to extract comments from strings looking like code_str
以下是生成的代码:
= re.compile(r'^\s*#.*$', re.MULTILINE) comment_re
这段代码不起作用,因为 ^
字符错误地将匹配绑定到行首。它也没有真正捕获注释,因为它缺少捕获组。(Copilot 的第二个建议正确地去掉了 ^
字符,但仍然没有包含捕获组。)
然而,与这段代码的大问题相比,这些都是小问题,即正则表达式实际上无法正确解析 Python 注释。例如,这个例子就会失败,因为 tag_prefix:str="#"
中的 #
会被错误地解析为注释的开头
code_str = """def find_tags(
input_str:str, # the string to search for tags
tag_prefix:str="#" # prefix marking the start of a tag
) -> List[str]: # list of all tags found
事实证明,使用正则表达式无法正确解析 Python 代码。但 Copilot 做了我们要求的:在提示注释中,我们明确要求使用正则表达式,而 Copilot 就给了我们一个正则表达式。提供此示例的社区成员在编写代码时正是这样做的,因为他们认为正则表达式是解决此问题的正确方法。(尽管即使我尝试从提示中删除“regex to”,Copilot 仍然建议使用正则表达式解决方案。)在这种情况下,问题并非 Copilot 做得有错,而是它被设计去做的事情可能不符合程序员的最大利益。
GitHub 将 Copilot 宣传为“结对程序员”。但我不确定这是否真正描述了它的功能。一个好的结对程序员会帮助你质疑你的假设、发现潜在问题并看到更广阔的局面。Copilot 并没有做这些事情——恰恰相反,它盲目地假定你的假设是恰当的,并完全专注于根据你的文本光标当前所在位置的即时上下文来生成代码。
认知偏差与 AI 结对编程
AI 结对程序员需要与人类良好协作。反之亦然。然而,人类尤其存在两种认知偏差,这使得协作变得困难:自动化偏差和锚定偏差。由于人类的这两大弱点,即使我们明确尝试不依赖,我们所有人都会倾向于过度依赖 Copilot 的建议。
维基百科将自动化偏差描述为
“人类倾向于偏爱自动化决策系统的建议,并忽视非自动化的矛盾信息,即使这些信息是正确的”
自动化偏差已经被认为是医疗保健领域的一个重要问题,该领域广泛使用计算机决策支持系统。在司法和警务领域也有许多例子,例如加利福尼亚州的一位市官员错误地描述了用于预测性警务的 IBM Watson 工具:“有了机器学习,有了自动化,成功率是 99%,所以那个机器人——将会——在告诉我们接下来会发生什么方面有 99% 的准确率”,导致市长说:“嗯,为什么我们不安装 .50 口径的枪?”(他声称自己是“开玩笑的”。)这种对 AI 能力夸大的信念也可能影响 Copilot 的用户,尤其是那些对自身能力不太自信的程序员。
决策实验室将锚定偏差描述为
“一种认知偏差,它使我们过于依赖关于某个主题我们收到的第一条信息。”
锚定偏差已被广泛记录,并在许多商学院作为一种有用的工具进行教授,例如在谈判和定价中。
当我们在 vscode 中输入时,Copilot 会完全自动地跳出来并建议代码补全,无需我们进行任何交互。这通常意味着在我们还没真正有机会思考要往哪个方向走时,Copilot 已经为我们规划了一条路径。这不仅是我们得到的“第一条信息”,而且也是“来自自动化决策系统的建议”的一个例子——我们正在经历双重认知偏差的冲击,需要去克服!而且这种情况不是只发生一次,而是每次我们在文本编辑器中多输入几个字时都会发生。
不幸的是,关于认知偏差,我们了解的一件事是,仅仅意识到它们并不足以避免被它们愚弄。因此,这不是 GitHub 仅通过谨慎地展示 Copilot 建议和用户教育就能解决的问题。
Stack Overflow、Google 和 API 使用示例
通常情况下,如果程序员不知道如何做某事,并且没有使用 Copilot,他们会去 Google 搜索。例如,我们之前讨论过的那个想在包含代码的字符串中找到参数和注释的程序员,可能会搜索诸如:“python extract parameter list from code regex”这样的内容。此次搜索的第二个结果是一个 Stack Overflow 帖子,其中有一个被接受的答案,正确地指出这无法用 Python 正则表达式完成。该答案反而建议使用 pyparsing 等解析器,例如 pyparsing。然后我尝试搜索“pyparsing python comments”,发现这个模块恰好解决了我们的问题。
我还尝试搜索“*extract comments from python file”,得到了一个第一个结果,展示了如何使用 Python 标准库的 tokenize 模块来解决这个问题。在这种情况下,提问者在介绍他们的问题时说:“我正在尝试编写一个程序来提取用户输入的代码中的注释。我尝试使用正则表达式,但发现很难写。”听起来很耳熟!
这比找到一个能让 Copilot 提供答案的提示词多花了几分钟,但它让我对问题和可能的解决方案空间有了更深入的了解。Stack Overflow 上的讨论帮助我理解了处理 Python 中带引号字符串的挑战,并解释了 Python 正则表达式引擎的局限性。
在这种情况下,我觉得 Copilot 的方法对经验丰富的程序员和初学者来说都更糟糕。经验丰富的程序员需要花时间研究提出的各种选项,发现它们不能正确解决问题,然后仍然不得不在线搜索解决方案。初学者可能会觉得他们已经解决了问题,但实际上并没有学习到需要了解的关于正则表达式局限性和能力的东西,最终会在没有意识到的情况下得到有问题的代码。
除了 Copilot,GitHub 的所有者微软还创建了另一个相关但不同的产品,名为“API 使用示例”。以下是直接从他们的网站截取的示例:
这个工具会在网上查找其他人使用您正在使用的 API 或库的示例,并提供真实的示例代码,展示如何使用它,同时附带示例源代码的链接。这是一种有趣的介于 Stack Overflow(但缺少有价值的讨论)和 Copilot(但没有根据您的特定代码上下文提供建议)之间的方法。这里关键的额外部分是它提供了源代码链接。这意味着程序员实际上可以看到其他人如何使用该功能的完整上下文。提高编程技能的最佳方法是阅读代码和编写代码。帮助程序员找到相关的代码进行阅读,似乎是一种既能解决问题又能帮助他们提升技能的绝佳方法。
微软的 API 使用示例功能最终是否会变得出色,实际上取决于他们按代码质量进行排名以及展示最佳使用示例的能力。据产品经理(在 Twitter 上)称,他们目前正在研究这一点。
结论
我仍然不知道本文标题中的问题“GitHub Copilot 是福是祸?”的答案。对于一些人来说它可能是福,对于另一些人来说它可能是祸。对于那些受其祸害的人来说,他们可能多年后才会发现,因为这种祸害在于他们学习得更少、学习得更慢、增加了技术债务并引入了细微的错误——这些都是你很可能不会注意到的问题,特别是对于新晋开发者来说。
对于样板代码多且元编程功能有限的语言,例如 Go,Copilot 可能更有用。(出于这个原因,很多人今天使用模板代码生成来编写 Go 代码。)它可能特别适合的另一个领域是经验丰富的程序员处理不熟悉的语言,因为它可以帮助他们掌握基本语法,并指出库函数和常见用法习惯。
需要记住的是,Copilot 是一个非常新技术的早期预览版,它会越来越好。在接下来的几个月和几年里,会出现许多竞争对手,GitHub 毫无疑问也会发布他们工具的新版本和更好的版本。
要看到程序合成方面的真正进步,我们需要超越仅仅使用语言模型,转向一个更全面的解决方案,该方案结合了人机交互、软件工程、测试和许多其他学科的最佳实践。目前,Copilot 感觉更像是机器学习研究人员设计和实现的产品,而不是一个包含所有必要领域专业知识的完整解决方案。我相信这种情况会改变。