
说实话这两年我发现自己花在“切换”上的时间比花在“干活”上的时间还多。上午在 A 项目改完接口下午切到 B 项目看数据报表晚上还要回到 A 项目的分支上写文档。每次切换都要重新 export 环境变量、翻项目笔记、找对应目录的配置文件甚至要重新告诉 AI 助手“我们现在在做什么”。这些动作单独看不重叠在一起就非常烦。后来我把这套流程做成了一个叫 context-mode 的小工具——本质上是一种“上下文模式”管理机制让终端、编辑器、AI 助手跟着当前任务自动切换预设配置。这篇文章把它的设计思路、核心实现和踩坑记录都写出来给同样被多项目切换折磨的朋友一个可以直接抄作业的参考。这个工具适用的人很明确日常要在多个代码库、多套环境变量、多种工具链之间横跳的开发者经常用 AI 编程助手但每次都要手动补充项目背景的人还有那些已经受够了靠记忆去记“哪个项目用什么命令”的懒人。它不复杂也不追求大而全核心只做一件事把“当前我在做什么”这个状态固化下来让环境去适配状态而不是人去记住环境。1. 为什么我坚持要给终端加一个 context-mode1.1 被多项目同时夹击的一天我先描述一个很常见的场景你看看是不是也遇到过这种情况。早上打开终端默认 shell 落在~/work这时你想去 service-api 项目看一眼于是cd service-api。然后要启动开发服务你脑子里的第一反应是“这个项目是用 Python 的 Django得先激活虚拟环境”于是source .venv/bin/activate接着还要 export 一个SERVICE_PORT8080再 alias 几个常用的测试命令。做完这一套五分钟没了。下午切到数据分析项目情况更糟。Django 那套环境变量还没清干净PYTHONPATH还是指向 service-api 的源码目录结果跑 Python 脚本时导入了错误的模块。我当时的第一反应是“我是不是老了记性不行了”后来想明白了这跟记性没关系是人脑不适合维护这种多套并行状态。每个项目都有自己的环境变量、端口偏好、别名、目录习惯甚至 AI 工具需要的背景信息都不同。这些状态分散在 shell 配置、项目文档、个人笔记里每次切换都要人工重新组织一遍。1.2 context-mode 到底在管什么context-mode 的核心思路很简单把“一个项目/一类任务需要的一组状态”打包成一个模式然后由工具负责加载和卸载。我把它理解成“终端里的场景化预设”——就像手机上的“驾驶模式”“睡眠模式”一套设置搞定一批相关配置。具体来说一个 context-mode 会管理四类东西。第一是环境变量比如 Python 的PYTHONPATH、服务的端口、数据库连接串等。第二是 shell 的别名和函数比如每个项目专属的构建、测试、部署命令。第三是提示符和终端标题让你一眼看出现在处于哪个模式避免“以为自己还在 A 项目结果在 B 项目执行了命令”的惨剧。第四是给 AI 工具用的“会话背景”比如当前项目的技术栈、代码规范、常用操作方式。这个思路的价值在于它把“状态”这个东西从人的脑子里搬到了机器里。你不需要记住 service-api 要走 8080 端口、数据分析要设置DATA_DIR你只需要进入那个目录context-mode 自动帮你把一切恢复好。1.3 谁最需要这套东西不是所有人都需要 context-mode。如果一个人这辈子只维护一个项目所有配置写死在~/.zshrc里就行加了反而多余。我建议这几类人重点考虑第一前端后端通吃的全栈工程师。前端项目要 nvm 切 Node 版本、后端项目要虚拟环境两套工具链混在一起很容易冲突。第二数据分析师或机器学习工程师。不同数据集、不同实验环境经常要切换DATA_ROOT、MODEL_DIR这类长路径变量手动改一次很痛苦。第三重度使用 AI 编程助手的人。AI 的生成质量极度依赖上下文你给它看什么背景它就回什么质量的内容。如果每次都要手敲“我们在用 Python 3.11、Django 5、代码规范是 PEP8”体验会大打折扣。第四运维或 SRE。生产、预发、测试环境之间的切换容错率低一个误操作可能带来严重后果把环境差异收敛到模式里至少能少犯低级错误。2. 整体设计思路模式、触发、动作2.1 三大核心要素模式定义、触发条件、加载动作在设计 context-mode 之前我把问题拆成了三块这也是整个工具的骨架。第一块是模式定义。你得明确每个模式叫什么名字、适用什么场景、要加载什么内容。名字建议用简短的小写英文比如frontend、service-api、>name: service-api match: - ~/work/service-api/** env: SERVICE_PORT: 8080 APP_ENV: dev aliases: sai: python manage.py runserver 0.0.0.0:8080但很快发现两个麻烦。第一YAML 的布尔值、数字、字符串转换容易出幺蛾子比如SERVICE_PORT: 8080会被解析成整数导出到环境变量时变成SERVICE_PORT8080没问题但有些字段会被加引号调试起来很心累。第二YAML 表达不了动态逻辑比如我想加载时自动检测当前目录下的.env文件并合并进来这就不是声明式配置能做的事了。最终我改用“纯 shell 脚本为主、TOML 索引文件为辅”的方案。shell 脚本可以直接被 source天然支持变量、别名、函数、条件判断也不用解析器。索引文件只记录模式名和路径方便快速列出可用模式。这个取舍的代价是模式文件对新人不够友好但作为个人工具或者团队内部分享收益远大于成本。2.4 作用域与优先级避免变量“串味”环境变量有个特点是继承export 了之后所有子进程都能看到。这意味着如果模式 A 设置了PYTHONPATH切换到模式 B 时没有清理B 的 Python 进程就会带着 A 的路径。这个问题不解决context-mode 就只是个花架子。我的设计是给每个模式文件声明两件事这个模式 export 了哪些变量名、定义了哪些别名。切换时先收集当前模式的“清理清单”卸载时挨个 unset 和 unalias然后再加载新模式。也就是把“加载”和“卸载”放在同一个事务里先卸后装避免中间状态的污染。至于优先级是另一个细节。用户主目录下可能有个人模式项目目录下可能有公共模式。我定的规则是项目内的.context-mode/modes/优先于用户级的~/.context-mode/modes/同名模式按后加载覆盖先加载处理。这样团队可以把公共配置放进仓库个人偏好留在本地两边互不干扰。3. 核心实现一个可以照抄的 context-mode 骨架3.1 目录布局和模式文件写法我建议把 context-mode 做成一个独立目录不要硬塞进现有的 dotfiles。我的布局是这样~/.context-mode/ ├── init.sh ├── lib.sh ├── modes/ │ ├── default.sh │ ├── service-api.sh │ ├── frontend.sh │ └──># ~/.context-mode/modes/service-api.sh context_nameservice-api context_hintAPI 服务开发 context_match~/work/service-api # 声明需要清理的变量和别名 context_export_vars(SERVICE_PORT APP_ENV PYTHONPATH) context_aliases(sai satt) # 加载时执行的逻辑 export SERVICE_PORT8080 export APP_ENVdev export PYTHONPATH$HOME/work/service-api/src alias saipython manage.py runserver 0.0.0.0:8080 alias sattpython manage.py test --keepdb # 给 AI 工具用的背景信息 context_prompt你正在协助开发 service-api 项目。技术栈Python 3.11 Django 5 PostgreSQL。代码规范PEP8注释用中文。关键在于文件顶部的三个声明数组。它不产生实际效果只是让卸载逻辑知道该清理什么。变量名和别名分开声明原因在于unset和unalias是两条完全不同的命令分开列才不会漏。3.2 核心切换逻辑先卸后装我直接贴一个精简可用的实现骨架。这段代码我拆成了三个函数ctx_switch负责入口、ctx_unload负责清理、ctx_load负责加载。# ~/.context-mode/lib.sh _ctx_active _ctx_exports() _ctx_aliases() ctx_unload() { local var_name alias_name for var_name in ${_ctx_exports[]}; do unset $var_name 2/dev/null done for alias_name in ${_ctx_aliases[]}; do unalias $alias_name 2/dev/null done _ctx_active _ctx_exports() _ctx_aliases() } ctx_load() { local mode_file$1 ctx_unload # 加载新模式之前先重置声明数组避免脏数据 context_name context_hint context_match context_export_vars() context_aliases() context_prompt # shellcheck disableSC1090 source $mode_file _ctx_active${context_name:-default} _ctx_exports(${context_export_vars[]}) _ctx_aliases(${context_aliases[]}) # 加载后重新渲染提示符 ctx_render_prompt } ctx_switch() { local target$1 local mode_file # 没有指定模式时尝试自动检测 if [ -z $target ]; then target$(ctx_detect) fi # 已经在目标模式就跳过避免无谓重载 if [ $target $_ctx_active ]; then return 0 fi mode_file$HOME/.context-mode/modes/$target.sh if [ ! -f $mode_file ]; then echo context-mode: unknown mode $target 2 return 1 fi ctx_load $mode_file return 0 }这段逻辑有几个值得注意的细节。第一ctx_load在 source 新文件之前先调用了ctx_unload而且把声明数组清空了。这样即使上一个模式 source 失败环境也不至于残留一堆变量。第二切换前检查“已经在目标模式”可以提高性能尤其在终端每次提示符刷新时都要调用的场景下这个判断非常关键。3.3 与 shell 集成自动检测和提示符显示有了切换逻辑接下来要让它在合适的时间自动运行。zsh 和 bash 的机制不一样我分开说。zsh 用chpwd钩子cd进入新目录后自动触发。在init.sh里加一行autoload -Uz add-zsh-hook add-zsh-hook chpwd ctx_chpwd_handler ctx_chpwd_handler() { ctx_switch }bash 没有chpwd我选PROMPT_COMMAND也就是每次显示提示符之前执行_ctx_last_dir ctx_prompt_command() { if [ $PWD ! $_ctx_last_dir ]; then _ctx_last_dir$PWD ctx_switch fi } PROMPT_COMMANDctx_prompt_command为什么 bash 里要缓存_ctx_last_dir因为PROMPT_COMMAND每次回车都会执行如果不加判断等于每次敲命令都要跑一遍目录检测。加了缓存后只有目录真正变化时才触发切换性能开销可以忽略。自动检测函数是命中的关键。我的规则是按目录前缀匹配ctx_detect() { local modes_dir$HOME/.context-mode/modes local mode_name local mode_match local matched for mode_file in $modes_dir/*.sh; do # shellcheck disableSC1090 source $mode_file if [ -n $context_match ]; then case $PWD in $context_match*) matched$context_name break ;; esac fi done echo ${matched:-default} }这里有一个性能问题每次切换都要 source 所有模式文件来读取context_match。项目少的时候无所谓模式多了就会有几百毫秒延迟。优化办法是写一个索引文件把模式名和匹配路径集中放在一个 TOML 或纯文本里检测时只读索引不 source 完整脚本。等确定要加载哪个模式再 source 对应文件。提示符显示是提升存在感的重要细节。我在ctx_render_prompt里把当前模式名拼到PS1前面ctx_render_prompt() { if [ -n $_ctx_active ] [ $_ctx_active ! default ]; then PS1[$_ctx_active] $PS1_BASE else PS1$PS1_BASE fi }这样终端里始终能看到当前处于哪个模式再也不会出现“在>ctx_export() { local export_file${1:-.ctx-session.md} { echo # Session Context echo echo 当前模式: ${_ctx_active:-default} echo 场景说明: ${context_hint:-} echo echo ## 项目约定 if [ -f .context-rules.md ]; then cat .context-rules.md else echo 未找到 .context-rules.md可补充代码风格、目录结构、常用命令。 fi } $export_file echo context exported to $export_file }我实际使用的 AI 工作流变成了进入项目目录自动切到 service-api 模式然后打开 AI 工具的会话把终端里生成的.ctx-session.md文件拖进去顺手把当前打开的关键源码文件也 进去。以前我会花几分钟把项目背景、技术栈、代码规范打一遍现在一个文件全搞定。context_hint和context_prompt的文本也能让 AI 生成的代码更贴合项目本身而不是泛泛而谈。4. 实测记录与踩坑实录4.1 从“能用”到“好用”的几个关键细节第一个坑是export PATH的清理。模式 A 为了服务某个工具在 PATH 前面加了一个目录卸载时如果把PATH整个 unset整台机器的命令都找不到了shell 直接废掉。后来我规定模式文件尽量不要export PATH实在需要就写成恢复式export PATH$HOME/tools/special/bin:$PATH同时把这个路径加入PATH的备份变量卸载时从 PATH 里精确剔除这一段。这个实现比较啰嗦我建议新手直接避免在模式里动 PATH环境变量干净很多。第二个坑是别名的“残留幻觉”。之前有两个模式都定义了alias ga一个指向 git add一个指向 go app。我卸载时只清理当前模式的 alias 清单但加载新模式时如果新别名和旧别名重名在个别情况下会静默失败。后来我在ctx_load的 source 之前加了一步强制 unalias 掉所有待加载别名但只限清单内声明的。第三个坑是首次进入目录不会触发chpwd。zsh 的chpwd只在cd之后触发终端打开时落在某个目录并不会执行一次。所以我让init.sh在加载完成时主动调用一次ctx_switch把初始目录的模式补齐。4.2 常见问题速查表整理一份排查表基本都是我实际踩过的现象原因处理cd 到项目目录后模式没变zsh 没注册 chpwd 钩子或 bash 的 PROMPT_COMMAND 没设置检查 init.sh 是否正确加载手动执行ctx_switch验证逻辑模式切换后环境变量残留卸载逻辑没有记录完整变量名在模式文件顶部完整声明context_export_vars并确认没有漏掉两个模式别名冲突旧别名未彻底清理ctx_load加载前强制 unalias 待加载别名清单中的所有别名自动检测总是切错模式context_match匹配规则太宽改用精确路径前缀比如~/work/service-api/而不是~/workbash 下每敲一次命令都卡几秒PROMPT_COMMAND 每次都会检测用_ctx_last_dir缓存当前目录目录没变就直接返回切换模式后终端提示符没刷新PS1 渲染时机不对在ctx_load加载完手动调用ctx_render_prompt或触发 zsh 的precmd团队里模式文件提交到 git 后频繁冲突各人的本地路径和别名不一致公共模式只放匹配规则和通用变量个人别名和个人路径放本地覆盖最常见的原因其实是同一个模式文件里没写好清理数组。很多人只关心加载时 export 了哪些变量忘了卸载时也要知道变量名所以变量的生命周期没有闭环。我后来在init.sh里加了 debug 模式切换时打印加载和卸载的变量清单排查效率立刻上来了。4.3 我后来才加上去的两个小扩展第一个是“父子模式”。比如service-api模式下数据库相关的项目可能有独立的service-api-db子模式。子模式可以继承父模式的环境变量只追加或覆盖一部分。这个功能听起来很优雅但实现复杂度指数上升涉及变量合并、优先级排序、卸载顺序。我目前克制住了没有做完整版只在加载模式时允许额外 source 一个~/.context-mode/overrides/mode.sh文件用本地覆盖来替代父子继承。第二个是“会话内提示”。由于 AI 工具越来越常用我在模式文件里不止放context_prompt还放了一组ai_conventions比如“提交信息用 Conventional Commits”“错误信息要包含堆栈和重现步骤”。ctx_export导出时会把这两块合并这样 AI 生成的产物会更贴近团队习惯。这个小改动我觉得比任何花哨功能都值回票价。如果你打算自己动手做一套 context-mode我建议从最简单的版本开始一个ctx_switch函数、两个模式文件、一个 chpwd 钩子跑通生命周期之后再加自动检测。别一上来就搞 YAML 解析、UI 面板、守护进程那只会让工具本身变成一个新的维护负担。先把“加载和卸载干干净净”这条底线守住这个工具就成功了一大半。