如果只用一个词概括我过去两年折腾工具链的心得,我会选context-mode,翻译过来就是“上下文模式”。它不是什么新框架,不是某个软件的隐藏功能,而是一套让我在多个项目、多种工具之间来回切换时不再手忙脚乱的工作流。说直白点,它解决的是这样一个问题:当你同时维护三四个项目,每个项目的环境变量、当前打开的文件、昨天思考到一半的设计、跟 AI 助手聊到一半的对话记忆,全都被打散在各自的终端窗口、编辑器标签页和聊天记录里,你能不能做到 10 秒内回到任何一个项目的“现场”,而且所有状态都能无缝接上。
这个需求听起来不复杂,但真正做到的人不多。大多数人的日常是一个项目开三四个终端,切来切去全靠肌肉记忆,换环境之后还要挨个 export 变量、翻历史命令、点开旧文件。我刚入行那几年也是这么过来的,直到有一次因为忘了切 virtualenv,把测试环境的配置打包发上了线,我才下定决心认真设计一套 context-mode。这篇文章就聊聊这套模式怎么拆解、怎么落地,以及我在实战里踩过的坑。适合终端重度用户、Vim/Emacs 玩家、AI 辅助编程的实践者,以及所有同时扑在多个项目上的全栈工程师。
1. 先聊清楚:context-mode 到底在解决什么问题
1.1 没有上下文管理的日子有多痛
我举三个真实场景,各位看看眼熟不眼熟。
第一个场景:你手上有一个 Python 后端项目和一个 React 前端项目,周一你在后端调接口,周二切到前端改页面。前端项目用的是 Node 20,后端项目用的是 Python 3.11,两个项目的依赖都装在各自的虚拟环境里。你在终端里 cd 进前端目录,正准备跑npm run dev,结果因为前一个窗口的 PATH 还残留着后端的 Python 路径,直接用错了包管理器版本。这种“环境变量串台”的问题我至少遇到过二十次。
第二个场景:你在编辑器里开了一堆文件,十几个 buffer 里既有后端路由、又有前端组件、还混着几份配置文件。隔了一天回来,你忘了哪些文件是核心,哪些只是随手打开的参考。重新找一遍文件浪费了时间,更致命的是写代码时容易改错文件。
第三个场景:你跟 AI 编程助手聊了十几轮,它已经慢慢理解了你的项目结构和编码偏好,结果你中午去吃饭,回来为了清空对话重新开了一个 session,于是又得从零开始解释“我们项目是 monorepo,后端用 FastAPI,前端不要用 TypeScript 泛型”。这种“对话记忆断层”在长周期开发里特别常见。
这三个场景的共同点是什么?是“上下文”散落太远。环境信息在 shell 里,文件状态在编辑器里,对话记忆在聊天工具里,它们彼此独立,没有一个东西能把它们打包成一个整体。
1.2 context-mode 不是“又一个工具”,而是一层设计
我理解的 context-mode,是把跟当前任务相关的所有状态抽象成四个层次,然后统一封装、统一切换。这四个层次分别是环境层、会话层、编辑器层和记忆层。
环境层管的是项目依赖、环境变量、工具链版本,解决的是“我在哪个项目里、该用什么跑”的问题。会话层管的是终端窗口布局、工作目录、运行中的开发服务,解决的是“我现在看到什么界面”的问题。编辑器层管的是打开的 buffer、光标位置、搜索历史,解决的是“我上次看到哪一行”的问题。记忆层管的是设计决策、任务进度、踩坑记录,解决的是“我为什么这么做、下一步该干嘛”的问题。
这套设计最关键的一点是:上下文必须可持久化、可恢复、可分享。如果每次切换项目都要手动重新组织一遍环境、会话、文件和记忆,那就不叫模式,叫手工活。真正的 context-mode 应该提供一种“快照”能力,把当前状态保存下来,下次回来时一条命令全部还原。
所以我给这套工作流定的目标非常朴素:任何项目,任何时间,ctx一下,环境、终端、编辑器、对话记忆全部就位。这篇文章后面给的例子,都是我实际用了半年多的一套组合,底层是 direnv、tmux、Vim session 和一份 CONTEXT 记忆文件,上层是一个简单的ctx命令把它们串起来。
2. 拆开揉碎:一个上下文包由哪几层组成
2.1 环境层:让每个目录自带“运行环境”
环境层是 context-mode 的地基。如果环境都是错的,后面的会话、编辑器和记忆再完备都没用。
我的首选工具是direnv。它的工作方式非常简单:在项目根目录放一个.envrc文件,当你cd进这个目录时,direnv 自动加载里面的环境变量;当你离开目录时,它会自动卸载这些变量。这就像每个项目自带一个小型“电源开关”,进出自动通断,不会把 A 项目的环境带到 B 项目。
除了环境变量,我还用mise(以前叫 asdf)来管理语言版本。它可以在.mise.toml里锁定这个项目用的 Node、Python 或者 Go 的版本。配合 direnv,当我在项目 A 和项目 B 之间切换时,不仅环境变量变了,连解释器和包管理器的版本都跟着换了。这一步搞定之后,“环境串台”问题基本消失。
2.2 会话层:用 tmux 给每个项目开“独立工作间”
环境层解决的是“用什么跑”,会话层解决的是“在哪里跑”。
如果你还没用 tmux,我把话放在这里:多项目开发的老手,迟早会服它。tmux 可以同时管理多个 session,每个 session 下可以有多个 window,每个 window 可以分屏成多个 pane。我用一套看着很简单的规则:一个项目,一个 tmux session,session 名称跟项目名一致。
比如order-service这个后端项目,它的 tmux session 大致是这样一个布局:
- window 1 是主代码区,打开一个 pane 跑开发服务器,一个 pane 放 Git 状态。
- window 2 是测试专用区,跑 pytest 或者单测。
- window 3 是日志区,tail 输出文件。
这套布局一旦定下来,我只需要tmux attach -t order-service,就能回到这个项目的完整终端现场。比重新开一堆终端窗口、自己手动调布局高效太多。
2.3 编辑器层:像回放录像一样恢复文件现场
编辑器层的核心诉求是“回到上次离开的编辑现场”。我用 Vim,所以这块用的是mksession。Vim 的 session 会把打开的 buffer、窗口布局、光标位置、甚至折叠状态都存成一个文件,下次vim -S瞬间还原。
如果你用 VSCode,对应的方案是 Project Workspace;用 Emacs 的话,desktop.el 也能达到同样效果。我个人的体会是,编辑器现场恢复的收益被低估了——它让你“再拖起来改两下”而不是“重新打开文件找半天”,这个微小的差异在一天内会放大很多倍。
2.4 记忆层:把脑子里的上下文“落盘”
前面三层都围绕工具和文件,记忆层才是 context-mode 的灵魂,它对应的是人脑里那些不可见的信息:为什么这里要这么写、上次讨论的结论是什么、下一步计划什么。
很多开发者把记忆留在这个物理大脑里,但这玩意儿极不可靠。我选择把记忆写进项目根目录下的一个CONTEXT.md,然后要求自己在切换项目之前必须更新它。内容不贪多,就四块:项目定位、技术栈约定、当前任务状态、备查的易错点。
如果说前三层是“硬件”,那这层就是“软件”,也是很多人不重视、但实际回报最高的一层。
3. 从零搭建 context-mode 工作流
3.1 第一步:让项目环境变量不串台
从环境层开始,我准备在/tmp/demo下建一个小而完整的 demo 项目,项目名叫pay-api,技术栈是 Python 3.12 + FastAPI。
direnv 的安装很简单,这里不展开。装完之后,在项目根目录写一个.envrc:
export PROJECT_NAME="pay-api" export APP_ENV="development" export DB_URL="postgresql://localhost:5432/pay_api_dev" export LOG_LEVEL="DEBUG"然后允许这个配置生效:
cd /tmp/demo/pay-api direnv allow当你在这个目录下执行echo $PROJECT_NAME,会输出pay-api。一旦cd出目录,变量就被自动卸载。这还不够,因为 Python 版本和虚拟环境也需要绑定。我通常配合 mise 建一个.mise.toml:
[tools] python = "3.12"这样每次进入目录,用的就是项目锁定的 Python 版本,不会因为全局版本升级导致本地环境炸掉。目录一进,环境备用,这是 context-mode 的第一脚油门。
3.2 第二步:用 tmux 搭一个可复用的会话
环境就位之后,第二步是把终端布局变成代码。我不用手工开窗口,而是写了一个 bash 脚本,每次进项目都执行它,让 tmux 自动帮你把会话建好。
这里以pay-api为例写一个tmux-start.sh:
#!/usr/bin/env bash set -euo pipefail SESSION="pay-api" if tmux has-session -t "$SESSION" 2>/dev/null; then echo "Session $SESSION already exists, attaching..." tmux attach -t "$SESSION" exit 0 fi tmux new-session -d -s "$SESSION" -c /tmp/demo/pay-api tmux rename-window -t "$SESSION:1" "code" tmux send-keys -t "$SESSION:1" "ls -la" C-m tmux new-window -t "$SESSION:2" -n "logs" -c /tmp/demo/pay-api tmux send-keys -t "$SESSION:2" "tail -f /tmp/demo/pay-api/logs/dev.log" C-m tmux new-window -t "$SESSION:3" -n "test" -c /tmp/demo/pay-api tmux send-keys -t "$SESSION:3" "pytest -x tests/" C-m tmux attach -t "$SESSION"这个脚本看起来平平无奇,但它体现了我上面说的“幂等性”原则:如果 session 已存在,就直接 attach;如果不存在,才从头创建。把它放在项目目录下,或者放进ctx命令里,以后只需执行一次,就能获得完整的项目终端现场。
3.3 第三步:把编辑器会话和 tmux 整合
我在 Vim 里给这个项目做了会话管理。执行一次:mksession! .vim_session/pay-api.vim,Vim 就会把当前打开的文件和布局存到.vim_session/pay-api.vim。下次恢复只需vim -S .vim_session/pay-api.vim。
但你不会想每次恢复时都手动敲这些命令,所以我在 tmux 的“code”窗口里预置了一个快捷键或者别名,把它跟 ctx 脚本串起来。你可以把恢复编辑器状态的逻辑并进tmux-start.sh的 code 窗口里,比如在发送ls -la之前先发送一条恢复命令:
tmux send-keys -t "$SESSION:1" "vim -S .vim_session/pay-api.vim" C-m这样进项目的瞬间,终端现场和编辑器现场一起恢复,我才真正觉得“外部的现场”回来了。需要注意的是,Vim session 文件默认不区分项目,所以要养成用项目名做 session 文件名的习惯,否则多个项目会互相覆盖。
3.4 第四步:写一份真正有用的 CONTEXT 记忆文件
最后一步是记忆层,我把它放进项目根的CONTEXT.md里。模板长这样:
# pay-api 项目上下文 ## 项目定位 面向支付网关的异步处理服务,核心职责是接收订单事件、同步商户余额、通知前端。 ## 技术栈约定 - Python 3.12 + FastAPI - 数据库使用 PostgreSQL,ORM 用 SQLAlchemy 2.x - 异步任务用 arq,不用 Celery - 请求入参校验一律走 Pydantic,禁止在业务函数里裸取值 ## 当前任务状态 - 正在进行商户余额更新接口的重构 - 重命名了 BalanceService → LedgerService,改到一半 - 下一步:把订单金额从 Decimal 改为 int(按分存储),需要改 3 个文件 ## 易错点 - 商户 ID 在请求头和请求体里都可能出现,读的时候统一以请求头为准 - 测试环境数据库是只读的,不要执行 DROP TABLE这份文件就是我的“项目大脑”。每天结束前花两三分钟更新一次,第二天回来读一遍,整个人的状态能立刻接上,几乎不需要“重新熟悉项目”的过程。
3.5 合成一个 ctx 命令
四个组件都齐了,我把它们合成一个简短的 shell 函数,放在~/.bashrc或者~/.zshrc里:
ctx() { local project="$1" local root_dir="$HOME/work/$project" if [ ! -d "$root_dir" ]; then echo "No project directory found: $root_dir" return 1 fi cd "$root_dir" || return bash "$root_dir/tmux-start.sh" }以后我只需执行ctx pay-api,就会先进入项目目录,再自动创建/接入 tmux session,编辑器会话也随之恢复。再加上 direnv 和环境变量自动加载,整条链路的体验非常顺滑。
4. 常见问题与排查技巧实录
context-mode 听着很美,实际跑起来还是有不少问题。这一节我挑几个高频问题,按照实际排查思路写下来,方便各位复现时对照。
4.1 切换项目后环境变量还是“串台”
表现:从项目 Acd到项目 B,echo $PROJECT_NAME还是 A 的值。这种情况极大概率是 direnv 没有被正确 hook 进 shell。
排查方法:先执行direnv status看看状态;如果提示“not loaded”,说明 bash/zsh 的 hook 没配。我最常犯的错误是只把 direnv 的初始化代码写进了.bashrc,但写在了早期return之后,导致没有生效。正确的做法是把钩子代码放在.bashrc的最后一行,然后重新 source 一遍。
另一种情况是.envrc里用了旧式写法,比如用export FOO但没给值,或者用了反引号导致解析异常。direnv 对语法比较严格,建议每次改完.envrc都执行direnv allow让它重新加载,并在终端里看输出。
4.2 tmux 会话恢复时报 session 已存在
表现:跑tmux-start.sh时提示 session already exists,然后脚本直接退出了,但我想看的 window 布局没恢复。
原因就是我在 3.2 写的幂等逻辑太“懒”:只要 session 存在就直接 attach。如果 session 存在但布局已经被你手动改乱了,那脚本就不会帮你修复布局。解法是加一个交互选项,比如ctx pay-api --reset,先tmux kill-session再重新创建。这种设计在长期使用中很有必要,因为人的操作习惯和手动调整场景比我们想象中多得多。
4.3 Vim session 文件覆盖导致所有项目恢复同一个现场
表现:切到项目 B 后恢复 Vim 会话,发现打开的却是项目 A 的文件。
这是最常见的低级错误:session 文件名没用项目名,所有项目共用同一个.vim_session.vim。我的经验是,给 Vim session 文件建一个独立目录,并且用项目名区分,比如:
mkdir -p .vim_session vim -S .vim_session/pay-api.vim如果你发现已经污染了,删掉旧的 session 文件重新 mksession 即可。还要注意的坑是:session 文件里记录的是绝对路径,如果仓库被 clone 到别的目录,恢复就会找不到文件。这个路径问题在团队协作时特别讨厌,我后面会讲一个相对路径的解法。
4.4 CONTEXT.md 写了但没更新,等于没写
记忆层最大的敌人不是格式,而是惰性。我个人的经验是:别把 CONTEXT.md 做成一个沉重的文档流程,而要让它变成“随手改、随手读”的轻量文件。我给自己定了一个强制习惯:每次 commit 之前至少更新一次“当前任务状态”那一节,哪怕只改一行。因为 commit 是代码状态的快照,CONTEXT 是思维状态的快照,两者配合才完整。
如果实在没有精力让每人都写,也可以退一步,只让项目负责人维护,其他人只读。但任何量的“写”都强过“想”,这也是一种投入产出比极高的妥协。
4.5 速查表:常见问题与解决思路
| 现象 | 可能原因 | 排查路径 |
|---|---|---|
| 环境变量跨项目残留 | direnv hook 未生效 / .envrc 语法错误 | direnv status,重新direnv allow |
| tmux 会话恢复后布局不符合预期 | 会话已存在,脚本走了 attach 分支 | 加 --reset 参数,kill 后重建 |
| Vim session 恢复错误文件 | session 文件名雷同 | 目录按项目隔离,文件名用项目名 |
| CONTEXT.md 记录过时 | 没人更新 / 更新习惯未建立 | commit 前强制更新“当前状态”一节 |
| 换机器后 tmux 脚本路径失效 | 脚本中的绝对路径写死 | 用$PROJECT_ROOT或相对路径生成 |
| 目录切换后虚拟环境失效 | mise/direnv 版本未绑定 | 在 .mise.toml 中锁定解释器版本 |
5. context-mode 还能往哪走:几个值得尝试的扩展方向
5.1 让上下文随目录自动激活
现在ctx命令需要你手动执行,一旦你忘了执行,整套上下文就还停留在上一个项目。进阶做法是用 shell 的钩子函数,让上下文跟随目录自动激活。
比如 zsh 的chpwd钩子,当检测到当前目录是一个已注册的项目时,自动加载对应的 tmux session 和编辑器 session。这种做法的代价是容易出现“灵异启动”——比如不小心进入某个目录,tmux 窗口就弹出来。我试过一阵,最后还是回到手动ctx,因为上下文应该由“意图”触发,而不是由“地理位置”触发。
5.2 把上下文变成团队资产
个人这个方案聊完,你可能会想:既然一个项目的上下文这么值钱,能不能让团队共享?答案是能,但要做减法。
团队共享时,环境层可以统一保留,但编辑器层必须去掉,因为每个人的窗口布局和打开文件都不一样。记忆层最有共享意义,但它需要约定格式。我会把团队共享的部分从CONTEXT.md抽到一个单独的CONTEXT_SHARED.md:只写“约定”和“易错点”,不写个人任务状态。这样既保留了项目级知识,又不会干扰个人工作流。
还有一个小技巧是给 session 文件加相对路径支持。把 Vim 的 session 文件放在项目内置的.vim_session/目录里,然后配置 Vim:
set sessionoptions=buffers,curdir,tabpages这样 session 文件里的文件路径会相对当前目录存储,换机器或者换 clone 目录不会导致所有 session 失效。代价是换目录后部分绝对路径可能找不回来,但实际使用中相对路径的收益远大于损失。
5.3 让 AI 编程助手也享用 context-mode
现在很多人的工作流里都有 AI 编程助手,而 AI 助手最大的问题就是“上下文断层”。我解决这个问题的方式很粗暴:每次开启新对话时,直接把CONTEXT.md的内容作为初始提示词的一部分贴进去。
这不只在告诉 AI “我的项目是什么”,更重要的是在告诉它“我们的约定是什么”。比如“禁止用 Celery、名称为 pay-api、金额按分存储”这些信息,如果不提前告知,AI 会在第 20 轮之后才能猜对。我把 CONTEXT.md 视为给 AI 的“入职培训”,把项目的背景、约束、当前任务一次性交给它,之后的问答质量完全不一样。
有人可能觉得贴文件太麻烦,其实可以用cat CONTEXT.md直接粘贴,或者给编辑器配置一个快捷键。习惯之后,你在新会话里给 AI 的信息量和旧会话里聊半天之后的信息量差不多,这本身就是在延续上下文。
最后再说点实在的
这套 context-mode 我前前后后用了大半年,最大的感受不是效率提升多少,而是“心理负担”明显小了。以前切换项目总有一种隐隐不安的感觉,总担心漏了什么、忘了什么;现在一条ctx命令把环境、终端、编辑器、记忆全部准备好,回到项目就像回到一张整理好的书桌,桌面上的东西摆放得明明白白。
如果让我给刚接触这套工作流的人一句建议,我会说:不要一上来就是全套四层,先从环境层和记忆层开始。环境层解决的是“能不能跑”,记忆层解决的是“知不知道接下来干嘛”,这两个性价比最高。等真正用顺了,再补上 tmux 和编辑器 session,不然一个细节没习惯,整套方案就容易半途而废。
我到现在依然保留着每天收工前“更新 CONTEXT 文件”的习惯,哪怕只是改一行。这个文件不光是给 AI 看的,更是给下周的自己看的。开发工作里最贵的不是机器,不是语言,而是重新熟悉上下文所消耗的那几个小时。能把这几小时省下来,这笔投入就值回票价了。