大模型在解释代码、回答代码问题、写单元测试等方面表现不错,但这些还只是辅助任务,真实项目需求开发中的设计及实现任务才是核心任务,而这方面尚未有成熟的方法和好的效果。一些 AI Developer 工具能够演示从零创建小应用的能力,但放到真实项目中几乎寸步难行。
在不久前举办的 AICon 全球人工智能开发与应用大会上,研发效能领域知名专家路宁做了专题演讲“大模型辅助需求代码开发”,结合在多类项目中借助 GPT-4 开发完整需求的实践经验,分享了针对复杂度持续增加的编码任务如何准备上下文、如何区别对待设计任务和实现任务、如何加工经验知识并利用它大幅提升生成效果、如何分步完成复杂的开发任务等不同场景下的问题解决思路,还探讨了组织或企业该如何建设知识工程,以便有力支撑工程师在核心开发任务中利用大模型提效。
在将于 2024 年 10 月 18-19 日举办的 QCon 全球软件开发大会(上海站),我们设置了【AI 重塑技术工作流程】这一专题,旨在探索那些超越单点 AI 应用,进一步利用 AI 技术重塑产品研发核心流程的最佳实践。我们关注实际案例和解决问题的策略,旨在解决当前研发团队面临的困境,让智能能真正赋能业务,创造价值。欲了解更多内容,可访问大会官网获悉。
以下是演讲实录(经 InfoQ 进行不改变原意的编辑整理)。
今天我将与大家分享的主题是《大模型辅助需求代码开发》,这个话题的含义可能非常广泛,容易引起误解,因此我给它加了一个副标题:“探索提升核心编码任务生成效果的方法”。背后的想法其实很简单:我是否能够在不写或少写代码的情况下完成真实项目的开发任务,特别是在面对一些难度高的编码任务时。那么,这些任务应该如何去探索和解决呢?
本次演讲的内容主要基于我们过去半年的工作,我将分享一些实践中发现的洞见。演讲将分为几个部分:首先,我会讨论大模型辅助开发的生态系统以及开发实际需求面临的挑战;其次,我将从知识生产和消费的角度来探讨如何生成这些任务的代码,并总结背后的知识分类;最后,我将探讨未来工程师如何利用大模型进行开发,最可能的形态是怎样的,是使用一个很强的 SaaS 工具,还是需要更多的方法和策略。
⼤模型辅助开发⽣态
任务全貌
在探讨大模型辅助开发时,我们首先要明确,是针对那个具体的任务。工程师的工作通常包含很多类的任务,如需求分析、设计、代码实现、修复错误等。这些是大类,其下还有更细致的任务类型。
从模型能力的角度来看,有一种特殊的任务是代码补全,但今天我们不讨论这个。而剩下的各类任务大多可以通过模型问答的形式来处理。我们注意到,目前效果较好的任务往往是下图中用蓝色标记的部分,我将这些任务定义为支撑性任务。与支撑性任务相对的是主干任务,这些任务是核心的不可或缺的工作步骤,包括需求分析、架构设计、任务规划、编码实现,以及后续的编译和测试。如果一切顺利,完成这些任务就能实现需求。
然而,实际情况往往并非如此顺利。在每个阶段都可能遇到问题,比如在编码阶段可能需要理解代码,或者需要编写单元测试。部署后如果出现问题,还需要分析日志、定位缺陷、以及修复问题。主干任务在图中用绿色标记,目前看来,大模型在这些任务上的表现都不尽如人意。相比之下,支撑性任务由于其难度往往相对低,很多能够取得较好的效果。
如何在主干任务中利用大模型来提高开发效率,我们可以通过几种不同的模式来实现这一点。最理想的情况是,我们能够将任务完全交给大模型或者基于大模型的应用来独立完成。如果这种自动化的效果并不理想,我们可能需要采取一种合作的模式,类似于 Copilot。在这种模式下,大模型会先执行一步操作,然后我进行检查和修正,之后再进行下一步,如此推进,直到最终得出满意的结果。对于更加复杂的任务,比如需求分析,大模型可能无法独立完成,但它可以提供一些启发和建议,这对我来说已经非常有帮助了。应用那种模式完全取决于效果,是当前技术能力的局限性所导致的。
主⼲任务相关应⽤现状
目前,几家大公司出的工具都在尝试从需求澄清、架构设计、任务分解到具体任务实现,端到端地引导工程师完成开发任务。这些实践在体验上与 GitHub Copilot 的 Workspace 相似。还有一些工具,如 Devin,它自称为第一个 AI Developer,擅长从零开始开发一些相对简单的 Web 应用,其公司也获得了可观的投资。然而,尽管这些工具在某些情况下表现出色,但要将它们应用到公司的实际开发环境中,表现就比较差。
还有部分人选择手动调用大模型来完成他们的主干任务。为什么要先手动操作呢?因为这样方便针对项目代码和具体任务准备针对性的上下文和知识,以达到可接受的生成效果。事实上,手动操作是能探索到效果的天花板的,因为可以投入大量时间来为模型准备上下文和相关知识,如果这样的效果还不理想,那么使用自动化工具的效果只会更差。从手动操作中总结的经验,可以反过来促进工具的改进和发展,这是一个不断迭代和优化的过程,以提高整体的开发效率和质量。
了解测评背后的任务
我们不得不指出,行业现有的评测方法过于简单化,无法代表真实需求编码任务。例如,GitHub Copilot 声称能够提升 55% 的工作效率,它的测试是基于两组工程师的一次实验,使用 JavaScript 开发一个 Web 服务器。多数人在一个多小时就能完成这样的任务,任务不涉及私有知识,这与我们日常工作中的任务相去甚远。现有的评测数据集,如 HumanEval,通常只包含函数级别的任务,这些任务的上下文在函数范围内就能描述清楚。
SWE Bench 是一个取自真实代码任务的评测集,由美国几所高校的学生创建。它从多个 GitHub 上的项目中挑选 issue 作为测试任务,这些 issue 可能涉及增强功能、修复 bug 或新的需求。如果这些 issue 背后有测试用例,实际上可以自动验证任务是否被正确实现。当我们刚开始接触这个评测集时,只有 Devin 作为一个应用来打榜,它的解决率达到了 13%,其他基于模型 + RAG 的方案表现都很差。随着时间的推移,这个评测变得越来越受欢迎,因为它比其他评测集更接近真实的工作场景。现在,一些应用能够做到 40% 以上的解决率,这是一个相对较高的数字。
SWE Bench 的问题在于它使用的是开源库,这些对于模型来说都是在训练数据中包含的。此外,这些测试集所在的项目都是 Python 库,而且大部分 issue 仅需极少的代码改动。例如,一个精选的 Lite 版测试集中 30% 的 issue 只需要 2 行及以内的代码改动就能解决问题。因此,尽管它比其他评测集更接近实际,但与我们实际工作中需要写近百行代码并有复杂的私有知识依赖的编码任务相比,它们仍然太过简单了。
开发真实需求的挑战
在开发真实需求时,我们面临的挑战是多方面的。以一个实际的例子来说明,我们当时正在做一个类似 Github Copilot 的项目,有个需求是“增加流式对话的能力到现有的代码中”。我们尝试使用大模型,比如 GPT-4,看看能做到什么程度。
初始版本的提示词可以是项目的目录结构和相关代码文件的全部内容,以及任务描述。根据生产的代码来调整提示词,增加更多必要信息,经过多次尝试,最终得到了一个相对满意的生成结果。
在这个过程中,我们发现,当使用大模型编写代码时,最常见的问题是模型不遵循设计,倾向于写一套新的。为了避免这种情况,我们需要在提示词中列一些明确的约束和指导,甚至需要详细说明实现步骤。如果工程师需要把任务描述到如此细致的程度,他是不会接受这么使用大模型的。但只要描述得足够详细,模型的效果就会非常好。任务描述甚至接近伪代码的程度,明确指出在何处添加什么函数以及它们的功能。为了提高生成效果,我们让模型每次只执行一个步骤。我们可以一步一步地进行,这样生成的代码基本上是可用的,当然,这个过程相当痛苦。
这个痛苦的经历却得出了一些有价值的经验。首先,指示足够详细,效果就足够好。其次,我们需要考虑提示内容的分类。一类是指出代码的改动入口点,通常以函数签名的形式表达,有时还包括输入输出约束。这些信息至关重要,画龙点睛,它指明了新写的代码如何在当前设计约束下与现有系统集成,框定了模型发挥的范围。我们当然希望模型根据上下文及任务能自己确定改动入口点,做不到你就需要告诉他,在提示词中加入一些经验知识能显著提升这方面的效果,后面会介绍。如果做了这些效果还不好,可能就需要给出更多实现级别的提示了。
这个过程和指导新人开发需求是类似的,你给他一个需求,他看了现有代码(提示词中的目录结构和代码文件内容)可能也不知道如何切入。你可能会一层一层提示他,第一层就是告诉他在那里加代码,输入输出是什么。还不行就告诉他每块实现的伪码。
分析任务难度
我们需要对任务难度建立一个认识,对利用大模型完成任务的预期效果有所预判。大模型的能力固然关键,任务本身的难度也会影响效果。
首先,模型的推理、规划、指令遵循、窗口大小、注意力特征、输出习惯等都会影响到效果。模型的注意力往往是离散的,我们往往需要在提示词中加入额外描述提及它需要在长上下文中关注的部分,干预注意力的分布。
任务本身的难度也是一个考量因素。任务本身所需的理解和推理难度会不同,可能还涉及复杂的分解和规划。任务所依赖的私有知识复杂程度也会影响难度,准备推理所需私有知识的过程可能非常繁琐。
在下图中,我们比较了 HumanEval 任务、AI Developer 任务和真实项目中的任务的复杂度。横轴代表所需私有知识的复杂度,即任务所依赖的经验和代码等。纵轴代表一次与模型交互生成内容的复杂度,这粗略反映了模型一次推理的复杂度。HumanEval 这类任务不太依赖私有知识,函数范围就能包含所有必要的信息。而 AI Developer 任务,如编写贪吃蛇游戏,特点是模型已经具备了相关的业务、架构和编码知识,不需要额外提供,但它生成的内容很复杂,所以它的点在于生成内容的复杂性,尽管它依赖的知识并不多。在真实的项目中,依赖知识的复杂度很高,生成内容也多。这三类场景在下图空间中的位置不同,有个演进过程,涉及到对 Agent 和知识工程要求的逐步提升。
切换到知识⽣产和消费的思维框架
要深入分析如何将大模型应用于软件开发中工程师的各项活动中,我们需要从知识生产和消费的角度来重新认识这些活动,尝试理解并刻画工程师大脑里的内心独白和思维轨迹,分析在这些活动中消费以及生产了哪些知识。
在项目中,我们可以看到被沉淀下来的知识产出,比如编写的代码、设计文档和需求文档。这些产出是需求开发在各阶段的产物,往往会被记录下来,作为下游工作的输入。编码任务消费上游的设计文档以及已经产出的代码,同时还需要消费任务相关的经验知识,最终生产出增量的下游产物。这些经验知识往往是隐性的,可能加工自上下游产物。比如,工程师之前通过看代码和设计文档掌握了如何在当前架构下完成一类功能的经验,他在应对新任务时可能就会在头脑中召回并消费这个经验,这便体现了:经验以前加工自上下游产物,并在当前任务中被消费。
可见,大模型推理可能同时完成了对已有知识的消费和对新知识的生产。按照这个思路分析不同的任务,尝试表达和刻画知识的有效方式,便可构建出一个完整的知识工程表达框架。
依赖经验知识的编码任务
在探讨大模型辅助编程时,常会先关注两类任务:编码任务和设计任务。编码任务不涉及对现有设计的修改,而设计任务则会改动现有设计。在这些任务之间,我们需要进行规划,决定先做什么,后做什么。除此之外还有需求的澄清和定义等任务。
编码任务中有些不依赖私有知识,例如,使用 Devin 编写贪吃蛇游戏这样的任务。还些任务只需要依赖项目代码就能有不错的生成效果。还有些任务只有引入了针对项目加工的私有知识才会得到好的生成效果。
我们这里主要讨论的是依赖经验知识的编码任务。我将通过两个例子来阐述这种知识的重要性。第一个例子是利用现有架构完成任务的方法,这涉及到如何抽象地表达任务经验。第二个例子是通过历史任务的记录来刻画经验知识,这便是具体的、实例化地表达任务经验。这些历史任务的记录可以帮助我们理解当时是如何完成任务的,从而为当前的任务提供指导。
利⽤"通过框架完成任务的⽅法描述"
在“利用框架完成任务的方法描述”中,我们可以看到,利用框架完成任务所需的指导说明。这些知识往往没有被正式记录成文档,而是存在于工程师的头脑中,通过口口相传或个人总结的方式流传。为了将这些隐性知识显性化,我们可以主动加工它们并将其放在提示词中。
以一个具体的例子来说,我们有一个需求,要为新增的页面元素编写交互脚本代码。这个任务的挑战在于让代码遵循现有的架构设计。为此,我们总结了在框架下生成页面元素交互脚本的方法。方法描述了框架在哪里,应该使用哪些 API,强调优先使用内置函数,如果内置函数无法完成任务,可提供其他方案等等。这些内容与我们在工作中指导新人如何完成任务时的沟通非常接近,只是我们将隐性沟通显性化了。
这个例子中,严谨来说,如何使用框架的知识并不是新东西,它们已经蕴含在代码中,如果充分理解代码,我们就应能知道怎么使用框架。为什么还要显式的经验知识来提示呢?因为上下文很大,模型的注意力分散,加之模型能力的限制,如果不加入这些知识,生成的代码倾向无视框架而自己重写一个。我们需要通过经验知识的提示来影响模型在当前上下文中的推理时的注意力分布,框架相关代码会因此获得更多注意力权重,从而引入架构约束,提高生成效果。这实际上是一个影响注意力的过程。
这与人之间的交互类似,需要一些提示来指导行动。进一步思考,我们是否可以批量加工这些知识,并在工具中根据相关性进行召回和使用。这样,我们就可以将这些隐性知识转化为显性知识,更有效地利用大模型来辅助编程任务。
利⽤"相关任务的实例化经验"
我们再来看一个例子,这个例子展示了如何利用一个具体任务的实例化经验。所谓实例化经验,就是一段描述,说明任务是什么,以及每一处代码改动具体是什么。这有点像是你要做功能 A,可以参考功能 B 的做法。模型不会复制功能 B,而是参考它的改动,这里蕴藏了大量有用提示。
这个示例是规则引擎的一个需求,目标是扩展这个规则引擎,增加一个新的操作符。如果工程师不太熟悉如何实现,你可以告诉他,这是规则引擎的代码,现在要增加一个新的操作符,可以参考其他已有的操作符。比如,可参考的是幂运算操作符,这个具体实例经验可以从代码提交的变更列表(change list)中加工出来的,具体包括改动了哪些文件,以及改动的部分,也可以为每一处改动生成了简要的描述,以降低模型理解负担。每一处改动的表达方式是提取代码片段,并标记出增加或删除了哪些行。
通过应用这个实例化经验,模型生成的代码几乎全部正确,即使这些改动涉及多个文件的多个函数。不同的实例化经验表达方式对生成的效果有显著影响。我们对比了几种方式,包括仅提供完整的代码文件内容,使用 Git Diff 格式表达每处改动,以及上面提到的代码片段加改动行标记。实验证明第三种方式效果最好,这也可以理解,仅提供完整代码其实并没有给出参考经验,而 Git DIFF 则太过简练缺少上下文。如果提供了代码片段并标记出改动的行,这对人类工程师和大模型来说都更完整和友好。
上面说明如何刻画经验才能提升生成效果。一般来说,你的经验刻画方法对人类工程师也很容易理解时,那么大模型的效果通常也会不错,其实这也更逼近工程师实际工作中的参考内容和思维轨迹。
知识分类及提⽰词框架
基于前面的例子,以及众多类似的分析,我们可以对知识进行分类,以建立全局认识并指导后续知识工程的建设。初步将知识分为三类:产物知识、经验知识和衍生数据知识。
产物知识:在流程中以产物形式显性表达的知识。这是在软件开发不同阶段的具体产物,如需求文档、架构设计文档、模块设计文档、代码及配置等。如果团队能够采用大模型友好的方式记录这些信息,那么它们就更容易在模型推理过程中被利用。如果使用 Word 文档或图片,那么这些信息就不太容易利用。
经验知识:在生产增量产物知识过程中使用的经验,工程师在头脑中加工过,但在团队中通常没有被显性记录下来。这类知识是关于需求、架构、设计、编码及问题修复等任务的经验。它们可以表达为文字和代码。例如,架构变更记录可能包含因为某个问题而进行的架构变更考虑及其效果。设计规范和开发任务的经验也属于这类。对于问题修复经验,可以从相关记录和代码中提取根因、现象和具体修改的代码等信息,做为经验的表达,以后使用。
以编码任务中的经验知识为例,它们可以是组件级别的知识(如函数、类的说明)或任务级别的知识(如何实现一个利用到多个组件的任务),也可以是抽象的知识(如何使用架构完成任务)或是具体的知识(如特定任务的实现细节)。
衍生数据知识:程序运行时的数据或基于代码分析的数据,用于知识相关性计算或直接提升生成任务的效果。这类知识不是由工程师脑力生产的,而是通过程序加工的。通过代码分析得到的 AST(抽象语法树)结构,可以用在生成单元测试的任务中以提升覆盖度。AST 数据也可以用在依赖代码的相关性计算中,在为代码生成任务准备提示词上下文时可能用到。
对知识进行分类后,我们可以总结一个提示词框架,表达在什么上下文下,基于什么经验约束,完成一个什么样的任务。它包括下面几部分:
项目的基础上下文:这部分主要是产物知识,比如项目目录结构以及模块的全部代码。如果受限于窗口大小或是发现无关信息的干扰严重,我们可以手工或通过相关性计算来精选信息,确保生成效果。
任务经验:与当前任务相关的经验描述,属于经验知识。
输出约束:对输出内容的约束规则,它们引导模型按照预期的方式生成结果。
任务描述及提示:这些内容不容易复用,往往是一次性编写的,包括具体任务介绍,以及在生成效果不够好时不得不增加的提示。在反复尝试改善效果的过程中,我们需要增加些关键的提示,它们虽然难以转化为可复用的经验,但对于提升特定任务的生成效果至关重要。
⼤模型辅助开发的实⽤形态
探讨未来应用形态,实际上也是在回答一系列问题。
⼯具能做到什么程度? 这取决于底层模型的能力,以及知识工程建设的程度,未来随着模型能力的显著提升,工具可能表现的更懂代码和工程师的意图。而现在,尽管工具已经表现的能引导完成从需求到代码的过程,但还做不到对主干任务有实质性的帮助。很多步骤带来的检查修改成本大过帮助,工程师更倾向于在痛点步骤或任务上去为大模型认真编写上下文。
工程师是否需要裸用 ChatGPT 这样的工具?未来结合一些小脚本裸用大模型的情况会很常见,对主干任务也是这样。工程师熟悉大模型后维护提示词模板非常容易。结合一些小脚本或工具管理知识具备可行性,特别是大部分工程师需要维护的代码量有限,维护私有知识成本不高。模型能力提升后那些繁琐的多步操作将大幅度简化,也降低了复杂工具的必要性。模型能力包括窗口和注意力的提升也大幅度降低了通过相关性检索和复杂上下文组装的必要性。大型工具在主干任务的优势未必明显,或者说工具建设需要开放性,以便融入工程师自己在项目中利用大模型的经验,包括自己的提示词、加工的知识和定义的推理路径等。
⼯程师是否必须能驾驭⼤模型?答案是肯定的。工程师需要能够深入理解并使用它们,而不是寄希望于用上工具就能大幅提升自己的效率。软件开发的场景非常丰富,即使是我前面提到的例子,覆盖的范围也非常有限,很多时候需要我们自己想办法提升效果。
⼈的精⼒能被释放到什么程度?追求多⼤⽐例的效率提升⽐较现实? 我们在利用大模型完成主干任务时,虽说要写的代码少了,但时间却花在了各种准备工作、反复尝试和提示、检查和修正这类事情上面了,当然其中一部分的工作量是有潜力工具化的,但可能并没有想象的比例那么高。当前已经比较成熟的代码补全,即使采纳率达到 30%,在编码环节上的提升可能在 5% 左右。2 年内,对于端到端整体效率提升,持谨慎乐观的态度,可能在 15% 左右。
最后,我想强调几点:
软件开发任务生态丰富,主干任务的难度远超支撑性任务。
从知识生产和消费的角度重新建模软件开发过程。
如何遵循设计是复杂编码任务的重要挑战。
从现有代码结构或完成任务的历史记录中加工经验知识。
利用合理的知识分类指导知识工程的建设。
演讲嘉宾介绍:
路宁,研发效能领域知名专家,目前在理想汽车探索代码智能实践,曾任 ThoughtWorks 架构师和互联网大厂资深技术总监。
评论