
前阵子我手头同时维护着三个项目一个 Go 写的 API 网关一个内部后台的前端还有一个帮朋友跑的定时数据分析任务。每天的节奏基本是上午看网关日志改队列下午切到前端调交互晚上还要回去盯数据脚本。说实话代码本身的难度不大真正磨人的是切换状态之后的“找回来”过程。回到项目里先得回忆上次改到哪、环境变量是什么、项目路径有没有换、临时的备注都写在哪个文件里。就是为这件事我写了个叫context-mode的命令行小工具把每个项目的工作环境、路径、变量、待办备注统统打包成一个上下文想切就切想回就回。这篇文章就是把这个工具的设计思路、实现细节和踩坑过程完整复盘一遍。如果你也在多项目之间来回折腾这篇应该对你有用。1. 上下文管理到底解决什么问题1.1 上下文切换的成本很多人觉得开发效率低是机器性能不够、代码写得慢但真正被忽略的是上下文切换成本。这玩意儿就像你在厨房同时炖汤、炒菜、蒸鱼锅盖一掀一盖火候全乱了。写代码时你脑子里装着的变量名、模块关系、数据结构、当前改到哪一行这些信息一旦被打断重新捡起来需要的时间远超你的直觉。我自己做过一个很简单的测试连续两个小时只写一个项目和每二十分钟切换一次项目同样总共写两个小时后者的有效产出大概只有前者的六成。不是因为手慢了而是因为每次切换后都在做同样一件事——回忆。先想刚才改到哪个文件再想那个文件里的变量叫什么还想临时记在备忘录里的那行命令是什么意思。所以我一直在想能不能把“回忆”这个过程自动化把每个项目相关的环境信息、路径、待办、甚至临时笔记都存下来切换项目时一条命令全部加载回来。这就是 context-mode 的初衷。它本质上不是在帮你写代码而是在帮你保存和恢复工作状态。你可能会说Terminal 开多个标签页不就行了窗口开得多确实能保留界面状态但你的大脑不会因为标签页开得多就自动记住每个标签页的前因后果。上下文管理的核心是把“脑内状态”显式地落盘让工具替你记。1.2 为什么我选择“命令行”来做做这个工具之前我也想过要不要做成一个图形界面软件甚至想过做成 VSCode 插件。后来都否了原因很简单命令行工具的可组合性更强。图形界面最大的问题是信息孤岛。你点开一个面板看到一堆上下文列表但你没法把这些信息传给另一个工具。比如你在某个上下文里存了一行备注你想让 AI 助手启动时自动读取它GUI 软件要做到这一步就得写一堆胶水代码。命令行不一样任何信息都是文件任何文件都能被 grep、被 awk、被 cat一条命令就能跟你现有的工作流串起来。我列了个简单的对比表当时是这样说服自己的维度命令行工具图形界面工具组合性高能跟 shell、编辑器、AI 工具互相调用低只能在自己的界面内操作可版本化配置即文件可直接纳入 git通常存在私有配置里自动化天然支持脚本 hook需要额外提供 API上手成本需要记忆命令视觉化鼠标点击即可另外还有一个实际考虑我大部分时间都在 Terminal 里待着工具跟着 Terminal 走才是最顺手的。如果你平时开发也主要靠键盘操作命令行的体验反而比图形界面更顺。2. 工具选型与核心存储设计2.1 用 Python 还是 Shell最开始我用纯 Shell 写了一个原型大概两百行核心功能就是切换目录、导出几个环境变量。说实话跑起来也还行但越往后越难受。Shell 处理字符串太痛苦了特别是项目路径里带空格、环境变量值里带特殊字符的时候转义转得我头皮发麻。后来果断换成Python 3只用标准库不碰第三方依赖。选择 Python 有几个理由标准库自带的argparse、json、os、subprocess足够覆盖全部功能不需要 pip 安装任何东西字符串处理和 JSON 读写比 Shell 舒服一个数量级跨平台能力还行macOS、Linux 都能直接跑Windows 上的 Git Bash 也能勉强用后期如果要加功能比如导出导入、模板复制、自动补全Python 的生态会让你少掉很多头发。有人可能会问为什么不用 Node 或者 RustNode 还得装运行时Rust 编译链太重我想要的只是一个自己能随时看懂、随时改的本地小工具Python 的性价比最高。如果你打算自己写一个类似的工具我给的建议是别在一门语言上纠结太久挑你最熟的、启动成本最低的先把功能跑通。工具是给自己用的不是拿来比赛的能解决问题比技术栈酷不酷重要得多。目录结构也很简单整个工具散养在~/.context-mode/下~/.context-mode/ ├── ctx # 主命令入口Python 文件 ├── contexts/ # 上下文数据目录 ├── backup/ # 自动备份目录 └── logs/ # 切换日志2.2 上下文数据格式JSON 比 SQLite 更合适存储格式我一开始想过 SQLite后来还是选了 JSON。为什么因为这一场景下的数据量实在太小了每个上下文撑死几十个字段SQLite 的查询优势完全用不上反而让数据变得不可读。JSON 的好处大家都懂任何文本编辑器都能打开人能直接看懂也能直接改文件之间的差异可以用git diff查看坏了也好修改回来看一眼就知道哪里不对。这对一个个人工具来说是极其重要的——我不希望工具挂了之后我的数据也跟着变成黑盒。每个上下文的存储结构大概是这样的{ name: api-gateway, description: 内部 API 网关项目Go 编写, path: ~/work/api-gateway, env: { APP_ENV: dev, LOG_LEVEL: debug, PORT: 8080 }, aliases: { gw: make run, gw-test: go test ./... }, hooks: { pre_switch: echo leave api-gateway, post_switch: tmux rename-window gateway }, tasks: [ 把队列消费的逻辑从轮询改成推送, 确认一下旧版接口兼容性 ], notes: 建了 feature/queue-push 分支进度在 60% 左右, tags: [go, backend], updated_at: 2025-03-10 14:32:00 }字段设计的时候我反复斟酌过两个点。第一个是hooks。这是这个工具的灵魂。切换上下文不是光改几个环境变量就完了套用一句俗话进入状态要触发动作。比如我进入前端项目时自动把 tmux 的窗口名改成frontend顺便打开项目的 dev server离开后端项目时自动执行git status打印当前状态防止忘掉手头的工作。hooks 就是干这个的。第二个是tasks和notes。这俩字段看起来简单实际上非常救命。我经常出现的情况是周末加班改项目 A周一回来全忘了。现在我会在每天结束时写两三句 tasks 和 notes第二天回来ctx show一看上一天的工作内容立刻亲切起来。为什么不用全局状态文件也是深思熟虑的。全局状态文件里放一个current_context字段看起来省事但会有并发问题——两个终端同时切换就串台了。我的做法是让每个 shell 自己维护当前上下文名写入一个.ctx_current环境变量不搞全局唯一状态。3. 实操从零跑通 context-mode3.1 安装与初始化这个工具我没发到 PyPI因为还在自己折腾的阶段装起来也很简单。两行命令的事git clone https://github.com/yourname/context-mode.git ~/.context-mode echo export PATH$PATH:~/.context-mode ~/.zshrc source ~/.zshrc前提是你的机器上有 Python 3.6 以上版本。装完先验证一下ctx --help能看到帮助信息就说明成功了。接着初始化数据目录ctx init这个命令会帮你创建contexts/、backup/、logs/三个目录同时生成一个空的默认配置文件。第一次跑的时候输出大概是这样的[context-mode] initialized at ~/.context-mode [context-mode] created contexts/ [context-mode] created backup/ [context-mode] created logs/ [context-mode] all set, use ctx add name to create your first context如果你机器上装了 multiple python 版本可能需要用python3而不是python来运行我在脚本里顺手做了处理会自动检测可用的解释器。3.2 常用命令与真实切换案例安装完了最关键的就是实际操作。我先说下整个工作流长什么样。假设我要新加一个项目上下文ctx add blog --path ~/work/blog --env NODE_ENVdevelopment PORT3000这条命令会在contexts/下生成一个blog.json文件并且把--env后面的键值对写进env字段。如果你愿意也可以不加--path手动编辑 JSON 文件来设置更复杂的 hooks 和 tasks。装好之后每天开始工作时的动作就变成了ctx use blog执行ctx use之后工具会读blog.json做这几件事如果存在pre_switchhook先执行它把path对应的目录打印出来让你知道接下来要往哪走把env里的变量导出到当前 shell把aliases注册到当前 shell执行post_switchhook比如改 tmux 窗口名、打开 dev server最后打印tasks和notes把当前状态一次性搬到你眼前。实际运行效果是这样$ ctx use blog [context-mode] now switching to: blog [context-mode] pre-hook: echo leaving previous context ... [context-mode] path: ~/work/blog [context-mode] exported 2 env var(s): NODE_ENV, PORT [context-mode] defined 2 alias(es): dev, build [context-mode] post-hook: tmux rename-window blog [context-mode] ------------------------------ [context-mode] tasks: - 写完 context-mode 的 README - 把 hooks 改成支持数组形式 [context-mode] notes: 遇到 tmux 联动问题明天继续查每次切换我都感觉像是把一块记忆卡从脑子里抽出来换成了另一块。查看所有上下文列表ctx list按名称、路径、更新时间和标签列出来一目了然。想详细看某个上下文的内容ctx show blog这个命令会把blog.json的内容以格式化后的形式打印出来。个人经验是每天晚上收工之前跑一下ctx show然后顺手改一下 notes第二天开始工作会无比轻松。3.3 和终端、编辑器、AI 工具联动单独跑起来只是第一步真正让它发挥威力的地方是跟其他工具联动。我踩过不少坑最后总结出三个比较实用的联动方案。第一个是 shell 集成。因为ctx use是在子进程里执行的单纯跑命令没法影响父 shell 的环境变量。解决方案是在.zshrc或者.bashrc里包一层 shell 函数ctx-use() { eval $(ctx use $1) }然后就可以在终端里用ctx-use blog来切换环境变量、别名、路径全部生效。这里的关键是ctx命令本身要输出可被eval的脚本而不是直接修改环境。我用的是把导出语句打印到 stdout再由函数吞进去执行。第二个是编辑器联动。我用 Neovim在init.lua里加了一小段local function load_ctx_notes() local ctx_file vim.fn.system(ctx current --json) -- 解析 JSON 并展示 notes 字段 end这样一来每次打开 Vim 都会自动读取当前上下文的 notes通过一个 float window 显示在屏幕侧边。写代码写到一半想查“我刚才记了什么”不用切回终端了。VSCode 用户也可以用类似思路写个简单 extension 或者用 Task 命令。第三个是 AI 工具联动。现在本地跑 AI 辅助编程很常见我发现把context-mode里的 notes、tasks 和目录结构导入模型的初始 context效果会好很多。比如用 OpenAI 的 API 时需要把关于当前项目的背景信息一起发给模型以前要手动复制粘贴现在一条命令就能把 JSON 转成 markdownctx export blog --as-markdown然后把输出喂给模型它就能在上下文里“知道”你正在改哪个项目、上次卡在哪个问题上。这个用法对写技术方案、写提交信息的场景格外好使。4. 常见问题与排查技巧实录4.1 环境变量不生效这个问题几乎每个第一次用的人都会遇到我自己也没躲过。现象是终端里执行ctx use xxx之后echo $PORT明明有输出但脚本一退出外面再echo $PORT就空了。原因很简单你在子进程里设置了环境变量不会影响父进程。命令行工具跑完就退出它的所有环境变量随之消亡。我当时排查的路径是先怀疑export语句写错了再加set -x调试后来才发现问题根本不在导出而在父子进程。解决方式就是上文提到的 shell 函数包装通过eval让导出语句在父 shell 里执行。注意如果你也在做类似工具一定要让use子命令输出的是export KEYVALUE这种可直接执行的脚本而不是自己试图去修改进程环境。把“输出”和“执行”解耦才是能配合eval的正确姿势。4.2 上下文文件损坏JSON 文件被手动编辑时很容易出问题最常见的是少了一个逗号或者引号没闭合。我第一版没有任何校验逻辑一读文件就抛JSONDecodeError报错信息还贼难看。后来加了两个改进。第一个是自动备份每次ctx use成功之前先把原文件复制到backup/目录带上时间戳。第二个是给ctx show和ctx use都加了异常捕获出错时直接打印提示[context-mode] error: failed to parse context file blog.json [context-mode] tip: use python3 -m json.tool blog.json to locate the issue如果真遇到文件损坏不用慌用python -m json.tool定位到具体行列把缺的符号补上就行。如果懒得修直接从备份目录里恢复cp ~/.context-mode/backup/blog.json.20250310-2230 ~/.context-mode/contexts/blog.json我后来学乖了不太直接手写这种复杂 JSON而是用ctx task add、ctx note set这类子命令来间接修改数据出问题的概率小很多。4.3 多终端不同步假设你在终端 A 里切换到 project-a又在终端 B 里切换到 project-b工具本身不会帮你同步状态。这未必是坏事——有时候我确实需要在两个终端分别处理两个项目互不干扰。但有网友问过我一个问题如果我的机器有两个窗口我想让它们共享同一个上下文有没有办法我的做法是把contexts/目录放到一个本地网盘目录下或者干脆把它做成 git 仓库。每次切换完自动 commit这样两个终端读写同一个文件虽然偶发冲突但只要你的操作频率不极端体验基本是顺滑的。注意容量问题contexts 目录很小撑死几百 KB丢到 git 里毫无压力。但不要把 hooks 里生成的日志也一起塞进去否则 commit 会很频繁。我在.gitignore里把logs/目录排除掉了。4.4 踩过的坑与建议第一个坑是路径写成了绝对路径。绝对路径在本地用没问题但如果你把.context-mode整个目录拷到另一台机器上路径大概率是废的。现在我都建议用~相对路径或者存项目名然后靠别名解析。第二个坑是把密码、密钥直接存进了 env 字段。一开始我只图方便后来发现 JSON 文件太容易被各种工具读取而且如果你不小心把它推到远程 git 仓库那就真的是安全事故了。现在我只存变量名真正的值放在.env文件里而.env被.gitignore排除。第三个坑与 hooks 有关。hooks 命令如果太复杂反而成了负担。我第一版设计 hooks 时写了一个能在切换项目时自动检查 git 状态的脚本结果每次切项目都慢半拍后来把脚本精简成一句话。价值主张应该是“快”而不是“全”。hook 只放最常用、最轻量的动作其他一律不做。还有一个经验如果你也要做 hook一定要把pre_switch和post_switch分清楚。我一开始只有一个 hook结果发现切换前的动作和切换后的动作总是混在一起。后来拆开语义清晰了很多也更容易调试了。最后就是别把上下文设计得太“满”。我看有人写类似的工具每个 context 里存了十几个字段光看列表都头晕。我的建议是先用最精简的 schema跑一周哪些字段用得频繁就留下用不上的删掉。工具是自己的越贴合自己的工作习惯越好千万别为了功能多而堆配置。我自己现在用的 context-mode功能其实不复杂但真正帮我省下的不是那几秒执行时间而是每天无数次“我刚刚在做什么来着”的卡顿。那种卡顿只有自己经历过才知道有多难受。