你是不是也有这种感觉:同一个 AI 编程助手,刚开新会话时聪明得像读过你所有代码,聊了半小时之后反而开始犯低级错误,重复问已经说过的需求,甚至把你刚否定的方案又原样端出来。我一度以为是自己用的模型不够强,后来把问题拆开才发现,大部分“变笨”的时刻,根子都在上下文管理上。模型本身没退化,是塞给它的上下文越来越乱、越来越臃肿。context-mode 是我给自己这套上下文管理方式起的代号——把 AI 协作时的上下文像配置文件一样拆成若干“模式”,按任务类型随时切换、按需打包,彻底告别“一个会话从头聊到尾”的原始用法。这篇文章会讲清楚 context-mode 解决了什么问题、核心设计长什么样、我是怎么用不到两百行代码把它实现出来的,以及实测下来的收益和踩过的坑。适合所有在重度使用 AI 辅助编程、写文档、做代码审查,又觉得“AI 时而聪明时而智障”的同学参考。
1. 上下文管理为什么成了开发里的头号问题
1.1 模型能力再强,喂给它的“饲料”不对也没用
先给一个直白结论:大语言模型的能力上限,是由“模型参数”和“上下文质量”共同决定的。参数是固定的,你改变不了,但上下文质量完全在你自己手里。很多人忽略了这一点,把 AI 表现忽好忽坏全部归咎于模型版本或者运气,这其实是在给一个能靠工程手段解决的问题交智商税。
打个比方。你请一位顶级厨师到你家做饭,他手艺确实好,但你的厨房只给他留了一张比 A4 纸大不了多少的操作台。食材堆得到处都是,要找一块姜得把所有东西翻一遍,而且每做完一道菜,操作台就会被之前的狼藉占掉一块。厨师水平没变,出菜质量却肉眼可见地掉。上下文窗口就是那张操作台,token 总量是操作台面积,内容组织方式是摆放习惯。context-mode 做的事情,就是帮你管理“操作台上该放什么、不该放什么、以什么顺序放”。
具体到数字上,主流模型的上下文窗口从十几万 token 到上百万 token 都有。听起来很大,但代码、文档、日志消耗 token 的速度远超直觉。一个中型项目的核心模块可能就有几千行代码,一次全量塞进去就是几万 token。如果会话里还混着之前聊过的十几个无关话题,真正对当前任务有用的信息可能只占整体上下文的十分之一。模型要在海量噪声里找信号,表现自然不稳定。这跟“觉得 AI 变笨了”完全是两码事,本质是输入侧出了问题。
1.2 三个让上下文失效的经典场景
我把自己和同事用 AI 辅助开发的经历复盘了一下,发现上下文失效基本逃不出三个场景。
第一个是跨会话记忆丢失。今天上午让 AI 帮忙设计了模块接口,下午开新会话,它就什么都不记得了。你只能把上午聊过的结论再复述一遍,而复述本身会消耗 token,还难免走样。我见过最夸张的一次,是同事在同一个任务上给 AI 连续讲了四遍同样的约束,每次开头都是“我刚说过了”。这种重复劳动不是因为模型蠢,是因为上下文根本没被留存下来。
第二个是上下文污染。会话聊得越久,里面积累的中间尝试、失败方案、试探性提问越多。这些内容并不会因为任务推进就自动消失,它们会一直占据窗口。等你聊到第 30 轮,模型依然“记得”第 5 轮你提过但已经放弃的方案,时不时拿出来建议一番。这就是污染的典型症状:旧信息压过新决策,甚至直接主导新决策。
第三个是无关信息挤占。有人喜欢把所有资料一股脑喂给 AI,觉得“多给点总比少给强”。但上下文窗口不是硬盘,塞满之后最早的内容会被丢弃,或者模型会自己决定忽略哪些。你辛辛苦苦贴进去的核心需求,可能排在最后面,占的空间还最小。给得多不等于给得对,这是我在实际项目里最深的体会之一。
这三个场景单独出现都还能忍,一旦叠加,AI 的表现就会从“偶尔犯蠢”变成“稳定智障”。而市面上大多数“优化 AI 提示词”的文章,讲的都是怎么把一段话写得更好,很少讲怎么系统性地管理多段上下文的组织方式。
1.3 模式化的本质:把上下文从“状态”变成“配置”
想清楚问题之后,我开始琢磨解决方案。最初想法很简单:开新会话时手动把需要的文件拖进去。用了几次就发现,手动整理上下文太容易遗漏,而且不同任务之间需要的上下文差别很大。写后端接口、改前端样式、审查部署脚本,这三种任务要的背景信息完全不同,硬塞在一起只会互相干扰。
后来我换了个思路。既然上下文是可以被“设计”的,为什么不把它当成配置来管理?就像 IDE 里的主题配置、Git 的 branch、kubectl 的 context 一样,定义一个当前生效的模式,需要时切换,切换后所有相关文件、规则、角色设定都随之更新。上下文不再是一个会话里的即时聊天状态,而是一份可保存、可版本化、可切换的配置文件。
这就是 context-mode 的雏形。它有三个关键特性:可命名,每种任务对应一个模式名;可组合,公共的、项目级的、任务级的内容分层叠加;可打包,切换模式后能生成一份结构清晰的 Prompt 文档,直接粘贴给 AI。有了这三个特性,上下文就从“你脑子里记得什么”变成了“文件系统里明确长什么样”,可维护性完全不一样。
2. context-mode 的核心设计:模式文件、目录约定与命令接口
2.1 一个可落地的目录约定
在设计工具之前,先把文件结构定下来。我自己用的是下面这套约定,放在项目的.context-mode/目录下:
.context-mode/ ├── profiles/ │ ├── workspace.yaml # 当前工作区信息与生效模式 │ └── history.log # 模式切换历史 ├── contexts/ │ ├── shared/ # 多模式共享的通用片段 │ │ ├── conventions.md │ │ └── review-guide.md │ ├── backend-api/ │ │ ├── mode.yaml # 模式定义文件 │ │ ├── personas.md # 角色设定 │ │ ├── stack.md # 技术栈与架构约束 │ │ ├── rules.md # 行为规则 │ │ └── references/ # 可选参考资料 │ ├── frontend/ │ │ └── ... │ └── ops/ │ └── ... └── cache/ └── last-fulldump.md # 最近一次打包结果缓存这套结构的核心思想是分层。shared/里放所有模式都用得上的东西,比如团队的代码风格规范、通用的代码审查清单;每个模式目录里放自己专属的内容。这样既避免重复维护,又保证每份打包出去的上下文足够聚焦。
模式目录内部,我用mode.yaml作为“入口”,它声明这个模式包含哪些文件、以什么顺序输出、有哪些变量需要填充。其余 Markdown 文件则是实际被打包的内容。为什么不用一个巨大的 Markdown 文件搞定所有模式?因为那样可维护性太差了——改一处得全文检索。拆成小块,每个文件只干一件事,符合模块化的直觉,AI 打包时也能按需拼接,而不是每次都把“全家桶”端出去。
2.2 模式文件格式:YAML 管元数据,Markdown 管内容
mode.yaml长这样:
name: backend-api version: 1.2.0 description: 后端 API 开发与接口联调 priority: - personas.md - stack.md - rules.md vars: project: "order-service" language: "python" framework: "fastapi" env: max_tokens: 6000 include_project_tree: true include_recent_commits: 10priority字段决定打包时的文件顺序,这很重要。我习惯把“角色设定”放最前面,让 AI 先进入正确的身份和思维模式;然后是技术栈,让它知道有哪些硬性约束;最后是规则,明确哪些事情不能做。这个顺序不是拍脑袋定的,我试过几种排列,效果差异很明显。角色设定如果放在中间甚至末尾,模型对“你是谁、你在做什么”的感知会弱很多,回答风格和严谨程度都会打折扣。
vars字段用来解决不同项目之间的差异。比如同一个stack.md里写“项目名是 {{project}},使用 {{framework}} 开发”,打包的时候把变量替换成实际值,就无需为每个项目单独维护一份上下文。这个设计后来被证明是 context-mode 里性价比最高的一个功能——我维护 4 个模式目录,实际内容只有 3 套,剩下全靠变量撑起来的。
Markdown 文件本身不需要特殊格式,就是平时写文档的写法。但我会在每个文件开头约定一个“标题 + 目的声明”的结构,方便打包时 AI 快速识别这段内容的用途。比如rules.md第一行就写“本文件包含必须遵守的行为规则,优先级高于模型默认偏好”,这种明确的元声明对模型的引导作用非常明显。
2.3 三条核心命令:list、set、pack
工具本身只有三个核心命令,我都做成了cm的二级子命令。
cm list列出所有可用模式,以及当前处于哪个模式,相当于kubectl config get-contexts。这个命令的价值在于,让你随时知道自己“现在在干什么、有哪些可切换的选择”。我把它绑成了 shell 的 alias,基本每天要敲十几次,已经变成肌肉记忆了。
cm set backend-api切换模式。切换的动作包括更新profiles/workspace.yaml里的当前模式记录,以及在history.log里追加一条切换记录。仅此而已,不做过多的“自动扫描代码”之类的事情,因为那些操作往往又慢又不可控,而且容易在你不注意的时候引入噪声。
cm pack是核心中的核心。它读取当前模式的文件列表,按 priority 顺序拼接,做变量替换,最终输出一份结构清晰的 Prompt 文档。衍生的子命令包括cm pack --out prompt.md输出到文件、cm pack --tail 200追加最近代码改动的关键信息等。
这三个命令设计得非常克制,因为我的原则是:能手工做的就不要让工具做。工具做得越少,出错面越小,越容易被信任。很多工具败就败在功能太多、边界模糊,用户根本不敢让它自动跑。
3. 手写精简版 context-mode:实现思路与关键代码
3.1 为什么用 Python 而不是 Node 或纯 shell
实现语言我选了 Python,不是因为 Python 最高级,纯粹是因为它在这类“读文件、拼字符串、替换变量”的任务上最省事,而且跨平台行为一致。用纯 shell 的话,YAML 解析就得依赖外部命令,不同系统的awk、sed行为不一样,Windows 上更是处处是坑;用 Node 又要处理 npm 依赖,对一个个人工具来说负担偏重。Python 3.8 以上的标准库加上一个yaml库就够了,最终整个实现大概 170 行。
安装依赖就一条命令:
pip install pyyaml这里有个取舍值得说一句:直接依赖 PyYAML 是不是过度设计?其实也可以不用 YAML,直接用简单的key=value格式,或者干脆用 JSON。但实际用下来,YAML 的可读性对长文本、列表、嵌套结构更友好,而且 AI 模型对 YAML 的理解也普遍很好,所以保留了这个依赖。
3.2 模式解析器:从 YAML 到结构化配置
解析器的职责很简单:给定一个模式名,找到对应的mode.yaml,解析它,然后按priority加载所有 Markdown 文件。我把它写成一个独立的类:
# cm/parser.py from pathlib import Path from typing import Dict import yaml class ModeParser: def __init__(self, base_dir: Path): self.base_dir = base_dir def parse(self, mode_name: str) -> Dict: mode_dir = self.base_dir / "contexts" / mode_name yaml_path = mode_dir / "mode.yaml" if not yaml_path.exists(): raise FileNotFoundError( f"mode '{mode_name}' 未找到,请检查 .context-mode/contexts/{mode_name}" ) with open(yaml_path, "r", encoding="utf-8") as f: config = yaml.safe_load(f) config["_dir"] = mode_dir config["_files"] = [] for rel_path in config.get("priority", []): full_path = mode_dir / rel_path if full_path.exists(): config["_files"].append({ "name": rel_path, "content": full_path.read_text(encoding="utf-8") }) else: print(f"[warn] 文件未找到,已跳过: {rel_path}") return config有几个细节是实践里加进去的。第一,某个 priority 文件缺失时只打警告、不直接报错。因为上下文文件本来就是逐步演化的,刚建模式时文件不齐很正常,直接抛异常会让整个流程很脆弱。第二,_dir和_files以下划线开头,表示它们是运行时信息,不是 YAML 里原本的字段,避免配置项和运行期数据混淆。第三,用encoding="utf-8"显式指定编码,这是 Python 读写文本最常见的坑,不写的话在中文 Windows 环境下会莫名出乱码。
3.3 打包器:产出可粘贴的 Prompt 文本
打包器读取解析器产出的配置,加上变量替换和格式包装,输出最终文本:
# cm/packer.py from datetime import datetime from typing import Dict class ContextPacker: def __init__(self, config: Dict, variables: Dict): self.config = config self.variables = variables def _resolve_vars(self, text: str) -> str: for key, value in self.variables.items(): text = text.replace("{{" + key + "}}", str(value)) return text def pack(self) -> str: parts = [] parts.append(f"[Context Mode] {self.config['name']} v{self.config.get('version', '1.0.0')}") parts.append(f"生成时间: {datetime.now().isoformat(timespec='seconds')}") parts.append("") for file_info in self.config["_files"]: parts.append(f"--- {file_info['name']} ---") content = self._resolve_vars(file_info["content"]) parts.append(content) parts.append("") return "\n".join(parts)为什么要在打包结果里保留“生成时间”?因为 AI 对时间信息很敏感,尤其是涉及“最近改动”“当前代码状态”这类问题时,一份带着时间戳的上下文能让模型更准确地判断信息的新旧程度。这是我用了很久之后才加上的细节,但加上之后 AI 给出的“这个代码可能已经过时”之类的判断明显更准了。
3.4 与 AI 助手对接的三种方式
打包好之后,怎么把这份上下文交给 AI?我试过三种方式,各有适用场景。
第一种是纯手动粘贴,适合对话次数少的场景。把prompt.md打开,全选复制,粘到 AI 输入框。好处是没有额外依赖,坏处是长文本粘贴容易出错,而且每次都要手动做,碎片化的样板。
第二种是配合 IDE 插件或浏览器扩展的“文件引用”能力。现在很多 AI 编程工具支持在会话中引用项目文件,我直接把.context-mode/prompt.md塞进引用列表就行。这种方式最推荐,它让上下文成为项目里一个普通文件,可以被版本控制、被同事共享,还能在 CI 里自动更新。
第三种是做成 MCP 工具或者一行命令调用,自动化程度最高。比如写个脚本,在每次会话启动时自动执行cm pack并注入到系统提示词里。这个方案对 CLI 类 AI 工具尤其有效。我目前的主力工作流就是第二种加第三种组合:编辑器里跑一个快捷键,生成 Prompt 文件,同时用脚本把它注入到终端里的 AI 会话。
4. 实测数据:同样的任务,上下文策略不同结果差距有多大
4.1 测试方法与场景设计
光说理念不够,我做了个小实验来验证:同一个任务,分别用“裸聊”和“context-mode 打包”两种方式让 AI 完成,对比效果。测试任务选的是我日常最常做的一件事:给一个已有模块新增一个 REST API 端点,要求符合项目现有的错误处理规范、日志规范和命名规范。任务本身不复杂,但需要模型理解项目的技术栈和约定。
裸聊组的做法是:直接开新会话,贴一句“帮我在项目里新增一个用户查询接口”,然后让它自己看项目。context-mode 组则是先cm set backend-api,再cm pack,把打包好的上下文一次性贴给 AI,然后提出同样的需求。两个组都用同一个模型、同一个仓库,每组跑十次,取中位数。为了减少主观影响,评判“是否合格”的标准是:代码能通过项目现有的 linter,错误处理与项目里其他接口一致,命名符合项目规范,三点缺一不可。
4.2 量化结果:轮次、Token、修改次数
结果差距比我预想的还要明显。裸聊组平均要 4.2 轮对话才能给出一个基本符合要求的方案,因为有大量来回在“澄清项目技术栈”“解释错误处理方式”“纠正命名习惯”上。context-mode 组平均 1.6 轮就能到位,几乎不需要额外解释。而且 context-mode 组给出的第一版代码,直接通过三项标准的次数是 7/10,裸聊组只有 2/10。
| 指标 | 裸聊组 | context-mode 组 |
|---|---|---|
| 平均对话轮次 | 4.2 | 1.6 |
| 首版通过三项标准比例 | 2/10 | 7/10 |
| 单任务平均消耗 token | 约 48k | 约 31k |
| 每任务平均耗时 | 约 18 分钟 | 约 7 分钟 |
Token 消耗上的差距更直观。裸聊组单任务平均消耗约 48k token,context-mode 组约 31k。别觉得奇怪,context-mode 组虽然一次性塞了更多上下文进去,但正因为这些上下文都是对的,AI 后续几乎不用再追问、再猜测,省掉了大量来回试探的 token。花在“提问”上的钱少了,花在“有效推理”上的比例高了,整体反而更省。
4.3 成本视角:每个月省下的真金白银
按我每天大概跑 20 个类似任务来算,一个月下来,光 token 开销就省了大约三分之一。更重要的是省下的时间:每任务少掉的 2-3 轮来回,一天就是接近一个小时的纯聊天时间。这笔账不用算得很精确也能看出,上下文管理的投资回报率非常高。尤其对团队来说,一份好用的上下文模式,比“换个更贵的模型”划算得多。我后来给团队搭的共享模式,半个月就收回了搭建成本。
5. 踩过的坑与几条实战经验
5.1 模式文件膨胀:三个月后没人看得懂
context-mode 用了一段时间后,我踩的第一个大坑是模式文件膨胀。最开始一个模式目录里只有三四个文件,后来每次遇到新问题就往里加一段,三个月后backend-api目录下堆了十几个 Markdown,加起来近两万字符。打包出来的 Prompt 越来越长,AI 的响应质量反而开始下降。
原因很简单:上下文文件的边际收益是递减的。第二十条规则对 AI 的约束力,远不如前五条。我不得不做了一次大清理,把所有内容按“必须遵守”“强烈建议”“仅供参考”三级重新组织,只把前两级默认打进包,第三级做成可选参数。清完之后,每套文件的规模控制在 2500 到 4000 字符之间,效果立刻回升。
5.2 模式切换的副作用:上下文污染
第二个坑和模式切换有关。我原本以为,切换模式后上下文就干干净净了,结果发现 AI 的会话历史并不会因为你在文件系统里切了模式而变干净。如果我忘了开新会话,而是直接在旧会话里粘贴新的打包内容,模型依然会受之前对话的影响,把两个模式的内容混在一起判断。轻则风格漂移,重则把 A 项目的架构假设带到 B 项目里。
教训很明确:切换模式必须配合新会话,否则模式切换形同虚设。现在我养成了习惯,cm set之后第一件事就是开新会话,再cm pack贴内容。这个习惯听起来简单,但解决了 80% 的上下文污染问题。
5.3 高质量上下文文件的五个原则
基于这些经验,我总结出五条写上下文文件的原则,可以直接抄作业。
第一,角色定义永远放第一。AI 先知道自己是谁,才知道怎么回答问题。第二,每条规则要能验证。别写“保持代码整洁”这种虚的,要写“函数超过 50 行必须拆分”这种可检查的。第三,避免和默认行为冲突。如果规则只是重复模型本来就会做的事,它反而会稀释真正重要规则的权重。第四,定期清理,像维护 README 一样维护上下文文件,每次打包内容变更都提交一次 commit。第五,线下测试,改完上下文文件后跑一两个已知任务,确认 AI 行为符合预期,而不是改完就直接上生产。
5.4 还能怎么扩展:从单人工具到团队资产
最后说说扩展方向。context-mode 目前是纯个人工具,但它完全可以团队化。把.context-mode/提交到仓库里,所有成员都能用自己的模式。更进一步,可以在 CI 里加入一个检查任务,验证上下文文件格式合法、引用文件存在、没有超长冗余。团队层面最有价值的一种方式,是让每次失败的代码评审都反向沉淀成一条新的上下文规则,形成“经验闭环”——AI 会越来越懂这个团队的习惯,而不是每个成员各自在提问时碰运气。
最后再分享一个小细节:我在cm pack的产物末尾加了一行“以上为项目背景,请在回答前先确认理解,如有冲突请优先遵守 rules.md 中的规则”。这一行不起眼,但显著减少了 AI 在规则冲突时自作主张的情况。如果你也要做类似的工具,别漏掉这个收尾。