我最近的精力几乎都花在了一个叫 context-mode 的小工具上。它做的事原本很简单——在终端里敲一条命令,切换当前的工作上下文。但做完之后我才意识到,它真正解决的是一个平时说不出口但又让所有写代码的人都头疼的问题:AI 助手也好、IDE 提示也好、甚至你自己临时看代码的状态也好,它们经常“不在状态”。不是模型不够强,而是你在用它的时候,根本没有告诉它“我们现在在干嘛”。如果你也遇到过“AI 答非所问”“写了半天文档发现收的是代码风格”“代码审查时总是被当成普通开发”这类场景,这篇文章值得你看看。
名字叫 context-mode,核心机制就是把上下文当成一种可以显式命名、切换、继承和自动触发的状态。下面我会从头到尾讲清楚它到底怎么设计、怎么配置、怎么跟 AI 辅助工具配合,以及在真实项目中踩过哪些坑。
1. context-mode 到底想解决什么问题
1.1 没有上下文时的典型混乱
先说一个高频场景:你在调一个线上 bug,排查了半天发现可能是缓存失效顺序出的问题。这时候你打开 AI 助手,问它“这段代码有什么问题,为什么数据不对”。如果你只是把代码粘进去,它大概率给你答出一个“代码规范”或“性能优化”方向的答案,跟你当前急迫的“找那个导致数据不对的逻辑漏洞”完全不在一条线上。
问题不在于提问方式不够好,而在于你缺了一个“视线约束”。人跟人协作的时候,你会先说“你现在帮我看一下支付流程里那个退款状态机,我怀疑是状态流转漏了一条分支”,然后对方才会动手去读代码。现在把“人”换成“AI”,你得想办法把这种视线约束固化下来。context-mode 干的事情,就是把“当前在做什么事情、代码在哪、该关注什么、输出长什么样”统一定义成一组上下文配置,再在合适的时候把它激活。
另一个高频场景是写文档。同一个代码仓库,你在阅读代码、调试问题、做重构、写注释说明这几种任务里,关心的重点完全不同。以前的做法是每个任务都手动在 AI 对话框里敲一大段“请站在资深代码审查者角度……”,打字都嫌烦。context-mode 把这段话抽出成可复用、可分享、可版本管理的配置文件,由工具负责在正确的时间把它加载进去,这比反复写提示词要省事得多,也稳定得多。
1.2 把上下文抽象成一组状态变量
很多工具都在做“上下文”,但我最大的体会是:要把它做好,第一步是别把它当成一个黑盒,而是当成一组可以量化的状态。我在设计 context-mode 时,把每一个上下文模式定义成五个维度的组合:角色定义(告诉工具用谁的视角思考)、任务目标(当前要完成什么类型的事)、知识范围(可以看哪些目录、哪些文件)、输出格式(回答是代码片段、报告、还是简短结论)、温度设置(答案是要稳定还是发散)。
这五个维度在 AI 辅助编程场景里几乎覆盖了所有“上下文误用”的情况。拿“知识范围”举例:你在调试模式里可能只需要看 src 和 logs 目录,但它不应该去读整个项目的文档目录;而在文档模式下,恰恰应该忽略 src 里的临时调试代码。所有的维度加在一起,就形成一个“上下文快照”。
我会用一个系统状态机来形容它:项目的上下文像一个水箱,水位高度就是当前的模式状态。你敲一条命令,水位被设定到一个新高度;你切换到另一个终端目录,水位可以按配置自动调整。状态是可查的,也是可变的,而不是模糊的“这个聊天窗口知道我在干嘛”。
2. 整体设计与核心配置格式
2.1 组件划分与选型
工具我用 Go 编写,单二进制,运行开销可以忽略。拆成四个核心部分:配置解析器、状态管理器、规则引擎、集成适配器。
配置解析器负责读取全局和项目级别的 YAML 文件,把它解析成内存里的上下文定义。状态管理器维护“当前激活的模式”,自动监听文件系统变化。规则引擎根据目录、分支、文件类型等条件决定要不要自动切换模式。集成适配器则是把激活结果以环境变量、提示文件和状态文件三种方式暴露出去,让 AI 工具、IDE 插件、终端提示脚本都能读到。
选择 YAML 作为配置格式主要考虑到两点:第一,它天然支持注释,团队协作时可以写“为什么这么配”;第二,嵌套结构清楚,规则引擎里那种多条件匹配写出来不会像 JSON 那样堆挤成一坨。而且 YAML 的 diff 友好,配置改动在代码评审时能看出真正的差别,而不是整行重排。
2.2 一组可落地的模式定义示例
下面是一份完整的模式定义示例。把这份文件放在项目根目录的.context-mode.yaml里,它定义了 debug、review、doc 三个模式:
modes: debug: description: 用于日志追踪、异常排查、性能定位 role: 你是一位熟悉分布式系统的调试工程师 goal: 快速定位异常原因,回答必须指向具体函数和代码行 scope: include: - src/** - logs/** - config/** exclude: - docs/** output: format: bullet_point max_depth: 3 temperature: 0.2 prompt: | 当前处于调试模式。你需要基于给定代码,先列出最可能导致问题的三处疑点, 再针对每一处给出验证方法,最后给出修复方向。不要给出无关的性能优化建议。 review: description: 代码审查模式,关注安全性、可读性和边界条件 role: 你是一位代码审查专家,非常挑剔 goal: 找出潜在的 bug、安全漏洞和风格不一致点 scope: include: - src/** - tests/** output: format: comment_list temperature: 0.1 prompt: | 当前处于代码审查模式。请按严重程度排列你发现的问题, 每个问题标注所在文件和行号,并给出修改建议。 doc: description: 文档撰写与代码阅读模式 role: 你是一位技术文档作家 goal: 把代码行为转化为通俗、准确的说明文字 scope: include: - src/** output: format: markdown_doc temperature: 0.4 prompt: | 当前处于文档模式。请忽略临时调试代码,只描述核心逻辑和对外接口。 输出适合放进项目 README 的中文说明。字段说明表格:
| 字段 | 含义 | 典型值 | 备注 |
|---|---|---|---|
| role | 角色设定 | 调试工程师 | 当 AI 工具支持 system prompt 时注入 |
| goal | 当前模式的核心任务 | 定位异常原因 | 模式切换时的摘要提示 |
| scope.include | 允许参考的路径 | src/** | 支持 glob 表达式 |
| scope.exclude | 排除的路径 | docs/** | 自动切换时减少噪音 |
| output.format | 输出格式 | bullet_point | 可配置为 comment_list 等 |
| temperature | 生成随机性 | 0.2 | 调试模式偏稳定,文档模式可稍高 |
| prompt | 附加的系统提示词 | 多行文本 | 支持 Go template 变量插值 |
2.3 全局配置与项目配置的合并规则
context-mode 支持两层配置:全局配置放在~/.config/context-mode/config.yaml,项目配置放在项目根目录的.context-mode.yaml。两层配置通过“字段级合并”而不是“文件级覆盖”来组合。例如全局配置里定义了一个default模式作为兜底,项目配置可以增加新模式而不需要重复 default 的完整定义。
合并时遵循项目优先、模式名唯一、scope 交叉取并集但 exclude 优先的原则。也就是说,如果项目配置中出现了与全局配置同名的模式,它的全部字段以项目为准,这样团队成员就能用各自的本机全局配置统一体验,但项目的强制规范不受影响。配置生效的优先级从低到高是:全局默认模式、项目级默认模式、全局自定义模式、项目级自定义模式、当前手动激活模式。
这个合并逻辑虽然小,却很值得注意。一开始我把两层配置设计成“哪个文件被加载就全用它”,结果在多个仓库之间切换时,每个人都要复制一遍公共模式,维护成本直线上升。改成字段级合并之后,公共的部分沉淀到全局,项目里只写差异,配置量直接下降了大概三分之二。
3. 模式怎么从配置变成可用的激活状态
3.1 命令行操作与状态持久化
安装完成之后,基础操作只有四条命令:
cm list cm status cm activate debug cm deactivatecm list会列出全局和项目两个来源里所有可用的模式,并标明当前生效的是哪一个。cm activate debug会把当前目录的上下文模式切到 debug,同时把状态写入状态文件。cm deactivate则回到默认模式。
状态文件放在项目目录下的.context-mode/state.json。之所以跟项目走而不放在全局,是因为大部分团队项目应该自带当前的状态切到哪里,尤其在多人协作时,别人 clone 下来就能知道应该用什么模式。文件内容类似这样:
{ "mode": "debug", "active_by": "cli", "timestamp": "2025-01-12T10:30:00+08:00", "locked": false }active_by标记这次切换是手动还是自动触发的,locked表示手动激活锁定后,自动规则不能覆盖。
需要注意的是,状态文件用 JSON 而不是 YAML,是因为它会被程序高频读写,JSON 不需要考虑注释语义,序列化和反序列化都更直接。而且状态文件不适合让人手动编辑,所以“给机器看”的数据格式选 JSON 是最干净的选择。
3.2 让 AI 辅助工具感知当前上下文
工具做得再漂亮,如果不能被外部工具消化,那就只是一个“自我满足的玩具”。我在集成层做了三条并行的通道,覆盖目前常见的所有外部工具类型。
第一条通道是环境变量。cm activate执行时会设置CM_MODE=debug和CM_CONTEXT_FILE=/path/to/.cm/context.md,挂载在同一个终端会话里的脚本、自定义命令都可以直接用。
第二条通道是生成聚合提示文件。激活一个模式之后,context-mode 会把模式定义里的 role、goal、scope、prompt 渲染成一个 Markdown 文件,默认写到.cm/context.md。这个文件就是为了给 AI 工具使用的,它的内容都是“人话”,比如“你现在处于调试模式,应该优先关注……可参考 src 目录,忽略 docs 目录”。你只要在 AI 助手的配置里把.cm/context.md设置成每次会话自动附加的上下文文件,它就立刻变成该会话的 system prompt。
第三条通道是监听状态文件的变化。我在工具里内置了一个文件监听器,当 state.json 变化时,会触发注册好的回调命令。IDE 插件可以靠这条路实时更新“当前模式”状态栏,终端主题脚本也可以靠它改变提示符的颜色。整体结构不复杂,但确实把上下文从静态配置变成了运行时动态事件。
4. 自动切换:规则引擎如何决定上下文
4.1 触发器来自目录、分支和文件类型
手动切模式解决了一部分问题,但真实工作中最大的痛点是“常常忘记切”。尤其在大型代码仓库里,你在前端目录调试样式,又切换到后端目录看接口,两个任务的上下文差异非常大。这时候,自动规则就派上用场了。
规则引擎允许你为模式配置触发条件:
rules: - name: hotfix_debug when: branch: "^(hotfix|bugfix)/" activate: debug - name: backend_develop when: dir: "services/backend/**" activate: review - name: default when: always: true activate: defaultwhen支持三类条件:branch 用正则匹配当前 git 分支名,dir 用 glob 匹配当前工作目录相对路径,filetype 匹配当前打开文件的扩展名或文件名模式。三类条件可以组合在同一规则里,例如既在src/algo/**目录里,当前分支又是feature/xxx,才激活 performance 模式。
自动切换的执行时机是终端当前目录变化、git 分支变化、以及编辑器保存文件这三种事件。我通过 shell 集成钩子和文件监听来实现,保证切换延迟不超过 500 毫秒,体感上基本是无缝的。
4.2 规则命中冲突与手动锁定的优先级
多条规则同时命中的情况非常常见,所以我做了一套评分机制。每条规则根据条件的精确度打分:branch 得 3 分,dir 长度每多一级加 1 分,filetype 命中得 2 分。得分最高者胜出;得分相同的情况下,配置文件中更靠后的规则优先,后写覆盖先写。
比较麻烦的是手动激活和自动规则打架。我最终采用了“手动优先”的策略:用户手动cm activate后,state.json 里会写入locked: true,后续自动规则即使命中也不会切换。只有用户手动cm deactivate或执行cm unlock后,自动规则才恢复。这一策略让工具既具备自动化能力,又把最终控制权留在开发者手上。
下面这个表可以帮你理解冲突时是怎么处理的:
| 场景 | 手动状态 | 自动规则结果 | 最终模式 | 原因 |
|---|---|---|---|---|
| 切到 debug 分支,但用户没手动操作 | 无 | 命中 hotfix_debug | debug | 规则自动生效 |
| 用户手动切到 doc,进入 hotfix 目录 | locked | 命中 hotfix_debug | doc | 手动锁定优先 |
| 用户手动 deactivate 后进入 hotfix 目录 | 无 | 命中 hotfix_debug | debug | 自动规则恢复 |
| 后方规则与前方规则得分相同 | 无 | 前方也命中 | 靠后方规则 | 同分时后写覆盖 |
实际用下来,这个评分机制比我想象的更可靠。一开始我也想用“最具体的规则必然该赢”这种朴素直觉,但后来发现复杂场景里往往有多条同级别的规则同时命中,如果没有一个可解释的评分标准,光是调试规则本身就够写一篇长文了。
5. 实测中真正踩过的坑
5.1 配置字段更名导致的兼容性问题
工具第一个版本里,模式定义用的是model字段,后来我发现这个字段名跟语境不符,改成role更准确。改编译代码很容易,但已部署配置文件的升级很难。实测中我的做法是在解析器里保留旧字段的别名,当检测到model字段存在而role缺失时,自动映射并输出一条警告日志。
这个坑提醒我一件事:上下文工具的核心资产不是代码而是配置。配置是长生命周期的东西,任何破坏性变更都要有迁移路径。后来我给所有字段写了版本号,配置里可以声明version: 2,解析器按版本兼容解析不同结构的配置。新项目的用户直接用 v2 配置,老项目用户跑一条cm migrate就能完成自动升级。
5.2 状态文件并发写坏的教训
最初cm activate是直接读写 state.json,结果遇到一个很尴尬的场景:我开着两个终端,一个切到 review,一个切到 debug,两个进程同时写同一个文件。传统的“读-改-写”直接丢数据,文件内容错乱,导致 IDE 插件读到一半时直接崩溃。
解决方式分两层:写入时先写临时文件再 rename 替换,保证读方永远看到一个完整的文件;同时在进程内用文件锁串行化写操作,同一时间只允许一个写入者。这不是新鲜方案,但对个人工具来说一开始容易被忽略。实测中让我印象最深的不是锁本身,而是“写入必须原子化”这一点。哪怕加锁了,如果直接写原文件,读取方仍可能读到半截内容。用了临时文件加 rename 之后,这个问题才算真正消失。
5.3 monorepo 嵌套目录里的规则失控
monorepo 场景暴露出另一个问题:一个仓库里有多个子项目,每个子项目都有自己的.context-mode.yaml,而用户从仓库根目录进入子目录时,规则引擎会叠加多个来源的模式。
最后采用的方案是从当前目录向上逐级查找所有.context-mode.yaml,并按“子目录优先”的规则合并。比如仓库根目录定义了一个默认模式,services/payment子目录定义了一个 payment 专用模式,用户在services/payment下工作时,payment 模式和根默认模式合并,payment 覆盖同名冲突字段,其他字段取根配置。这个方案的优点是不需要单独的“嵌套控制开关”,目录结构自然决定上下文层次。
配套的还有一个保护机制:如果某个子项目启用了isolated: true,那么它上层的所有配置都会被忽略,适合那些完全独立的技术栈或外包模块。实测中遇到 use case 不多,但一旦遇到,没有这个开关就很麻烦。
5.4 与 AI 工具的提示词长度上限较劲
跟 AI 辅助工具集成时,常遇到提示词长度上限的问题。一个 review 模式如果配置了很长的 prompt,再加上要分析的代码,整体很容易超限。解决思路是在生成.cm/context.md时做两级截断:基础上下文最多 1500 字,扩展上下文按需加载。关键信息放在最前面,代码引用尽量用“路径+行号”而不是贴原文,这既能控制长度,又保留了定位能力。
这里要强调一点:把全文代码贴进 prompt 并不等于上下文质量高。路径和行号反而更精确,也更节省 token。每次切换模式时 context-mode 都会重新计算当前文件引用,只保留那些在 scope 内的路径,从源头减少无效信息。
6. 进阶玩法:把个人工具变成团队约定
6.1 项目级共享配置与校验
当 context-mode 在团队里推广开后,.context-mode.yaml就从个人配置文件变成了项目规范的一部分。要让它具备“规范”的约束力,我加入了cm validate命令:检查已知字段类型、glob 表达式语法、规则条件是否存在、模式名是否重复、prompt 是否为空。CI 里可以挂一条cm validate --strict,任何不合规的配置变更都会直接让流水线失败。
团队场景里更实用的是模板变量。配置里的 prompt 可以引用{{project_name}}、{{branch}}、{{target_file}}这类变量,在激活时会被替换成真实值。这样可以避免每个子项目都维护一份几乎相同的配置,把差异压缩到最小。
6.2 用历史记录评估不同模式的实际效果
我会把所有模式切换动作写入.cm/history.log,字段包括时间、模式名、触发来源、当时的分支和目录。这个日志不只是为了排查问题,我开始用它来评估“哪种模式对我的 AI 助手产出最有帮助”。比如某段时间我大量使用 debug 模式,我会统计 debug 模式下 AI 给的修复建议被采用的比例,跟 review 模式下的代码审查意见对比,然后反过来优化每个模式的 prompt 描述。
这是整个工具带给我的意外收获:原本我只想做一个“切换上下文”的命令行工具,但它附带的行为记录让我对“自己是怎么工作的”有了量化认识。什么场景该切什么模式,是否频繁手动切换,是否因为模式设置不合适导致反复切换,都能从日志里看出端倪。
6.3 可预见的扩展方向
后续最想做的扩展有三个方向。第一个是遥控与同步,让状态文件可以同步到远端,团队其他成员能实时看到项目当前处于哪个上下文。第二个是插件系统,允许每个团队写自定义上下文源,比如从公司内部知识库拉取最新规范文本注入 prompt。第三个是引入更多样的触发条件,比如按时段、按 open 文件数、甚至按当前加载的调试器类型来切换模式。
在这些方向落地之前,我会继续保持当前工具的核心简洁性。一个上下文切换工具,最重要的始终是稳定、快速、可解释。我在后续迭代里每次加功能之前都会问自己:这个功能是不是让上下文状态更明确?如果只是增加了自动化的噱头但让系统变得难懂,那就先砍掉。
最后分享一个我自己的配置习惯:我在全局默认模式里写了一句很短但很有用的 prompt——“先仔细阅读用户提供的代码,再基于代码事实回答;不确定时明说不确定,不要编造”。不管你之后切到 debug、review 还是 doc,这句底色提示都不会变。它让 AI 在“知道当前上下文”之前,先养成了不瞎编的习惯。我在多种辅助工具上试过,效果相当稳定。