代码补全训练原理:填充中间与下一个令牌预测

FreeGuideOnline 最新 2026-06-25

代码补全训练原理:填充中间与下一个令牌预测

无论是通义灵码还是 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),前面的部分仅作为上下文。

在这种设定下,模型看到 PRESUF 标记后,就明白它需要对接下来的 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. 影响补全质量的关键因素

  1. 上下文窗口长度
    模型能看到的 prefix 和 suffix 的令牌数直接影响其对全局语义的理解。窗口越长,越能避免生成重复或冲突的代码。

  2. FIM 切割策略
    是按 token 随机切,还是按照语句、代码块边界切,会显著影响模型对完整语句的预测能力。当前许多工程会选择基于抽象语法树(AST)的切割。

  3. 特殊标记设计
    <PRE><SUF><MID> 等标记需要保证不会与真实代码令牌冲突,并且在预训练 tokenizer 中被作为特殊 token 加入词表。

  4. 训练数据多样性
    包含多语言、多种项目结构的数据能让 FIM 模型更好地掌握通用的补全规律,而不仅仅局限于某种语言的特异性模式。

  5. 推理时的采样策略
    温度系数、top-p、top-k 以及重复惩罚等参数会影响生成的多样性与可靠性,通常对代码补全任务需要更偏“保守”的采样。


7. 从“填词”到“编程助手”的演进

代码补全训练已经从单纯的下一令牌预测,进化到结构化填充中间。这使得模型不再只是一个“从左到右敲代码的机器”,而是一个可以理解代码上下左右语境的智能体。

未来,随着仓库级上下文执行反馈以及用户行为建模的加入,FIM 范式还将进一步扩展,让代码补全工具在复杂工程中提供更精准、更安全的代码建议。

理解了填充中间与下一个令牌预测的原理,你就能更好地调试和评估你团队所用的代码补全工具,甚至尝试自己训练一个基础的代码语言模型了。