大语言模型在软件编程领域的现状及挑战
大语言模型在软件编程领域的现状及挑战
代码生成技术自上世纪50年代诞生以来,经历了从高级编程语言到模型驱动开发的演变。近年来,大语言模型(LLM)的出现为软件编程领域带来了革命性的变化。本文深入探讨了LLM在软件编程领域的应用现状、技术实现、数据挑战以及未来的发展方向。
代码生成的演进
代码生成,又称程序合成,其历史可以追溯到上世纪50年代的FORTRAN等高级编程语言。这些语言被视为用高级指令合成机器码的方法。随后,以Prolog为代表的逻辑编程语言通过定义事实和规则来描述问题,程序可以自动推理并找出答案。进入新千年之后,UML开始流行,一些UML图(例如类图和状态图)可以通过规则和模版机制自动转换为实现代码。模型驱动开发(MDD)的思想随后发展出低代码(Low-code)和无代码(No-code)概念,至今仍很活跃。然而,这些方法存在表达细节和动态特性的效率不高、定制化能力有限等问题。
大约2015年开始出现使用神经网络生成代码的研究,On End-to-End Program Generation from User Intention by Deep Neural Network是最早的论文之一,它使用RNN网络结构以逐个字符的方式来生成代码。随后,CugLM和CodeBERT等模型通过预训练语言模型来改进代码理解和生成。GraphCodeBERT等模型进一步探索用数据流等代码特有的数据结构和任务来训练模型。第一个参数量达到10B以上的代码模型Codex通过在更大更广泛的代码语料上进行训练取得了突破性表现。此后的ChatGPT进一步巩固了因果语言模型(CLM)在代码任务上的主导地位。相继涌现的Code Llama、DeepSeek Coder、CodeQwen、StarCoder等都是这种结构。2023年初之后,Agent技术实现的“AI程序员”、“AI软件公司”等方案进一步提高了LLM的能力,引发了新的关注。
整个历史是一个人类追求用抽象度更高的表达方式来生成机器指令的过程。大语言模型内置的广泛知识、不言而喻的共识和多种推理能力,以及向人类提出问题的反向交互能力,为继续提高抽象程度和效率提供了新的机会。
现实需求与开发者态度
调查显示,编写代码是最耗时的软件工程活动之一,而AI辅助编码功能的使用频率仅次于代码补全。有趣的是,开发者最不愿意将编写代码的任务交给AI代劳。这种现象值得深思。进一步研究发现,不同经验水平的程序员对AI的态度存在差异。GitHub Copilot的数据显示,较少编程经验的初级用户有更高的AI生成代码采纳率。这可能是由于初级用户的任务较简单,AI生成质量更高;也可能是初级用户使用AI的水平更高,得到更好的结果;或者高级用户更能发现AI生成的问题,从而更少采纳。这些现象表明,任务分层的思想值得进一步探讨。
主要AI辅助工具的代码补全功能效果
目前,主要的AI辅助工具在代码补全功能方面表现出不同的效果。GitHub Copilot、Tabnine、Kite等工具在代码补全方面的表现各有优劣。GitHub Copilot在代码补全方面表现最佳,而Tabnine在代码生成方面表现突出。这些工具的性能差异主要体现在代码的准确性和完整性方面。
代码辅助功能的实现方式
整体来看,各种代码辅助功能的实现方式如下图所示:
数据基础:与传统机器学习技术一样,数据是基础。模型的代码能力主要来自于GitHub上的开源数据以及厂商专有的代码和文档。随着这些数据逐渐耗尽,合成数据将扮演越来越重要的角色。代码数据不同于自然语言数据,需要特别处理才能进行更有效的预训练和微调。针对代码补全等需求,预训练增加了中间插入(FIM)的训练任务等,以增强代码能力。
代码补全与生成:较简单的任务如代码补全和生成测试代码,通常通过“提示词工程”(Prompt engineering)实现。
特定要求的问答:对于有特定要求的问答,如针对给定的代码库,一般会使用检索增强生成(RAG,Retrieval Augmented Generation)技术,通过增加问题的上下文和知识库中的相关信息来控制LLM的生成。
复杂任务处理:对于更复杂的任务,需要借助LLM本身的能力进行任务拆解和编排,组织多次的LLM调用,记录对话中的信息,整合外部工具,使得每次调用能使用更恰当的信息,逐步生成越来越完整和正确的结果。
数据处理与模型训练
自然语言的训练主要是根据之前的文字序列预测下一个词元(Token)。而代码的结构化特性更突出,一部分代码与其之前和之后的代码以及物理存储上可能很遥远的依赖文件都高度相关。因此,往往会将代码解析为抽象语法树(AST)后,以树的节点或者子树为单位进行数据处理。要根据代码语义上的依赖和相互调用等关系调整数据顺序。
我们常常听说一些小得多的开源模型达到或者超越了GPT-4的编码能力,其“秘诀”的核心是用合成指令数据来对开源模型进行监督微调(SFT)。最新的StarCoder2-Instruct项目值得重点关注,因为之前生成高质量数据往往使用ChatGPT-3.5/4或者Llama-70B等最大的模型,存在使用许可证和成本问题。而这个研究发现参数量小的多而且开放使用许可的StarCoder2-15B模型也能生成数据来提高自身的任务表现。
类似项目的经验可以总结和提炼出以下要点:
- 高质量的种子例子、人类的知识、标准操作流程(SOP)是合成数据的基础
- 现在的训练数据往往只有简单的意图记录和最终的代码,工程上大量的中间生成物(artifacts)很有价值但还没有加入进来,比如设计文档等。代码的commit历史等变化过程已经被关注了,比如diff models(https://carper.ai/diff-models-a-new-way-to-edit-code/)。
- “智力飞轮”(Intelligence flywheel)效应——较弱模型合成数据训练较强的自己——形成的可能性提高了。
除了训练,评估模型的性能也需要数据集。目前主要是OpenAI的HumanEval数据集及其衍生产物,都存在相似的问题,包括评测任务是算法和竞赛题,目标是生成独立无依赖的函数(Function),与现实软件开发工作差异大;在训练中容易被污染,不能体现模型真实的能力。SWE-bench是一个新的评测,目标是解决真实GitHub项目中的Issue,但是对目前的模型来说还太困难,成功率都非常低。现实的软件工程期待更实用的评测:任务更接近真实软件开发场景,包含更复杂的依赖和上下文关系,提供更准确的指标和测试方法。另外就是前面谈到的问题,模型不仅与人类的较高水平比较,也要跟不同水平标准比较,以便为重构软件工程组织方式和工具做准备。因此工程实践中除了普遍的Copilot辅助方式,还应该将工作任务进行更细致的层次划分,以便模型能以足够高的成功率全自动完成较容易的任务。比如对一个有多种经验程度的开发者参与的项目进行分析,提取不同难度层次的任务和产出物,形成评测和训练数据集。目前的模型也可以对这种分析工作提供帮助。参考下例里ChatGPT以不同职级的程序员角色编写Hello World程序的不同表现。
提示词工程(Prompt Engineering)
相对最简单的代码补全任务往往作为IDE的辅助功能提供,必须平衡生成代码的质量和延迟,一般不会使用大于百亿的参数量规模。GitHub Copilot等实现都会尽量先使用基于规则的判断和调用缓存来尽量减少调用AI模型。然后根据当前IDE中编辑的光标上下文,所有打开的文件和显式的依赖等来组织出信息丰富的同时又尽可能简短的提示词。对模型进行针对性的监督训练,或者以下讨论的RAG技术,也有项目采用以获得更高的生成质量。因为代码具有简单和严格的语法结构,很多情况下一系列Token大概率呈现一致的顺序,因此可以采用推测解码算法(Speculative Decoding)提高生成速度,可参考此例https://github.com/FasterDecoding/REST。
对模型生成结果的验证也是个难点,用户要快速判断是否采用是相当大的认知负担。研究(https://arxiv.org/abs/2206.15000)发现用户倾向于在生成结果中寻找特定的关键词或控制结构来判断是否采用。可以通过在UI上显示该生成所依据的上下文信息,自动比对编程规范,自动编译、执行代码或者调用静态检查工具等手段来帮助用户判断。第一个结果的响应时间要求很高,在用户进行判断时第二第三个可以以稍高的延迟完成,可以使用规模更大的模型进行推理,多个生成也可以相互对比得到更好的结果。此类IDE工具不但应监测性能指标,同时也可以持续记录用户编辑代码的过程,形成大量可用于训练和微调的数据,比随机“挖空”已完成的代码而得到的训练数据质量更高。
检索增强(RAG)
检索增强(RAG)技术在自然语言的问答等任务中已经得到广泛的研究和应用,这里只关注与代码特点有关的要点。检索(Retrieve)到正确的信息——知识库单元,文档(Document)——自然是首先要解决的问题。代码领域通常的实践是把函数或者方法作为最小单元,通过AST得到。代码片段之上还要补充编程语言、文件路径、行号等等元数据(Metadata)信息,建立索引后用FAISS等算法工具来搜索,或者借助现成的向量数据库。
一个现实中的软件项目代码库往往包含数量庞大的函数和方法。如果要基于代码库来生成新代码,或者与AI进行问答以便理解和分析,则需要将代码库转变为一个知识库。一种方式是对函数和方法及其元数据用LLM做概括(summary),同一个代码文件或类的概括聚合起来再做概括,同一个文件夹(directory)或包(package)再一层层做概括。另外用代码静态分析工具提取调用图(Call-graph),图中的节点用LLM做概括,节点及其依赖的节点一起再做概括,并沿着图做层层概括卷积。这些概括用嵌入模型转化为向量以便进行语义检索。这样建立一个抽象层次层层提高的知识库(KB)。
另外,在前面程序合成的历史中讨论到2021-2022年进行了大量代码小模型的研究,他们采取的分析数据流等方法也可以借鉴到这里的知识库的构建。知识图谱(Knowledge Graph),例如GraphGen4Code(https://wala.github.io/graph4code/)可以作为另一种底层结构,并且能补充元数据,帮助改善概括操作的效果,然后多层次地概括以提升抽象程度仍是有必要的。实践中经常是把基于向量的语义检索与基于关键字的检索结合起来使用。召回超过需要的结果数量后再用排序(re-rank)模型或者直接用生成模型选择最合适的少数结果。因为两种检索的召回率实际都很难达到非常高的水平,而性能更好的cross-encoder模式或者规模更大的生成模型计算量都太大,所以采取多召回再排序和甄选的方式来平衡性能和计算量,这与自然语言任务相同。而新出现的超大上下文窗口(比如Gemini的1M)能容纳整个代码库的话,相当于在推理时自动建立了一个神经网络知识库(Neural KB),有可能比上述人工建立的层次化知识库(Hierarchical KB)达到更优化的状态。
智能体(Agent)技术
除了前面提到的Agent的四个要素,现在的做法往往还会引入多个角色,实现Multi-Agent System。AgentCoder项目就用LLM实现了程序员(Programmer)和测试设计师(Test Designer)Agent,用Python运行时实现了测试执行者(Test Executor)Agent。实际上Agent的大部分秘密都存在Prompt里,在代码或配置文件中找出来就很容易理解。GPT-Pilot、MetaGPT、ChatDev这些以模拟软件公司为目标的方案都根据现在软件开发项目的经验实现了多种角色,制定了相似的工作步骤和流程。值得思考的是因为AI的引入,角色和流程应引入什么不同的修改?2024年一个备受关注的新闻是“Devin, 第一个AI软件工程师”,SWE-Agent是一个开源实现。它与AgentCoder和GPT-Pilot没有本质上的不同,只是实现了更适合Issue处理所需的工具(tools)和工作流程。
总结Agent技术用于软件编程,供LLM使用的工具和标准操作流程(SOP)是主要内容。LLM是否能真正理解问题,执行逻辑推理,还是存在争议的。但很多实证研究也显示我们可以操控LLM以达到足够好的输出,通过借鉴组织人类工作的方法。而人类也往往是非理性和不精确的。
开源行动倡议
最后,基于以上分析,提出开源行动倡议。数据(不一定要“大数据”,因为小数据也可以借助LLM的力量合成大数据),比如种子案例、各种领域的标准操作流程、工程中间生成物等,将成为像以往的代码一样的开源运动重要角色。新的数据脱敏和匿名化技术的开发也越来越有必要,以便专有数据的拥有者能够放心将数据开放。差分隐私,以及LLM本身作为新的工具将有助于实现这样的技术。一个模型在所有场景满足所有用户的模式不应是唯一追求。要能够制造或选择合适的模型,以便在合适的时机和场合,为合适的人完成合适的任务,开源模式非常重要。