ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

context-mode:用显式上下文对象终结多任务切换的思维混乱

context-mode:用显式上下文对象终结多任务切换的思维混乱

我同时推进三四个小项目的时候,最难受的不是写代码,而是每次切任务都要重新捡起上下文。窗口可以切,标签页可以开,问题在于脑子里那个“我现在到底在做什么”的状态,没人帮我记住。后来我给自己写了一个小工具,核心就一件事:把上下文变成一个显式的、可以随时存取、随时恢复的对象。这个设计,我叫它 context-mode。这篇文章会把它的设计逻辑、落地步骤和真实踩坑都写出来,给同样被多任务上下文搞得焦头烂额的人一个能直接复用的参考。

1. 为什么我要自己造一个 context-mode,而不是继续忍下去

1.1 单画布容不下所有任务的记忆成本

我做技术类工作的时间不短,手头同时转的项目通常有三四个。写代码要换仓库,写文档要换目录,查资料要开新浏览器窗口,聊天工具里还有一整套待办。表面上看,这些窗口都在桌面上,想找哪个点哪个就行。真正的问题出在“思维状态”上:你写完一段需求分析,接着去调接口,再回头改文档,这三次切换其实各有一整套前置记忆。比如问题背景、已经试过的方案、刚看到的关键线索。这些记忆全部藏在人脑里,没人替你持久化。

我试过用“多开几个窗口”“多建几个文件夹”“多建几个标签组”来解决,结果都不理想。窗口开多了还是乱,文件夹归类还是难,标签组必须手动整理,而且要命的是,一旦我关掉浏览器或者电脑重启,这些临时积累的线索就全没了。这不是工具数量的问题,而是缺少一个抽象的“上下文容器”:一个能统一记录任务相关状态,并且能在你切换时完整取回的东西。

1.2 把“上下文”当成一等公民,而不是背景噪音

大多数工具把上下文当成隐性的东西:工作目录、当前分支、打开的文档、剪贴板里的临时片段,都是散落在各个角落的状态。我用 context-mode 的思路,是反过来把这些东西显式打包成一个独立对象。这个对象有自己的名字、自己的保存时间、自己的归属项目,像一个“快照容器”。

用起来就像这样:我打开编辑器、终端、资料页,把状态整理好,然后执行一次保存。这条上下文被标记为“需求评审准备”。几个小时后我想接着做,只要执行一次恢复,编辑器会打开当时的文件列表,终端会切到当时的工作目录,浏览器会打开当时的资料页,我之前贴的临时备注也会回到待办区。整个过程不是靠记忆,而是靠一条命令。

1.3 它到底适合谁用

这东西不是大厂框架,也不是分布式方案,它就是个人效率层。最适合三类人。第一类是同时参与多条产品线或多份合同的开发者,分支和目录太多,经常搞混自己在哪个业务上下文里。第二类是内容创作者,写作、配图、素材、审校来回切换,思路断了很难接上。第三类是运营和客服这类“会话密集”岗位,每天处理大量相似问题,不同客户的信息必须隔离清楚。如果你只是单线程做事,任务本来就简单,这工具帮不上什么忙。但只要你有跨任务的切换需求,这套模式就能省下大量“重新进入状态”的时间。

2. 核心设计拆解:一个显式上下文对象到底要装什么

2.1 上下文的三层结构

在动笔写代码之前,先要把“上下文”这件事拆清楚。我在实际设计中把上下文分成三层,每一层的有效期和恢复策略都不一样。

层级包含内容例子失效条件
任务上下文任务名称、目标、当前阶段“给用户中心做权限改造”任务结束或被放弃
数据上下文文件、目录、数据库连接、环境变量项目代码路径、本地数据库库名数据被移动或删除
界面上下文打开的标签页、窗口位置、临时输入浏览器资料页、编辑器分屏布局人工清理或应用退出

三层分开管理的原因很简单:它们的变化频率完全不同。任务上下文可能一周才变一次,数据上下文可能一天变几次,界面上下文则可能十分钟就变一次。如果全部塞进同一个快照,那恢复的时候要不就是太僵硬,把不该重开的重开了;要不就是太松散,该恢复的位置没恢复。

我的做法是:快照整体保存,但恢复时按层拆分执行。界面上下文负责快速还原现场,数据上下文负责保证路径和配置正确,任务上下文则只作为备注和校验信息。这样既保留了“一键恢复”的爽快感,又不会因为界面状态的频繁变动把体验搞得迟钝。

2.2 两级状态机和一条上下文栈

设计选择时我对比过好多方案,最后选了“开关状态 + 上下文栈”这个两级结构。第一级是一个开关,表示“当前是否处于某条上下文的管理之下”。开关打开时,我会约束自己的操作范围,尽量只触碰当前上下文相关的窗口和文件;开关关闭时,一切回到普通工作状态。第二级是上下文栈,用来记录切换的历史顺序。

栈而不是树,这个选择很关键。树结构适合表达“一个任务下分成多个子任务”,但它引入的复杂度很高,因为你还要决定父子节点之间怎么继承上下文。栈就简单得多:你只需要记住“我从哪来,现在在哪,接下来回哪”。人一天的工作路径其实是线性的,虽然逻辑上任务有嵌套,但实际操作永远是串行的。栈的入栈和出栈天然对应“切走”和“切回”,插入成本低,心智负担小。

栈还需要裁减。如果不裁,切了一百次之后栈就会变得很深。我设置了三类裁减规则。第一,固定某个上下文为“常驻”,它可以被临时压栈但永远不被清理。第二,最近最少使用的上下文会被标记为“可回收”,当栈深度超过 20 条时自动清理最底层。第三,两个连续且相邻的上下文如果内容高度相似,合并成一条,只保留最新的保存时间。

2.3 上下文标识符和时间戳,看着简单其实很重要

每条上下文需要一个标识符。我一开始直接用任务名,比如“权限改造”,很快发现不行:同一个任务可能被反复保存,名字是一样,但不同时期的状态完全不一样。后来改成“名称 + 序号 + 时间戳”的结构,类似task: auth-refactor / seq: 12 / 2025-01-18T14:30。序号用来区分同一任务的多次快照,时间戳用来做生命期管理。

保存时机同样是核心问题。我发现两套不同的保存策略各有各的适用场景。手动保存适合关键阶段:比如我刚完成一个功能自测,想留个备份点,那我来一次“保存”。自动保存适合持续记录:我固定在三个时机自动落盘,一是空闲五分钟,二是标签页切换完成三秒后,三是收到系统锁屏信号。自动保存的粒度不需要太细,否则很容易把临时状态都存下来,等到恢复时反而分不清哪条是有意义的。

2.4 为什么最终用了混合存储,而不是一张 SQLite 大表

刚开始我打算用 SQLite 存所有上下文,后来发现恢复时候会有一个绕不开的延迟:把数据库里的快照内容转成实际工作区,需要经历组装过程。快照越多,组装越慢。现在我的方案是“热点上下文走独立目录 + 归档上下文走 SQLite”。保存时先把快照写入当前目录下的独立 JSON 文件,文件名就是上下文 ID,读取时直接加载这个文件;同时把一条索引信息写入 SQLite,用于检索历史版本。两者通过同一个 ID 关联。

这样做的好处很实际。当前正在用的上下文最多七八条,都是小文件,读取基本是毫秒级。更早之前的版本只保留条目和索引,真正需要回溯时再去做水合(hydrate)。数据是分成两层的,一层照顾速度,一层照顾容量。这背后其实就是“热点数据单独存”的老道理,放在上下文管理上同样适用。

3. 落地实现:一个最小可用的 context-mode 切换器

3.1 项目结构和依赖选择

为了让这套思路能直接跑起来,我做了个很轻的命令行工具,代码量不大,依赖只用了 Python 3.11 标准库和 SQLite。目录结构大概是这样的:

ctx/ saved/ auth-refactor__12.json auth-refactor__13.json marketing-plan__04.json index.db ctx_core.py

saved目录放热点 JSON 文件,index.db放 SQLite 索引。ctx_core.py是核心逻辑,不过几百行。我刻意没有用复杂框架,因为上下文这种工具一旦被过度工程化,维护成本会很快反超收益,最后变成没人再用的玩具。

3.2 命令设计:保存、切换、恢复、清理

命令我用的是朴素风格,跟 git 的走向靠拢。常用命令就四个:save、ls、switch、drop。

命令作用示例
ctx save <name>保存当前上下文ctx save auth-refactor
ctx ls列出可用的上下文,标出最近使用ctx ls
ctx switch <ctx_id>切换并恢复上下文对应状态ctx switch auth-refactor__13
ctx drop <ctx_id>删除某个上下文快照ctx drop auth-refactor__12

额外还有attach和detach,用来把某个窗口或某个临时文件挂到当前上下文名下。这个在设计里很重要,因为不是所有资源都适合放进自动快照,手动脉冲更可靠。

3.3 核心代码,逐行拆开看

我抽了最核心的保存和恢复逻辑,代码不长,但把上下文管理的几个重点都覆盖了。

import json import sqlite3 from pathlib import Path from datetime import datetime, timezone BASE_DIR = Path.home() / ".ctx" SAVED_DIR = BASE_DIR / "saved" INDEX_DB = BASE_DIR / "index.db" def save_context(name: str, snapshot: dict) -> str: SAVED_DIR.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(INDEX_DB) conn.execute( "CREATE TABLE IF NOT EXISTS ctx_index (id TEXT PRIMARY KEY, name TEXT, saved_at TEXT)" ) now = datetime.now(timezone.utc).isoformat() seq = _next_seq(conn, name) ctx_id = f"{name}__{seq}" snapshot["ctx_id"] = ctx_id snapshot["saved_at"] = now (SAVED_DIR / f"{ctx_id}.json").write_text(json.dumps(snapshot, ensure_ascii=False)) conn.execute( "INSERT INTO ctx_index (id, name, saved_at) VALUES (?, ?, ?)", (ctx_id, name, now), ) conn.commit() conn.close() return ctx_id

保存逻辑分四步。先建目录和索引表,这是防御式初始化。然后计算序号,_next_seq会查 SQLite 里同一个名字下已经有多少条快照,返回序号 13,这样 ID 不会落地重复。接着把整个快照对象写进文件,最后把索引登记进去。这里有一个细节:我刻意把saved_at同时写进 JSON 和 SQLite,恢复的时候只读文件就够,检索的时候只查数据库就够了,两边各自独立。

恢复逻辑我愿意多花一点时间说明。

def restore_context(ctx_id: str) -> dict: snapshot_file = SAVED_DIR / f"{ctx_id}.json" if not snapshot_file.exists(): raise FileNotFoundError(f"snapshot {ctx_id} not found") snapshot = json.loads(snapshot_file.read_text()) # 分层恢复 for handler in handlers: handler.restore(snapshot.get(handler.layer_key, {})) return snapshot

handlers是一个列表,里面分别注册了文件恢复器、终端目录恢复器、浏览器标签恢复器。每个恢复器只认自己那一层的数据。我在设计的时候刻意把“恢复器做什么”和“快照里存什么”解耦,这样以后想新增一个层,比如数据库连接恢复,不用改动保存逻辑,只要往handlers里多注册一个实现就行。

3.4 接层与前端的配合

光有命令行还不够,工具要真正帮上忙,必须融入每天的操作流。我在编辑器快捷键上注册了两个组合键。一个组合键是“保存当前上下文”,另一个是“快速切换最近两条上下文”。浏览器方面,我写了一个小扩展,负责把当前标签页的 URL 和标题写入一个临时 JSON 文件,下一次执行restore的时候把对应标签重新打开。

前端状态提示条也值得一提。我在终端提示符左侧加了一个小尾巴,显示当前上下文 ID。比如auth-refactor__13和一段箭头。这样我在任意时刻或者在任意目录里,都能一眼看到“我现在被包在哪条上下文里”。别小看这个提示,它是最容易让人坚持用下去的功能。状态可见性一旦建立,手动操作的频率就会大大下降。

4. 在实践里踩过的坑,和对应的修复思路

4.1 上下文漂移:你以为保存了,其实没保存全

我第一次跑通这套工具之后,最常出现的意外是:切换回来,发现界面没有完全恢复。特别是浏览器标签,总有少数几个没回来。查了很久才发现,问题出在“保存时正在加载的页面”上。有些页面要好几秒才能加载完,我在页面还没完成加载时就抓了 URL,属于没抓全;等加载完又产生了跳转,比如登录跳转、重定向,URL 已经不是最初那个了。

解决思路是两层:保存时在浏览器扩展里做一次延迟读取,等页面进入空闲状态后再补一次 URL;恢复时拼接键盘,把“恢复 URL 列表”变成“恢复 URL 列表 + 延时重新匹配已打开标签”。现在恢复接口会先尝试匹配已有的标签,如果 URL 已经打开就不重复开,避免产生几十个一模一样的页签。这个修复让我少了不少烦心事。

4.2 自动保存太勤快,反而把有价值的版本挤掉

自动保存本来是为了省心,结果第一次上线测试我就发现,它把重要的版本给覆盖了。原因是我用“同一个名称 + 最新序号”作为文件写入路径,没做去重。比如我手动保存了“auth-refactor__12”,三个钟头后自动保存又生成了一条一模一样的名字,序号却变成了 13。这时候我想回退到手动保存的 12,发现它已经被自动保存的 13 覆盖了。

我把机制改成“手动保存和自动保存分开命名”。手动保存叫auth-refactor__manual__12,自动保存叫auth-refactor__auto__13。同时增加一个protect标记,手动保存的版本默认带保护,不能被自动清理删除。这样自动记录成了后备,不会淹没真正重要的检查点。这条规则后来让我回退了很多次,每次都能精准找回关键版本。

4.3 长任务中间的上下文漂移会越滚越大

上下文另一种让人头疼的问题是漂移。比如我保存了一条“运营月度报告”,但隔了三个小时才回来继续,中间我又悄悄改了若干配置文件、临时拿终端跑了几条命令。回来恢复时,这些中途产生的改动也会被认为是“恢复的一部分”,于是引起冲突。它不是一次性发生的,而是逐步累积的。

我通过“时间戳差异记录”来处理。每次恢复时,我并不是把整条上下文状态一股脑倒回去,而是对比保存时的快照和当前的工作区状态,生成一个“差异清单”。清单里有“新增”“变更”“删除”三种操作,操作前会在终端二次确认。这个流程听起来冗余,实际做起来很快,因为它把“全量覆盖”变成了“有针对性的小步调整”,更贴合真实使用的感受。

4.4 上下文快照里的隐私和细节,比你想象中多

快照会存下很多意想不到的敏感内容。比如浏览器标签 URL 可能会携带查询参数,里面可能包含用户 ID 甚至临时的访问令牌;编辑器打开的文件路径可能暴露内部项目结构;终端历史命令可能留下不可见的信息。这个问题不解决,工具只能用在自己一个人、一台机器上,根本不敢拿到团队里。

我后来引入了两个保护机制。第一是脱敏规则:URL 查询参数默认被替换成占位符,需要显式开启才保留;文件路径里匹配到.env、token、secret等关键词的条目,保存时自动从快照里移除。第二是加密存储:热点 JSON 默认不加密,因为我需要读取速度;归档到 SQLite 前会用本地密钥加密,密钥放在系统 keyring 里。这个设计至少保证同步到其他设备时不会把全部上下文裸露在外。

4.5 上下文的生命周期,不该只有“永远存在”

刚开始所有上下文不分大小,一股脑放在列表里,三个月后列表变得非常臃肿。每次ctx ls都要翻半天。我意识到上下文跟代码分支一样,也需要生命周期管理。于是加了三条规则:超过 90 天没有被访问的上下文自动标记为“已归档”,从主列表隐藏;明确标记为“已结束”的上下文进入只读归档,不能继续切换;手动固定常驻的上下文不受上述两条规则影响。

生命周期规则落地后,列表清爽了很多。这不是什么高超技术,就是逼自己在设计里承认一件事:上下文不是为了堆积,是为了让“找到当下该用的那条”变得更快。

5. context-mode 和“多开几个标签页”“VM 快照”的真实区别

5.1 再多标签页,也只是状态近似

很多人觉得搞这么多幺蛾子干嘛,直接多开几个窗口、多建几个浏览器标签组不就行了。我的体会是:标签页是状态的近似,不是状态本身。

方式保存内容能不能跨机器迁移能不能按意图检索心智负担
多开标签/窗口可见窗口布局通常不能不能低启动高维护
VM 快照整个系统镜像能,但成本高不能高
context-mode分层快照 + 索引能,体积小能按名称和标签查适中

标签页保存的只是“哪些页面开在哪里”,但不会告诉你这些页面是为了什么开的。我经常面对二十个标签,真正需要的可能只有其中三个。context-mode 保存的是“意图”,所以恢复之后不只是有标签,还会有“这条上下文要干嘛”的说明文字。对长期项目来说,这个差异非常大。

5.2 VM 快照和容器镜像的力气使错了方向

VM 快照和容器镜像解决的是环境一致性问题,它可以保证你每次回到同一份操作系统环境。但它有几个毛病:体积大、恢复慢、保存频率低。你不可能每五分钟就做一次 VM 快照,也不可能为一个“改两行文案再切回来”的动作做镜像切换。context-mode 的定位恰好不同,它保存的是轻量的逻辑状态,而不是文件系统本身。文件系统有 git、有同步盘在管,上下文工具不应该抢这个活。

所以在架构上我设计了清晰的边界:context-mode 只管“入口”,不管“内容”。工作目录里的实际代码变动还是交给 git,资料页面还是交给浏览器的历史记录,目录里的文件还是交给文件同步工具。context-mode 只负责把“刚才那套入口组合”放进一个可复用的对象里。边界越清晰,跟其他工具组合起来越顺。

5.3 用了半年之后的实际变化

如果你问我这东西到底值不值得做,我的答案是因人而异,但对我来说变化非常明显。过去我每次换任务,平均要花五到八分钟“找回思路”:翻聊天记录、看最近改的文件、回忆问题在哪。现在这个时间被压缩到了不足一分钟,核心原因是思路找回了。另外意外的好处是,因为每条上下文都会带一段“目标描述”,我很少再做那种“不知道自己为什么打开这个页面”的操作,行为变得比以前有目的性。

5.4 我还在打磨的方向

当前版本还不是最终形态。我接下来打算做两件事。第一是让上下文可以跨设备同步,因为经常需要在台机和笔记本之间移动。同步时只同步已经脱敏且启用了加密的条目,热点条目不参与跨设备的实时复制。第二是让上下文支持团队共享,不同成员可以把“某个任务相关的上下文文件”发给对方,而不是发一长串无从下手的指示。这俩方向短期内不会做成很重的服务,大概率还是本地优先,外部只做冷同步。


如果你也想搞一套类似的 context-mode,我的个人建议是别急着写一堆代码,先从最笨的办法开始。手动维护一个文本文件,记录你当前任务的名称、打开的文件、关键链接和下一步动作。等这个文本文件坚持用了两三个星期,你就会自然发现哪些信息最常用、哪些恢复动作最重复,那个时候再逐步自动化,比闭门造车高效得多。上下文工具本质上是帮你记住“你为什么在这里”,而不是把你所有操作都搬进数据库,这个分寸从一开始就不能丢。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进