背景

在大语言模型越来越火的今天,越来越多的应用场景开始使用大语言模型来解决实际问题。而辅助编程可以算是大语言模型应用得最成功的场景之一了。早先的时候,更多使用的还是代码补全的能力,但是现在,各家产品都开始支持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. Java JUC&多线程 基础完整版

    Java JUC&多线程 基础完整版 目录 Java JUC&多线程 基础完整版 1. 多线程的第一种启动方式之继承Thread类 2.多线程的第二种启动方式之实现Runnable接口 ...

  2. 【深度学习】Tensorflow学习(1)张量与常用函数

    关于张量 张量可以表示0阶到N阶的数组 在TensorFlow中,张量(Tensor)表示某种相同数据类型的多维数据 因此张量有两个重要特征: 数据类型 数组形状(各个维度的大小) 张量的数据类型 t ...

  3. oracle用命令执行sql脚本文件

    当sql命令过多(sql文件过大)时,用plsql执行时比较慢而且容易超时,此时可以用sqlplus命令直接执行sql脚本文件,方法如下: 1.sqlplus登录 >sqlplus userna ...

  4. [转]Makefile教程

    从0开始教你编写Makefile文件 跟我一起写Makefile-陈皓2005.pdf:链接: https://pan.baidu.com/s/1kKPfosCiPQONyZ1oeCmuAA 提取码: ...

  5. Cesium 在线地图访问总结

    参考:https://deyihu.github.io/src/maptalks-tileLayercollection/examples/?tdsourcetag=s_pcqq_aiomsg 以下u ...

  6. 1. C++快速入门--变量和基本类型, 类别

    文章使用obsidian编写, 双链部分可能失效 1 基本内置类型 1.1 算术类型 算术类型介绍 bool 类型 字符类型 整数类型 实数浮点.虚数浮点和 复数浮点 参看如下表 带符号和无符号类型的 ...

  7. c# WindowsCommunityToolkit--- Shade Animation

    WindowsCommunityToolkit: https://github.com/CommunityToolkit/WindowsCommunityToolkit You can also pr ...

  8. 项目PMP之七项目成本管理

    项目PMP之七--项目成本管理   一.定义:在预算内管理成本:预测项目成本 核心理念:重点关注项目活动的成本:同时决策的影响.相关方的不同时间不同方法的测算 趋势:挣值进度(ES)逻辑:敏捷的方式则 ...

  9. OxyPlot:一个功能强大、漂亮.Net跨平台开源绘图库

    推荐一个支持多平台.多框架的.Net绘图库. 01 项目简介 OxyPlot是一个基于.NET开发的.跨平台的绘图库,可用于多种平台和框架,如WPF.Windows 8.Windows Phone.W ...

  10. MongoDB 常用指令(详细)

    # MongoDB 常用指令## 基础命令### 启动与连接```bash# 启动 MongoDB 服务mongod# 连接 MongoDB 客户端mongo```### 数据库操作```bash# ...