远程容器里跑本地补全,上下文不同步让建议全错位了

在远程容器里跑本地补全模型,上下文不同步的根因几乎只有一个:IDE 的补全上下文生成在本地,而模型推理在远端,两端对“当前文件状态”的认知出现了时间差或内容差。要解决这个问题,不能靠调参或换模型,必须先把“上下文”定义清楚,再决定它到底应该在哪一端生成、以什么格式传输、多久刷新一次。

先定位你的“本地补全”到底跑在哪一层

很多人说的“本地补全模型”其实混了三件不同的事:补全请求的触发点、上下文采集逻辑、模型推理进程。在远程容器场景里,这三者经常被拆到不同机器上,而拆开的那一瞬间,同步问题就埋下了。

最常见的有两种架构:

  1. IDE 在本地,代码在容器里,模型也在容器里跑。比如 VS Code + Remote-Containers,补全插件安装在容器内,模型服务(llama.cpp、Ollama、vLLM 之类)也起在容器里。这时候“本地”其实是相对的——你的键盘输入在本地,但文件内容、语言服务、模型全在远端。上下文不同步通常表现为:你刚改了几行,补全建议还基于十几秒前的文件快照;或者你在 A 文件编辑,建议里混进了 B 文件的旧内容。

  2. IDE 在本地,代码在容器里,模型在本地跑。这种更拧巴:文件通过 volume 或同步机制映射到本地,模型读本地文件,但 IDE 的编辑缓冲区和磁盘上的文件不是实时一致的。补全插件从磁盘读文件生成 prompt,可你屏幕上还有一堆未保存的修改。模型看到的永远是“上一次保存的版本”,而你的光标可能已经移动了很远。

所以第一步不是找同步方案,而是确认你的插件到底把哪份文件内容塞进了 prompt。拿 Continue 或 Tabby 这类支持远程开发的补全工具举例,它们在容器场景下通常会调用容器内的语言服务器或直接读容器内文件系统来构造上下文。如果你在插件配置里硬编码了本地路径,或者反过来把容器路径映射错了,补全建议错位就是必然结果,跟模型能力无关。

上下文不同步的三个具体来源

编辑缓冲区和磁盘快照不一致,是最隐蔽的一种。 很多补全插件为了性能,不会在每次击键时都重新读取整个文件,而是维护一个内存中的文档镜像,配合增量更新。远程场景下,这个镜像可能由 IDE 的 Language Server Protocol(LSP)维护,也可能由插件自己通过 didChange 事件维护。一旦容器内外的 LSP 不是同一个进程,或者插件绕过了 LSP 自己读文件,就会出现“IDE 显示的内容”和“模型看到的内容”分叉。我在一个 Kubernetes 远程开发环境里实测过:用 Continue 连接容器内的 Ollama,在文件未保存时触发补全,建议里能明显看到上一版代码的残留片段。抓取插件日志后发现,它传给模型的 prompt 里包含的是磁盘上的 old buffer,而不是编辑器的 dirty buffer。

跨文件上下文过期,比单文件错位更常见。 现代补全模型(DeepSeek Coder、Qwen2.5-Coder、StarCoder2 这些)都吃多文件上下文,比如当前文件的 import、相邻的同类文件、最近编辑过的文件。远程容器里,插件往往通过文件系统事件(inotify、fs.watch)来感知其他文件的变化。但容器文件系统的 inotify 事件可能延迟、丢失,或者干脆因为 overlayfs 的层级特性不触发。结果就是你改了 types.ts 里的一个接口,补全 api.ts 时建议还在用旧接口签名。这个问题的诡异之处在于:单文件补全看起来正常,一旦跨文件就错位,而且很难复现,因为它是事件驱动的竞态。

提示词模板里的“文件边界”标记错乱,会让模型把两个文件的内容拼在一起理解。 很多补全 prompt 用特殊分隔符(比如 // Path: src/foo.ts===== 之类)来告诉模型“接下来是另一个文件”。如果上下文采集逻辑在远程容器里读文件时,路径分隔符、换行符、编码出了问题——比如 Windows 本地挂载 Linux 容器路径,\r\n 被转成 \n 再转回来——分隔符可能被吃掉或错位。这时候模型看到的不是“两个文件”,而是一个被拼坏的大文件,补全建议自然张冠李戴。这种情况在 Git 的 core.autocrlf 配置和容器 volume 挂载同时存在时尤其高发。

解决思路:让上下文生成和模型推理在同一侧完成

既然错位的根源是“生成上下文的逻辑”和“消费上下文的模型”分处异地,那最直接的修法就是让它们合并到同一端。具体操作取决于你的插件架构。

以 Continue 为例,它的 context 配置项里可以指定用哪些 provider 来采集上下文。如果你在容器内跑 Continue,就要确保 context provider 也是容器内的,而不是回退到本地。比如 @file provider 应该解析容器内的绝对路径,而不是你本地挂载目录的路径。配置文件里如果同时出现了 /workspace/src(容器路径)和 /Users/xxx/project/src(本地挂载路径),插件可能会随机选一个,导致补全时读到不同步的文件。我见过的有效做法是:在 .continuerc.json 里把所有 context provider 的路径统一写成容器内路径,并禁用本地文件系统 provider。

对于第二类架构(模型在本地,代码在容器),我建议直接放弃“本地模型读容器文件”的幻想。要么把模型也迁到容器里,要么在本地维护一个与容器同步的代码副本,并让补全插件只读这个本地副本。后者可以用 Mutagen 或 docker-sync 做双向同步,但同步延迟依然存在。真正可靠的方案是把模型放进容器,因为容器内的文件系统状态就是唯一的真相来源,不存在“本地磁盘 vs 远程磁盘”的分叉。

如果暂时没法改架构,先用这两个临时手段止血

强制补全插件在每次请求前重新读取文件,而不是依赖缓存。 这个选项在很多插件里都有,只是默认关闭。以 Tabby 为例,它的 Vim/VS Code 插件里有一个 completion.ignoreAutoTriggerserver.requestTimeout 相关的设置,但更关键的是它内部对 buffer 的处理。如果你用 Tabby Agent 跑在容器里,可以检查日志里 textDocument/didChange 事件是否与你的击键频率一致。一旦发现事件丢失,说明 LSP 层已经不同步了,需要重启语言服务器或插件,而不是继续调模型参数。

在 prompt 里显式加入文件版本标记,让模型至少知道“这份上下文可能过期”。 这不是治本,但能显著降低错位建议被采纳的概率。比如在上下文末尾追加一行注释,内容包含文件路径、最后修改时间和一个递增的编辑序号。模型看到这个标记后,对冲突内容的处理会更保守。我在 CodeGPT 和 Continue 的 prompt 模板里都试过这个技巧,对于 DeepSeek Coder V2 和 Qwen2.5-Coder 这类对上下文结构敏感的模型,确实能减少“自信地给出错误建议”的情况。

一个容易被忽略的配置:LSP 与补全插件的时序

远程容器开发里,语言服务器通常也跑在容器内,而补全插件可能同时依赖 LSP 提供的信息(比如类型、定义位置)和自己采集的文件内容。如果 LSP 的文档同步是异步的,补全请求到达模型时,LSP 可能还没把最新的 didChange 事件处理完。这个时序差在本地开发时几乎不可感知,但在远程场景下,网络往返加上容器内进程调度,会被放大到几十甚至上百毫秒。

解决办法是让补全插件等待 LSP 空闲后再发起请求。VS Code 的补全 API 里有一个 isIncomplete 标志,配合 requestToIncompleteCompletionsRepository 的行为,可以控制补全请求的时机。但大多数第三方补全插件没有暴露这个粒度。如果你用的是自研方案或开源插件,可以在发起补全请求前,主动调用一次 LSP 的 textDocument/documentSymboltextDocument/hover 这类“轻量但强制同步”的请求,迫使 LSP 把当前文档状态处理完。这个 hack 的原理是:LSP 的请求是按顺序处理的,你发一个无关紧要的请求,等于在队列里插了一个屏障,确保之前的 didChange 事件都被消费掉了。

常见问题

为什么我在容器里跑 Ollama,补全建议总是慢半拍,跟上下文不同步有关吗?

慢半拍和不同步是两件事,但在远程容器里经常同时出现。慢半拍通常是推理延迟或网络往返造成的,模型本身生成速度没问题。不同步则是你看到的建议内容基于旧代码。如果建议内容正确但出现得慢,那是性能问题;如果建议出现得快但内容对不上,那是上下文问题。两者都会让你觉得“补全很怪”,但要分开排查。

我把模型也放进容器了,为什么跨文件补全还是错位?

单文件同步了不代表跨文件同步了。检查你的 context provider 是不是真的在容器内采集其他文件,而不是通过本地路径回退。另外看文件系统事件监听是否生效——overlayfs 上 inotify 有时不触发,可以改用轮询方式(比如每 2 秒扫一次最近修改文件列表)作为兜底。

有没有办法在 prompt 里直接告诉模型“这个文件可能过期了”?

可以,但效果有限。你可以在上下文末尾加一行类似 // WARNING: this file snapshot may be stale, last modified at 2025-01-15T10:32:11Z 的注释。模型看到这个提示后,对明显冲突的内容会更谨慎,但它无法主动判断哪些内容过期了。所以这只是降低错误采纳率的手段,不能替代真正的同步机制。

远程容器里用本地模型,延迟和上下文同步哪个影响更大?

从实际体验看,上下文同步问题影响更大。延迟高你还能等,建议错位你却很难察觉——尤其是在大段补全时,你可能会下意识接受一个看起来合理但基于旧代码的建议,等编译或测试失败才发现问题。延迟是显性的,错位是隐性的,后者更危险。