现代代码补全的技术路线:从 LSP 到大模型补全
这一篇单独聊补全:IDE 里“按点一下点号出来一堆建议”这件事,底层到底走过哪些演进阶段,以及现在大模型补全是怎么和 LSP 一起工作的。
早期补全:符号表 + 模板的年代
在 LSP 出现之前,很多编辑器的补全更多是“高级版的字符串匹配”:
- 扫一遍当前文件/项目里的标识符,做一个简单的索引;
- 当前光标前的前缀和这些标识符做匹配;
- 按字典序/出现频率排序,列出候选。
再加上一些:
- 固定模板(比如
for、if、try/catch的代码片段); - 关键词补全(语言关键字、常见内置类型)。
这种补全的特点是:
- 完全不知道“当前位置语法上该出现什么”;
- 不理解类型,也不理解上下文;
- 但实现简单,对所有语言都能凑合用。
LSP 补全:真正“语法/类型感知”的补全
有了 LSP 之后,补全的底座基本变成了:
- 编辑器只负责把“光标位置 + 文件内容”等信息发给 Language Server;
- Language Server 基于语法树、类型系统、项目索引给出 CompletionItem 列表;
- 编辑器根据这些 CompletionItem 渲染 UI、插入代码。
这类补全的能力明显上了一个台阶:
- 知道当前位置是“类型位置”“表达式位置”“参数位置”等;
- 知道有哪些符号在当前作用域可见;
- 知道类型信息,可以只给出类型匹配的候选;
- 可以带上文档、参数信息、重载签名等。
它的上限也很清楚:
- 大多数建议是“已有符号”的组合,很少生成新的结构;
- 跨文件时更多依赖静态索引,对“隐式约定”“业务语义”毫无感觉;
- 对于动态语言(类型信息弱)时显得有点吃力。
大模型补全:从“列出选项”变成“生成一段逻辑”
大模型进来后,补全这件事多了一种完全不同的风格:
- 输入不再只是“光标前的一点点上下文”,而是整段代码片段甚至多个文件;
- 输出不再只是一个标识符,而是一整行甚至一整段逻辑;
- 模型会模仿当前项目的风格:变量命名、错误处理习惯、日志格式等。
一个典型的大模型补全请求会包含:
- 当前文件中光标附近的代码(前后若干行);
- 相关文件的一些片段:接口定义、类型定义、调用方代码;
- 明确的任务指示:比如“只补全当前行后面的部分”“从光标位置开始往后写几行代码”;
- 语言/框架提示:当前是 TypeScript/Go/Java,使用了哪些框架。
模型的输出通常会经过一层 IDE 侧的处理:
- 对齐缩进和风格;
- 避免重复补全已经存在的代码;
- 如果它产生了多行/多分支逻辑,按策略截断到合适的长度。
LSP 与大模型补全的协同与分工
在实际 IDE 里,很少只用单一来源的补全。一个比较自然的分工是:
LSP 提供语法/类型级别的精准补全:
- 成员列表、方法签名、重写/实现接口等;
- 保证“不会编译不过”,是一个安全下限。
大模型提供上下文/风格感知的“整段建议”:
- 一句 SQL、一个完整的
if/for逻辑、错误处理和日志等; - 根据已有代码推断你“打算怎么写”。
- 一句 SQL、一个完整的
很多实现会把两路结果一起拉回来,在 IDE 里做一层排序和过滤:
- 当前只是简短成员访问时(比如
obj.),优先用 LSP 的成员列表; - 当前在写复杂表达式或控制流时,优先展示大模型的整段补全;
- 有些环境还会把“LSP 的候选”作为提示拼进大模型的 prompt,帮助模型做更精准的生成。
上下文构造:让补全“刚好知道够多”
大模型的补全质量很大程度取决于“喂给它的上下文”:
当前文件里:
- 最近定义的变量、函数、类;
- 光标附近的注释、Todo、类型;
- 同一个函数/组件的前后代码。
项目级:
- 当前文件所在模块/包里的接口定义;
- 被当前文件频繁调用的其他模块;
- 最近打开/编辑过的文件。
如果没有精心筛选,很容易出现两种极端:
- 上下文太少 → 模型补的东西风马牛不相及;
- 上下文太多 → prompt 被挤爆,成本和延迟飙升。
所以一个成熟的补全系统,往往在模型调用之前有一层上下文选择/压缩逻辑:
- 基于路径和依赖关系选相关文件;
- 只抽取函数签名/类型定义,而不是把整文件塞进去;
- 对注释和文档做适度摘要。
Post-process 与“最后一道防线”
大模型给出的补全结果,在真正插入编辑器前通常还要过一层“体检”:
- 语法快速检查:括号/引号是否闭合,缩进和当前文件保持一致;
- 简单的 LSP 校验:
- 插入之后是否马上产生明显语法错误;
- 类型是否完全不可接受(例如把 number 赋给 string 很明显不对)。
在一些高要求场景里,还会:
- 用小型的静态分析或单元测试对子补全做快速验证;
- 如果模型结果明显不合理,就降级回传统 LSP 补全。
这样可以在“保持模型带来的生产力”的同时,尽可能降低“瞎补”“补坏代码”的概率。
对使用者意味着什么?
从开发者视角看,现在的补全体验之所以比早年 IDE 强这么多,本质是:
- 语法/类型这条链路(LSP)已经非常成熟,能保证下限;
- 大模型在上面加了一层“语义 + 风格”的能力,把日常重复劳动大量自动化;
- IDE 把这两者编排在一起,做了不少“脏活累活”:上下文筛选、结果排序、风格对齐、错误兜底。
了解这些细节之后,再看各种“智能补全”相关的配置和行为,就会更容易判断:
- 哪里是“模型本身的问题”;
- 哪里是“上下文没选好”;
- 哪些场景应该更相信 LSP,哪些可以多依赖大模型来“帮忙多想一步”。