
最近在调试一个多模块项目时我又双叒叕陷入了上下文分裂的泥潭手里同时开着六个文件脑子里装着重构A模块的方案手上却在改B模块的接口AI辅助编程工具的建议时对时错——不是它变笨了而是我喂给它的上下文乱了。反复横跳的体验让我下决心把“上下文”做成一个显式的、可切换的机制这就是 context-mode 项目的由来。它用配置化、可组合的方式把“当前在做什么”和“项目里哪些文件值得被当前任务关注”绑定在一起切换模式就像切换工位一样干脆。如果你也经常在大项目和AI辅助工具之间来回拉扯或者想给团队沉淀一套“不同任务看不同文件”的协作规范这篇文章值得你花十分钟读完。这个项目最终做成的是一个 VS Code 扩展但核心思路其实与具体编辑器无关通过一个 JSON 文件描述若干种“上下文模式”每个模式包含自己的文件包含/排除规则、Prompt 模板、快捷键和触发条件。插件启动后会维护一个实时文件集合并把集合里的相对路径加上文件摘要拼成一段结构清晰的上下文文本一键复制给 AI 工具或者放进评审清单。下面我会按“痛点—设计—实现—踩坑—扩展”的顺序完整复盘一遍。1. context-mode 要解决的三个真实痛点1.1 多任务切换时的认知碎片化一个典型的痛苦场景上午你在负责一个订单系统的性能优化已经定位到慢查询相关的三个文件中午线上忽然报了个权限校验 bug你被迫切过去修复下午又要对另一位同事的 PR 做代码评审。这三个任务的上下文彼此完全不相关但 VS Code 的标签页会被你越开越多最终形成二十几个标签页的壮观场面。你试图用工作区、TODO 注释、收藏夹来管理但项目稍微大一点就会失效。问题本质不在意愿而在“上下文”这个东西没有被显式建模——编辑器知道你有几十个打开的文件但不知道哪些文件属于当前任务。context-mode 的第一个发力点就是把“当前任务需要哪些文件”从大脑潜意识里搬到配置文件中让工具能像人一样分辨“这个标签页当前是否重要”。1.2 AI 辅助编程“记不住”真正重点现在很多 AI 辅助工具都支持“添加上下文”你可以把某个文件、某个选中片段或某个错误输出喂给模型。但这类操作是零散的你喂什么是你说了算工具不知道你的目标。对于新手最常见的问题是喂太多垃圾上下文——把整个项目的文件列表丢进去模型被海量无关代码淹没对于老手则是喂太少的上下文——模型没有足够信息生成靠谱的重构建议。context-mode 把这个问题转化为模式选择问题如果当前是“重构模式”插件自动带领重构相关的调用方、测试文件和配置文件如果模型窗口有限还能自动按依赖深度排序如果当前是“审计模式”则聚焦在数据流路径上其他业务代码全部忽略。换句话说AI 工具的“记忆”质量取决于我们如何做上下文减法而不是加法。1.3 不同任务需要的“视野”不同我用一个例子说明为什么不能只有全局“上下文”。假设你在改一个支付相关的模块有三个文件payment/service.ts、payment/api.ts、shared/logger.ts。在“功能开发”模式下你可能希望把这三个文件全部放进上下文。但在“日志规范检查”模式下shared/logger.ts是核心payment/api.ts几乎没有价值。在“安全审计”模式下你甚至需要把payment/webhook.ts和infra/db.ts也加入——这两个文件在平时的开发模式中完全无关。如果你只有一个固定的上下文集合要么过度包含要么漏掉关键信息。所以 context-mode 将上下文建模为“模式的集合”每个模式定义自己的视野范围切换成本就是要操作一次快捷键或命令换来的却是每个模式下 AI 建议的精确度显著提升。痛点传统做法context-mode 的做法多任务切换手动开关文件配置模式一键切换AI上下文过载手工挑选文件按模式自动收集任务与视野不匹配全局工作区每种模式独立视野到了这一步需求已经非常清晰我需要一个能快速切换、可配置、能自动维护文件集合的开发工具。这个工具不需要非常复杂但它必须把“上下文”从隐性变成显性从无序变成有序。2. context-mode 的模型设计模式、作用域与上下文收集2.1 模式定义一份 JSON 配置文件描述所有状态做项目的第一步是定义数据模型。我在项目根目录放一个.context-modes.json所有模式都在这份文件里描述结构大概像下面这样{ $schema: ./context-modes.schema.json, activeMode: dev, modes: [ { name: dev, label: 功能开发, description: 日常功能开发关注业务代码与测试, hotkey: ctrlalt1, include: [src/**], exclude: [**/*.spec.ts, **/node_modules/**], autoIncludeRelatedTests: true, maxFiles: 20, promptTemplate: 当前任务{{description}}\n项目上下文\n{{files}}\n请基于以上上下文给出建议 }, { name: refactor, label: 重构模式, description: 重构时更看重调用方和依赖关系, hotkey: ctrlalt2, include: [src/**], exclude: [], maxFiles: 40, priority: [src/modules/**/index.ts, src/modules/**/*.test.ts, src/shared/**], collectRelatedFiles: true } ] }这里最核心的是include和exclude它们定义了模式的“视野边界”。priority用于在文件数量超限时决定谁先保留。autoIncludeRelatedTests是我从实际开发中沉淀出来的需求——重构时我们几乎总是需要查看测试文件因为它们往往精确地表达了对模块行为的期望。为什么要用 JSON 而不是直接用 VS Code 设置项有三个原因第一JSON 可以放进 Git方便团队共享和 code review第二它不依赖特定平台的 UI命令行用户也能快速编辑第三schema 文件可以给编辑器提供补全和校验误配时能第一时间发现。后面我在写 schema 时还特意加了additionalProperties: false可以避免拼写错误悄悄通过。2.2 上下文收集的两种策略显式标签与自动推导光有配置还不够插件运行时要回答一个更实际的问题怎么知道当前模式关心哪些文件我实现了两种策略它们可以同时启用。第一种是“显式标签”开发者手动在某个文件上运行命令context-mode: pin钉住文件插件会把文件路径记录进当前模式的pinnedFiles字段。这种方式适合那些模式规则覆盖不到的特殊文件比如某个临时的分析说明、某个加密配置的样例文件。钉住的文件始终排在最前面不会被自动收集顶掉。第二种是“自动推导”插件监听编辑器里所有打开的文件、最近修改记录、Git 工作区变更然后用当前模式的 include/exclude 规则过滤加上活动编辑器所在目录的相邻文件最后按 priority 排序生成文件集合。自动推导里最讨巧的一个细节是当用户切换活动编辑器时插件不会立刻把新文件纳入上下文而是观察 2 秒后如果用户停留在这个文件才进行增量更新——这能有效避免快速翻阅文件时的抖动。2.3 切换机制手动命令、快捷键、自动检测模式切得顺不顺直接决定这个工具会不会被长期使用。我提供三种切换方式适配不同习惯的人。命令面板输入context-mode: switch会弹出一个 QuickPick 列表显示当前可用模式和一个小的描述。快捷键直接切换比如 ctrlalt1 到 9这适合已经能盲打快捷键的深度用户。状态栏左下角增加一个按钮点击后循环切换模式并用不同的颜色后缀区分——比如运行中显示context-mode: 功能开发重构模式显示橙色。自动检测是我后来加的一个小创新如果某个模式配置了fileExtensions或triggerOnGlob当活动编辑器满足条件时插件会弹出一行通知问你要不要切换到这个模式。比如你打开了*.spec.ts文件插件会建议你切换到“测试模式”。这个功能默认关闭因为弹通知太频繁会让人烦躁但打开的团队都说手感还不错。实测下来手动切换 状态栏是最常用路径自动检测属于锦上添花。3. 从设计到落地关键技术决策与实现细节3.1 技术选型为什么选择 VS Code 扩展而不是 CLI 工具做这个工具之前我其实纠结过一个纯 CLI 工具能不能做到同样的效果毕竟 context-mode 的本质是从文件系统里筛选文件并拼装文本CLI 完全可以做到。最后我还是选了 VS Code 扩展原因有三个。第一上下文不是死的。CLI 只能在你执行的一瞬间扫描文件但我希望文件集合能随着你打开、编辑和保存动态更新。VS Code 提供 DocumentOpen/Change/Close 事件能让 FileSet 始终保持最新。第二AI 辅助编程的用户通常已经把 VS Code 作为主战场扩展可以直接读取当前活动编辑器的内容、选中区域、甚至终端输出这些信息是 CLI 拿不到的。第三也是最重要的一点扩展可以和状态栏交互。状态栏按钮的存在让“当前模式”这件事变得可见。命令行用户的话他现在也可以基于同一个核心库包装一个 CLI但那是后话。如果你也想做类似工具我的选型建议是核心逻辑模式解析、文件过滤、优先级排序做成纯 TypeScript 模块不依赖 VS Code API方便以后独立成库扩展层只负责事件绑定和 UI。3.2 状态管理与文件监听增量更新避免全量扫描在开发早期我写过一个很“朴素”的实现每次切换模式时用 glob 全量扫描一遍工作区。模式简单、项目小的时候没什么问题项目一旦到了几万文件的规模切换一次要等两三秒状态栏卡在“正在收集上下文”上体验非常糟糕。后来我重构为增量更新的方案。核心是一个ContextCollector类它维护一个 MapModeName, FileSet并通过 VS Code 的 workspace events 不断更新 FileSet 的增删变化。伪代码如下class ContextCollector implements vscode.Disposable { private fileSets new Mapstring, Setvscode.Uri(); private subscriptions: vscode.Disposable[] []; constructor(private config: ContextModeConfig) { this.subscriptions.push( vscode.workspace.onDidOpenTextDocument((doc) this.addDoc(doc)), vscode.workspace.onDidChangeTextDocument((e) this.touchDoc(e.document)), vscode.workspace.onDidCloseTextDocument((doc) this.removeDoc(doc)) ); } private async addDoc(doc: vscode.TextDocument) { for (const mode of this.config.modes) { if (mode.matches(doc.uri.fsPath)) { this.fileSets.get(mode.name)?.add(doc.uri); } } } private async touchDoc(doc: vscode.TextDocument) { // 标记为待刷新而不是立即改变顺序 this.dirtyDocs.add(doc.uri); this.scheduleRefresh(2000); } }注意代码里这一点touchDoc并不会立刻改变文件在 FileSet 中的优先级而是把文件标记为“dirty”然后统一在 2 秒后的刷新里处理。因为每次按键都会触发onDidChangeTextDocument如果每次都真实地排序收集性能开销会非常高。用“先标记、再节流”的思路我可以把高频输入变成低频排序实际使用中卡顿基本消失。排序时有一条规则dirty文件临时排在最前面。这模拟了人的注意力机制——你最近动过的文件大概率是当前任务的核心。这个规则很简单但非常有效多个团队用过之后都反馈“上下文终于像是我正在想的东西了”。3.3 Prompt 生成模板将收藏的上下文拼装成 AI 可用的文本文件集合收集好之后下一步是把它变成模型能看懂的输入。我不直接拼路径列表因为模型没有任何文件内容可供参考。这里我借鉴了 RAG 非常朴素的思路对每个文件提取摘要。默认的摘要是文件前 80 行的内容加上一个“文件级别摘要”框——文件大小超过 10KB 的取前 150 行、中间的关键函数签名通过正则匹配function|class|interface抽取以及最后 30 行。再结合文件路径生成一段类似下面这样的上下文块 src/modules/payment/service.ts (summary: 258 lines) ...前80行... // exports: createPayment, refundPayment, retryPayment ...最后30行...在 promptTemplate 中{{files}}会被替换成这些上下文块。我还内置了一个maxTokens的粗略估算默认按字符数maxFiles * 平均行长度来限制。当文件数量超过限制时根据 priority 排序“砍文件”如果maxFiles允许的数量不足以覆盖所有文件再启用截断策略保留文件路径列表每个文件的正文只保留第一屏。这一部分可能看起来像是为 AI 工具设计的但实际上单纯用来做 code review 也很方便——你可以一键复制出“当前模式”下所有相关文件的摘要结构作为评审 checklist。3.4 性能优化缓存、节流与避免正则灾难调试了几个星期后我发现性能瓶颈往往不在文件扫描上而在那些“看起来没问题的正则表达式”上。先说缓存。模式配置解析成结构对象后我不会每次切换都重新JSON.parse而是只在文件变更时重新加载并同时维护一份include/exclude预编译后的micromatch闭包。这样每次匹配只是简单地调用闭包而不是重新解释 glob 字符串。再说节流。前面提到的 dirty 标记 2 秒延时这属于事件节流另外我还在读取文件摘要时用了 LRU 缓存文件内容如果没变化摘要直接从缓存里取不重复读盘。实测在一个约 2 万文件的中型仓库里切换模式的耗时从最初的 2.6 秒降到 180ms 左右状态栏按钮几乎点下去就完成。最后是正则。早期我在抽取 exports 时写了一行正则/(?:export\s(?:default\s)?(?:function|class|const)\s)([A-Za-z_$][\w$]*)/g。它能匹配大部分情况但在一个文件包含大量字符串字面量和嵌套模板字符串时正则回溯会明显卡顿。后来我改用了一种更保守的两阶段策略先把代码块和字符串字面量粗略替换成空白再抽签名虽然会漏掉一些字符串里的“伪签名”但性能稳定了不止一个量级。这是我在实际使用中收获的一个很有价值的教训在解析用户代码时宁可保守也不要让正则失控。4. 我在使用过程中的踩坑记录与排查链路4.1 状态栏一直显示“正在收集上下文”事件循环死锁第一个问题出现在我引入增量更新不久后。具体现象是模式切换后状态栏显示“正在收集上下文”然后永远不消失与此同时CPU 占用率 30% 上下浮动但没有任何操作能恢复。排查链路很值得记下来。第一步我先停掉扩展用 Extension Development Host 窗口而不是 main window在collect()函数入口和出口各打一条日志发现出口一直没有打出来。第二步我专门在addDoc里加了计数器发现onDidOpenTextDocument被反复触发——原因是collect()里面有一段从磁盘读取文件内容并生成摘要的代码但读取方式是用fs.readFileSync并且我错误地在读取完成后调用了markDirty()而这个markDirty()会触发scheduledRefresh最终又打开一个新文件……一旦读到某个超过内存大小的文件就形成“读取—dirty—schedule—打开—读取”的死循环。修复方案是把读文件改成fs.promises.readFile并且在解析阶段产生新的 dirty 时只把文件加入待处理列表而不立即触发下一轮 schedule。代码改完后再测试状态栏就正常了。教训是任何需要在事件回调里触发的异步任务必须严格区分“初始事件”和“次生事件”否则很容易写出自刺激循环。4.2 glob 排除规则在 Windows 路径上失效分隔符与大小写的双重陷阱第二个坑是和跨平台相关的。我初始在 macOS 上开发完全正常推到 CI 后发现有一台 Windows 构建机跑测试失败——排除规则**/*.spec.ts没有生效导致测试文件全被收进上下文。打开日志一看FileSet里全部是反斜杠路径而micromatch默认对反斜杠是原样匹配**/*.spec.ts永远匹配不上**\\service.spec.ts。解决办法是在ContextCollector里对所有路径做一次归一化把\换成/同时把盘符统一转成大写。这个操作必须在所有模式匹配之前完成而且缓存 key 也要基于归一化后的路径否则 Windows 上同一文件可能既被当成C:/project又被当成c:/project出现在两个不同的 FileSet 项里。自从清了这条坑我再也不在代码里直接拼path.basename了。提示如果你也在做跨平台文件匹配最好在入口统一路径风格规则文件里永远写/代码里永远写归一化后的路径。这个约定能省掉大量后续排查。4.3 与 Git 文件的“幽灵映射”为什么上下文里多了个奇怪目录有一个阶段我发现自己生成的上下文里竟然包含类似.git/objects/ab/xyz这样的路径。一开始我以为只是收集到了不该进的文件删掉 exclude 后重启还是出现。打了FileSet的 debug 输出后才发现是因为我写 include 规则时用了**/*而 glob 库的默认行为会匹配以.开头的隐藏路径。exclude里写**/.git/**理论上应该能挡住但如果 exclude 规则用的是负向 glob有的实现会因为dot选项不一致而漏掉。这个坑的排查链路不复杂但让我意识到glob 匹配的dot选项在不同库之间默认值完全不同。我在配置加载层显式设置dot: false确保任何规则都不会误入.git目录。同时我在ContextCollector入口加了一层硬过滤如果路径包含/.git/或/node_modules/直接丢弃不再依赖用户编写的规则。这个“安全兜底”概念非常有用后来连.next、dist、coverage也默认被过滤了。4.4 上下文容量限制当 prompt 超过模型窗口怎么办最后一个实践问题跟 AI 工具相关。收集到的文件数量虽然控制在maxFiles内但有的文件很大几十个文件拼出来的 prompt 轻松超过 8k token。我第一版直接用了“从前往后截断”结果核心文件被截没了AI 的回答质量断崖式下跌。我后来的做法是分三级策略。第一级按priority排序后所有文件只保留路径列表第二级在maxFiles内但 token 仍超限时对每个文件依次截断最后 30 行并保留函数签名列表第三级如果还是超限就把priority排后面的文件从“整文件摘要”降级为“路径首次出现的关键函数列表”。这套策略保证上下文永远不会爆窗口而且最重要的文件永远保留在最前面。如果你也做类似工具强烈建议加上一种“文件降级”机制不要只做简单的截断。5. context-mode 后续扩展方向与个人体会5.1 自动模式推荐现在模式切换主要靠手动但实际使用中我发现一个有趣的现象用户在打开测试文件时绝大多数人都想切到“测试模式”打开middleware文件时几乎切到“调试模式”。所以我在设计一个基于简单规则引擎的自动推荐收集最近 30 分钟的模式切换历史计算“当前打开文件后缀目录名 → 目标模式”的转移概率。当概率超过 0.7 时状态栏按钮会显示一个小圆点提示可以切换点击才生效。这比纯弹窗温和很多。我试过直接用通知推送用户反馈“像爬满蚂蚁”改成小圆点提示之后几乎没人抱怨。如果你想让模型直接决定模式也可以接入 AI在每次切换前把文件列表和模式描述发给模型让模型打分。但对这个场景来说规则引擎大概率更便宜、更可解释。5.2 适配 Neovim 和 CLI保留核心替换壳层开发这个项目时我把核心逻辑放在context-mode/core包里完全依赖 Node 文件系统不碰任何编辑器 API。这就意味着适配 Neovim 只需要写一个 Lua 插件调用 Node 脚本获取当前模式文件集合再映射到 quickfix 列表适配命令行只需要一个context-mode snapshot命令把当前模式的文件集合、prompt 模板直接输出到 stdout。我个人目前只在 VS Code 里深度使用了它但已经收到两个用户来问 Neovim 适配的问题。后续如果有时间我会先补一个简单的 CLI 参考实现让 Vim/Emacs 用户也可以基于同样的数据格式接入。核心思路不变模式模型、文件过滤、优先级排序这些逻辑是复用度最高的部分shell 再薄也不嫌薄。5.3 我在开发中的两条核心体会第一条体会是“最小可用闭环要先跑通再谈性能”。早期我全量扫描的实现虽然慢但它让整个功能链路变得清楚从配置到模式切换再到文件收集和 prompt 拼接每一步都有日志和可见的输出。真正把性能做上去反而是在功能验证完之后。如果你一开始就惦记着增量更新、LRU 缓存和事件节流很容易陷入优化泥潭而忘记核心功能。第二条体会是“上下文质量的本质是做减法不是做加法”。context-mode 最大的价值不是我实现了多少种模式而是它强迫每个用户去定义“当前任务不需要什么”——这个exclude字段、这层优先级排序往往比 include 更能决定 AI 建议的质量。我见过有人配置了include: [**]然后抱怨上下文太大这种时候问题不在工具而在模式设计。最后再分享一个小习惯我会在每周一早会上花十分钟 Review 一下项目的.context-modes.json看看哪些模式已经没人用了哪些模式需要根据新增模块调整 include 规则。上下文模式和代码一样也会腐烂需要经常维护。希望这个项目能给你一些关于“上下文管理”的启发动手把它捡起来用或者直接实现一个属于你自己的版本。