context-mode:把终端工作上下文变成可切换的模式,告别环境错乱 前两周有个瞬间让我特别想动手做点工具我坐在终端前面准备切换到当前项目的一个分支结果发现环境变量还是上一个项目的连提示符都写着别人的仓库名。这已经不是第一次了——上次是写代码写到一半发现编译报错最后定位到根因是当前目录还停在另一个服务里。单独看都是几秒钟的小事但一天里反复发生十几次累积起来非常可怕。我就是在这种状态下开始琢磨context-mode这件事的能不能用一种明确、可复用的方式把每一个工作任务的上下文冻结下来切换时一键生效而不是靠脑子记。context-mode这个想法说白了就是把我现在正在干什么变成一种可切换、可保存、可分享的显式状态。它适合那些每天要在多种任务之间来回横跳的人——写业务代码、查线上日志、做代码审查、给团队写文档、临时改个脚本每种任务需要不同的环境变量、不同的命令习惯、甚至不同的AI提示词视角。文章不会涉及任何复杂框架核心是我自己落地的一套配置和组织方法你能直接抄走用。1. 为什么我会自己做一套context-mode1.1 那些被上下文吃掉的时间我先算一笔很保守的账。假设你一天要切换六次工作任务每次切换产生的重新进入状态的成本大约是五分钟——这五分钟里你在找上次看到哪一行、回忆这个项目的环境变量是什么、翻历史命令找上一次用到的启动参数、还要重新给AI解释一遍背景。一天就是三十分钟一个月就是十一个小时。问题在于这种成本在感官上是不存在的因为它被分散在无数个细小的操作里。你不会觉得哪一步特别慢但整个人的工作节奏就是拖沓的。我试过用待办清单来缓解发现清单只能解决记得要做什么解决不了做的时候环境对不对。环境不对的时候你人已经进入状态了结果是命令报错、AI答非所问、上下文彻底断裂然后又要从头开始解释。context-mode的想法很简单把一份任务对应的所有隐性前提——目录、环境变量、别名、AI提示词、终端布局——全部显式写到一个模式文件里。切换任务不再靠脑子重新加载而是执行一条命令让终端带着全套环境一次性跳转。1.2 现成工具无法覆盖的三个盲区我不是没试过现成的方案但它们的覆盖面都有缺口。第一类是IDE自带的workspace比如VS Code的工作区、JetBrains的项目级配置。它能保存打开的文件夹、运行任务、调试配置但限制很明显它管不到终端里那些shell层的状态更管不到AI工具的system prompt。我日常一半的操作在IDE之外光靠它不够。第二类是tmuxinator、tmux-resurrect这类终端布局工具。它们能把窗口、面板、启动目录编排得很好但本质上只管窗口长什么样不管环境变量是什么和AI看到什么背景。布局是外壳上下文是内核两者经常是分离的。第三类是dotfiles里的环境变量和别名比如在.zshrc里写一堆export和alias。这种方式能全局生效可是它把所有项目的变量揉在一起项目切换时变量不会跟着变反而容易串味。所以我的结论是需要的是一个处在环境管理和任务编排之间的东西它既不是包管理器也不是窗口管理器而是专门管当前工作语境的薄层。1.3 我给context-mode定的五条设计原则一开始我很容易把方案做复杂后来收敛到五条原则每条都是从使用体验倒推出来的。第一切换要快。不管有多少环境变量要改、多少个hook要跑整个切换过程必须控制在几百毫秒内绝不能有那种等它加载的卡顿感。第二状态要透明。任何时候我都应该能问一句我现在在哪个context并且立即看到这个context里定义了哪些变量、哪些别名、哪些路径。不能出现系统悄悄改了我的环境这种失控感。第三模式要可版本化。所有模式定义都是纯文本文件放在Git仓库里改过什么、为什么改都能追溯。这样即使三个月后我忘了当初为什么这么配也能从提交记录里看出来。第四不覆盖用户手动意图。context-mode只管它自己定义过的变量。如果我在当前会话里手动export过一个变量切换模式时绝对不能把它清掉除非我在配置里显式声明要管理它。第五团队可复用。模式文件应该是可以分享的新人拿到一份仓库配置不需要我再口述就能拥有和我完全一致的工作语境。这点到后面尤其重要。2. context-mode 核心设计一个模式定义全部状态2.1 一个context到底代表什么我最初把context简单理解成一组环境变量后来发现这个定义太窄了。实际工作中一个任务状态至少包含四样东西。第一是目录锚点。每个模式都应该有一个默认的工作目录切换时自动cd过去省去在多个项目间跳来跳去的我到底该在哪个目录敲命令的犹豫。第二是环境变量。不同的任务需要不同的运行时变量比如调试模式可能需要DEBUG1、日志模式可能需要LOG_LEVELdebug、发布时可能需要NODE_ENVproduction。这些变量如果不跟着模式走很容易在错误的环境里执行了正确的命令。第三是命令别名和函数。进入代码审查模式后我需要一组专门看diff、看提交历史的快捷命令进入写作模式后我需要一组检查语法的命令。它们不属于某个特定项目而是属于我当前在做的事。第四是AI的提示词视角。这个是我后来才加的也是我觉得价值最大的部分。同一个AI工具写代码时它应该站在开发者视角帮你补逻辑做审查时它应该站在挑剔的审查者视角挑毛病写文档时它应该站在读者视角看表达。上下文决定了AI应该以什么身份回答问题而不是每次都在聊天框里重新描述背景。2.2 模式文件长什么样我最终选择的存储格式是YAML原因很简单它是可读性最好的配置格式适合给人和程序共同维护。每个模式对应一个文件命名就是模式名放在~/.context-mode/modes/下面。一个典型的review模式文件看起来是这样的name: review description: 代码评审模式专注于diff阅读和风险发现 workdir: {{PROJECT_ROOT}} # 将被自动替换 env: LOG_LEVEL: warn GIT_PAGER: cat REVIEW_STRICT: 1 aliases: diff: git diff --stat git diff log: git log --oneline -20 conflict: git diff --name-only --diff-filterU hooks: on_enter: - echo 已进入review模式严格检查为主 - git status --short | head -20 on_exit: - echo 退出review模式 ai_context: | 你现在是一名代码审查者。请以挑剔的态度审查代码变动 重点关注安全隐患、边界条件缺失、与既有架构不一致、 可读性问题。先给结论再给具体行级建议不要客套。这个文件里每一段都对应我前面说的四样东西。env是环境变量aliases是命令别名hooks是切换时自动执行的命令序列ai_context会被喂给AI工具作为它处理当前会话时的背景设定。workdir用了模板变量是因为团队不同成员的项目路径可能不同具体值统一写在全局配置文件里。2.3 加载顺序与变量优先级设计context-mode时最头大的是变量优先级问题。我查阅Linux环境加载顺序的经验结合shell初始化的常规实践整理出了一套和自己习惯匹配的规则这里说明一下。加载顺序从低到高是系统级环境变量比如/etc/profile→ 用户级dotfiles比如.zshrc→ context-mode的全局配置文件 → 当前激活的模式文件 → 用户当前shell里的手动export。最大的原则是用户手动export永远最大。比如我在debug模式下手动把LOG_LEVEL改成了verbose那就是真实的调试需求context-mode绝不会在下一次切换时悄悄把它改回去。要实现这一点不是靠写逻辑去判断哪个变量是用户手动设的而是采取了一个很笨但有效的方法切换模式时不直接修改当前shell的变量而是通过重新加载一段上下文脚本来构建环境。这个上下文脚本每次切换时都会完全重新生成里面包含模式文件定义的全部状态。你之前在这个session里export过的变量如果模式和它不冲突则原样保留如果冲突则以后者为准。这样既避免了残留又不会把手动操作覆盖掉。3. 从零搭一个可用的context-mode脚手架3.1 目录结构与初始化项目的主目录结构很简单我用的是最朴素的方案~/.context-mode/ ├── init.sh # 全局初始化脚本放在 .zshrc 里 source ├── config.yaml # 全局配置模式间共享的信息 ├── modes/ # 每个任务模式一个 YAML 文件 │ ├── feature.yaml │ ├── review.yaml │ ├── debug.yaml │ ├── gitops.yaml │ └── writing.yaml └── commands/ └── cm.zsh # 核心切换命令实现init.sh负责在shell启动时加载基础别名、定义cm函数然后把commands目录里的实现source进来。我选择用zsh函数而不是独立的Python脚本来做核心切换是因为zsh函数运行在当前shell进程里可以直接修改环境变量、别名、当前目录这些操作是外部脚本做不到的。3.2 核心切换逻辑实现核心切换逻辑是一个叫cm的zsh函数它负责解析参数、加载配置、执行hook。这里分享一下最关键的切换动作我简化了错误处理保留了主干。# ~/.context-mode/commands/cm.zsh _cm_modes_dir$HOME/.context-mode/modes _cm_active_mode function cm() { local mode_name$1 local mode_file$_cm_modes_dir/${mode_name}.yaml if [[ -z $mode_name ]]; then echo 当前context-mode: ${_cm_active_mode:-none} return 0 fi if [[ ! -f $mode_file ]]; then echo 未找到模式: $mode_name 2 ls $_cm_modes_dir | sed s/\.yaml$// return 1 fi # 1. 触发旧模式的 on_exit hook if [[ -n $_cm_active_mode ]]; then _cm_run_hook $_cm_active_mode on_exit fi # 2. 解析 YAML 中的 env / aliases / workdir / hooks _cm_parse_mode $mode_name _cm_apply_env ${_cm_parsed_env[]} _cm_apply_aliases ${_cm_parsed_aliases[]} # 3. 切换目录 if [[ -n ${_cm_parsed_workdir:-} ]]; then cd ${_cm_parsed_workdir} fi # 4. 写入 AI 上下文元信息 _cm_write_ai_context $mode_name # 5. 触发新模式的 on_enter hook _cm_run_hook $mode_name on_enter # 6. 记录当前模式 _cm_active_mode$mode_name export CM_MODE$mode_name echo context: ${mode_name} }这段代码里有几个细节值得说。第一步先跑旧模式的on_exit是为了清理现场。比如debug模式退出时一般会执行一条unset DEBUG的hook防止变量带到下一个模式。第二步解析YAML和第五步执行hook解析用了一个小的命令行工具来读取YAML字段然后存到全局的_cm_parsed_*变量里。这部分的实现很长但思路很简单就是把YAML中env下的键值对逐个变成export语句。第三步切换目录放在设置环境变量之后因为有些环境变量的值里会引用workdir。比如PROJECT_ROOT就是一个全局模板变量在apply阶段会被替换成当前激活的项目路径。第四步写AI上下文我会在下面单独展开。3.3 把上下文喂给AI助手上下文要能直接传达给AI才能解决AI每次都要重新解释背景这个痛点。我的方案是在进入每个模式时生成一个标记文件指向当前模式及其描述。文件路径是~/.context-mode/current_context.txt。内容很简单mode: review description: 代码评审模式专注于diff阅读和风险发现 project: my-server timestamp: 2025-01-15 14:22:03然后我在常用的AI命令行工具或IDE插件的配置里让它自动读取这个文件并作为system prompt的前置内容。这里要注意的是提示词内容的写法越具体、越带约束的提示词越容易把AI限制住尤其是代码生成场景。我给ai_context字段定了一个尺度——只告诉AI你现在是谁、关注什么、输出风格是什么不告诉它具体怎么做、每一步干什么。比如feature模式的AI上下文是你现在是一名前端业务开发工程师负责在既有代码库中实现新功能。 请优先保持已有代码风格考虑边界状态和错误处理。 回答时先说明思路再给代码片段。review模式的AI上下文就是严格审查者关注安全和边界先给结论再给证据。同一个AI因为上下文文件被切换而自动改变视角这就是context-mode最直接的价值。3.4 五组开箱即用的模式配置我把自己日常最常用的五种模式整理成了标准配置放进仓库里。这里给一份简表模式名适合场景关键环境变量核心别名AI视角feature开发新功能NODE_ENVdevelopmentdev、test:watch业务开发工程师保持风格review代码审查REVIEW_STRICT1diff、conflict挑剔审查者先结论后建议debug排查问题LOG_LEVELdebugtrace、gc调试助手引导定位证据链gitops版本库维护GIT_PAGERcatclean、sync、amend版本管理助手强调可回滚writing写文档/方案FORMAT_MODEmarkdownfix-typo、lint-doc文档编辑关注表达清晰度每种模式的文件结构完全一致团队里其他人想新增模式只需要照着已有格式写一个YAML文件不需要改核心逻辑。4. 三场真实场景演练4.1 场景一从功能开发切到线上日志排查我以一次真实的线上事故为例来演示。上午我在feature模式下写新功能环境变量是标准的开发配置别名也都是跟测试相关的快捷命令。突然群里报接口超时我得立刻切到debug模式去查日志。操作只需要一条命令cm debug。执行之后发生了什么首先它运行了feature模式的on_exit把开发模式下设置的那些测试专用别名清掉。然后读取debug.yaml把LOG_LEVEL从error改成debug把NODE_ENV切回production-likecd到日志项目目录顺手把滚动日志的tail命令作为别名加好。最后on_enter hook自动跑了一条echo 当前日志级别: debug让我清楚地知道环境已经变了。这个过程中我没有手动做过任何一件回忆项目路径、翻历史命令的事。从发现问题到输出第一条日志筛选命令时间缩短了一半以上。4.2 场景二把AI从生成者切成审查者代码在review模式下的表现和平时写代码时完全不一样因为AI看到的上下文变了。在feature模式里让AI解释一段算法它会顺着你的思路补充实现细节切换到review模式后同一个AI会更倾向于挑毛病比如指出这里的并发更新是不安全的这个边界值在金额为负时没有处理。执行cm reviewAI工具的插件检测到current_context.txt里的mode字段变了自动切换system prompt。它不会再以为自己是来帮忙写功能的而是以审查视角工作。我在自己的实际使用中这个模式对减少低级遗漏非常有帮助相当于每一版代码都自动过了一道用挑剔视角读代码的关卡。4.3 场景三团队共享一套context-mode配置把模式文件放到Git仓库后团队共享就顺理成章了。新人接入时只需要做两件事把context-mode配置仓库clone下来然后修改全局config.yaml里的PROJECT_ROOT指向自己的项目路径。因为所有模式定义都在版本控制里即使配置有过变更也能通过提交记录追溯到为什么这个模式里的变量被改了。这种共享还有个隐形收益团队对代码审查应该关注什么线上排查时应该看哪些日志级别这类问题的理解被统一了。context文件本身变成了一份活的团队协作规范。它没有强制任何人但所有人都默认用了同一套工作语境。5. 常见问题与排查套路5.1 切换后环境变量不生效怎么办最常见的问题是切换模式后当前终端里新开的子进程没有拿到新环境变量。原因通常是环境变量只在当前shell进程里export了但子进程比如tmux里创建的新面板不会继承当前shell的环境变量。解决办法是在init.sh里加一个update命令它会把当前模式文件里的环境变量重新export到所有已注册的tmux面板。这里有一个关键操作需要主动告知tmux有几个面板需要同步否则同步机制不会覆盖全部面板。5.2 alias和函数被旧模式锁住切换模式后旧的别名不一定立即失效因为zsh的alias一旦定义在会话生命周期内是固定的除非显式unalias。我在debug模式里踩过这个坑从debug切到writing后写文档时敲了一个本来应该在debug模式里的命令结果还是执行的是旧逻辑。解决方案很简单在on_exit hook里统一跑unalias -m *debug_*这类批量清理命令或者在切换时先禁用当前模式定义过的所有别名再应用新模式。这样能避免出现旧模式的幽灵别名。5.3 context的嵌套与堆栈管理实际操作中不可能永远单层模式。我经常在review模式下临时要查一个debug日志如果直接从review切到debug之前建立的审查状态就丢了。我为此给cm加了一个栈机制cm push debug会暂时进入debug模式cm pop会切回之前的review模式。栈管理用起来特别顺手它允许你在一个核心任务中临时借道另一个上下文办完事再回来。需要注意的点是栈的容量要有限制我限制为4层防止自己一路push下去忘记自己最初在干什么。5.4 与shell框架的加载顺序冲突如果你用了oh-my-zsh或者zinit它们的别名和补全系统加载顺序可能和context-mode冲突。我自己遇到的情况是.zshrc里先source了oh-my-zsh再source init.sh导致部分别名被覆盖因为oh-my-zsh的一些插件会在它之后执行。解决的常规做法是让init.sh最后加载并在cm命令里执行一次rehash和compinit确保补全信息是最新的。同时我在init.sh开头做了一个防御检测是否已经加载过框架避免重复source导致变量被重置。5.5 常见问题速查表现象原因处理办法切换后env未更新子进程继承的是旧环境使用同步命令更新所有tmux面板旧alias仍然生效zsh alias不会自动清理在on_exit统一批量unalias嵌套模式下栈溢出忘记pop设置最大栈深度超限自动报警与oh-my-zsh别名冲突加载顺序不对init.sh放在最后切换时rehashAI没有切换视角current_context.txt没被读取检查AI插件的配置路径是否指向该文件团队内变量路径不同workdir硬编码全部改为引用全局PROJECT_ROOT6. 复盘三个坑和两条心得6.1 几乎让我放弃的三个设计第一个坑是过度自动化。我最初想做一个智能检测根据当前目录和近期命令自动判断上下文并切换结果误判率高得吓人经常在关键时刻跳到错误模式比手动切更浪费时间。后来果断去掉自动切换改回手动触发加推荐提示只在终端右侧显示一条建议不自动执行。第二个坑是AI提示词写太满。早期版本的ai_context字段我写了一大段详细规则结果AI变得极度死板连基本的灵活性都没了。后来我把提示词压缩成身份关注点输出风格三要素效果反而好了很多。第三个坑是试图把context-mode变成一个通用的、可配置的框架加入了插件系统、扩展点、事件总线。做了一半发现抽象层越多配置负担越重最终变成一个只有我自己能维护的玩具。后来我刻意砍掉所有可扩展设计回归到纯粹的状态文件几个shell函数。6.2 后续我打算怎么扩展目前context-mode已经稳定运行了一段时间我考虑的两个扩展方向都不涉及大改动。一个是把模式定义从YAML迁移到JSON Schema这样可以用工具自动校验字段团队共享时能更早发现配置错误。另一个是让context的状态可以跟着Git分支走每个分支维护自己的CM_MODE值checkout时如果不匹配就提示这样分支切换和工作上下文切换能协同起来。我个人在实际操作中最强烈的感受是能感觉到工作状态正在取代记忆文件成为我日常开发的真正入口。过去切换任务靠的是在脑子里翻找上次做到哪的碎片记忆现在只需要问自己一句接下来是什么模式然后按一下切换。这个转变不大但实实在在影响了我每一天的工作效率也是我推荐你也试一把的原因。