context-mode:为AI编程助手构建可切换的代码上下文模式 那段时间我几乎每天都在跟同一个问题搏斗代码库越来越大AI 编程助手却越来越健忘。明明改一个 Service 会影响整个模块的依赖注入模型却自信地给出一个删掉 provider 的建议明明仓库里有 800 多个文件我却只能把整个目录拖进对话框结果 token 烧掉几万回答还停留在最无关紧要的 utils 工具函数上。我开始意识到问题根本不是模型不够聪明而是我从来没有认真回答过一个问题每一次任务到底该让 AI 看哪些上下文这个问题的答案就是我做context-mode的起点。它不是一个花哨的框架也不是什么新算法而是一套让我把给 AI 看什么代码从手工复制粘贴变成可声明、可切换、可复用模式的小工具加工作流。这篇文章我会把完整的设计思路、核心实现、真实配置以及我踩过的坑都写出来适合正在用 AI 辅助开发、并且被大仓库上下文问题折磨的工程师参考。1. 让我决心写 context-mode 的两个真实翻车现场先讲两个我亲身经历的翻车场景它们直接决定了这个工具的形态。1.1 翻车现场模型不知道改这里会影响注册表当时我在维护一个 NestJS 项目任务是把某个 Service 里缓存逻辑抽出去。因为涉及依赖注入我特意把那个 Service 文件完整贴给了 AI 助手想着这肯定够了。结果模型给出的重构方案里直接把Injectable()装饰器下面的构造函数参数顺序改了还建议删掉一个看起来没用的 provider。方案一执行整个模块在应用启动阶段就报错提示无法解析依赖。原因很简单真正决定依赖关系的是同目录下的*.module.ts文件那段注册 Providers 的代码还有另一个被注入到构造函数里的接口定义。我贴给模型的上下文里根本没有这些信息模型只能靠猜而它猜得还挺有逻辑——只是逻辑建立在缺失的信息上。这个案例让我明白一件事代码的上下文不是文件而是依赖关系图。单独看一个文件信息就是不完整的只有把它的调用者、被调用者、依赖声明、类型定义放在一起才算一份可用上下文。1.2 翻车现场token 预算全被别人家代码吃掉另一个更常见的场景是写新功能。为了保险我把整个src/目录拖进了 AI 对话框想着反正模型上下文窗口大全给它总没错。结果是灾难性的。那次任务一共生成了 68,000 多个 token 的内容模型花了大量篇幅分析仓库里跟本次需求毫无关系的文件——历史遗留模块、过时的工具函数、甚至还有某个已经被废弃的数据迁移脚本。真正涉及的表结构变更、API 路由注册、前端页面调用的那三个文件反而因为排在大量无关内容之后被模型平均用力地对待给出的代码直接偏离了项目现有的分层约定。那时候我发现AI 的注意力是有限的上下文窗口再大也架不住噪音。给它塞得越多它反而越抓不住重点。1.3 结论我们要的不是更多上下文而是被选择的上下文两次翻车之后我做了个总结大多数开发者在用 AI 辅助编程时都在给太少和给太多之间反复摇摆。给太少模型缺上下文开始胡编给太多模型被噪音淹没关键信息反而被稀释。我们需要的是一个中间层——它不是把上下文当作一个固定文件包而是当作可以按需切换的模式不同任务修 bug、做 review、加新功能需要看不同范围、不同粒度的代码。这就是context-mode的核心思想上下文不是数量问题是结构、范围和选择问题。2. context-mode 的设计三个组成模块加一次模式切换想清楚要解决什么问题之后工具的整体结构很快就定下来了。它由三部分组成模式定义文件、上下文骨架生成器、内容组装流水线。整个使用过程就是一次模式切换。2.1 模式定义文件用一段 DSL 描述这次任务要看什么我平时跟团队协作最怕的就是每个人用 AI 的方式都不样你贴三个文件他贴整个目录。所以context-mode的第一步是把这次任务看什么固化成配置文件。我设计了一个 YAML 格式的模式文件放在项目根目录的.context/modes/下# .context/modes/bugfix.yaml name: bugfix description: 针对某个 bug 的定位与修复聚焦调用链和依赖关系 rules: include: - src/**/*.ts exclude: - **/*.spec.ts - **/*.test.ts - src/migrations/** - src/vendor/** focus: # 核心必须包含的文件即使 include 规则没命中也会强制加入 required: - src/app.module.ts - src/**/*.module.ts # 骨架需要生成符号级摘要的文件目录 depth: 2 transform: maxFileTokens: 4000 keepImportLines: true includeCallers: true includeCallees: true instructions: | 你是一名资深后端工程师。下面是某次 bug 修复任务的上下文包。 请先阅读代码骨架理解模块间的依赖关系再针对焦点文件给出修复方案。 注意只修改被明确标记为 FOCUS 的文件不要顺手重构其他模块。字段设计上我刻意区分了三类规则include是哪些文件可能相关focus是哪些文件必须精读transform是在组装上下文时如何裁剪加工。这三者分开是因为它们在流水线里处于不同阶段混在一起写会导致配置极难排查。用配置文件而不是写死在代码里是因为模式是需要团队沉淀的资产——新人来了看一眼配置就知道项目里常见的任务长什么样而不是各自为战。2.2 上下文骨架生成器先让模型知道项目长什么样很多人会直接把代码全文塞给 AI但我不这么做。context-mode在真正组装上下文之前会先生成一份骨架用 tree-sitter 解析代码提取出每个文件里的类、函数、接口、关键类型定义以及它们之间的调用关系。骨架长这样## 模块: billing 文件: src/billing/billing.service.ts (215 行) 类 BillingService 方法 createPaymentIntent(data: CreatePaymentIntentDto): PromisePaymentResult 调用: userRepo.findByExternalId, billingConfigRepo.getActiveConfig 方法 handleWebhookEvent(rawBody: string): Promisevoid 被调用: PaymentsController.postWebhook 依赖导入: UserRepository from ../user/user.repository BillingConfigRepository from ./billing-config.repository为什么不直接把代码都贴上去因为骨架给的是地图模型看地图就能建立对项目结构的认知比盲目阅读全文高效得多。真正需要模型精读的只有focus标记的少数文件。这符合一个很朴素的认知人看新项目也不会一上来读全部源码而是先看目录结构、类图、关键调用链。2.3 内容组装流水线过滤、裁剪、补充、打包所有配置最终都要过一遍流水线。我把链路分成四个步骤每一步都有对应的可观测输出文件树收集基于git ls-files获取仓库内受版本控制的文件列表天然过滤掉node_modules、dist等未跟踪目录。规则过滤依次应用include、exclude规则生成候选文件列表。代码块裁剪对候选文件做函数级裁剪——只提取符号对应的 AST 节点而不是整个文件同时保留 import 头、类型定义等必要上下文。组装与统计把骨架 焦点文件代码 自定义指令打包成一个 Markdown 文件输出到指定位置并打印 token 预估和文件命中统计。流水线的设计原则是每一步只做一件事。最初我想把过滤和裁剪放在一起做省一次遍历后来发现当某个文件意外被裁空时根本分不清是规则写错了还是 AST 解析失败了。改成四步之后配合--debug参数每一层都能输出中间结果排查问题的时间从小时级降到了分钟级。2.4 模式切换的默认值设计安全兜底为了避免有人图省事不写配置直接裸奔我设计了一套安全默认值。当没有指定任何模式时context-mode仍然会工作但行为极其保守只包含src/下受版本控制的源码文件默认排除测试文件、迁移脚本、生成代码、锁文件所有内容包括在内时打上只读标记明确要求模型只读不修改任何文件这套默认值后来帮了大忙。有一次我在 CI 环境里忘了指定模式工具没有报错而是以安全默认值启动避免了把整个仓库的测试文件打包给模型的灾难。默认值必须安全而不是必须功能强大——这是我在这个项目里学到的重要教训。3. 从零搭一个可用的 context-mode关键技术选型与实现纸上谈兵结束下面说具体实现。这部分是给想复刻或者改造的人看的。3.1 语言选择为什么先用 TypeScript 而不是 Go 或 Rust我把原型对比过 Rust 和 TypeScript。Rust 的性能确实好一个几十万行代码的仓库全量解析几十秒内能跑完但我的首要需求不是性能而是生态——我需要在 VSCode 里做插件需要跟现有 Node.js 工具链无缝集成还需要快速迭代配置格式。TypeScript 只需要一个 npm 包就能随处运行还能直接复用fast-glob、picomatch、yaml这些成熟库。而且context-mode的瓶颈根本不在解析速度上而在规则设计和上下文组装策略上——这些都是逻辑问题TypeScript 完全够用。最后我选了一个折中核心用 TypeScript但符号提取的部分调用 tree-sitter 的 WASM 版本性能关键路径再单独优化。3.2 代码结构解析tree-sitter 还是正则硬啃这是最核心的技术决策。我对比过三种方案方案优点缺点我的取舍tree-sitterAST 语义精确能拿准函数/类边界需要配置语言包WASM 加载有成本最终采用ctags / universal-ctags速度快多语言通用只有符号定位没有结构边界信息用于兜底提取正则硬匹配零依赖最简单大括号嵌套、注释字符串里全踩坑只用来提取TODO/关键行最终选了 tree-sitter但它带来的复杂度比我想象中高。最麻烦的是每个语言的 AST 结构都不一样TypeScript 里函数声明是function_declarationPython 里是function_definitionJavaScript 里则有可能是arrow_function嵌在变量声明里。这意味着我写了一套查询配置const languageQueries: Recordstring, string { typescript: (function_declaration name: (identifier) name) function (class_declaration name: (type_identifier) name) class (method_definition name: (property_identifier) name) method , python: (function_definition name: (identifier) name) function (class_definition name: (identifier) name) class , }然后用 tree-sitter 的查询 API 拿到所有符号的起始位置和结束位置。这让我能在字节级别精确地切割函数体而不是靠大括号配对这种碰运气的方式。3.3 函数级裁剪的实现细节裁剪边界与上下文前缀拿到符号位置后真正的难点来了如何在不破坏可读性的前提下裁剪函数代码。一开始我简单粗暴地把函数体截取出来结果模型看到的就是一堆孤零零的函数缺了 import、缺了类型别名、缺了外部依赖的 key。这种上下文模型根本没法用。后来我调整成保留上下文前缀策略默认保留文件开头的所有import语句因为类型依赖集中在这里保留被裁剪函数直接引用的类型声明通过 AST 查询函数的identifier引用再反查同文件里的类型定义函数体内部不做二次删除完整保留。按这个策略一个 400 行的文件裁剪后通常只剩 80~120 行30 行 import10 行类型定义加上 1~2 个完整函数。实测下来模型对这些裁剪过的代码的理解准确度明显高于完整文件——因为噪音更少关键路径更突出。3.4 token 预估与多模型适配token 估算这件事比我想象中复杂。不同的模型 tokenizer 差异巨大Claude 系列、GPT 系列、本地跑的 Llama 系对同一段代码的分词结果完全不同。一开始我图省事直接用字符数 / 3估算结果发现对中文注释严重低估——一个中文字往往只算 1 个 token 而实际可能是 1.5~2 个对纯代码又高估很多符号可以合并成一个 token。后来我改成按内容分类加权代码行按字符数除以 4注释和字符串行除以 2纯中文文本再单独打折。精度上升了不少但最后还是接入了正确的 tokenizer——用 tiktoken 处理 OpenAI 兼容模型、用dqbd/tiktoken和 Claude 的官方 tokenizer 包处理各自生态精度才算真正达标。多模型适配还涉及输出格式。同一个上下文包发给 Claude Code 时习惯用file_path加代码块的格式发给 Cursor 或 GitHub Copilot 时又偏爱纯 Markdown 拼接。我在配置文件里加了一个target字段指定不同的渲染模板。3.5 CLI 与编辑器集成的最小实现工具必须足够顺手才有人用所以我做了一个极简 CLI# 构建 bugfix 模式上下文包输出到 .context/output/bugfix.md context-mode build --mode bugfix --output .context/output/bugfix.md # 构建后直接复制到剪贴板 context-mode build --mode review --clipboard # 打印当前模式的文件命中统计和 token 预估 context-mode build --mode feature --stats # CI 里校验模式定义是否合法 context-mode validateVSCode 集成也做了但不复杂就是绑定一个快捷键选中当前文件名一键执行对应模式把生成的上下文包作为注释块插到聊天输入框里。核心逻辑还是 CLI编辑器只是个入口。我个人更推荐把这个命令接到 Claude Code 的/context命令或者 Cursor 的自定义指令里毕竟现在的 AI 编程工具都已经原生支持引用文件了。4. 三种真实模式配置与输出样例光讲设计不落地等于白写。这一节是我日常工作中最常用的三种模式直接照着配置就能用。4.1 bugfix 模式聚焦调用链修 bug 时模型最需要的信息是三样报错位置的代码、该函数被谁调用、该函数调用了谁。我的 bugfix 模式用两个开关实现transform: includeCallers: true includeCallees: trueincludeCallers会让流水线在裁剪时额外引入当前文件的调用方签名includeCallees则引入被调用方。配合 call graph 信息模型能准确判断这个改动会不会引发连锁错误。实际输出的上下文包结构如下## 任务说明 修复支付回调重复入账的问题。焦点文件为 src/billing/billing.service.ts 中的 handleWebhookEvent。 ## 项目骨架摘要 ...全部 547 个符号仅保留名称与调用关系 ## 焦点代码 文件: src/billing/billing.service.ts (裁剪后: 147 行 / 原始 263 行) ## 调用方上下文签名级 - PaymentsController.postWebhook - billingService.handleWebhookEvent - PaymentEventsSubscriber.onPaymentSucceeded - billingService.handleWebhookEvent ## 被调用方上下文签名级 - userRepo.findByExternalId: (externalId: string) PromiseUser - billingConfigRepo.getActiveConfig: () PromiseBillingConfig同样一个任务全库打包要 67,000 token用 bugfix 模式只需要 3,800 token而且首轮回答的准确性明显提升。有一次还因为它带了调用方签名模型主动提醒我这个函数同时被事件订阅器调用需要考虑事件重放的幂等性——这个细节我手动整理上下文时从来没注意过。4.2 code-review 模式关注变更与影响面代码评审最怕的就是审阅者不了解这段代码在项目里的位置。review 模式不基于固定文件而是基于git diffname: review rules: include: - git-diff exclude: - **/*.lock - *.lock focus: # 变更文件自动成为焦点 fromDiff: true # 同时引入变更文件的直接调用方 includeCallers: true transform: maxFileTokens: 3000流水线执行时先跑git diff --name-only把变更文件作为焦点再通过 call graph 找到它们的影响面。输出会额外生成一段变化概览## 变更影响面 - src/user/user.service.ts (变更) 影响: src/auth/auth.service.ts, src/billing/billing.service.ts - src/user/user.repository.ts (新增) 影响: src/user/user.service.ts这个模式在团队里推了之后review 效率提升比我想象中明显。大家以前是我只看diff现在能带着上下文看diff很多设计层面的问题在 review 阶段就暴露了。4.3 feature 模式跨模块新增功能写新功能时最麻烦的是上下文分散在多个模块。比如实现一个用户积分功能可能要同时看user模块、billing模块、database schema、api route。feature 模式的做法是指定一个目录集合并强制包含 schema 和路由文件name: feature rules: include: - src/**/*.ts - prisma/schema.prisma - src/**/*.route.ts - src/**/*.controller.ts focus: required: - src/app.module.ts - prisma/schema.prisma transform: maxFileTokens: 5000 keepImportLines: true includeCallees: true这个模式通常跨时长一次对话可能要几轮所以我会额外输出一个已有标记清单把已决策的信息比如表名、字段类型以摘要形式放在上下文包最前面方便模型在后续对话中反复确认不会因为对话轮次多了就遗忘。4.4 模式的组合与复用base 模式模式多了之后我发现每个配置里都有相同的字段比如排除测试文件排除生成文件保留 import 前缀这些全局约定。于是加了extends支持name: feature extends: base # 只写 feature 特有的差异base模式里统一放语言约定、项目说明、通用排除规则子模式只写差异。这样团队加新模式时只需要关心这次任务有什么不同而不是复制粘贴一大堆公共配置。我强烈建议用这套方法——否则配置文件会成为新的维护负担而工具本身也变成了一种上下文灾难。5. 我踩过的坑上下文工具自己也需要上下文工具写出来是一回事真正用起来是另一回事。这部分是我被现实教育后的记录每一个坑都实打实让输出质量降级过。5.1 坑一剪裁过度导致局部代码孤立第一次用函数级裁剪时我特别激进只保留目标函数体。结果模型总是问我这个函数返回的类型PaymentResult是什么userRepo是从哪来的——我明明已经保留了 import 和类型定义但还是漏了关键信息。排查后发现问题出在间接依赖上。PaymentResult这个类型可能不在 import 列表里因为它是在某个d.ts文件里通过 namespace 挂到全局的userRepo可能在构造函数里被注入但构造函数本身没有被裁剪进去。解决办法是增加一个引用完整性检查裁剪完代码后扫描所有 identifier检查在可视范围内是否有对应定义没有定义的就从全库符号表里尝试匹配把缺失的定义追加到上下文包末尾。这个检查逻辑很笨但效果立竿见影。裁剪上下文是一道减法题但减完之后必须证明剩下的部分仍然自洽。5.2 坑二token 统计在中文注释上差 3 倍这个问题已提过但它值得再展开一次。我的项目有大量中文注释第一次用字节数/3估算时预估值只有 1,800 token实际上传到模型后显示 5,200 token。也就是说估算和实际差了将近 3 倍。后来的修正方案是按行分类统计纯代码行按字节/4 估算注释行按字节/1.5 估算中文字符再乘 1.2 系数。这个粗糙的启发式方法准确率能到 85% 左右。等到接入真实 tokenizer 之后才算彻底解决。我看很多类似工具都忽略了这个问题但这直接影响用户会不会超额——特别是用免费额度的时候超了真的会中断任务。5.3 坑三glob 规则叠加导致模式悄悄失效这是让我排查最久的一个问题。某个模式配置了exclude: [src/legacy/**]而项目的.gitignore里恰好也有legacy目录。文件树收集阶段是基于git ls-files的所以 legacy 目录下的文件本来就不会出现在候选列表里但exclude规则又会再过滤一次。看起来没问题直到有一天我加了--debug才发现一个被标记为focus.required的文件因为同时命中了.gitignore和模式里的 exclude被第二道过滤静默剔除了。整个模式看似成功其实核心文件根本没进上下文。解决方案是在--debug输出里增加三块信息每个规则的命中次数、最终文件列表、以及为什么某些 required 文件没有被包含。排查这类问题最忌讳的是静默失败——宁可多打日志也不让工具悄悄做决定。5.4 坑四大型仓库的解析性能最开始在公司的核心仓库1,200 多个 TypeScript 文件上跑context-mode buildtree-sitter 全量解析需要约 2 分钟。这个时间对命令行工具来说太长了。我做了三个优化按需解析先根据规则过滤出候选文件只对候选文件做 AST 解析。这个改动直接把 2 分钟降到 30 秒左右。增量缓存在.context-cache/目录缓存文件哈希与解析结果只有文件变更时才重新解析。二次运行通常在 5 秒内完成。并行解析用worker_threads按文件数分片并行8 核机器上能再快 3~4 倍。优化之后日常使用的体验基本无感。我还是很建议做缓存的因为上下文工具本身也是每天要跑多次的工具如果每次都要等几十秒人很快就会放弃用它。5.5 坑五团队协作中模式文件漂移工具引入团队后配置文件本身也成了需要维护的代码。我们遇到过某个目录重构后配置文件里的路径全失效了但没有任何报错某个模式被加上了过强的 exclude导致新代码永远进不了上下文还有一次有人把mode: bugfix写成了mode: bug-fix字段校验没拦住模式直接空转。解决办法有三层第一层是 JSON Schema 校验保证字段名、枚举值合法性第二层是启用了context-mode validate作为 CI 检查项拉取最新文件树对比模式里所有 include/exclude 规则的命中情况命中数为 0 就告警第三层是人肉约束——模式文件也要走 code review如果修改模式定义提交信息里必须附上之前和之后的 token 对比。这三层下来配置文件漂移的问题基本得到控制。6. 这个工具给我的日常带来了什么变化工具稳定运行了几个月我记录了它给工作流带来的真实改变。任务类型平均 token 使用首轮回答可采纳率人工返工次数每周手写上下文45,000约 40%8~10使用 context-mode4,500约 75%2~3数字只是表面我更看重的是思考方式的变化。以前我花大量时间整理上下文本质是在手动维护一个隐形的知识图谱现在我把这个图谱本身变成了可声明的配置每次新建模式都是一次对项目架构的重新梳理。这款工具并不是万能的。它没有解决需求本身不清楚的问题——如果任务目标模糊上下文再准也没用它也没办法帮你判断一个方案是否设计得优雅那是模型和人的事。它做的最重要的一件事就是把该给 AI 看什么从一个每次都要重新思考的问题变成了一个沉淀好的、按一下就能用的工程资产。如果你也在大仓库里用 AI 辅助开发我的建议很直接不要急着优化 prompt先把你项目的上下文分层设计好。定义三五个模式——修 bug 的、做 review 的、加功能的——比你背一百个提示词技巧都管用。项目代码的组织方式才是 AI 最好的提示词。context-mode只是把我这个想法变成了一个可以每天用的命令行工具而真正让工作发生变化的是开始认真对待上下文这件事本身。