“这个函数为什么报错?”是 IDE 里最自然的一句话,也是最考验上下文的一句话。
人类同事看到你把光标放在src/auth/token.ts:42,通常能立刻理解“这个函数”。模型没有眼睛,必须有人把现场翻译给它。
2.1 现场快照包含什么
src/extension/runtime/vscodeRuntimeContext.ts从 VS Code API 读取当前状态,再交给collectCodeRuntimeContext做规范化。最终上下文可以包含:
- workspace 名称和根目录;
- active editor 的路径、语言、行数、脏状态;
- 光标位置和用户选区;
- 光标附近有限行数的代码片段;
- visible editors 与 open tabs;
package.json、配置文件和文档等项目文件摘要;- 当前诊断信息,包括级别、行号和消息。
contextPrompt.ts再把这些字段渲染成带标题的 Markdown 块。路径、语言、行号和截断状态都被显式写出,模型不必从一团代码里猜“这段来自哪里”。
2.2 预算不是附加功能
运行时上下文有一个字符预算。每次budget.take(text)都先计算剩余空间,超出后截断,并在最终 prompt 中写出usedChars/maxChars和“是否截断”。
这很像急诊室的白板:空间有限,必须先写病人的姓名和生命体征,再写病史摘要,不能把整本病历贴上去。预算还让模型知道自己看到的是摘要,而不是完整文件。
优先级示例: 当前文件路径与选区 > 光标附近代码 > 诊断信息 > 打开的标签 > 项目文件摘要这里的优先级不是为了“省 token”这么简单,而是为了让上下文在被截断时仍保留决策所需的最小现场。
2.3 “当前文件”为什么不能等于“整个文件”
用户的注意力通常集中在一个很小的范围。将整个 3000 行文件注入,可能把真正相关的 20 行淹没,还会挤掉工具规则和历史结论。LoopAgent 默认收集选区或光标附近片段;需要更多范围时,模型可以调用readFile精确读取。
这形成了两阶段策略:
- 快照阶段:低成本告诉模型“现场在哪里”。
- 取证阶段:通过工具读取“现场发生了什么”。
2.4 失败时不能拖住主循环
providerRegistry对运行时上下文采用 best-effort 策略:收集失败时吞掉异常,继续进入模型和工具循环。原因很现实:编辑器状态偶尔变化、文档可能已关闭、诊断 API 也可能暂时不可用。一个辅助上下文源不应该让整个 Agent 无法回答。
2.5 从 VS Code 对象到稳定数据结构
直接把 VS Code API 对象交给模型并不可行。它们包含 URI、事件对象、可变编辑器状态和大量模型不需要的字段。LoopAgent 因此把采集拆成两层:
VS Code API -> createVsCodeRuntimeContextSource() -> CodeRuntimeContextSource(宿主无关的原始结构) -> collectCodeRuntimeContext() -> CodeRuntimeContext(预算化、相对路径化、截断后的快照) -> renderCodeRuntimeContextPrompt()这个分层带来两个工程收益。采集层可以独立处理 VS Code 的 API 细节;规范化层不依赖真实 Extension Host,可以用普通 TypeScript 测试各种边界。未来即使数据源换成其他编辑器,只要能生成同样的 source 结构,后半段逻辑仍可复用。
2.6 默认预算背后的优先级
当前实现给运行时文本设置了几个明确上限:
- 总字符预算默认
12_000; - 光标附近默认向前、向后各取
80行; - 最多保留
20个打开标签; - 最多保留
20条诊断; - workspace intelligence 还会单独限制文件数量和单文件大小。
这些数字不是模型上下文窗口的上限,而是“运行时快照愿意占用多少空间”的局部预算。system 规则、对话历史、工具结果仍需要空间,因此运行时快照不能独占全部窗口。
预算的消费顺序同样重要。活动编辑器和选区先于项目文件摘要进入,意味着容量不足时,用户正在看的内容更有机会被保留。这是一种基于交互意图的排序,而不是平均分配。
2.7 选区、光标和脏文件的细节
编辑器里存在三个容易混淆的事实:磁盘文件、内存文档和用户选区。
用户可能已经修改代码但还没保存。此时磁盘上的readFile与编辑器里的 document text 不同。运行时快照会携带isDirty,提示模型当前视图可能领先于磁盘。可靠的 Agent 不能一边引用未保存选区,一边声称磁盘测试已经覆盖了它。
有选区时,选区通常比光标附近文本更能表达用户意图;没有选区时,再退到以光标为中心的片段。片段记录起止行和是否截断,让模型能说“我看到第 35–68 行”,而不是含糊地说“在上面的代码里”。
2.8 路径为什么要相对化
toWorkspaceRelativePath会尽量把绝对路径变成工作区相对路径。相对路径有三点好处:
- 输出更短,减少重复的磁盘前缀;
- 回答更容易在项目内复现;
- 降低无意义暴露本机用户名和目录结构的机会。
多根工作区会让相对化更复杂:同一个src/index.ts可能属于不同根目录。因此实现必须结合 workspace roots 判断,而不能简单删除字符串前缀。
2.9 Prompt 渲染也要防“格式逃逸”
运行时片段会被包进 Markdown 代码围栏。如果源码本身包含三反引号,它可能提前关闭围栏,让后续内容看起来像新的 prompt 指令。sanitizeCodeBlock会替换这类围栏标记,保持内容仍被解释为代码数据。
这不是完整的 prompt injection 防御,但它修复了一个明确的结构问题:数据不能通过伪造格式边界改变自己在 prompt 中的角色。
2.10 一个可复现的现场测试
可以构造以下场景验证运行时上下文:
- 打开
src/auth/token.ts,在第 42 行选中refreshToken。 - 在内存中把函数参数改掉,但暂不保存。
- 制造一条 TypeScript error diagnostic。
- 提问:“这个函数为什么报错?先只分析。”
理想输出应体现:当前相对路径、TypeScript 语言、选区范围、脏状态、错误位置和消息;模型随后若需要文件其他部分,再调用读取工具,而不是假装快照已经包含完整文件。
测试还应覆盖空工作区、无活动编辑器、选择跨越超长文本、标签超过 20 个、诊断超过 20 条和项目文件读取失败。只有空状态也能工作,快照系统才算真正稳健。
2.11 诊断信息是线索,不是判决
VS Code diagnostics 可能来自 TypeScript language server、ESLint、Java 插件或其他扩展。它们的生命周期和准确性不同:有的在敲键盘后立即更新,有的需要保存或重新构建。
因此诊断上下文应包含 provider 能提供的路径、级别、位置和原始消息,却不应该被渲染成“已确认根因”。例如Cannot find name 'config'可能是变量真的不存在,也可能是语言服务尚未看到刚生成的类型文件。
模型合理的下一步是读取附近代码、检查导入和项目配置;如果需要证明修复成功,再运行对应 typecheck。诊断负责把注意力引向现场,编译或测试负责作最终判定。
2.12 运行时上下文的隐私边界
IDE 现场可能包含用户尚未准备共享的内容:未保存草稿、环境配置、打开但无关的密钥文件、诊断消息中的绝对路径。采集器不能因为“当前可见”就默认全部适合发送给外部模型。
工程上至少需要考虑:
- 文件类型和排除规则,特别是
.env、凭据和大型日志; - 将绝对路径相对化;
- 只收集必要片段,不默认读取所有可见编辑器全文;
- 在模型请求日志中避免打印完整 prompt;
- 对图片附件和文本上下文使用相同的数据边界说明。
上下文相关性和数据最小化在这里方向一致:少发无关内容既提高质量,也降低泄露面。
2.13 一个“现场与磁盘冲突”的故事
用户把timeout = 30改成timeout = 300,尚未保存,然后问:“为什么还是 30 秒超时?”运行时快照看到脏文件中的 300,磁盘上的测试和运行进程仍读取 30。
如果模型只看编辑器,它会怀疑运行时缓存;只看磁盘,它会说用户没有修改。正确回答必须同时承认两个事实:编辑器中的未保存状态是 300,进程可见的磁盘状态仍是 30。
这个例子说明上下文不仅要记录值,还要记录值属于哪个世界。isDirty只有一个布尔字段,却能阻止一次非常典型的错误归因。
2.14 本篇原则
运行时上下文的目标不是“尽可能多”,而是“让模型知道当前现场,并能沿着工具继续调查”。快照负责定位,工具负责证明,预算负责防止定位信息被淹没。