背景

在大语言模型越来越火的今天,越来越多的应用场景开始使用大语言模型来解决实际问题。而辅助编程可以算是大语言模型应用得最成功的场景之一了。早先的时候,更多使用的还是代码补全的能力,但是现在,各家产品都开始支持Chat和Agent的能力了。

之前一直有个疑问,生成的代码明明只是片段,也没有一个很好的规则能直接定位到源文件的位置,甚至有些生成的代码和现有代码没有任何重叠的部分,那这些代码是怎么精准地合并到源代码中的呢?今天就带着大家一起看一下,在Chat、Agent的场景下,如何将生成的代码精准且快速地合并到现有的代码文件中。

从全量重写到Planning+Applying的转变

一个最暴力的方法就是,每次在Chat/Agent里,都让模型生成完整的代码,然后直接全量替换,这样就不用考虑代码合并的问题了。

但是,这种方法的缺点也很明显:

  1. 成本高:每次模型都需要输出大量的代码,如果源文件有1000行,那每次模型生成代码就需要输出1000行,这样一次就用掉了大量的token,成本是不可接受的。
  2. 速度慢:大模型的输出模式是一个一个token地输出,如果源文件有1万token,那每次模型生成代码就需要走1万次的Decoding的过程,这是极其耗时的。

显然,现在的产品都没有使用这种模式,我们在Chat/Agent界面中看到的都是代码片段。所以,我们先将这个问题进行拆解一下,分成两个步骤:

  1. Planning:大模型先生成代码片段
  2. Applying:将代码片段合并到原始代码中

可能的代码片段形式

接下来我们会从这个原始代码出发,讲一下几种可能的代码片段形式。

import json

def main(args):
# show a greeting
print("Hello!")
return if __name__ == '__main__':
main("")

Code Diff Patch

--- a/greeting.py
+++ b/greeting.py
@@ -10,4 +10,4 @@
def main(args):
# show a greeting
- print("Hello!")
+ print("Goodbye!")
return

让模型生成完整的Code Diff Patch,然后直接通过Patch程序合并到原始代码中。

这样做的好处是,Applying的过程非常简单,只需要调用Patch程序合并即可。

缺点也很明显,Patch程序对于输入的格式要求非常严格,如果模型生成的Code Diff格式不正确,那合并就会失败。大语言模型对于数字的敏感程度不够,很容易产生幻觉,插入的位置有时候即使是偏了一行,合并出来的代码也就不可用了。

Unified Diff

@@ ... @@
def main(args):
# show a greeting
- print("Hello!")
+ print("Goodbye!")
return

这个Diff的格式来自于Aider。和完整的Diff相比,Unified Diff删除了位置信息,只保留了代码的修改。合并的方式也很简单,只要将以空格和减号开始的行,替换成以空格和加号开始的行即可。这样,就可以有效避免模型关于数字的幻觉了。

这两种代码片段有一个共同的缺点,就是他们的数据都属于不常见的类型。code diff更多还是我们平时用git diff命令一眼用的,并不会将其存成文件留存下来,模型自然也就很少见到这种数据,所以也就很难准确生成类似的数据了。模型更喜欢的还是生成一段完整的代码片段。

Lazy Format

# ... existing code ...
def main(args):
# show a greeting
print("Goodbye!")
return
# ... existing code ...

相信如果大家让模型生成过代码,就会发现模型生成代码的时候会更偏向于这种Lazy Format的形式。就是用# ... existing code ...来表示代码的上下文,然后只生成中间相关的代码。

这种形式的好处是,模型可以更专注于生成代码,而不需要受到代码变更的干扰,也就是能提升生成的代码的准确性。

缺点也很明显,Applying就变成了一个比较复杂的过程。

基于Lazy Format的Applying

基于AST的代码块替换

对此,Continue的解决方案是借助AST来分解代码,进行代码块的替换:

如上图所示,原始代码被拆分成了3部分,分别是:

  1. import代码块
  2. main代码块
  3. Python入口代码块

而生成的代码也被分为了3部分,分别是:

  1. existing代码块,用...表示
  2. main'代码块(注意这个代码块和原始文件的代码块不是完全相同的)
  3. existing代码块,用...表示

替换步骤则是:

  1. 先用...来匹配任意代码块,也就是import代码块
  2. 再用main'代码块替换main代码块
  3. 最后用...来匹配任意代码块,也就是Python入口代码块

这里只是举了一个简单的例子,详细的算法可以参考Continue的文章。

这个方法的好处是,Applying的过程非常快,也很准确。

缺点则是,有些生成的代码,通过这种方法是没法替换的。比如:

# ... existing code ...
# show a greeting
print("Goodbye!")
return
# ... existing code ...

压根就没有提供main函数的签名,那替换要从何找起呢?

基于全量代码生成的替换

为了解决这个问题,只好又求助与大语言模型了,让大语言模型根据原始代码和生成的代码,直接全量生成。这个时候大家可能会问了,之前不是说了全量生成成本高吗,为什么又要提出这种方法呢?

这就要仰仗于我们前面提到的Planning+Applying的思路了。在我们把问题拆解成两部分后,就可以对每一部分进行优化了,Planning的过程更关注于用户问题的理解,精准的代码生成,这通常依赖于大模型的通用能力,要求模型是一个比较强的模型。所以我们只能使用GPT-4o/DeepSeek R1/Claude 3.7 Sonnet这种大模型来生成代码,如果让它们全量生成代码,那成本确实就会非常高了。但是Applying的过程,只是把两个代码合并到一起而已,这对模型的要求就低很多了,所以我们就可以用小模型来处理该任务。

蒸馏大模型

第一个想法就是用大模型蒸馏小模型,FastApply-7B-v1.0就是专门做代码合并用的小模型,通过让Claude Sonnet 3.5 (70%)和GPT-4 (30%) 生成合并代码,并微调Qwen2.5-Coder-7B的模型,得到了该模型,细节可以参考该github仓库

该模型在DeepSeek的评估下,准确率达到了99.95%,可以说是非常高了。

基于该模型,我用A6000的显卡进行了实际的速度测试。未量化的版本在vllm的加持下,可以得到45tokens/s的速度。我随机找了某个代码库统计了一下数据分布,每个文件的token中值在939左右,平均在1150左右。按照1000token来算,那么该模型就需要22s左右的时间才能合并完所有代码。换算成Qwen2.5官网提到的A100的速度(85tokens/s),那也需要12s左右的时间,依然是不可用的。

Speculative Decoding

好在,我们还有Speculative Decoding技术,这种技术的原理就是用小模型来打草稿(生成可能的Decoding token),然后用大模型来验证草稿。这样只要小模型生成出来的token被接受一部分,就可以显著减少大模型Decoding的时间了。具体的过程可以参考下图:

一般情况下,Speculative Decoding的加速比可以达到2-3倍,也就是说,用A6000来推理的话,可以达到120tokens/s左右的速度,这样得到的结果就是8s左右的时间,虽然快了挺多的,但是还是不大可用。

Prompt Lookup Decoding

又好在,代码合并这个任务有很好的性质:

  1. 生成的内容全部来自于原始代码和代码片段。这样我们就可以用“规则”来替换小模型,省掉小模型生成token的时间。
  2. 一旦正确定位到了某个位置,后续的一大段token都是可以被接受的。可以一次“生成”大量的token草稿,让大模型验证,并且验证成功的概率也很大。

基于这两点,我们就可以用Prompt Lookup Decoding来加速代码合并的过程。

原理其实很简单,先通过n-gram匹配Decoding的token和输入部分(原始代码和代码片段)的token(下图中的import),然后“摘抄”k个token(下图中Decoding中虚框部分),让模型来验证,模型会一次输出所有token的概率。前N个绿色框的token的概率都是最大的,所以可以被接受,,从第一个被拒绝的token(("Hello"))开始,后面的token都需要拒绝。因为将该token替换成("Goodbye")(概率最大)后,后面的token就应该基于("Goodbye")token来生成了。(注:这里拆分的token和模型的tokenizer拆分的token并不是完全一样的,只是用于让示例更直观)

vllm已经支持了PLD,经过测试,用上PLD之后,将生成的token数设置成100,1000个token的场景,加速比来到了12倍,也就是用A6000来推理可以达到550tokens/s左右的速度,耗时2s左右,看起来已经可以接受了。

PLD+

虽然PLD的加速比已经很高了,但是还有没有更进一步的可能性呢?我找到了一篇叫PLD+的论文,论文中提出了PLD+的技术,基本的思想如下:如果ngram一次匹配上了多个候选的token,那到底该选哪一个呢?按照PLD的话,就是直接选最新的一个,这样就有概率选错。而刚好我们的模型的中间输出可以给我们提供参考。模型推理的时候会生成每个token的hidden states和attention,这俩都可以用于帮助选择token。hidden states可以通过计算最新的一个token和所有候选token之间的余弦相似度,选相似度最高的一个。针对attention则可以选attention score最大的一个token。

参考下图:

基于vllm,我实现了一个简单版本的PLD+,论文中提到要使用9层hidden states来计算,但是vllm在推理的时候,只返回最后一层,所以我就直接用最后一层来计算了。得到的结果是:有提升,但是提升的幅度并不大。从12倍提升到了13倍,这起码验证了PLD+的思路是可行的,后续如果更进一步优化,应该可以得到更大的提升。另外,如果是大规模应用,那这多一倍的提升,也意味着很大的收益了。

最终的探索结果:在1000个token的场景下,用PLD+的加速比达到13倍,也就是用A6000来推理可以达到600tokens/s左右的速度,耗时1.7s左右,非常不错了。

未来的探索方向

  1. 基于测试的结果,如果将生成的token数设置成100,当token数很大的时候,加速比并不会提升,意味着耗时会线性增长,对于更大的原始代码,将生成的token数设置得大一些可能会取得更好的加速比。
  2. PLD+提到的实现如果能在vllm上完整复现,可以期待一下更大的提升。
  3. 还有一些背景知识并没有被利用,比如:可以通过定位existing code的token来动态设置生成的token数。
  4. 在更大的模型上验证一下加速比。

参考

LLM生成代码后,如何一键合并到源代码中(FastApply技术研究)的更多相关文章

  1. 解决IDEA中Lombok生成代码后提示错误的问题

    一.背景介绍 因为我们在使用Lombok的时候,Lombok为我们生成的代码是在字节码中(*.class),而不是在source code中,所以存在IDE提示Lombok生成的方法未定义的错误,导致 ...

  2. EJB生成代码后遇到transient错误

    启动服务的时候遇到这样的错误: 解决方案: 1.找到对应的模块的SesBean文件 2.去掉transient 3.重启服务即可

  3. 自定义Mybatis自动生成代码规则

    前言 大家都清楚mybatis-generate-core 这个工程提供了获取表信息到生成model.dao.xml这三层代码的一个实现,但是这往往有一个痛点,比如需求来了,某个表需要增加字段,肯定需 ...

  4. python之gui-tkinter可视化编辑界面 自动生成代码

    首先提供资源链接 http://pan.baidu.com/s/1kVLOrIn#list/path=%2F

  5. [小tips]使用vscode,根据vue模板文件生成代码

    本着苍蝇虽小也是肉的精神...... 目标: 我们希望每次新建.vue文件后,VSCODE能够根据配置,自动生成我们想要的内容. 方法: 打开VSCODE编辑器,依次选择"文件 -> ...

  6. 生成代码,从 T1 到 T16 —— 自动生成多个类型的泛型

    当你想写一个泛型 的类型的时候,是否想过两个泛型参数.三个泛型参数.四个泛型参数或更多泛型参数的版本如何编写呢?是一个个编写?类小还好,类大了就杯具! 事实上,在 Visual Studio 中生成代 ...

  7. 前端学习笔记系列一:7 在vscode中根据vue等模板生成代码

    目标:希望每次新建.vue文件后,VSCODE能够根据配置,自动生成我们想要的内容. 方法:打开VSCODE编辑器,依次选择“文件 -> 首选项 -> 用户代码片段”,此时,会弹出一个搜索 ...

  8. 使用 Velocity 模板引擎快速生成代码(zhuan)

    http://www.ibm.com/developerworks/cn/java/j-lo-velocity1/ ****************************************** ...

  9. 使用Velocity 模板引擎快速生成代码

    Velocity 模板引擎介绍 在现今的软件开发过程中,软件开发人员将更多的精力投入在了重复的相似劳动中.特别是在如今特别流行的MVC架构模式中,软件各个层次的功能更加独立,同时代码的相似度也更加高. ...

  10. 【工具引入】uiautomatorviewer 查找元素后自动生成代码

    缘起 公司部门调整PC部门和无线部门合并,原本负责主站PC端自动化的同事需要马上上手安卓,IOS自动化.对于初次接触移动端的测试者来说,跨度还是有点大的.加之人员有些变动,不得不搞个工具降低学习成本, ...

随机推荐

  1. 【Linux】【UOS】为挂载的磁盘创建快捷方式(软链接)

    打开项目或者保存文件的时候,如果需求路径不是系统盘路径,那么找起来还真是麻烦.以下时候通过创建快捷方式(软链接)的方式,将打开磁盘的快捷方式放在用户目录下,就方便寻找打开了. 1.查询挂载点 sudo ...

  2. Linux 添加开机自启动

    rc.local 方式 一.& 在 Linux 命令后加上 &  可以在后台运行 二.nohup 对 SIGHUP 信号免疫,对 SIGINT 信号不免疫,可用 shopt | gre ...

  3. Timestamp和LocalDateTime 互转

    jdk:1.81.Timestamp 转 LocalDateTime Timestamp time = Timestamp.from(Instant.now());LocalDateTime loca ...

  4. Qt编写地图综合应用59-经纬度坐标纠偏

    一.前言 地图应用中都涉及到一个问题就是坐标纠偏的问题,这个问题的是因为根据地方规则保密性要求不允许地图厂商使用标准的GPS坐标,而是要用国家定义的偏移标准,或者在此基础上再做算法运算,所以这就出现了 ...

  5. Qt开源作品13-三套样式表

    一.前言 在做各种各样的项目中,难免遇到需要设定自己界面风格样式的时候,而Qt提供的qss就是牛逼的为了实现定制各种各样的皮肤的,其实一个完美的UI界面,主要由两大块组成,一个是颜色搭配,一个是布局, ...

  6. Qt音视频开发24-ffmpeg音视频同步

    一.前言 用ffmpeg来做音视频同步,个人认为这个是ffmpeg基础处理中最难的一个,无数人就卡在这里,怎么也不准,本人也是尝试过网上各种demo,基本上都是渣渣,要么仅仅支持极其少量的视频文件比如 ...

  7. [转]CLion 2019去掉灰色参数提示(parameters hints)

    众所周知,clion是一个很好用的c plus plus IDE,刚装好的clion默认的设置多少有一些不符合口味的地方,在查看代码或者敲代码的时候看到如下这样的灰色提示,我是有点受不了的: 之前用的 ...

  8. [转]在WorldWind中加入*.x格式的三维模型

    Nasa支持的WorldWind项目最近推出了1.4RC5版,可以加入三维模型,效果如下图所示: 点击查看大图 WW1.4对XML配置文件增加了许多新的元素,其中ModelFeature就是用来增加三 ...

  9. Solution Set - “也许我们早已经共鸣在那约定之地”

    目录 0.「AGC 024D」Isomorphism Freak 1.「APIO 2018」「洛谷 P4631」选圆圈 2.「UR #2」「UOJ #31」猪猪侠再战括号序列 3.「UR #3」「UO ...

  10. 最新AI智能体开发案例:辅助写作神器!教你用Coze平台搭建「文匠智创 1.0」智能体!

    各位小伙伴们,大家好呀!我是疯狂老包.我精心打造的<疯狂AI智能体开发:100个实战案例, 从 入门到精通 >正在开发中!要是你对 AI 应用搭建满怀热忱,渴望深入学习其中的奥秘与技巧,那 ...