行内补全:实现思路与关键细节(FIM + Diff + 多层过滤)
想把 “行内补全” 做得好用,核心不是 “能生成”,而是生成后如何稳定落到光标处:不重复、不乱插、不无限循环、单行 / 多行行为符合预期。
先看结论
- 使用标准 FIM(Fill-In-the-Middle) 流程:
prefix + suffix -> middle - 内置模型:Pro/Qwen/Qwen2.5-Coder-7B-Instruct(通过统一的
"FIM"模型名对外提供) - 非流式请求拿到完整输出,但内部用 “类流式管道” 逐层过滤(字符级→行级→后处理)
- 单行补全额外做 Diff:自动判断 “插入” 还是 “替换到行尾”,避免括号 / 后缀重复
- 多行补全不做 Diff:直接替换到行尾(更符合用户预期,也省成本)
1) 整体架构(从 VSCode 到最终插入)
流程上分 6 段:预过滤 → 分类 → 模板 → 调用与过滤 → 后处理 → Diff(单行)
InlineCompletionProvider
-> 预过滤(跳过不该补全的场景)
-> 构建 prefix/suffix(HelperVars)
-> 单/多行分类
-> FIM 模板渲染
-> 调用 FIM API(非流式拿完整结果)
-> 过滤器管道处理(字符级/行级/后处理)
-> 单行:Diff 决定 range;多行:替换到行尾
-> 返回 InlineCompletionItem
2) FIM 请求格式与参数
FIM 模板(Qwen 标准)
<|fim_prefix|>{prefix}<|fim_suffix|>{suffix}<|fim_middle|>
关键约定
- API 模型名统一用:
"FIM" - 请求方式:非流式(一次性拿完整结果)
max_tokens:单行 64、多行 128(策略性限制 “胡乱续写” 概率)
3) 预过滤:哪些场景直接不补全
目的:便宜地跳过 “补全只会添乱” 的场景,减少无意义请求。
常见跳过项:
- 配置文件 / 特殊文件模式
- 未命名空文件
- 多光标(multi-cursor)
- 其它明确不应触发 inline completion 的场景
4) 单行 vs 多行:分类策略(决定用户体验)
分类逻辑按优先级(越靠前越强):
- Intellisense 选中项:强制单行(避免和补全列表打架)
- 单行注释检测:强制单行(注释里多行补全通常噪音大)
- 语言特定规则:按语言做微调
- 默认:允许多行
5) 过滤器系统:三层拦截,解决 “能生成但不好用”
即使是非流式输出,也可以把 “完整文本” 当成流来处理:逐步截断、跳过、清洗。
LLM 完整输出
-> 字符级过滤(stop token / suffix 重复 / 首字符换行)
-> 行级过滤(行数限制 / 重复行 / 相似行 / 空注释 / 双换行)
-> 后处理(空白、重复上一行、极端重复、去 markdown backticks)
-> 最终补全
5.1 字符级过滤(更早、更快止损)
典型能力:
- Stop Token 检测:遇到终止标记立即截断
- 后缀重复检测:避免生成 “补全 + suffix + 继续乱写”
- 首字符换行过滤:不让补全以空行开头(体感很关键)
5.2 行级过滤(防循环、防抄下一行)
典型能力:
- 行数上限:例如超过 50 行直接停止
- 重复行检测:同一行反复出现(模型陷入循环)就停
- 相似行检测:生成内容与 “光标下一行” 相似度过高就停(防重复已有代码)
- 空注释过滤:只输出
//、#这种空注释行直接丢弃 - 双换行过滤:控制空行数量,避免 “稀碎的大段空白”
5.3 后处理(最终把关)
典型规则:
- 空白补全直接拒绝
- 重复上一行(基于 Levenshtein 相似度)直接拒绝
- 极端重复模式(例如 6 行以上重复)拒绝
- 去掉模型偶发输出的 Markdown 代码块标记(```)
6) 单行 Diff:解决 “补全重复后缀 / 括号” 的关键
问题本质
FIM 模型对单行位置常见三种输出:
- 纯插入:只生成新内容
- 重复后缀:把光标后的文本也生成了一遍
- 部分重叠:生成内容与后缀部分重叠
如果不处理,最常见的坏体验就是:多一个括号、多一段重复 token。
解决方式(单行专用)
用 diffWords() 比较:
currentText:光标后到行尾的已有内容completion:模型给的最后一行补全
然后根据 diff 模式判断:
- 该不该只 “插入”
- 还是要 “替换光标到行尾”(即设置
range)
Range 语义(VSCode 行为)
range = undefined:在光标处插入range = { start: cursor, end: lineEnd }:先删除光标到行尾,再插入(用于清理重复后缀)
7) 多行为什么不做 Diff,而是直接替换到行尾?
多行补全的期望更像 “接管后续内容”,而不是 “精确对齐每个 token”。
选择 “替换到行尾” 的原因:
- 多行 Diff 成本高,收益不稳定
- 多行重复后缀概率相对低(且过滤器已拦截一部分)
- 用户更希望多行补全能 “把后面那段写完”,而不是半插半改
附:代码组织(给想看源码的人)
关键文件(按职责):
VvCompletionProvider.ts:主入口,串起 6 阶段流程vvCompletionStreamer.ts:API 调用管理(非流式)vvAutocompleteTemplate.ts:FIM 模板prefiltering.ts:预过滤multiline.ts:单 / 多行分类filters.ts:后处理processSingleLineCompletion.ts:单行 DiffstreamFilters/:字符级与行级过滤管道
评论区(暂无评论)