
我记得很清楚第一次意识到上下文重要性的场景是在写一个小型终端助手的时候。这个工具需要用户连续完成几步操作先选择数据源再指定处理方式最后输出报告。但每次命令跑完它就什么都不记得了用户下一步如果漏掉某个参数只能重新敲一遍。那一刻我意识到没有上下文的工具就像一个每次都重新认识你的柜台服务员你不得不一遍遍重复自己的需求。context-mode就是为解决这类问题设计的。简单来说它是一种让软件在多次操作或多次会话之间自动保存、读取并利用历史上下文信息的运行模式。它解决的问题很具体减少重复输入、保持操作连续性、让程序能根据之前发生了什么做出更合理的判断。如果你是做CLI工具、自动化脚本、编辑器插件或者自己在折腾AI助手相关的东西这篇文章值得看完——我会从设计思路讲到代码实现再分享实测中踩过的三个坑。1. context-mode是什么先从一次尴尬的失忆说起1.1 没有上下文的世界你每天都在重复填表先想想你平时用的命令行工具尤其是那些要传一堆参数的工具。比如一个数据迁移脚本你可能要写成这样python migrate.py --source /data/old --target /data/new --format csv --encoding utf-8如果跑到一半挂了或者你想换个格式再跑一次你大概率会把这条命令翻出来改一改或者按一下上箭头重新编辑。这在单次操作里问题不大但如果是连续几十条命令呢比如你要依次做预处理、清洗、校验、导入、导出每一步都要重复指定路径、编码、分隔符时间一长忘参数几乎是必然的。没有上下文的世界本质上是把本该由程序记住的信息全压在了用户身上。这就像你每天去同一个快递点取件每次都报一遍姓名、电话、地址。快递点的工作人员不是不想记住你而是他的系统没这个能力。context-mode要做的就是给程序加上这个记忆能力。1.2 context-mode的定位让工具记住刚才发生了什么那么context-mode具体指什么我倾向于给它一个精确的定义一种运行模式在这种模式下程序会将用户当前的操作、输入参数、选中项、临时状态等信息记录下来并在后续操作中自动读取和套用从而形成连续的操作体验。它和普通的日志、缓存有本质区别。日志是写给开发者调试用的缓存是为了加速重复读取而context-mode关心的是语义上的连续性。举个例子你在一个文件管理器里选中了一个文件然后准备执行压缩操作——压缩工具预先把当前选中了哪个文件这个信息记下来这就是上下文。再比如AI聊天助手它能记得你五分钟前问过什么这个记得就是上下文模式在起作用。我见过的context-mode实现有很多种形态基于会话服务器端为每个连接维护独立上下文比如SSH会话。基于项目目录进入某个项目目录后自动加载该项目相关的偏好设置。基于全局用户级别的默认配置任何地方都能访问。基于时间窗口只记录最近N分钟或N条操作形成滚动上下文。这里面的核心难点不是记录数据而是决定哪些数据值得记录、什么时候该遗忘、上下文冲突时怎么处理。1.3 什么场景真正需要它三个最典型的应用场景我在实际项目里总结出三类最需要context-mode的场景你可以对照自己的需求来判断。第一类是多步骤CLI工具。我之前写过一个批量图片处理工具包含导入、去重、压缩、重命名、导出五个步骤。如果每一步都要求用户重新输入输入目录、输出目录、质量参数用起来会非常痛苦。加上context-mode之后用户只需要在第一次运行时提供完整参数后续跑任何单步操作都会自动带上省事很多。第二类是IDE和编辑器插件。比如你正在编辑一个Python文件然后按下快捷键跳到一个工具函数定义处改完之后返回原来的位置——编辑器记住了你刚才在哪里。这种上下文记录已经非常普遍了我们在做插件开发时也常常需要记录光标位置、当前文件路径、最近打开的文件列表这就是一种典型的window-level context。第三类是AI助手的连续对话。当前所有主流AI对话产品都在做上下文管理因为把整段历史全部塞给模型既浪费tokens又会超出模型的上下文窗口。实际工程中我们往往需要保留最近几轮对话而对更久之前的内容做摘要或裁剪。这个保留多少、裁剪多少的策略本质上是context-mode的策略选择。除此之外运维自动化、构建系统、数据管道调度等领域也大量依赖上下文。判断一个系统适不适合引入context-mode我有个简单的经验如果你发现使用者在反复输入或反复选择相同的信息并且这些信息跟上一次操作强相关那这个系统就值得改造。2. 设计一个合理的上下文模型数据结构和生命周期2.1 上下文粒度全局、会话、项目三层的取舍在动手写代码之前必须先想清楚上下文数据要分成几层。如果不分层所有数据混在一起很快就会出现张三的项目配置被李四带走了这种问题。我在context-mode项目里设计了三个层级非常够用层级作用范围典型内容生命周期global当前用户所有操作用户名、默认编辑器、常用路径长期保留session一次工具进程的运行周期当前打开的临时文件、临时的token会话结束即删除project当前项目/工作目录项目根路径、依赖目录、构建命令项目存在期间这套设计解决了两个关键问题。一个是隔离性不同项目的上下文不会互相污染即使是同一个工具在项目A和项目B下会自动加载各自的设置。另一个是时效性session里的状态本来就该用完即焚比如某次登录产生的临时凭证如果放到了global里那就是给自己埋雷。有人可能会问为什么不做成数据库表用外键关联我的答案是对于大部分工具型项目三层结构用嵌套字典就够了。数据库适合海量数据检索而context数据通常单用户、低并发、量也不大用树状结构反而更直观调试时候一眼就能看到当前上下文里有什么。2.2 持久化选型为什么用JSON加内存两级缓存确定层级结构后接下来要考虑数据存哪。我先说结论我采用了内存字典做热存储 JSON文件做冷持久化的组合而不是直接上SQLite。内存字典的好处是快读写都在纳秒级适合高频访问。JSON文件的好处是方便调试——你可以直接用vim或cat打开来看如果用了SQLite还得装个客户端才能查。对于context-mode这种偏工具性质的组件易调试比查询性能重要得多。具体做法是启动时把JSON文件加载进内存形成一个三层嵌套字典。每次set操作先更新内存再决定是否写盘。写盘不能每次set都做否则高频操作时IO会成为瓶颈。我的做法是做一个脏标记在程序退出、或者每隔一段时间批量落盘。下面是我最初的存储结构设计示意{ global: { default_editor: vim }, session: { login_time: 1700000000 }, project: { /home/user/myapp: { build_command: make, last_output: Build OK } } }这里把project的键设计成了项目绝对路径天然实现了项目级隔离。后来又发现一个细节值得注意路径里可能含有特殊字符所以我在加载时对路径做了规范化处理去掉末尾斜杠、解析符号链接避免同一个项目因为路径写法不同出现两个上下文副本。2.3 上下文有效期过期策略与自动清理机制上下文不是无限堆积的否则用户会发现三个月前的一次操作记录还影响了今天的执行结果这反而变成了一种困扰。我设计的过期策略是给每条上下文数据附加元信息{ value: data, created_at: 1700000000, updated_at: 1700000000, ttl: 3600 }created_at和updated_at分别记录创建时间和最后更新时间。ttl是存活时间秒例如3600表示1小时后过期。读取时如果now - updated_at ttl就认为这条数据已过期不返回给调用方。这里我要强调一个容易犯的错很多人把TTL只写在层级上比如整个session层1小时过期这会引发连锁问题——一个长任务可能只需要某些字段长期有效但整个层级过期会把它一并杀掉。正确的做法是字段级TTL不同字段可以单独指定有效期。比如当前登录用户可以永久上次选中的文件只需要5分钟。清理机制我用的是惰性删除 定期扫描双保险。惰性删除是在get时发现过期就顺便删掉定期扫描是每隔10分钟把过期数据批量清掉避免脏数据堆积占内存。这样既保证实时性又不至于每次操作都触发大量删除。3. 动手实现一个可用的context-mode模块Python版3.1 核心类设计ContextManager的接口定义设计完成后实现阶段需要用代码把想法固定下来。这一节我用Python来写一个可直接运行的context-mode模块语言无关性很好你换成Go、Node.js也是一样的思路。先定义核心类的接口。我习惯先写注释再写实现因为这样能逼自己把接口想清楚import json import os import threading import time from pathlib import Path class ContextManager: Manage context data at global/session/project levels with TTL support. def __init__(self, storage_path): self.storage_path Path(storage_path) self._lock threading.Lock() self._context {global: {}, session: {}, project: {}} self._load() def _load(self): if self.storage_path.exists(): with open(self.storage_path, r, encodingutf-8) as f: loaded json.load(f) self._context.update(loaded) def set(self, level, key, value, ttlNone): Set a value in the given level. level: global | session | project key: string key value: any JSON-serializable value ttl: optional time-to-live in seconds with self._lock: meta { value: value, created_at: time.time(), updated_at: time.time(), ttl: ttl, } self._context.setdefault(level, {})[key] meta def get(self, level, key): Get a value. Return None if key doesnt exist or is expired. with self._lock: meta self._context.get(level, {}).get(key) if meta is None: return None if meta[ttl] is not None: if time.time() - meta[updated_at] meta[ttl]: del self._context[level][key] return None return meta[value] def save(self): Persist current context to disk. with self._lock: tmp_file self.storage_path.with_suffix(.tmp) with open(tmp_file, w, encodingutf-8) as f: json.dump(self._context, f, ensure_asciiFalse, indent2) tmp_file.replace(self.storage_path)这个类虽然只有几十行但已经包含了最核心的能力设置、读取、过期判断、落盘。其中save用了一个临时文件加原子替换的技巧避免直接写原文件时如果程序崩溃导致文件损坏。这是一个我踩过坑后的经验后面第4节还会详细说。3.2 一个简单的CLI集成示例从保存到读取光有类还不够我把它集成进一个真实的CLI场景来演示。假设我们要做一个图片压缩工具压缩之前需要用户指定输入目录和质量参数。我用argparse来解析命令命令行没有提供时就从上下文读取import argparse from pathlib import Path ctx ContextManager(~/.myapp/context.json) def require_dir(arg_valueNone): Return provided arg value, or read from context if available. if arg_value: ctx.set(project, input_dir, arg_value) return arg_value cached ctx.get(project, input_dir) if cached: return cached raise SystemExit(请输入输入目录或者先设置过context-mode) if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--input-dir, defaultNone) parser.add_argument(--quality, typeint, defaultNone) args parser.parse_args() input_dir require_dir(args.input_dir) quality args.quality if quality is None: quality ctx.get(project, quality) or 85 ctx.set(project, quality, quality) ctx.save() print(f开始压缩输入目录{input_dir}质量{quality})注意require_dir的设计如果用户传了新参数就会更新上下文如果没传就尝试从上下文取值。这就是context-mode的核心体验——今天少敲几个参数明天少敲更多越用越顺手。3.3 代码走读关键的边界条件处理这几十行代码里有几个边界条件是实际跑起来之后才会发现的。第一个是存储路径的扩展符处理。Path(~/.myapp/context.json)在Python中不会自动展开波浪号必须调用Path.expanduser()。我在最初版本里漏了这一步导致一直找不到文件后来排查了很久。正确写法是self.storage_path Path(storage_path).expanduser()第二个是过期数据在get之后才删除。我的实现里如果get时发现TTL过期会先把数据删掉再返回None。但如果在删除之前另一个线程正好也在读同一个key就会出现竞态。我在get外层加了线程锁这就是_lock的作用。不过对单文件来说这个锁只在进程内有效如果多个进程同时读写同一个JSON文件还需要加文件锁这个坑我第4节会专门讲。第三个是set的值必须可JSON序列化。如果你存了个自定义对象json.dump时会直接抛TypeError。更隐蔽的是如果存的是tuplejson会把它变成list下次读出来类型就变了。如果想保留类型信息必须在set时做转换检查。我在实际项目中加了这样一个断言def _ensure_json_serializable(self, value): try: json.dumps(value) except TypeError: raise TypeError(f值 {value} 无法序列化为JSON)这些边界条件看起来不复杂但往往就是能用和好用之间的差距。4. 实测中的坑与排查过程数据覆盖、过期混乱和隐私泄漏4.1 坑一并发写入导致上下文互相覆盖代码看起来没问题的部分一上实测就暴露了第一个大坑。我的工具支持并行处理多个任务于是会同时跑多个进程每个进程都会调用ctx.save()结果经常出现进程A把上下文写到一半进程B也写过来了最终文件里数据错乱甚至JSON解析直接失败。排查过程是这样的我先检查了代码里的线程锁_lock发现它只能锁住当前进程内的线程进程之间完全不管用。两个进程同时执行tmp_file.replace(self.storage_path)时虽然临时文件是各自独立的但替换操作本身不是原子的关联操作集合——A替换成功B替换成功后面的覆盖了前面的无法合并。解决办法是让进程间也能串行化。我引入了一个简单的文件锁基于操作系统层面的fcntlimport fcntl def save(self): with open(self.storage_path, a) as lock_file: fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX) # 原子写入内容 with open(tmp_file, w, ...): json.dump(...) tmp_file.replace(self.storage_path) fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)但这里还有个微妙的问题锁的竞争粒度是整个文件的写入如果多个进程同时修改的是不同层级的数据理论上是可以合并的。我图省事直接全文件互斥了实测下来因为context本身写得不频繁性能损失可以忽略。如果你的项目写得很频繁就需要把锁粒度细化到字段级。4.2 坑二单字段过期机制导致的连锁混乱第二个坑是在我加字段级TTL之后才浮现的。最初版本我把TTL放到了level层级也就是session层整体2小时过期。后来改成字段级TTL后我发现一个新的问题决定是否过期的时间基准有问题。一开始我给的逻辑是now - created_at ttl就过期但这有个反例用户在一个长任务的刚开始设置了待处理文件列表这个列表的created_at是一个小时前但任务还没跑完。按created_at算它已经过期了可实际上它一直被正常使用着。后来我改成now - updated_at ttl每次get命中就刷新updated_at这样只要这个字段还在被持续使用它就会一直存活。这个逻辑目前验证下来比较合理但它也有代价一个长期不用的上下文永远不会被彻底清掉因为每次get都会给它续命。所以我加了一个上限当now - created_at超过某个最大值比如7天时强制认为过期防止数据无限膨胀。实际排查过程中我用了一段脚本模拟了反复读取同一个旧字段的情况确认了updated_at确实被刷新也发现了created_at一直没变。这类问题只有在把预期行为和实际行为对照着看才容易暴露单纯看代码很难发现。4.3 坑三上下文日志里存了不该存的密钥第三个坑比较吓人。我在调试context-mode时经常打印整个上下文看看数据对不对有一次发现打印内容里包含了用户临时输入的数据库密码。最开始没在意觉得反正本地开发后来才意识到一旦这个模块被集成到线上的自动化脚本里并且开启debug日志就等于把密钥明文写到了日志文件里如果日志被上传到日志中心那就是事故。修复方案是加一个脱敏层。具体在打印和序列化时需要识别常见的敏感字段名SENSITIVE_KEYS {password, secret, token, api_key} def safe_dump(data): if isinstance(data, dict): return {k: (*** if k.lower() in SENSITIVE_KEYS else safe_dump(v)) for k, v in data.items()} if isinstance(data, list): return [safe_dump(i) for i in data] return data然后把所有对外输出改成走safe_dump底层的存量数据保持原样。这里关键在于展示层脱敏存储层不脱敏——如果你把存储层的值都改了那后续需要用真实密码时就会出错。这个坑给我一个更深的教训context-mode这类工具天然就是什么数据都往里塞的类型所以必须在设计阶段就明确哪些数据碰都不能碰对于高敏感数据宁可完全不放进去。4.4 修复后的验证方案我是怎么确认问题真解决的修完三个坑之后我整理了一组回归测试用例专门用来验证context-mode模块的稳定性。这组用例现在被我放在项目的tests目录下每次改动后都会跑一遍并发测试同时启动8个进程每个进程往context里写入不同的key重复100轮最后检查JSON文件里是否所有key都完整存在。过期测试创建一条TTL为1秒的数据sleep 1.2秒后读取确认返回None并且该key从存储中删除。隐私测试用一个包含了password的假上下文跑一次打印确认输出中对应字段是***。边界测试key名称为空字符串、value为None、value为中文和带Unicode的字符都要能正常写入和读取。这些测试全部通过的时候心里才踏实下来。尤其是并发测试第一次跑就失败后来加上进程锁才通过那种测试救了一命的感觉写代码的人都懂。5. 从context-mode到智能化在LLM会话场景中的扩展5.1 把context-mode用在大模型交互上滑动窗口和摘要搞定了基础模块之后我把它用在了另一个更贴近当前热点的场景大语言模型LLM会话的上下文管理。很多人问为什么AI聊天工具不能无限记住所有对话核心原因就是上下文窗口有上限。context-mode在这里的价值是可以设计一种滑动窗口 摘要的组合策略。具体做法是维护一个会话历史列表新消息不断加入。当列表长度超过N条或者总token数超过阈值时我们取前面若干条消息做一次摘要然后把摘要作为一个新的系统消息再丢掉最老的消息。这样实际上形成了一种滚动上下文。我把这个逻辑复用了之前的ContextManager只是在session层里存了一个chat_historyMAX_HISTORY 10 def append_message(role, content): history ctx.get(session, chat_history) or [] history.append({role: role, content: content}) if len(history) MAX_HISTORY: old_part history[:-MAX_HISTORY // 2] recent_part history[-MAX_HISTORY // 2:] old_text \n.join(f{m[role]}: {m[content]} for m in old_part) summary llm_summarize(old_text) # 调用模型生成摘要 ctx.set(session, chat_summary, summary) ctx.set(session, chat_history, recent_part) else: ctx.set(session, chat_history, history)这套方案的关键是摘要的更新时机。如果每来一条新消息就跑一次摘要成本太高我的经验是等旧部分累计超过阈值时再整体生成一次摘要。另外要注意摘要本身也应该有TTL因为它保存的是会话的临时状态会话结束或被新摘要替代后就不再需要了。5.2 后续还能怎么扩展共享上下文、权限控制、可视化调试现在这个context-mode模块已经能稳定运行了但还谈不上完善。我自己整理了三个明确的扩展方向如果你也在做类似项目可以参考。第一个方向是共享上下文。目前每个用户各自维护一份context文件如果想在团队里共享项目级上下文就需要引入同步机制。我的初步思路是给每条context记录加一个owner字段拉取时先检查权限只返回自己有权限的部分。这样可以做到项目组共享构建参数个人私有配置不泄露。第二个方向是权限控制。这跟上面的共享是配套的。context里往往包含敏感信息比如数据库连接串、临时密钥。改造方向是在set/get的接口中加入一个权限校验层按照用户角色判断当前操作是否被允许。最简单的实现是建一个权限表每个key对应一个可访问的角色列表复杂一点的方向是做成与现有SSO/身份系统对接。第三个方向是可视化调试。前端工程师调试时最想看的是上下文树的实时变化。我已经用Python做了个简单的Web页面把ContextManager暴露成一个HTTP接口用WebSocket推送变更事件界面上能看到global/session/project三层的数据新增、过期、删除。这个扩展对排查为什么某次操作行为异常特别有用因为你能看到程序到底记住了什么。把context-mode从一个小模块扩展成完整的上下文管理框架这条路其实很长。但至少起步阶段的方向是清晰的先保证数据结构合理再确保并发安全最后再往智能化和协作方向走。我在实际项目中体会最深的一点是context-mode看起来像是个不起眼的辅助功能但对用户体验的提升是实实在在的。它让工具从一个每次都要重新认识你的陌生人变成了记得你习惯的同事。开发过程中踩过的这三个坑——并发覆盖、过期混乱、隐私泄漏——几乎每一个都值得单独写篇文章但总结下来核心原则也就三条上下文要分层、过期要按字段、敏感要脱敏。记住这三点你也能做出一个稳定好用的context-mode。