代码补全训练原理:填充中间与下一个令牌预测
代码补全训练原理:填充中间与下一个令牌预测
无论是通义灵码还是 GitHub Copilot,现代代码补全工具的背后都依赖强大的语言模型。这篇教程将从训练目标的角度,讲解代码补全模型如何学会“猜你接下来要写什么”以及“补全你漏掉的那段代码”。
1. 核心任务:把代码变成“填空题”
在训练代码补全模型时,我们通常会把源代码切分成一个个令牌(Token),然后把任务转化为两种基本形式的填空:
- 下一个令牌预测(Next Token Prediction):给定前面的代码,预测紧接着的一个或多个令牌。
- 填充中间(Fill-In-the-Middle, FIM):给定上文和下文,预测中间缺失的那一段代码。
一种现代代码模型往往会同时学习这两种任务,从而既具备从零开始续写的能力,又能在已有结构的中间进行智能补全。
2. 下一个令牌预测:训练出代码续写能力
2.1 自回归语言建模基础
下一个令牌预测采用的是经典的自回归语言模型(Autoregressive LM) 训练方式。给定一个由 $T$ 个令牌组成的代码片段 $x = (x_1, x_2, \dots, x_T)$,模型需要最大化下一个令牌在历史令牌条件下的概率:
$$ \mathcal{L}{\text{AR}} = - \sum{t=1}^{T} \log P(x_t \mid x_{1}, \dots, x_{t-1}) $$
这个过程让模型逐步学会代码的语法模式、常见表达式、API 调用顺序等。
2.2 因果注意力掩码(Causal Mask)
为了让模型在预测 $x_t$ 时只能看到 $x_1$ 到 $x_{t-1}$,不能“偷看”后面的内容,Transformer 中的自注意力机制会施加一个下三角掩码。这也是为什么这类模型常被称为“因果语言模型”或“单向模型”。
2.3 训练数据构造
对代码仓库中的每一个 .py、.js、.ts 等文件,直接按行级或 token 级将其作为一条训练样本。不需要额外标注,因为下一个令牌就是天然的标签。
这种方法让模型在行尾补全、从空白区域续写时表现优秀。但它有个盲区:无法很好地补全段落中间的缺失部分。
3. 填充中间:让模型学会“补窟窿”
3.1 为什么需要 FIM
真实编码场景中,程序员往往会先写好函数签名和框架性注释,再去填充中间实现;或者回过头来修改一段已有代码中间的逻辑。如果模型只会向右续写,就必须人为把光标移到空白处并不断触发补全,效率低下。
Fill-In-the-Middle 直接训练模型在给定 <prefix> 和 <suffix> 的情况下生成 <middle>,让模型天然具备“任意位置补全”的能力。
3.2 FIM 的两种经典表示方式
为了让同一个自回归模型能理解“先看两边,再补中间”的指令,通常会在 token 序列中插入特殊标记,并改变输入的顺序。这里介绍两种主流方案,以代码片段 prefix + middle + suffix 为例。
方案一:PSM 模式(Prefix-Suffix-Middle)
将训练序列重排为:
<PRE> <prefix> <SUF> <suffix> <MID> <middle> <EOT>
训练时损失只在 <middle> 的 tokens 上计算(有时也会包括 EOT),前面的部分仅作为上下文。
在这种设定下,模型看到 PRE 和 SUF 标记后,就明白它需要对接下来的 middle 部分进行预测。
方案二:SPM 模式(Suffix-Prefix-Middle)
另一种对称的做法是:
<PRE> <suffix> <SUF> <prefix> <MID> <middle> <EOT>
将后缀提前,这样模型可以先“预览”后面的代码,再结合前缀完成补全。研究表明,SPM 在一些代码模型(如 CodeGen、StarCoder)上可能有更好的小样本表现,因为它更强调后缀的引导作用。
3.3 FIM 训练数据构造
从完整代码文件中随机切割出一个区间作为 middle,切割点可以基于 token、行或 AST 节点。middle 的长度不宜太长或太短,通常遵循某种分布(如均匀或截断正态分布)。然后在 middle 两端分别提取 prefix 和 suffix。
最终,每个原始文件会被处理成多个 FIM 样本,与下一个令牌预测样本混合训练。
3.4 损失函数与训练细节
FIM 的损失依然可以用标准的交叉熵,但仅限于 middle 部分(以及可能的后缀/结束标记):
$$ \mathcal{L}{\text{FIM}} = - \sum{t \in \text{middle_positions}} \log P(x_t \mid \text{context}) $$
这种“条件生成”的框架并未改动模型结构,只改变了输入序列的排列和注意力掩码的控制范围,因此可以无缝共享参数。
4. 混合训练:一个模型,两种能力
现代代码补全模型(如 StarCoder、Code Llama、DeepSeek-Coder 等)普遍采用多任务训练。一个典型的 mini-batch 中,会按一定比例混合:
- 下一令牌预测样本(保持从左到右的顺序)
- FIM 样本(采用 PSM 或 SPM 格式)
- 有时还会加入其他任务,如代码填充率预测、克隆检测等,但 FIM 与下一令牌预测是代码补全场景的核心。
这样的混合训练让模型在补全对话框(单行续写)和代码块补全(函数体、循环体)中都能获得高质量的生成结果。
5. 推理阶段如何调度 FIM 模型
训练完成后,当你在 IDE 中触发补全时,客户端会将光标前后的代码(通常截取一定 token 长度)发送给模型。
- 如果光标在行末或空行,模型可能判断 prefix 占比极大,suffix 几乎为空,此时等同于下一令牌预测。
- 如果光标在函数体中间的某行,prefix 和 suffix 都较完整,模型就会进入 FIM 模式,生成一段 middle,直到输出
<EOT>或满足停止条件。
所以用户在使用代码补全时,实际上同一套模型权重在后台同时响应多种补全场景。
6. 影响补全质量的关键因素
-
上下文窗口长度
模型能看到的 prefix 和 suffix 的令牌数直接影响其对全局语义的理解。窗口越长,越能避免生成重复或冲突的代码。 -
FIM 切割策略
是按 token 随机切,还是按照语句、代码块边界切,会显著影响模型对完整语句的预测能力。当前许多工程会选择基于抽象语法树(AST)的切割。 -
特殊标记设计
<PRE>、<SUF>、<MID>等标记需要保证不会与真实代码令牌冲突,并且在预训练 tokenizer 中被作为特殊 token 加入词表。 -
训练数据多样性
包含多语言、多种项目结构的数据能让 FIM 模型更好地掌握通用的补全规律,而不仅仅局限于某种语言的特异性模式。 -
推理时的采样策略
温度系数、top-p、top-k 以及重复惩罚等参数会影响生成的多样性与可靠性,通常对代码补全任务需要更偏“保守”的采样。
7. 从“填词”到“编程助手”的演进
代码补全训练已经从单纯的下一令牌预测,进化到结构化填充中间。这使得模型不再只是一个“从左到右敲代码的机器”,而是一个可以理解代码上下左右语境的智能体。
未来,随着仓库级上下文、执行反馈以及用户行为建模的加入,FIM 范式还将进一步扩展,让代码补全工具在复杂工程中提供更精准、更安全的代码建议。
理解了填充中间与下一个令牌预测的原理,你就能更好地调试和评估你团队所用的代码补全工具,甚至尝试自己训练一个基础的代码语言模型了。