ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

context-mode 实战:用一份状态文件根治多任务切换的上下文丢失

context-mode 实战:用一份状态文件根治多任务切换的上下文丢失 1. 为什么 context-mode 值得被当成一个正经功能从三个具体场景说起很多人第一次看到context-mode这个词会把它当成某个编辑器插件或 AI 工具的专属开关。实际上我在自己项目里做的是一个很朴素的东西把“我现在正在做什么”用一份显式状态记录下来然后让所有工具都围绕这份状态自动调整。说白了我受够了每次切换任务都要花十分钟恢复记忆才决定把“上下文”从人脑里搬到一个文件里。1.1 任务切换不是心累是时间成本过去我一直高估自己切换任务的能力直到有段时间同时在修旧系统、开发新接口、跟进另一个环境问题才意识到最贵的不是代码本身而是“找回现场”。我当时记录过一个很典型的周期上午 10 点从新功能开发切回一个历史 bug需要先找到那个服务现在部署在哪、确定收到的是哪条日志、回忆起上次排查到哪个函数、再翻 git 记录确认最近有没有人动过这个模块。等我真正开始改第一行代码已经过去了 11 分钟。这还只是“找到文件”不包括重新理解问题背景。一个上午如果来回切三四次光恢复上下文就吃掉将近一小时。这还不算切完以后因为脑内缓存被刷新掉又得重新读一遍原有代码。所以后来说到context-mode是什么我更喜欢这么解释它不是让你变得更聪明而是让你每次切回任务时少做五六个“天知道刚才在想什么”的动作。1.2 三个场景引出同一个需求我总结了一下真正让我下决心写这套机制的其实是三个高频场景。第一个是“后端修 bug 和前端重构并行”。这两个任务处于同一仓库但关心完全不同的目录、服务、测试命令和启动参数。切到后端时我希望编辑器只高亮后端代码测试只跑后端集合切到前端时我希望端到端测试相关上下文浮上来后端日志安静一点。这需要工具知道当前工作模式。第二个是“同一仓库里维护两套周期不同的产物”。比如一边在做常规版本开发一边在给客户做定制版修复。两者代码有大量重叠但分支、构建目标、验证清单都不一样。光靠分支名区分完全不够因为分支名只会告诉你当前在哪个分支不会告诉你当前要不要按客户环境做特殊配置。第三个是“写技术方案和写代码穿插”。这不是隔几个小时切一次而是半小时内反复横跳。写方案时需要桌面保留文档和示例代码写代码时需要工程目录清晰、控制台干净。没有状态标记的话每跳一次都要手动调整一堆窗口。这三个场景共同暴露出一件事项目里缺一个“当前模式”的概念。分支、窗口、笔记都只能描述局部拼不出一个完整的当下。1.3 我之前试过的“假方案”和它们的缺失在把context-mode做成独立状态之前我试过一堆土办法。它们不是完全没用但总在某个节点上掉链子。维护notes/today.md是最早的尝试。我每天写“今天在做什么、卡在哪”结果发现只要同时处理超过两个任务这份文档就跟不上真实状态。先花三分钟读笔记再花两分钟确认笔记没过期最后还要手动更新它维护成本高到让人想放弃。用 Git 分支表示上下文也很诱人。分支确实能带走代码和部分运行状态但表达不了“当前重点是数据修复不是界面联调”这种不在 diff 里、却影响判断的信息。分支是代码快照不是“我在做什么”的快照。还有一阵子我坚持开多个编辑器实例每个任务一个窗口。效果确实最直观但资源消耗很快超标三四个窗口同时开着找窗口本身变成了新的搜索任务而且切换焦点后经常忘记某个窗口里改到一半的临时内容。我也用过 tmux 会话来恢复布局它比编辑器多开轻能保留窗口拆分和工作目录。可是恢复布局不等于恢复判断哪个会话重要、哪个环境可以关掉、当前该用哪组测试命令这些还是要靠脑子记得。方案保存了什么缺了什么手写笔记任务文字描述难以保持同步Git 分支代码状态无法表达工作意图多编辑器实例文件窗口布局资源消耗高、焦点难管理tmux 会话终端布局没有语义标签这些方案本质上都把“上下文”当成布局或笔记来对待而我真正需要的是一个所有工具都能自动读取的语义状态。这个状态要回答的问题很简单当前工作在哪个模式下关注点应该落在哪里。2. context-mode 最小实现一个状态文件、三条命令、一个过期策略既然要跨工具生效就不能把模式信息只放在某个编辑器里。我用了一个非常原始、但足够通用的方案在项目根目录放一个.context/context.json里面记录当前模式、影响范围、创建时间和过期时间。任何终端、编辑器脚本、甚至 CI 任务只要能读文件就能感知当前模式。2.1 context-mode 的数据结构与目录状态文件设计得越简单越好。我用这种 JSON 结构{ mode: bugfix-1140, scope: api, createdAt: 2026-05-14T09:23:0008:00, expiresAt: 2026-05-14T15:30:0008:00, message: 修复订单重复推送 }这里mode是唯一必须存在的字段它是一个人类可读的标签比如bugfix-1140、frontend-refactor、customer-docs。scope表示这个模式主要影响哪块领域编辑器可以据此决定显示哪些文件优先级。expiresAt是过期时间后面我会专门讲为什么它必须存在。message只用来记录一句话防止切回来时完全想不起当时的判断。目录层面我保持.context/下只有两个东西context.json和hooks/。hooks 目录用来放切换模式时需要执行的项目级脚本比如重新加载环境变量、切换.env文件、启动某个 mock 服务。它能被 git 忽略也能被团队成员共享取决于你们怎么约定。2.2 cm status / cm set / cm clear 的雏形命令行脚本我起名叫cm暴露三个核心子命令cm status、cm set、cm clear。cm status读取context.json如果文件不存在就输出“当前没有 context-mode”如果存在就打印当前模式、过期时间以及是否已过期。cm set负责写入新状态cm clear负责清空状态。下面是cm set最核心的一段逻辑我刻意让它保持低技术含量方便在不同项目中复制#!/usr/bin/env bash set -euo pipefail CONTEXT_DIR${CONTEXT_DIR:-$PWD/.context} STATE_FILE$CONTEXT_DIR/context.json LOCK_FILE$CONTEXT_DIR/.lock cmd_set() { local mode$1 shift local scope expires message while [[ $# -gt 0 ]]; do case $1 in --scope) scope$2; shift 2 ;; --until) expires$2; shift 2 ;; --why) message$2; shift 2 ;; *) shift ;; esac done mkdir -p $CONTEXT_DIR # 用 flock 避免两个终端同时写入导致半个文件 exec 9$LOCK_FILE flock 9 future if [[ -n $expires ]]; then future$(date -d $expires %Y-%m-%dT%H:%M:%S%z 2/dev/null || echo ) fi jq -n \ --arg mode $mode \ --arg scope $scope \ --arg message $message \ --arg created $(date %Y-%m-%dT%H:%M:%S%z) \ --arg expires $future \ {mode: $mode, scope: $scope, createdAt: $created, expiresAt: $expires, message: $message} $STATE_FILE if [[ -f $CONTEXT_DIR/hooks/after-switch.sh ]]; then bash $CONTEXT_DIR/hooks/after-switch.sh $mode $scope fi }用法也符合直觉cm set bugfix-1140 --scope api --until 15:30 --why 修复订单重复推送 cm status第一次用的时候可能觉得这东西小得像个玩笑但它的价值不在命令本身而在“一份所有工具共同认可的状态”。只要其他工具愿意读这个文件等于整个工作环境获得了统一的切换开关。2.3 过期时间为什么是必须的给状态加过期时间是我踩过坑之后才坚持的设计。最早我没有expiresAt字段结果模式一旦设置就会一直留在那里。周一设置了一个deploy-notes模式周五再看还显示是“当前模式”可我已经完全不记得这个模式当时代表什么。更麻烦的是它还会压制其他自动化逻辑的判断因为很多工具会优先采用显式状态。后来我给状态加了默认过期时间比如 8 小时具体场景可以手动覆盖。cm status看到过期状态时会显示(stale)但不会自动删除。这里有个细节过期只能是一种提醒不能是强制清理。因为有时候你早上设了一个demo-prep模式中午暂时离开下午回来虽然已经过期但这个模式依然有意义自动清掉反而会丢失线索。过期时间还有个意想不到的好处它帮我留下了任务切换的痕迹。看一眼状态文件里createdAt和expiresAt的分布就能大致复盘今天在哪些模式之间游荡哪些模式待的时间特别长。这比任何时间统计工具都诚实。2.4 状态锁与并行写入多终端并行是常态尤其在桌面上开着好几个项目终端。如果不加锁两个终端同时cm set就可能写出一份损坏的 JSON之后所有依赖这个文件的工具都会读失败。我在脚本里用flock锁住.context/.lock文件用exec 9$LOCK_FILE打开一个文件描述符再加锁。这样能保证写入是原子操作至少不会出现文件内容错乱。锁解决的是文件完整性问题解决不了语义冲突。假设终端 A 在执行backend-debug模式终端 B 突然执行frontend-cleanup模式最后写入者会直接覆盖前一个模式。这种冲突当然不应该让一个脚本去猜所以我在cm set之前强制要求cm status看一眼现有状态如果当前模式还很重要就先cm clear或手动确认。习惯久了以后这变成了一种很自然的工作纪律。3. 把 context-mode 接到编辑器、终端和 AI 工具上状态文件本身不会产生生产力只有被各种工具消费掉才有价值。下面说说我平时怎么让这棵“状态树”长到编辑器、终端和辅助工具上。3.1 Neovim 读出当前状态在 Neovim 里读一个 JSON 是再简单不过的事。我写了一个很小的模块在打开文件或切换 buffer 时读取当前目录下.context/context.json然后根据mode和scope决定启用哪些能力。local uv vim.uv or vim.loop local function current_context() local ctx_file vim.fn.getcwd() .. /.context/context.json local fd uv.fs_open(ctx_file, r, 420) if not fd then return {} end local stat uv.fs_fstat(fd) local data uv.fs_read(fd, stat.size, 0) uv.fs_close(fd) local ok, decoded pcall(vim.json.decode, data) if not ok then return {} end return decoded end local function apply_context() local ctx current_context() local mode ctx.mode or local scope ctx.scope or if mode bugfix-1140 then -- 只保留与 api 相关的诊断与 LSP vim.diagnostic.enable(false) vim.cmd(setlocal errorformat%f:%l:%m) elseif mode frontend-refactor then vim.diagnostic.enable(true) end end vim.api.nvim_create_autocmd({ BufEnter, VimEnter }, { callback apply_context, })这套设计最值钱的地方在于我不用打开十几份文件去确认自己是否处在一个需要 LSP 的模式里。只要切到frontend-refactor编辑器会自动把后端无关的诊断藏起来专注面立刻变窄。相反在bugfix-1140模式下我反而不想让前端 lint 的噪音盖过后端运行日志。不过这也有一个容易踩的坑Neovim 的自动命令触发频率很高如果每次BufEnter都重新读文件和开关诊断切换 buffer 时会产生明显的卡顿。我后来加了一个缓存只有在context.json的修改时间变化时才真正应用配置。3.2 VS Code 场景下的用法VS Code 比 Neovim 复杂一些它没有直接提供“监听任意文件变化然后动态改插件状态”的官方接口所以我没有硬塞一个插件进去而是利用任务系统传递环境变量。我可以在.vscode/tasks.json里定义任务让终端启动时自动带上当前模式{ version: 2.0.0, tasks: [ { label: run backend in context, type: shell, command: cm status --export npm run dev:backend, options: { cwd: ${workspaceFolder} } } ] }cm status --export会输出一组export CONTEXT_MODEbugfix-1140之类的变量后续的运行脚本就能据此决定启动哪个服务、加载哪份环境配置。这样 VS Code 本身不用做太多改动但终端里的进程确实感知到了模式。如果你用的扩展支持读取本地文件也可以直接把.context/context.json列进工作区文件树快速手动查看当前模式。多数时候这比装一个新插件更稳妥。3.3 shell 提示符与 tmux 状态栏模式状态最该出现在用户一眼能看到的地方否则跟没有差不多。我把它放到了 shell 提示符里。在 zsh 里我可以这样写function _cm_prompt() { local mode mode$(command cm status --short 2/dev/null) if [[ -n $mode ]]; then echo %F{cyan}[$mode]%f fi } PROMPT%B%F{blue}%~%f%b $(_cm_prompt) $ 这个函数每次绘制命令提示符时执行一次开销很低。cm status --short只输出模式名比如bugfix-1140不会打一大段描述否则提示符会变得很臃肿。tmux 的状态栏也可以绑定同一个命令但要注意刷新频率的问题。如果status-interval设得太短比如 1 秒那么状态栏会频繁调用外部命令终端会出现肉眼可见的闪烁。我一般把它放在status-left并且把刷新间隔调到 10 秒以上set -g status-interval 10 set -g status-left #(cm status --short)用 tmux#()调用命令时它是在子 shell 里执行的路径可能和你当前窗口的工作目录不一致。我的做法是在启动 tmux 后手动执行一次export CONTEXT_DIR$PWD/.context让状态文件路径始终有明确指向否则容易读到隔壁项目的模式。3.4 给 AI 辅助工具一个可以消费的 context最近很多人习惯让 AI 助手参与代码修改但 AI 助手往往不知道当前项目的关注点。context-mode正好能补上这块信息。我在.context/下放了一个自动生成的prompt.md每次执行cm set时同步生成一段简短说明当前 context-modebugfix-1140 影响范围api 目标修复订单重复推送 过期时间2026-05-14 15:30然后把这段说明当作项目级提示词的开头给 AI 工具。它不需要知道全部项目历史只需要知道此刻的重点是什么。这样一来AI 生成代码时会更倾向于围绕api相关文件而不是跑偏到前端样式调整。这里我特别节制。AI 工具有时候很乐于“自作主张”地把上下文扩大而我想要的恰恰是缩小。把 context-mode 的边界写进提示词相当于给 AI 画了一个活动半径。半径外的事情暂时不做半径内的事情集中火力。4. 实际使用三周后我发现 context-mode 的边界在哪任何好东西都有边界context-mode也不例外。三周里我反复调整规则碰到的问题很有代表性写出来给打算照做的读者做点预警。4.1 模式数量爆炸会让功能失效一开始我兴致勃勃几乎为每个任务都设了一个模式两周内积累了十几个。结果cm status变得意义模糊我要在十几个模式名里找自己此刻真正该用的那个这本身就是一种上下文切换。后来定下的规则特别简单同时只保留一个有效模式且模式总数不超过五个。如果确实需要同时跟踪多个任务那就用外部任务管理工具而不是塞进context-mode。我想提醒的是模式是“当前焦点”不是“所有待办”。一旦模式变成 todo list它的信息密度就会迅速下降最终连状态文件也没人愿意读。少即是多这句老话在这里非常适用。4.2 “过期了就自动失效”这个策略需要二次确认前面说过我默认给模式设 8 小时过期时间但实际使用时发现过期时间接近午夜时会变得尴尬。比如下午 4 点设置模式到晚上 12 点自动标记为 stale可你还在同一任务上加班第二天早上看到 stale 状态后反而要花时间判断“这个模式还能不能用”。我后来改成了一个折中方案过期状态会显眼地提示“已经过期是否需要保留”但保留操作很简单输入cm keep就能把expiresAt延长 8 小时。这样做了一个“二次确认”又不会让有效状态被误删。还有个细节当模式过期后编辑器不会立刻自动退出模式因为退出模式本身可能带来更大的干扰。状态过期只表示“提醒你重新关注”至于要不要继续把决定权交给人。4.3 并行写入与团队协作的坑单人使用时flock足够保证文件完整性但团队协作时还要考虑另一个问题.context/到底要不要进版本库。如果团队约定所有人共享同一套模式比如每个分支都对应一个context-mode写进版本库可以保证大家看到的状态一致。但现实里每个人的工作焦点不同把个人状态提交到仓库里很容易造成“我在处理模块 A你切过来看到的是模块 B”的混乱。我现在的建议是默认把.context/加进.gitignore只有那些确定要全组共享的钩子脚本比如启动数据库或加载环境变量的脚本才单独抽到仓库里。个人上下文留在本地团队上下文通过脚本共享两者不要混在一个文件里。4.4 context-mode 不应该管哪些事state 文件的边界越清晰它就越可靠。我用了几周后明确划出了三类不该放进context-mode的内容。第一类是临时的、只有几分钟生命周期的事。比如“我要去问一下配置项在哪”这种状态打开和关闭的成本比收益还高不值得记。第二类是任务管理系统已经记过的事。context-mode不是任务管理工具它不负责“做什么”只负责“当前关注点在哪”。第三类是敏感信息。.context/context.json里如果塞了环境密钥、账号密码那它迟早会变成泄密点。模式里只需要写着“api”真正的连接信息应该由环境变量去管理。5. 下一步扩展context-mode 从状态变成能力状态只有被利用起来才有价值所以我正在把context-mode从“一条命令”扩展成一套可复用的项目工作区模板。5.1 按模式展开一个 workspace 模板我现在会给项目配一个.context/templates/目录里面放不同模式对应的启动脚本。比如bugfix-api/模式关联的脚本会自动执行三件事加载.env.api、启动本地 API 服务、关闭前端 watch 任务。而frontend-refactor模式则反过来启动前端的构建监听同时把后端服务降到最小。这样做的效果是cm set frontend-refactor不只是改了一个 JSON 文件而是真正把整套环境推到正确状态。虽然也有人说这相当于自己实现了一遍容器编排但对小团队来说它比拉起完整 Docker Compose 要轻量得多也更容易调试。5.2 接入 PR 描述与日志归档模式切换记录其实是非常好的工作日志。我写了一个很小的context-log脚本每次cm set都在本地追加一行时间 | 模式 | 说明。一周下来这些记录可以用来生成 PR 描述里“本次涉及范围”的草稿减少从 git log 里猜上下文的时间。更重要的是它能帮助发现工作节奏的问题。比如某一天我的日志里全是context-mode切换记录却没有任何一个模式持续时间超过 40 分钟那我就可以判断自己陷入了碎片化状态。这种信息不是自动统计工具能给出来的因为它们不知道字符级 diff 背后真正的任务焦点。5.3 我保留的一个底线模式必须由人手动触发现在市面上有很多插件会尝试“自动检测上下文”比如根据当前打开的代码判断你在做什么。我不反对自动化但我始终保留一个底线context-mode的切换必须由人主动触发。原因是自动判断的准确率很难达到 100%一旦插件猜错后续所有工具的调整都建立在错误状态上那种连锁混乱比没有状态更可怕。手动cm set只花一秒却能迫使我在切换前清醒地问一句我现在到底在做什么接下来关注什么。这一个小小的仪式感本身就是对抗上下文混乱的武器。如果非要自动化的我只会自动化“提醒”比如后台检测到某文件长期处于 stale 状态时提示一下绝不会自动帮你切到另一个模式。工具应该辅助人做决定而不是替人做决定。我从这套机制里最实际的收获是终于敢在任务之间快速切换了。因为我知道当前状态存在文件里编辑器、终端和 AI 工具都跟着它走回来时不需要从零开始。你如果也经常在多任务之间挣扎可以先用最简单的方式从今天开始记录当前模式再慢慢把更多工具接到这份状态上。动手之后你会发现难点从来不在技术而在愿不愿意给自己留一个“现在在做什么”的位置。
返回列表