赞
踩
【导读】代码生成由来已久,其最早可以追溯到上世纪 50 年代 FORTRAN 等高级编语言。在本文中,作者卢威从代码生成的历史发展、现实需求、技术实现、数据挑战到未来倡议,深入剖析了大语言模型(LLM)在软件编程领域的应用。
作者 | 卢威
责编 | 唐小引
出品丨GOSIM 开源创新汇
在 GOSIM 2024 欧洲站的人工智能与智能体(AI & Agents)论坛,卢威作为演讲嘉宾,带来了《大语言模型赋能软件编程:现状与倡议》(LLM for Coding, the State and Initiative)的主题演讲。演讲介绍了当前大语言模型在软件编程工作上的应用方式、技术原理和最佳实践,分析了关键的问题和解决之道,提出了开展相应开源工作的倡议。
注:本文并非文字直录之翻译,还增补了大量因演讲时间所限而未能讨论的内容。演讲视频可通过以下链接访问:
https://www.youtube.com/watch?v=P2T3MHJBvYM
https://live.csdn.net/v/389550
https://www.bilibili.com/video/BV1kz421Y7ga
代码生成的演进
卢威首先回顾了代码生成——又称程序合成——任务的历史。FORTRAN 等高级编程语言曾被视为用高级指令合成机器码的方法。后来以 Prolog 为代表的逻辑编程语言通过定义事实和规则来描述问题,然后程序可以自动推理并找出答案。进入新千年之后 UML 开始流行,一些 UML 图(例如类图和状态图)可以通过规则和模版机制自动转换为实现代码。模型驱动开发(MDD)的思想随后发展出低代码(Low-code)和无代码(No-code)概念,至今仍很活跃。表达细节和动态特性的效率不高,定制化能力有限是其主要问题。
大约 2015 年开始出现使用神经网络生成代码的研究,On End-to-End Program Generation from User Intention by Deep Neural Network 是最早的论文之一,它使用 RNN 网络结构以逐个字符的方式来生成代码。首先使用预训练语言模型来做代码理解和生成的是 104M 参数量的 CugLM 和 BERT(4M~1270M)基础上训练的 CodeBERT 模型。
在 GraphCodeBERT 等模型继续探索用数据流等代码特有的数据结构和任务来训练模型或者改善模型结构时,第一个参数量达到 10B 以上的代码模型 Codex 通过在更大更广泛的代码语料上进行训练取得了突破性的表现。此后的 ChatGPT 进一步巩固了因果语言模型(CLM)在代码任务上的主导地位。相继涌现的 Code Llama、DeepSeek Coder、CodeQwen、StarCoder 等都是这种结构。在 2023 年初之后 GPT-4 没能继续扩大规模,Agent 技术实现的“AI 程序员”、“AI 软件公司”等方案进一步提高了其能力,引发了新的关注。
整个历史是一个人类追求用抽象度更高的表达方式来生成机器指令的过程。卢威认为,继高级编程语言和模型驱动等方式之后,继续提高抽象程度和效率的机会在于,大语言模型内置的广泛知识、不言而喻的共识和多种推理能力,以及向人类提出问题的反向交互能力。
那么现实软件工程上的需要和开发者的态度是怎样呢?首先最耗时的活动还是编写代码。
调查统计表明,次常用的 AI 辅助功能也是编码。而开发者最不愿意交给 AI 代劳的也是它。这现象颇为耐人寻味。
卢威提出了另一个问题:不同经验水平的程序员对这些问题的回答是否一致?另一个调查显示了不同经验水平的开发者的不同。更少编程经验的 GitHub Copilot 用户有更高的 AI 生成代码采纳率。这是因为较少编程经验的初级用户的任务较简单所以 AI 生成质量更高?还是初级用户使用 AI 的水平更高而得到更好的结果?抑或是高级用户更能发现 AI 生成的问题从而更少采纳?这些都有待研究。这就带来任务分层的思想,将在后面讨论。
目前几个主要 AI 辅助工具的代码补全功能效果如下。
然后是整体上分析各种代码辅助功能是如何实现的,如下图。
左上部分,与以往的机器学习技术一样,数据仍是一切的基础。模型的代码能力最主要来自于 GitHub 上的开源数据,以及厂商专有的代码和文档。随着这些数据已经基本耗尽,合成数据也会扮演越来越重要的角色。代码数据不同于自然语言数据的方面需要一些特别的处理,才能进行更有效的预训练和微调。针对代码补全等需求,预训练增加中间插入(FIM)的训练任务,等等。以此实现增强了代码能力的 LLM。
图中右上角代码补全和生成测试代码等较简单的任务通过所谓“提示词工程”(Prompt engineering)一般就可以实现。
右下角所画的有特定要求的问答,比如针对给定的代码库,一般会使用检索增强生成(RAG,Retrieval Augmented Generation)技术,通过增加问题的上下文、知识库中的相关信息来控制 LLM 的生成。
在左下角,对于更复杂的任务,需要借助 LLM 本身的能力进行任务的拆解和编排,组织多次的 LLM 调用,记录对话中的信息,整合外部的工具,使得每次调用能使用更恰当的信息,逐步生成越来越完整和正确的结果。
随后,卢威对各部分的重点问题也进行了进一步的讨论。
数据
自然语言的训练主要是根据之前的文字序列预测下一个词元(Token)。而代码的结构化特性更突出,一部分代码与其之前和之后的代码以及物理存储上可能很遥远的依赖文件都高度相关。
因此,往往会将代码解析为抽象语法树(AST)后,以树的节点或者子树为单位进行数据处理。要根据代码语义上的依赖和相互调用等关系调整数据顺序。后面讨论 RAG 时也会更深入地展示相关处理。
我们常常听说一些小得多的开源模型达到或者超越了 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)技术在自然语言的问答等任务中已经得到广泛的研究和应用,这里只关注与代码特点有关的要点。
检索(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 本身作为新的工具将有助于实现这样的技术。
一个模型在所有场景满足所有用户的模式不应是唯一追求。要能够制造或选择合适的模型,以便在合适的时机和场合,为合适的人完成合适的任务,开源模式非常重要。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。