ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

多项目开发必备:用 context-mode 管理上下文切换

2026/10/6 21:22:51 拓冰建站 浏览量
多项目开发必备:用 context-mode 管理上下文切换 说实话我算不上一个喜欢折腾效率工具的人。手机上的番茄钟、桌面上的窗口管理器、笔记软件里的各种插件我基本都是坚持不到三天就换回最笨的原始方法。真正让我意识到“上下文”是个问题的是今年接二连三在不同项目之间救火的那些日子上午还在 A 项目里看一个 Python 服务的内存泄漏中午被拉去 B 项目确认前端打包问题下午又要在 C 项目把上次和客户对过的方案整理成文档。每切到 B 项目我都得先回答自己一堆问题这个仓库我上次用的是哪个分支排查到哪一层了环境变量指向哪个环境哪些文件改了一半还不能提交这些问题搜索引擎救不了聊天记录也救不了。也就是那段时间我给自己写了一个叫context-mode的小工具专门把这种“切换工作上下文”的过程固化下来。这篇文章就是它从想法、到设计、再到实测踩坑的完整记录。如果你也常年在多个仓库、多种身份、多个议题之间切来切去这篇应该值得你花十分钟读完。1. 为什么我最终决定自己造一个 context-mode1.1 一次典型的多线操作现场假设你同时有三个项目压在身上。我先把这三个项目会用到什么信息列清楚因为这是后续所有设计的源头项目每次切换时最想找回的信息没有工具时找回这些信息大概要多久A 服务端性能分析当前分支、上次的 git diff、采样脚本位置、上次生成的火焰图命令10-15 分钟B 前端构建排查未提交的半成品文件、node 版本、临时补丁内容10 分钟C 方案文档上次和客户对齐的版本、关键素材链接、遗留问题清单15-20 分钟久吗听起来好像只是读一下文件、翻一下 git log。但真正的问题是这些信息分布在不同的工具里git 状态要看命令行环境配置要看 .env未完成的想法散落在编辑器标签和笔记里还有一部分藏在某个聊天软件的历史会话中。每找回一类信息都要从“我记得好像……”开始重开一次搜索。次数多了之后切换一次项目的成本根本不是 10 分钟而是 20 分钟起步因为中间还会顺带处理两个新冒出来的问题然后陷入新的上下文。1.2 现成工具各管一段却没人管“理解”我对现成方案做过一次认真盘点结论是不好不坏但都覆盖不到最重要的点。tmux能保存窗口布局和运行中的进程但重启机器后经常丢而且它不关心我这个项目的背景故事。direnv按目录自动加载环境变量很省心但它只解决环境变量这一项不解决“我上次想到哪一步了”这种认知层面的东西。项目管理器 / 工作区快照类插件能把打开的窗口和文件列表恢复但恢复的是界面不是记忆文档里的待办、跟客户对齐过的结论、排查到一半的思路这些它一概不知。AI 聊天记录对话历史会被丢掉而且同一个项目里不同对话之间的上下文是隔离的每次都要重新讲一遍背景。换句话说我需要的是一个介于“环境管理”和“笔记系统”之间的东西它既知道当前仓库、分支、环境变量这类客观状态也能承载一份主动记录的、可以复现的“思考上下文”。这就是context-mode的出发点不追求全自动而追求在我主动保存时能把我认为重要的东西一次性收进一个文件里在我切换回来时能迅速把这份文件恢复成一个真的可用的开发环境而不是只能看看的 memo。1.3 我只做三件事采集从当前终端、git 仓库、目录结构里提取一段基础状态并允许我补充自由文本。保存按名字存成一份独立快照随时可以查看、修改、删除。恢复把快照中的目录、分支、环境变量、待办提示恢复到当前 shell并打印一份可读的摘要。因为目标就这三个后面所有实现都没有扩大得很离谱。很多人做这种工具到最后会做成“第二个笔记软件”我刻意把范围收紧了不管代码搜索、不管日历提醒、不管自动同步。范围一旦扩大这个工具就会变成另一个需要维护的负担那和我的初衷就完全相反了。2. 上下文快照里到底应该装什么2.1 一份快照的字段设计我最终把一份 context 快照定义成四个部分环境数据、记忆数据、引用数据、可选负载。在文件层面我把它拆成四个文件存在~/.context-mode/contexts/名字/下context.yaml结构化数据机器能读主要存目录、分支、环境变量白名单、最近命令等。notes.md自己写的自由文本人可以读也是生成 AI 上下文卡时最核心的部分。env.sh保存的一小段 export 语句恢复时由 shell 函数 source 或 eval 进当前 shell。files.txt手动维护的“关键文件路径”清单用来快速定位而不是把所有文件都复制进来。context.yaml 一个示例name: service-perf root: /home/tom/work/service-a git: branch: feature/fix-memory-leak worktree: /home/tom/work/service-a-worktree env_files: - .env.local allow_env: - SERVICE_ENV - LOG_LEVEL - NODE_OPTIONS last_used: 2025-06-20T10:24:00这里每一行都不是随便写的。root 是恢复现场的基础git.worktree 用于在切到较重的分支时避免污染主目录env_files 是可选的手动加载项allow_env 是白名单只有白名单里的变量在恢复时会被 export这个设计后面救了我好几次。2.2 为什么我不能只依赖自动收集自动收集看起来很美保存时直接把env、git status、history全拍进去恢复时原样还原。我一开始确实试过但很快发现三个问题。第一噪音。env命令输出通常有几十行大部分是LS_COLORS、XDG_*这类当前 shell 的场景变量。恢复这种变量没有任何意义还会在切换时把上一个项目的 PATH 混乱带到下一个项目。第二历史记录过长。把最近 500 条命令存下来恢复时根本没人看AI 读取时也会被无关信息稀释。第三自动收集会给人一种“系统在帮我记”的错觉于是自己不再动手写任何笔记。可真实工作里最值钱的恰恰是手写的那几行比如“这个泄漏疑似和连接池有关下一步改配置验证一下”。这种思路任何自动化都捕捉不到。所以我的原则是系统自动收集的是确定性状态比如仓库路径、分支、环境变量白名单、目录列表而不确定性内容比如结论、疑点、下一步计划一律由人主动写进 notes.md。工具不帮你思考只帮你不丢你思考过的结果。2.3 notes.md 的书写范式为了让 notes.md 和后续 AI 导出都好用我给自己定了一套轻量模板# 目标 这个 context 最终要解决什么问题 # 当前进度 已经做到哪一步有哪些验证过的事实 # 怀疑点 / 阻塞点 排查方向卡在哪里 # 下一步 回到这个任务时第一件要做的事 # 相关链接 - ...这套模板没有新概念唯一要求是每条尽量一句话说清。不要写长篇大论写长了就不会再维护这是我在笔记软件里反复吃亏后的教训。一个合格 notes.md 的大小控制在 50 行以内、1KB 上下比较合适。超过这个规模你就要开始怀疑自己是不是只记笔记没有真正在推进任务了。2.4 为什么用“快照”而不是“实时同步”也许你会问为什么不把 context 做成后台 service监听目录和 git 变化实时更新我做过对比之后选择不做核心原因是代价实时监听就要常驻进程意味着端口占用、开机启动、配置文件、崩溃恢复一堆工程问题全部冒出来。而开发任务的上下文变化其实是低频的一天真正值得记录的节点可能只有三五个。快照机制配合手动save既能卡在这些节点上又避免了一切监听带来的复杂度。我的经验是切走之前五秒钟随手 saved比任何智能采集都可靠。3. 实现细节命令、存储与加载3.1 为什么选 Python 而不是 Shell工具主体我用 Python 写核心逻辑只依赖一个 PyYAML 用来处理 context.yaml其它全是标准库。选 Python 的理由很实际这类工具的核心逻辑是字符串处理、文件操作、git 命令封装Python 写起来比 shell 更不容易出错尤其在做配置数据整理的时候一套dict操作远比awk的边角情况直观。为什么不直接用 Go 或 Rust因为一开始的需求就不稳定我需要一种“改两行就能立刻跑”的语言。等到接口稳定下来如果想做成发行版二进制再用 Go 或者 Rust 重写一遍也不迟反正核心逻辑只有两三百行。如果你更熟悉 Go用 Go 写也是一样的思路下面的代码都是概念示例语言不是重点。3.2 完整命令集我最初只设计了两个命令后来补到七个。看一眼最终的命令结构命令作用context-mode init name初始化一个空白 context生成模板文件context-mode save name把当前目录、分支、白名单环境变量收集进 contextcontext-mode list列出所有 context显示名称、root、最近使用时间context-mode use name恢复现场真正的切换入口context-mode edit name打开 notes.md 和 context.yaml 进行编辑context-mode export name --ai把 context 转成给 AI 的上下文卡context-mode remove name删除一个 context这里的关键点是save不会冲突。这个命令只是把当前 shell 里的信息覆盖到已有 context 上它不要求你从零开始。所以实际使用流程是init一次、save无数次越到后面这个 context 越贴近真实状态。3.3 保存逻辑的代码骨架保存部分的 Python 代码大概长这样#!/usr/bin/env python3 import datetime import os import pathlib import subprocess import yaml ROOT pathlib.Path.home() / .context-mode def git_info(): repo subprocess.run([git, rev-parse, --show-toplevel], capture_outputTrue, textTrue) branch subprocess.run([git, rev-parse, --abbrev-ref, HEAD], capture_outputTrue, textTrue) if repo.returncode ! 0: return None return { repo: repo.stdout.strip(), branch: branch.stdout.strip(), } def save(name: str, env_allowlist: list[str]): ctx_dir ROOT / contexts / name ctx_dir.mkdir(parentsTrue, exist_okTrue) data { name: name, root: os.getcwd(), git: git_info(), updated: datetime.datetime.now().isoformat(timespecseconds), } with open(ctx_dir / context.yaml, w, encodingutf-8) as f: yaml.safe_dump(data, f) env_lines [] for var in env_allowlist: val os.environ.get(var) if val is not None: env_lines.append(fexport {var}{val}) (ctx_dir / env.sh).write_text(\n.join(env_lines), encodingutf-8)这里我没把完整实现贴出来因为三百行贴出来也没人看。但最重要的思路就是save 只负责把“此刻的 shell 状态”写进固定目录不负责分析、不负责清理。环境变量只保存白名单里明确列出的项这个白名单来自 context.yaml 的allow_env我在设计上一节已经提到过它后面承担了几乎所有安全相关的防护。3.4 load 为什么必须是个 shell 函数use命令不能做成普通可执行程序这是整个工具最容易忽略也最关键的实现细节。因为一个子进程无论怎么输出都改变不了父 shell 的环境变量。你运行./context-mode use xxx程序可以在终端打印一堆信息但它无法让“当前这个 bash/zsh 窗口”真的把目录切过去。要让环境变量在当前 shell 生效必须使用 source 或 eval 这种“由当前 shell 自己执行脚本”的方式。所以我在.bashrc/.zshrc里放的是context-use() { local name$1 eval $(python3 ~/bin/context-mode.py export-env $name) cd $(python3 ~/bin/context-mode.py get-root $name) || return python3 ~/bin/context-mode.py print-summary $name }export-env输出的是合法的 shell export 语句。之后在 shell 里执行context-use service-perf就能一次性完成环境变量恢复和目录切换。这个功能用函数封装之后还能顺手加一层确认如果当前目录有未提交改动会先提示一句再切换避免我手滑丢现场。3.5 fzf 速选与别名切换命令敲全称还是有点累我配了一个短别名和 fzf 的联动alias cucontext-use $(context-mode list | fzf --height 40% --header select context | cut -d -f1)这样在终端输入cu会直接弹出一个可搜索的列表选中即切换。fzf 的好处是它不需要我记住 context 的完整名字我只记得 project 名的一部分就能模糊匹配。如果你不用 fzf也可以简化为alias cucontext-use然后靠补全效果差一点但完全可用。4. 把 context-mode 接到日常工具链上4.1 一条命令生成 AI 上下文卡这是我觉得整个工具最值钱的部分。以前跟 AI 助手聊一个项目开场总要花三五轮解释背景这是什么项目、目录结构怎样、当前分支有没有没提交的改动、上次排查到哪了。现在我用context-mode export service-perf --ai它会直接在终端打印一张包含上述所有信息的卡片context 名称: service-perf root: /home/tom/work/service-a branch: feature/fix-memory-leak 未提交改动: 3 files changed, 120 -14 环境: SERVICE_ENVstaging, LOG_LEVELdebug 关键文件: - src/http/handler.py - src/db/pool.py notes: - 目标: 定位内存泄漏 - 当前进度: 已排除连接泄漏疑点集中在异步任务队列 - 下一步: 用 tracemalloc 跑 10 分钟对比两次采样我只需要复制这一段粘贴到对话窗口里第一句话甚至不用写AI 就能立刻理解问题背景。以前那种“打开聊天窗口却要从零开始描述项目”的挫败感基本消失了。4.2 导出时不直接把整份文件喂进去导出 AI 上下文卡有一个反直觉的原则不要为了省事把 notes.md 或 files.txt 里对应文件内容全部粘进去。原因很简单你的核心诉求是让 AI 知道“站在哪、在看什么”而不是替你把所有代码重新读一遍。如果你把几百行代码直接丢进上下文prompt 会变得巨长AI 反而抓不住重点甚至开始复述代码而不是回答问题。所以我的导出逻辑里做了两件事一是对files.txt中列出的文件只生成文件路径和每个文件的前几行摘要二是 notes.md 按原样输出因为那是人写的浓缩判断信息密度最高。这种做法和“给新同事一份 README 而不是一本代码大全”是同一个道理。4.3 与 git worktree 的配合跨分支并行开发的时候主分支上往往是没法直接工作的因为一批未提交的改动可能横跨多个文件。我一开始的做法是 stash后来发现 stash 也是一个会丢失上下文的地方恢复时经常想不起来 stash 里的是哪一坨。现在我把 worktree 路径写进 context.yaml 的git.worktree字段context-use时如果检测到该 worktree 存在就直接 cd 进去如果不存在就提示是否创建。这样一来每个 context 都对应一个独立的登机口主工作区的改动不会互相干扰context 本身也不用手动清理。4.4 和 tmux session 的联动思路我没有把 tmux 管理做成强制功能但提供一个可选联动在 context.yaml 里如果有tmux: session-name那么context-use会检测同名 tmux session 是否存在存在则切换过去不存在则创建一个新 session 并在其中的 shell 里执行同一套 load。这个联动最大的好处是环境变量、目录、打开的窗口布局和“认知上下文”一起恢复而不是只恢复一个空壳。实现上就是在 shell 函数里多包一层简单的 tmux 判断不复杂。唯一要注意的是不要在 tmux 内部触发递归创建自己否则会陷入循环。4.5 与 direnv 的边界不少人以为 context-mode 和 direnv 是重复的。我的用法是让它们分工direnv 负责“当我在某个目录里时自动加载这个目录需要的环境”这种确定性逻辑context-mode 负责“当我在某个任务上下文里时恢复我对这个任务的记忆和偏好”它是手动触发的。两者不冲突。甚至可以把 env.sh 里要保存的变量做成和 direnv 一致的命名这样我在.envrc里写同一套变量名时不会有二义性。底线还是那句任一层都不要把密钥和 token 直接写进明文文件。5. 实测两周后踩过的坑5.1 第一个坑快照比现实老得更快我保存好一个 context 后省了三天三天后切回来发现分支已经不是那个分支了notes.md 里记的待办也早就在别的地方被推进了。这是快照类工具的宿命保存的和现实一定会有偏差。我的解法是在print-summary里不仅输出 context 里存的状态还实时跑一次git status --short和最近三条 commit 信息让输出永远反映“context 视角 现实最小修正”。另外每周最好固定做一次context-mode refresh式的更新把 notes 里过时的结论清理一遍否则 notes 很快会变成坟场。5.2 第二个坑eval 是把双刃剑用 eval 加载环境变量很方便但也意味着任何能写进 env.sh 的内容都会在你的 shell 里被当成命令执行。如果某个 context 曾经保存了一个变量值里带分号或者其它特殊字符eval 之后就是一场事故。我的防线有三条第一白名单变量默认全部用export VARvalue单引号包裹值里的单引号做一次转义。第二env.sh 的内容生成后加载前由 shell 函数做一次校验只允许export [A-Z_]开头的行存在其它行一律拒绝。第三也是最重要的不要顺手把 API token、数据库密码这类敏感信息存进 context要用${REF_HOME}/.secret这样的引用方式或者用系统密钥管理器读取。表格可能比列表更清晰但这里三条都属于安全纪律顺序本身就是优先级。第三点是我坚持得最狠的一条因为一旦在某个 context 里存过一次真实 token它就会出现在你的备份、导出、diff 输出里清理成本远高于最开始多写两行引用。5.3 第三个坑Windows 上的 source 与 eval 差异我在一台 Windows 机器上试跑过Git Bash 里source可用但纯 cmd 或 PowerShell 环境下这套 shell 函数是不成立的。如果你的开发环境以 Windows 为主建议把加载部分做成“为当前 shell 输出一段文本再由各自 shell 的 wrapper 执行”的架构PowerShell 里对应的 wrapper 是Invoke-Expression。换句话说核心工具只管输出标准文本跟 shell 绑定的事永远只发生在用户自己写的 wrapper 里不在工具内部硬编码 bash 语法。这是我踩完坑后重构的结论。5.4 第四个坑上下文膨胀notes.md 写起来毫无成本之后很容易养成随手写的习惯但时间一长 notes 就会失控从 30 行涨到 300 行从 1KB 涨到 30KB。更麻烦的是导出 AI 卡片时这些内容会全部算进 token 成本。我后来在 export 子命令里加了一个--depth参数--depth short只导出最新 10 行 notes--depth full才导出全部。在日常工作中我九成用的是 short因为 AI 真正需要的只是“当前进度、怀疑点、下一步”这三板斧而不是完整工作日志。5.5 第五个坑多个 context 之间的 diff一开始是没打算做 diff 功能的。但有一次我同时维护三个 context切了几轮之后逐渐分不清哪个 context 里记录的环境变量和我正在用的环境一致。于是加了一个context-mode diff A B命令逐字段对比 root、git.branch、env 白名单和 notes 里的前三行。这个命令的排查价值很高它能直接告诉你“我在 A 上下文里设的是 staging但当前终端里跑的是 production”避免在错误环境里白忙一下午。如果你的 context 不超过两个可以不开这个功能一旦超过两个diff 就是刚需。写了这么多最后想分享一个我在实际使用中的体会工具不要追求一步到位。我第一版做的 context-mode 只有 save 和 use 两个命令后面所有功能都是真在使用过程中撞出来的。比如 fzf 联动是因为记不住名字diff 是因为同时维护三个 context 会搞混--depth是因为 AI token 成本爬得太快。如果你也想照着做一个类似的工具我的建议是从最轻量的一两个命令开始先坚持用它记两周的 notes然后你会非常清晰地知道下一步该加什么。而一旦这个习惯养成我现在的日常就是每天早上先cu切到今天要做的任务再也不用花二十分钟去回忆“上次到底改到哪了”。对我来说这就是 context-mode 最值得保留的价值。