GitHub Copilot Inline Suggestions模型架构解析
VS Code官方博客披露Copilot如何将补全、编辑预测、远距离建议统一为一个模型,以提升响应速度和一致性。
完整中文译文
2026 年 9 月 16 日,作者:Julia Gong、Ben Liggett、Ulugbek Abdullaev
补全风格的灰色文字(ghost text)、光标附近的下一编辑建议、以及较远处的编辑,以前都由不同的模型提供支持。我们构建了一个统一模型来搞定这三种场景,并从中认识到:最佳效果来自于训练、评估和编辑器设计的协同演进。
在 GitHub Copilot,我们的使命是支持所有开发工作流——从使用内联建议的 AI 辅助编码,到 VS Code Agents 窗口中以智能体为中心的软件工程。内联建议已被数百万开发者使用并受到喜爱,我们将持续提升其质量,以及所有 GitHub Copilot 使用体验。
编写代码很少是线性的。下一个有用的更改可能是光标处的几个字符、近处的重写,或者代码库中其他地方的关联编辑。内联建议通过补全风格的灰色文字、附近的下一编辑建议和远程编辑来适应这些不同的工作流。
此前,每种建议类型都由专门的模型路径提供支持。成熟可靠的补全模型无需太多介绍,在之前的文章中,我们分享了如何为下一编辑建议训练自定义模型,以及如何将这些建议扩展到更远的编辑。现在,我们已经将这三个模型统一到一个"三合一"模型中,使其能够完成所有任务。
统一模型能够提升建议质量,因为它允许单个模型端到端地为开发者当前的工作选择最佳编辑,而不是使用程序逻辑在专门模型中进行选择。例如,假设开发者在企鹅喂养程序中输入了 class Fa(见下图)。使用之前的独立模型(左图),最成熟、经过大量实践验证的补全模型总是优先触发。由于它只能追加到前缀 Fa 之后,因此它做了最擅长的事——建议 FastingPenguin。然而在这个特定案例中,右侧统一模型建议的更好的 NES 编辑实际上是将 Fa 语义重写为 Fish。
我们不仅可以用一个模型处理所有现有任务,而且由于该模型可以在一次响应中输出多个编辑,如果之前的建议是符合需求的,我们还可以缓存这些额外编辑,从而为后续编辑提供更快的体验。这在编辑流程中创造了更流畅、更干脆的 tab-tab-tab 体验。
为了实现更好的编辑体验,我们首先重新定义了建模任务——尤其是提示和输出格式——以使其能够在代码编辑任务中泛化。然后,我们将独立模型在数据质量、模型训练与评估以及客户端设计方面的学习成果结合起来,此外还进行了大量额外实验(超过 200 次离线实验和 20 次在线实验,我们将在后续文章中详细阐述其过程),以仔细优化模型和端到端体验。结果是打造了一个更高质量、更快速、更统一的输入伴侣,其价值超越了各部分之和。

在统一模型之前,VS Code 中内联建议的生产实现由三个独立模型驱动:
补全(大家都熟悉和喜爱的灰色文字),
NES(下一编辑建议,或限制在光标上下几行范围内的附近编辑),
远程 NES(距离光标更远的 NES 风格建议)。
这些针对单个任务的专业工作——成熟的补全模型加上较新的编辑模型——产生了一种客户端编排的体验,在适当的地方触发相应的功能。
虽然这些工作对各个单独任务非常有效,而且是向用户带来新编辑功能的必要步骤,但它也在几个关键方面造成了缺陷:不仅是用户体验质量方面的(有时会做出次优的或零散的编辑,而不是最适合上下文的单一清晰编辑),还有延迟方面的(每次机会最多可能触发 4 次模型调用,如下图所示),还有对未来功能扩展性的影响(例如多文件编辑)。

图 1. VS Code 内联建议的原始阶梯式生产设置,包含三个独立模型:补全、NES 和远程 NES。注意,此图已简化以便于说明,代表了用户视角的行为(实际 provider 调用行为更为复杂,涉及向补全和 NES 发出并行调用,例如,取决于用户光标位置和优先级模型的响应速度)。每个模型处理各自独立的任务,而客户端编排了何时触发哪个模型的逻辑。补全模型总是优先被触发,以预测用户光标处的灰色文字;如果产生了灰色文字,则显示。如果未产生灰色文字,则调用 NES 模型重写用户光标周围的窗口;如果重写产生了净编辑,则使用适当渲染的视图类型显示。如果仍然没有预测到编辑,则触发远程编辑模型,预测可能跳转的目标行号;如果没有产生行号,则不显示任何编辑。如果跳转到了新行号,则再次调用 NES 模型,在远程行号周围重写一个窗口,最终产生远程 NES 编辑。
统一还带来了复合收益。由于一个共享模型驱动所有内联建议,每次模型改进或新模型特性现在都可以惠及完整体验,而不再局限于单一建议类型。
重构代码编辑任务
我们知道统一模型是北极星,我们意识到这是一个从模型输出格式开始重构代码编辑问题的机会。三种独立的 silo 任务——预测后缀(补全)、窗口重写(NES)或行号(光标跳转)——这些都是我们随时间逐步系统地单独解决的——它们各自的表达能力都不足以完全表示其他任务。要将这三个任务合并到一个能完成所有工作的单一模型中,我们需要一个类似的统一输出格式——这催生了一个我们称之为"diff patch"输出格式的优雅解决方案。
试想开发者正在修改函数签名。下一个有用的动作可能是在光标处补全一个新参数。过一会儿,可能是更新下面的一处调用点。再之后,可能是调整文件中其他地方的验证逻辑。这些是不同的交互,但它们同属一个编辑任务。共享的补丁语言让模型能够将它们作为同一问题的顺序步骤进行推理,从而可以更流畅地呈现给用户。
要完全理解模型输出格式的变化,可以将三个独立模型任务各自看作是 LLM 下一个 token 预测能力的自然扩展——补全延续未完成的代码,NES 重写本地编辑窗口,远程编辑预测下一个跳转的行号,并伴随对 NES 的调用。三合一模型将它们整合在一起,同时处理这三个任务,并允许模型一次预测多个编辑。
具体来说,看这个例子,其中符号 <|cursor|> 代表用户光标的位置。
def feed_penguin(penguin, fo<|cursor|>):
penguin.meals += 1
penguin.hungry = False
penguin.last_ate_time = datetime.now()
print(f"{penguin.name} ate {fish.type}")
penguin = Penguin("emperor")
feed_penguin(penguin, fish="silverfish")
补全、NES 和远程编辑模型可能独立预测以下内容,以为其相应任务做出最佳编辑:
def feed_penguin(penguin, food):
penguin.meals += 1
penguin.hungry = False
penguin.last_ate_time = datetime.now()
print(f"{penguin.name} ate {food.type}")
diff patch 输出格式将所有这些元素整合到一个编辑链中,在光标处、光标更远处,甚至在相关文件中做出最合理的编辑:
/path/to/penguins.py:0
-def feed_penguin(penguin, fish):
+def feed_penguin(penguin, food):
/path/to/penguins.py:5
- print(f"{penguin.name} ate {fish.type}")
+ print(f"{penguin.name} ate {food.type}")
/path/to/penguins.py:8
-feed_penguin(penguin, fish="silverfish")
+feed_penguin(penguin, food="silverfish")
/path/to/antarctica.py:42
-feed_penguin(penguin2, fish="krill")
+feed_penguin(penguin2, food="krill")
多补丁响应还可以让我们缓存后续补丁(上面示例中,第 5、8 和 42 行的编辑)。当前置补丁被接受时,后续补丁可以低延迟呈现,从而实现快速的 Tab-Tab-Tab 代码编辑体验。
这种格式完整表达了之前的三个任务,并为优雅地表达未来行为(如跨文件编辑,即在与当前文件相关但不同的文件中建议的编辑)提供了灵活性。像工具调用这样的更通用格式在预训练中更丰富,并支持编辑之外的操作,但对于代码编辑,我们发现这种格式在保持表达力的同时最为紧凑。
除了重新制定输出任务外,我们还借此机会重新审视了输入给模型的上下文。原本模型的输入包括:
- 不带行号的最近查看的代码片段
- 带行号的当前文件内容
- 用户最近在统一 diff 格式下的编辑历史
- 待编辑代码周围区域,以用户光标为中心上下 15 行,用于填充待编辑代码区域
- 待编辑代码,即以用户光标为中心上下 2 行到 5 行的重写窗口
统一的输出格式让我们也能简化输入上下文。由于任务范围现在允许在文件中任意位置进行编辑,而不仅限于预测特定位置的后缀或重写固定窗口,我们可以移除输入的最后两部分——待编辑代码周围区域和待编辑代码——并用一个包含当前光标位置行号和文件内容的 3 行块来替代。从之前的模型中我们获得的一个重要经验是,由于用户的光标位置不断变化,我们将光标位置部分保留在提示的末尾,而不是直接放在当前文件内容中。这通过最大化提示与之前提示的公共前缀来优化键值缓存。
我们希望系统地解决统一问题,因此分两个阶段来处理。首先,我们使用统一的 diff 补丁格式将 NES 和长距离编辑能力统一到一个模型中,我们称之为 2-in-1 模型。代码补全模型保持不变,客户端编排逻辑被简化,优先处理代码补全,其次是 2-in-1 模型。然后我们将 2-in-1 模型与代码补全模型统一,引入 ghost text 能力,最终得到 3-in-1 模型。我们将描述我们的实验过程,并分享我们开发的一些方法、遇到的挑战以及一路走来获得的经验。

图 2. 我们首先将 NES 和长距离 NES 模型统一为 2-in-1 统一模型,然后将 2-in-1 模型与代码补全模型统一,得到 3-in-1 模型。
第一阶段:2-in-1 模型
我们有意选择先解决 NES 和长距离编辑,然后再处理代码补全。根据我们的经验,代码补全模型占据了内联建议流量的绝大部分——超过 70% 的编辑机会。团队之前的实验也表明,NES 建议比 ghost text 更容易被用户忽略。因此,我们选择首先统一其他两个模型,最后再处理核心的代码补全模型。由于 diff 补丁输出格式可以用于 2-in-1 模型,这也意味着后续 3-in-1 模型的训练可以直接利用 2-in-1 模型的数据和其他经验,加快实验速度。
要构建一个好的系统并不断改进,我们必须从确定要衡量什么开始。没有任何单一指标能够全面捕捉内联建议模型的质量。因此,我们使用了四个互补的评估阶段,类似于我们之前用于下一个编辑建议发布的过程。
离线评估:针对性的离线测试帮助我们检查补全行为、困难编辑场景、补丁有效性、建议长度以及不同类型编辑之间的平衡。
内部 dogfooding:日常使用暴露了聚合分数可能会遗漏的问题,例如建议合理但时机不佳、过于急切、不完整或呈现方式别扭。
大规模人类评估:我们还对来自不同背景的程序员进行了人类评估,使用结构化反馈模板对数千个编码任务进行评估,以从真实开发者那里获得模型质量的定量评估。
对照在线实验:promising 的模型候选者通过 A/B 测试在线上飞行,以便我们能够评估该模型对真实用户的最终影响。
每个阶段回答了不同的问题。离线评估告诉我们候选者是否值得测试。Dogfooding 让我们从真实工作流程中第一时间了解其感受。人类评估量化了 dogfooding 体验并提供了结构化反馈。在线实验告诉我们完整的系统与现有的生产设置相比是否产生了实际使用价值。
原始 NES 模型的离线评估主要由 3 个基准组成,每个基准评估 NES 模型的不同维度:
Simulation Tests (STests)。由我们团队手动策划的一组工作区记录,展示了必须有、最好有和必须避免的建议行为。
HumanEval。OpenAI 的 HumanEval 数据集的增强版本,扩展用于评估模型在单行、多行和随机跨度的代码上的补全能力。
Output View Kind。通过先前生产模型传递的示例,包括模型输出的视图类型(ghost text、side-by-side NES、无编辑等),用于跟踪新候选者与原始模型相比产生的视图类型分布的差异。
训练 v1 模型:Diff-patch NES
我们首先构建工具,将原始 NES 模型的评估和训练数据 robustly 转换为 diff 补丁格式。数据转换完成后,我们训练了一些初始的监督微调(SFT)模型,然后在仅包含 NES 的训练数据上进行强化学习(RL)——我们称这一轮模型为 v1 模型。
正如预期,生成的模型在离线基准测试中能够与 NES 模型的表现相媲美,但它们学到的编辑分布正是 NES 编辑,几乎没有长距离编辑。我们还发现,尽管使用相同的数据样本,数据格式的差异有时会导致不同的行为。例如,编辑往往更具增量性,这可能是因为 diff 补丁格式自然地在特定位置表达针对性更改,而不是重写整个目标窗口(这在我们讨论 3-in-1 模型时还会再次出现)。在飞行测试这些模型时,我们还发现了处理这种新输出格式的几个客户端方面的细微差别,包括缓存和错误处理,这些在早期就得到了完善,以便未来的模型候选者可以从这次试运行中受益。
现在是时候加入长距离编辑了。在问题的 diff 补丁表述中,长距离 NES 情况下出现了一个优雅的特性:一条 NES 编辑 + 一条长距离 NES 编辑 = 一个多补丁响应。多补丁响应也很关键,因为缓存后续补丁以实现快速后续编辑可以带来用户体验的规模经济。
但是,当现有模型没有多补丁能力时,我们如何获得多补丁响应?
训练 v2 模型:加入长距离 NES
加入长距离编辑能力并非易事,因为长距离编辑和 NES 的现有数据集无法合并,因为它们来自完全不同的文件和编辑历史。我们需要从单编辑数据引导多编辑数据。我们最终想出了两种方法来解决这个问题:
使用 v1 模型伪标签引导响应。我们首先通过将输出标签移入编辑历史来快进 diff 补丁格式的 NES 数据。然后我们将这个快进的数据反馈回我们训练的最佳 v1 模型。如果模型产生了编辑响应,我们将这个伪标签作为另一个补丁附加到原始数据的输出标签上。我们继续重复这个过程,直到 v1 模型在其响应中不再产生编辑。这产生了具有 n 个补丁的样本,然后我们可以教模型产生这些样本。有关详细信息,请参见下图。

图 3. 使用 v1 模型伪标签从单编辑数据引导多补丁响应。
Edit playback。我们解析了 NES 数据模型输入,其中包含用户的编辑历史记录,并利用这些编辑将工作区文件的状态「回退」1 到 n 步。然后,我们将这些编辑从输入的编辑历史中移除,并前置到输出中。

Figure 3. 两种通过快速前推单补丁数据来反复引导多补丁数据的过程。上图:我们首先通过将输出标签移入编辑历史,对输出中包含 n 个补丁的数据进行快速前推。如果模型对该快速前推样本产生了响应,则该输出将被添加到原始样本的输出标签中,以创建一个包含 n+1 个补丁的响应。下图:我们执行了编辑回放,通过将最近的编辑从编辑历史移出,以回退会话状态,并将其作为模型输出的一部分进行预测。
在这些方法产生足够的数据后,我们使用 LLM-as-a-judge 和基于规则的质量过滤器对数据进行了筛选。过滤标准源自我们对先前代码编辑模型开发过程中开发者偏好的理解,包括正确性、必要性、以及意图和编辑历史的契合度,同时基于规则的过滤器还涵盖了补丁有效性和不良行为(如代码重复)。
我们随后使用 SFT 在这批新数据与原有 NES 数据的混合上训练了一个新模型,得到平均产生 1.3 到 1.4 个补丁的模型,而此前原始数据接近 1。这个数字听起来可能很小,但我们后来发现这是一个最佳区间,能够显著改善用户体验而不降低质量。毕竟,要获得每次响应平均 2 个补丁,长距离编辑需要在每次编辑机会中平均发生,而这在现有生产环境中由专用长距离编辑模型处理的设置下并非如此。
RL 以类似方式应用于 v2 SFT 模型,使用相同的技术来引导多补丁数据。评分逻辑也进行了调整,以纳入对多补丁响应的指导。我们发现该模型的离线结果相比 SFT 模型有了显著提升。
当存在多个编辑时,自然会问到如何排序的问题。这也是我们做出的一个慎重选择。
在数据筛选阶段,除了编辑质量标准外,我们还添加了一个新的标准,针对编辑排序——要求编辑在优先级和贴近用户光标位置方面具有流畅的逻辑流程。这一选择听起来很直观,因为它源自我们编排客户端中三个独立模型的经验。其逻辑是:先查询补全模型,再调用 NES 模型,然后是光标跳转模型,如果光标跳转模型选择跳转,则随后再次调用 NES 模型。
除了将这些标准纳入数据筛选流程外,我们还将它们纳入 RL 评分器,并在奖励函数中加入了一些额外的细微差别,我们观察到这对于优先排序逻辑连贯的编辑非常重要。
Pseudo-Online Evaluation 基准
v2 模型相比生产环境的三个模型对照组,在线上 A/B 测试中是一个阶梯函数式的改进。除了 dismissal 率升高外,其他指标如接受率、展示率、累计保留字符数以及用户参与度指标均无统计显著变化。但出现了一个奇怪的发现:离线结果对线上结果的预测能力并不强。例如,在 STests 中获得史上最高分的最强离线候选模型,在 A/B 测试中的表现却不如另一个 STests 分数较低的模型。这促使我们思考:如何缩小离线结果与线上结果之间的差距,从而更高效、更有信心地迭代模型候选?
结果就是我们创建了第四个离线基准,称为 Pseudo-Online Evaluation(POE)。该基准由三个等规模的样本桶组成,分别代表生产模型输出的接受、拒绝和无编辑案例,并附带建议质量信号。基准将每组中的样本通过候选模型运行。然后使用 LLM 评判器,通过上下文学习来判断给定该样本的已知结果,候选模型的响应是否会被接受或拒绝。从该基准中产生的最关键指标是:拒绝率(越低越好)、接受率与拒绝率之比(越高越好),以及从拒绝变为无编辑的比例与从接受变为无编辑的比例之比(衡量模型是否学会了对之前较差的响应更频繁地弃权,同时不丢失之前较好的响应)。我们还量化了补丁有效性错误等问题(后续会讨论)以及其他指标,如响应中的补丁数量。这些指标虽然不可否认仍不是完美的代理指标,但成为了后续模型候选预测线上性能的更强指标。
评分器与质量过滤器的校准
开发 POE 基准也引出了另一个问题:我们的新型 diff patch 专用 RL 评分器是否有遗漏?能否通过额外的数据和质量信号来改进它?我们决定尝试通过对评分器采用自我改进的方法来更好地校准其与用户反馈的关系。我们开发了一个 agent 技能,执行以下循环:
从 POE 中获取样本,通过评分器运行,获取评分器对生产模型响应的判断。
在运行所有样本后,构建混淆矩阵,包括假阳性、假阴性、真阳性和真阴性。
从每个类别中随机抽样样本,以了解评分器的常见失败模式和盲点模式。
调整现有的评分器指令,纳入任何观察到的、明显形成模式的反馈。
值得注意的是,质量信号始终需要人工判断,因为模型自身的反馈信号本身可能存在偏差,需要纳入考量。