做 AI 辅助开发大半年,我最大的感受不是模型不够聪明,而是它经常被"塞进上下文的无关代码"带偏。明明只改一个支付模块的 bug,AI 却把订单、库存、用户积分全翻了一遍,最后给出一段逻辑错乱的代码。后来我做了个小工具context-mode,用一套轻量的规则把"该给 AI 看什么"这件事管起来,实测效果立竿见影:上下文体积压到原来的十几分之一,回答质量反而稳定了不少。这篇就把它的设计思路、核心实现和踩坑记录从头到尾摊开讲。
context-mode不是一个复杂框架,本质是一个命令行的上下文管理工具:它扫描当前仓库,按照项目级、文件级、任务级三种模式筛选出真正相关的文件,打包成标准格式的上下文块,供你直接粘贴给各种 AI 编程助手。适合受够了"AI 回答跑偏"的开发者,也适合团队里想统一上下文格式、减少 token 浪费的人。
1. 从"上下文失控"说起:为什么需要 context-mode
1.1 你可能会问:直接把整个仓库丢给 AI 不行吗?
表面上确实能跑。现在不少助手支持把整个目录拖进去,或者通过工具自动拉取全部文件。但问题也随之而来:
- Token 成本暴涨。一个中型仓库几万到几十万行代码,全量塞进去动辄几万甚至十几万 token,一次对话就要烧掉大量额度,团队多人用起来成本更明显。
- 无关信息干扰判断。模型是概率生成,上下文越长、干扰越多,它越容易把某个不相关文件里的命名风格、历史接口误认为当前任务的约束。我见过最离谱的一次,AI 因为扫描到了旧版接口定义,在提交信息里把我改掉的函数名又写回去了。
- 响应延迟明显变大。上下文越长,首字延迟越高。在交互式编码场景里,卡几秒再出来第一个字,体验其实挺劝退。
- 结果不可复现。如果每次给的上下文都不一样,AI 给出的方案自然也不一样,排查问题的时候很难说清"上次它为什么能写对"。
那是不是该上 RAG(检索增强生成)?我试过,对代码仓库来说 RAG 有它的价值,但有两个痛点:一是搭建和调优成本不低,二是它默认"模糊匹配",对精确引用(比如某行的导入路径)并不稳定。context-mode走的完全是另一条路线:确定性的规则筛选,每次打包的结果可预期、可复现,不引入额外的大模型调用,也没有索引和向量库要维护。
1.2 context-mode 到底解决什么问题
这个工具的核心目标有三个:
- 作用域控制。明确告诉 AI "这次只需要看这三类文件"。
- Token 预算控制。设定一个上限,超出就按优先级裁剪。
- 上下文可复用。同一份上下文打包结果可以存成文件,团队成员共用、CI 里生成,避免每个人手动复制粘贴。
用一句话概括:它不是让 AI 更聪明,而是让 AI 不犯"看太多"的错。
它的设计原则也刻意保持简单:
- 不做语义检索,只做路径规则和关键词打分;
- 不强制改变 AI 使用方式,输出标准文本块,粘贴就能用;
- 配置放在仓库里,和代码一起走版本管理。
2. context-mode 的三种作用域:项目级、文件级、任务级
2.1 项目级作用域:全局视角下的"重点文件"
这是默认模式,适合需求评审、架构梳理、技术方案讨论。它的思路是:不把所有文件都带上,而是根据仓库特征挑出"最能代表全局"的文件。
核心规则如下,按优先级排序:
- README、项目说明文档;
- 构建文件(如
pyproject.toml、go.mod、package.json); - 入口文件(如
main.py、cmd/、src/index.ts); - 目录结构树(用
tree命令生成的文件清单); - 最近 7 天有提交记录的文件摘要(通过
git diff --stat拿)。
配置文件长这样:
# .contextmode.yaml mode: project token_budget: 6000 priority: - "README.md" - "pyproject.toml" - "src/main.py" - "src/**/*.py" ignore: - "docs/**" - "**/*.lock" - "node_modules/**" - "**/*_test.go"priority里的 glob 规则决定文件优先级,ignore自然不用多说。项目级模式我主要用于给 AI 讲背景:"我们是一个用 FastAPI 写的订单服务,技术栈是什么样、入口在哪、最近改了什么",让 AI 先从全局角度给方案。
2.2 文件级作用域:单点修改时的"最小上下文"
文件级模式解决的是"改一个函数但要带动一堆依赖"的问题。它的逻辑是:以指定文件为起点,解析它的导入/引用关系,把直接依赖的文件也一并带出来。
实际命令:
cm mode file src/payment/service.py --depth 2--depth表示依赖层级,默认是 1。层级 1 只包含该文件和它直接 import 的文件;层级 2 还会包含这些文件各自 import 的文件。
这个模式的底层实现依赖一个轻量 AST 解析函数,比如对 Python 文件提取import和from ... import语句:
import ast from pathlib import Path def extract_imports(path: Path): tree = ast.parse(path.read_text(encoding="utf-8")) imports = [] for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: imports.append(alias.name) elif isinstance(node, ast.ImportFrom): if node.module: imports.append(node.module) return imports拿到导入列表后,按照项目根目录映射成相对路径,再递归解析。注意--depth不要超过 3,超过三层后依赖爆炸,token 预算很难控制住。
文件级模式最适合的场景是:AI 在哪里报错了,你只想让它集中精力搞明白这个文件以及它直接牵连的代码。
2.3 任务级作用域:用自然语言定位"相关文件"
任务级模式是我平时用最多的。它接受一句自然语言描述,通过关键词匹配和路径打分,从仓库里筛出最可能相关的文件。
cm mode task "订单超时未支付,状态一直停在 pending"内部做的是这样几件事:
- 把任务拆成词项,比如"订单""超时""未支付""状态""pending";
- 对文件路径和文件名做关键词命中统计,
order、timeout、status这类词命中越多,得分越高; - 结合近期 git 提交记录里涉及的文件,加权加分;
- 按得分排序取前 N 个文件,再走 token 预算裁剪。
这里没有用 AI,而是简单的关键词倒排索引。因为对代码仓库来说,文件名和路径本身就是最好的语义标签,src/order/services.py这个词就把"订单服务"写在脸上了,没必要动用向量模型。
2.4 三种模式的选型建议
| 模式 | 典型上下文大小 | 主要使用场景 | 不适合场景 |
|---|---|---|---|
| 项目级 | 6k-10k token | 架构梳理、方案设计、新成员熟悉仓库 | 精确到函数级的 bug 修改 |
| 文件级 | 2k-5k token | 修 bug、改单个模块、重命名重构 | 需要全局视野的需求讨论 |
| 任务级 | 4k-8k token | 功能开发、跨模块排查、代码审查 | 任务描述含糊且仓库过大时 |
实际使用时还可以加--auto参数让它自动判断:如果一条命令里既指定了文件又给了任务描述,就按任务级处理并把指定文件设为必选。
3. 核心实现拆解:路径打分、token 预算与上下文打包
3.1 路径打分机制是怎么运行的
任务级模式是最依赖打分机制的部分,我把打分公式简化成下面这个加权和:
score(file) = priority_score + match_score(file, keywords) * weight_match + recent_change_score(file) * weight_recency - size_penalty(file)具体参数在配置里可以调:
match_weight: 1.0 recency_weight: 0.3 size_penalty_threshold_kb: 100 size_penalty_factor: 0.02match_score的计算方式是自己实现的简单计数器,对关键词做词干化处理后统计命中次数。recent_change_score则取git log --name-only --since="7 days ago",文件出现次数越多得分越高。
我举个例子,一个电商仓库里搜索"订单超时未支付":
路径 命中词 recency得分 得分 src/order/services.py order+timeout+status 4 12.7 src/order/models.py order+status 1 9.2 src/payment/webhook.py 支付 2 8.1 src/user/serializers.py 无 0 1.5最后打包时只取前两个文件,效果通常不错。打分的逻辑很朴素,但胜在透明,出了问题也容易调试。
3.2 token 预算:宁可少给,不能乱给
这是整个工具里最值得认真对待的部分。模型上下文窗口虽大,但"够用"和"塞满"之间差距很大,塞得越满,越容易激活那些不相关的注意力。
token_budget的默认值是 6000,我根据实践总结出一个经验值:
- 修 bug:1500-3000 token 足够;
- 一个小功能(一个模块内):4000-6000 token;
- 跨模块重构 / 方案设计:8000-12000 token。
实现上用的是tiktoken库统计每个文件的 token 数,然后做贪心选择:先把必选文件放进去,再按得分从高到低依次添加,直到预算耗尽。必选文件不会被裁剪,这是硬约束。
import tiktoken enc = tiktoken.get_encoding("cl100k_base") def count_tokens(text: str) -> int: return len(enc.encode(text)) def pack_files(file_scores, budget, must_include): chosen = [] total = 0 for f in must_include: total += count_tokens(f.read_text()) chosen.append(f) for f, score in sorted(file_scores, key=lambda x: -x[1]): if f in must_include or total >= budget: continue t = count_tokens(f.read_text()) if total + t <= budget: chosen.append(f) total += t return chosen, total如果你希望超出预算时直接报错而不是静默裁剪,可以加一个fail_loud: true,防止关键时刻重要文件被悄悄丢掉。
3.3 打包成标准上下文块
选完文件之后,context-mode会把它们拼装成一个标准格式的文本块,开头是文件清单,后面依次是文件内容,用标记分隔:
<cm-context mode="task" generated-at="2025-01-03T14:22:00"> <cm-file-list> - src/order/services.py - src/order/models.py </cm-file-list> <cm-file path="src/order/services.py"> # 文件内容... </cm-file> <cm-file path="src/order/models.py"> # 文件内容... </cm-file> </cm-context>这个标签故意做得机器可读,后续接脚本、接插件都很方便。平时直接cm build -o context.md输出到文件,然后整块内容粘到对话里。
这个打包格式有一个容易被忽略的好处:它让 AI 能区分"当前对话的上下文"和"引用文件的内容",减少混淆。实测发现,加上<cm-file>这样的明确边界后,AI 在回答中引用具体文件路径的次数明显变多。
4. 实测效果:一次 3 万行仓库的真实数据
4.1 测试前提
我拿一个真实的 Django + PostgreSQL 项目做过一次对比,仓库大概 3 万行 Python 代码、1200 多个文件,平时给 AI 用的提示词是"订单超时未支付问题排查"。测试分两组:
- A 组:直接把整个
src/目录拖给 AI,大约 150k token; - B 组:先用
cm mode task "订单超时未支付,状态一直停在 pending"生成上下文,大约 6.8k token。
4.2 数据对比
| 指标 | A 组(全量目录) | B 组(context-mode) |
|---|---|---|
| 上下文大小 | 约 150k token | 约 6.8k token |
| 首字响应时间 | 约 18 秒 | 约 3 秒 |
| 单次对话成本(估) | 约 0.15 美元 | 约 0.01 美元 |
| AI 代码中错误引用旧接口次数 | 3 次 | 0 次 |
| 一轮对话内给出可用方案概率 | 40% | 85% |
轮次本身也很有意思。A 组经常在第三轮之后就"忘记"最初的问题,开始顺着某个不相关文件发挥;B 组因为上下文里都是相关文件,AI 每一轮的回答都围绕同一个目标展开,很少跑偏。
4.3 回答质量的变化不只体现在 token 数量上
量化之外,更明显的是行为层面的差异。A 组里 AI 会自己做"文件联想",比如看到订单模型就自动脑补了退款逻辑,然后在代码里加了从没讨论过的字段。B 组由于上下文边界清晰,AI 倾向于直接复用上下文里已有的模型定义,很少自创接口。
还有一个观察:小上下文更容易触发 AI 主动提问"XX 模块的兼容性需要确认吗",而不是闷头写一大堆。这个差别对代码 review 非常友好,因为问题被前置暴露了,而不是等提交完才发现。
5. 实际使用中的坑与对策
5.1 配置项的优先级比想象中容易出问题
priority和ignore同时命中时,我最初的实现是ignore优先,结果导致用户写了高优先级 README 也被忽略掉。后来改成显式优先规则:priority>ignore> 默认。建议在文档里写清楚优先级,否则团队里早晚有人踩这个坑。
5.2 增量扫描的缓存失效
第一次扫描后我加了文件缓存,只检查mtime来决定是否重新解析。问题在于:git pull或者git checkout会批量修改大量文件 mtime,导致缓存大面积失效,每次切换分支后第一次运行特别慢。解决方法是缓存时记录文件内容的哈希,而不是只记 mtime。
5.3 大文件和二进制文件是隐形杀手
仓库里常有一些巨大的 JSON、pb.go或者图片占位文件,它们既占 token 又没营养。我在默认ignore里加了规则:
ignore: - "**/*.min.js" - "**/*.map" - "**/*.pb.go" - "**/data/*.json" size_limit_kb: 200超过 200KB 的文件默认跳过,如果要强制包含则必须显式写进priority。
5.4 模型其实不需要太多"相关"文件
这个问题是最难用代码解决的。打分机制很容易做到"相关就给分",但真正的艺术是"刚好够用就停"。我现在的策略是:--strict模式下,同一目录的文件最多取 3 个,同一模块的兄弟文件优先选最近有 git 提交记录的。因为经验告诉我,AI 看到一个模块里 5 个相似文件时,很容易选错那个。
这个限制在刚上线时被不少同事吐槽"文件太少了",但跑了两周之后大家反而接受了。因为 AI 写错的频率低了,改的时间比翻上下文省得多。
5.5 调试难:上下文不完整时很难判断是"没给对"还是"AI 笨"
这是所有上下文管理工具的通病。我的建议是给每次打包加一个--diff参数,显示本次打包和上次打包的文件差异,至少能快速定位"是不是又少了某个关键文件"。
在输出上下文块时还会附带一行注释,标注每个文件的得分和命中关键词,方便人工检查:
<cm-file path="src/order/services.py" score="12.7" matched="order,timeout,status">这个设计很大程度上减少了"AI 答错了但不知道是不是我的锅"的情况。
6. 这个工具还能怎么扩展:从个人脚本到团队基建
6.1 与 CI/CD 集成:自动生成上下文快照
一个很自然的扩展是把context-mode加进pre-commit钩子,每次提交自动生成一份context-snapshot.md,随 PR 一起提交。这样 review 的人不用手动复制粘贴,直接看快照就能知道提交者给 AI 提供了哪些上下文,对审查代码合理性很有帮助。
6.2 与编辑器插件联动
目前我是把上下文块手动粘贴到对话窗口,但接口留好了之后完全可以做编辑器插件:选中几行代码,按快捷键直接调用cm mode task "xxx",把生成的上下文插入到旁边的新对话面板。格式标准化的好处就在这里,换模型、换工具都不用改内部逻辑。
6.3 继续改进的方向:评估集与回放
如果想把这件事做得更严谨,可以建一个小的"评估集":收集历史上有明确正确答案的任务,自动运行context-mode生成上下文,再让 AI 基于这些上下文输出代码,和标准答案对比。这个思路能帮你比较不同token_budget、不同打分权重对最终效果的影响,比拍脑袋调参靠谱得多。
最后分享一个我自己的使用习惯:context-mode生成完上下文后,我不是立刻粘贴,而是先扫一眼文件列表,手动删掉一两个"看起来相关但实际无关"的文件再发给 AI。这个动作看着小,但对回答质量的提升非常明显。工具能帮你把 100 个文件裁到 5 个,剩下的那一步人工筛选,才是决定 AI 输出质量上限的关键。