
你有没有遇到过这种情况上午刚让AI帮你梳理完A项目的接口方案下午切到B项目希望AI还记得A项目里那个技术选型的限制条件结果它一脸茫然甚至给出跟之前完全相反的建议。我一度以为这是模型能力问题后来才明白真正的坑在于——上下文没有管理。这就是我为什么写了 context-mode 这个小工具。context-mode 是一个轻量级的命令行工具专门用于管理不同任务、不同项目之间的AI对话上下文。它把每个项目的背景资料、当前进展、关键决策和对话摘要保存成独立的上下文文件按需切换、随时恢复彻底解决AI“换个话题就失忆”的毛病。如果你经常用AI辅助写代码、写方案、做数据分析又在多个项目之间来回切换那这个工具和它背后的管理思路会非常值得参考。我最早只是想给自己写个脚本后来发现同事和朋友也有同样的痛点于是把它打磨成了一个小项目。这篇文章会把设计思路、核心实现、完整实操和踩坑记录都写清楚代码也不长你可以直接照着搭建一套自己的上下文管理模式。1. 为什么需要 context-mode上下文管理的核心痛点1.1 上下文分裂AI为什么总是“跑偏”先还原一个真实场景。那天我在做两个事情一个是给旧系统的支付模块补一个对账接口另一个是在新项目里设计用户权限的数据模型。两个项目同时在IDE里开着AI辅助工具也挂在同一个会话流上。我让AI帮我写支付对账SQL它写完我觉得不够健壮让它“按之前说过的错误处理思路改一下”。结果它引用了权限模型里的角色继承关系问我要不要按这个来设计异常链路。我当时就明白了——它把两个上下文混在一起了。这种“上下文分裂”是大模型辅助开发中最常见、也最隐蔽的问题。模型本身没有时间线也不清楚你当前正在处理哪个项目。它只能从当前对话窗口里能看到的所有文本中推测你的意图。如果你把两个项目的讨论都塞在同一个对话流里它自然会把A项目的决策当成B项目的背景或者把B项目的约束当成A项目的需求。结果就是逻辑混乱、代码风格漂移、无用建议一堆最后你还得花时间纠正它。1.2 现有方案的不足会话列表、复制粘贴都不够用很多人会用多会话窗口来区分不同项目这确实比混在一起好但远远不够。第一模型应用的会话列表一般只能按时间排序你很难快速回到三天前那个项目的某个关键决策点。第二单个会话本身有长度限制聊得久了早期的重要背景会被挤出去模型就开始“短线失忆”。第三切换项目时你需要关闭当前会话、打开另一个会话、重新讲述一遍项目背景这个重复劳动本质上就是在消耗时间和漏失细节。复制粘贴更不靠谱。我试过把一段项目背景文档复制到新会话开头效果呢比没有强一点但前提是你还记得要把哪些内容贴进去。如果连续切换三次你很可能忘了某个重要约束而模型又不会主动提醒你“你上次说过不要用某某方案”。人类本来就是靠外部记录来管理注意力的指望大脑在多个项目间精准记忆是不现实的。1.3 context-mode的设计目标隔离、持久、快速恢复基于这些痛点我给context-mode定了三个目标隔离每个项目、每个任务有独立的上下文空间互不干扰。持久上下文不仅存在当前对话里而是存到本地文件长期保存随时翻看。快速恢复一条命令切换项目相关背景自动载入几秒钟内进入状态。这就像同时开多个项目会。开会的时候你总得先翻一下上一次会议纪要才能接上话题。但如果你每次开会前手上都有当前项目的纪要而且只需要一秒钟就能拿到那会议效率就会高很多。context-mode 就是干这个的——给AI聊天的每个项目都建一份“会议纪要”切换时自动摆到它面前。2. context-mode 的整体设计与核心机制2.1 上下文文件结构用Markdown加头信息而不是塞进数据库我一开始想得很简单把所有上下文塞进一个SQLite里。后来发现既然要给人看、给AI用最好的格式反而是文本文件。一个上下文对应一个Markdown文件文件名就是上下文ID文件开头有一段YAML格式的元数据后面是结构化正文。每个上下文目录位于~/.context-mode/contexts/下目录命名采用“项目名-任务名-短ID”的方式。上下文文件context.md的结构大概长这样--- name: pay-api-refactor project: backbone created: 2025-03-10 updated: 2025-03-14 tags: [支付, 对账, SQL] status: active --- # 当前目标 为支付模块补充交易对账接口保证订单流水和第三方支付流水每日对账一致。 # 关键决策 - 对账口径以本系统订单流水为准第三方流水仅作为核对源。 - 失败订单不参与对账单独生成异常清单。 - 使用批次号 支付流水号作为唯一匹配键。 # 技术约束 - 数据库为 MySQL 8.0禁止使用外键约束。 - 对账任务每天凌晨跑需要幂等重复执行不会重复生成差异记录。 - 接口必须兼容现有返回结构不能破坏旧客户端。 # 当前进展 - 已写完对账SQL的核心查询待补充异常清单插入逻辑。 - 已完成接口参数校验未开始联调。 # 对话摘要 用户希望按批次号对齐当天流水并输出差异表格。 助手设计了 base 查询和 diff 查询建议用 union all 合并。 用户要求去除手续费分摊逻辑简化成订单金额对比。 助手已调整下一步封装为存储过程。 # 待办 - [ ] 处理重复对账的幂等键 - [ ] 增加超时重试机制这个结构有个好处AI能看到明确的目标、决策、约束、进展和近期对话摘要。这些内容比溅射在对话窗口里的内容更精炼、更结构化模型理解起来非常快。而且Markdown是纯文本任何人可以直接打开看也可以用脚本解析。YAML头信息则方便做筛选、排序和状态标记。2.2 核心命令设计像git分支一样管理上下文命令行是这个工具的主体。我设计了一套非常精简的命令全部以cm开头和“context-mode”的缩写对应cm init在当前项目目录初始化一个上下文配置文件生成.cm/config.yml。cm new name新建一个上下文并切换到它。cm save把当前的上下文状态保存到文件。cm switch name切换到指定上下文。cm list列出所有上下文显示名称、项目、更新时间。cm cat输出当前上下文文件内容方便喂给AI或复制到其他地方。cm clear清空当前上下文中的对话摘要保留目标和约束。这套命令的语义非常直观。你不需要去理解内部存储结构只需要把它想象成一个“上下文分支管理工具”。就像git checkout切换分支一样cm switch切换的是你当前的工作话题和AI的记忆。2.3 与AI辅助工具的集成思路把上下文变成标准输入工具本身不直接绑定任何AI客户端。它只负责管理上下文文件并提供一个“导出”机制。你想要在某个AI工具中使用当前上下文只需要把cm cat的输出拼到你的提示词里或者让AI工具读取这个文件。比如我的使用方式是在shell里定义一个别名alias ai-chatai chat --system-file ~/.context-mode/current.md每次执行cm switch时工具会更新~/.context-mode/current.md这个软链接让它指向当前上下文的文件。这样AI在启动对话时自动就能读取当前项目的目标、约束和聊天摘要相当于给它喂了一份“项目简报”。这个设计的关键在于上下文管理不应该和具体的AI模型或客户端耦合。因为模型更新很快、客户端五花八门而上下文格式是慢变量用标准文件格式隔绝变化工具才能稳定用下去。2.4 关键技术选型为什么是Python加Click这个工具我用Python写命令解析用Click没有引入复杂依赖。选择原因是Python直接可用跨平台方便用户不需要额外装Node或Go。Click可以快速定义子命令和参数使用体验和git接近。文件操作用Pathlib读取写入都很干净。我不推荐为了一个几十行的CLI工具引入重型框架或者去用ORM、消息队列什么的。工具的核心逻辑其实很简单复杂度主要在于上下文格式设计和使用习惯而不是技术栈。3. 实操过程从零搭建并接入你的工作流3.1 初始化项目并创建第一个上下文拿到源码后先安装依赖并执行安装pip install click pyyaml python setup.py install # 或者把 cm.py 直接放到 PATH然后进入你的某个项目目录初始化上下文配置cd ~/work/backbone cm init这条命令会在当前目录生成一个.cm/config.ymlproject: backbone owner: zhang default_context: pay-api-refactorconfig.yml里记录了项目名和默认上下文。以后你在这个目录下执行cm switch时如果不带参数就会自动切换到default_context。这样做的好处是每个项目的缺省上下文都不同你不需要每次手动指定。接着创建第一个上下文cm new pay-api-refactor工具会在~/.context-mode/contexts/下创建pay-api-refactor目录写入一个模板文件。然后自动把当前上下文软链接指向这个文件。你可以打开~/.context-mode/contexts/pay-api-refactor/context.md开始编辑内容填入项目目标、约束等。3.2 保存上下文快照记录关键信息而不是流水账真正开始用的时候你不需要一次性把整个项目细节都写进去。我的习惯是每天工作时先更新三个部分当前目标、关键决策、待办事项。对话摘要则可以交给AI生成每天下班前跑一次cm save --summary 今天完成了对账SQL查询开发确定了批量匹配键待处理幂等如果你希望更自动可以把摘要保存做成一个分支流程让AI根据今天的对话生成摘要然后你复制到cm save的输入里。实测下来这比手动回想快很多而且AI生成的摘要往往更贴对话内容的重点。保存完之后context.md会更新updated字段并把对话摘要部分替换成新内容。为了避免历史记录被完全覆盖我把旧的摘要追加到archive.md文件中这样你随时可以往回查。这个设计在实际使用中非常有用因为你可能过两周需要回顾之前某个决策是怎么做出的。3.3 切换上下文一行命令换项目当你准备切到另一个项目时执行cm switch user-permission-model此时~/.context-mode/current.md会指向新项目上下文。为了验证是否成功可以执行cm list输出类似name project updated pay-api-refactor backbone 2025-03-14 10:32 user-permission-model portal 2025-03-15 09:12然后用cm cat输出当前上下文cm cat | head -60你会看到当前上下文的目标和约束已经是新项目的了。这时候再启动AI聊天就不需要你手动复制粘贴AI直接通过system file获取这些内容。这里有一个关键实现细节cm switch默认是一个普通子命令但它要改变的是父shell中的环境变量或软链接。软链接问题不大直接更新即可如果你想进一步在shell中设置CM_CONTEXT环境变量就得让cm switch输出一段可被eval执行的shell代码常用做法是eval $(cm switch user-permission-model)cm switch检测到当前shell为/bin/bash或/bin/zsh时输出export CM_PROJECTportal export CM_CONTEXTuser-permission-model这样父shell环境也更新了。这个小细节是很多新手写CLI工具时会踩的坑子进程只能改变自己进程的环境变量没法影响父进程。所以必须用eval输出环境变量赋值语句。3.4 用别名和shell函数实现极速切换每次都要敲cm switch xxx还是有点烦。我在.zshrc里加了一个函数function cm() { if [[ $1 switch -n $2 ]]; then eval $(command cm switch $2) if [[ -f ~/.context-mode/current.md ]]; then export CM_FROM$(basename $(dirname $(readlink ~/.context-mode/current.md))) echo switched to $CM_FROM fi else command cm $ fi }这样执行cm switch user-permission-model后不只更新软链接还自动把环境变量设置好了。习惯之后切换几乎成为肌肉记忆。我还给最常用的三个项目做了单字母别名alias cpacm switch pay-api-refactor alias cpucm switch user-permission-model彻底告别长命令打开终端直接输入别名就能开始工作。3.5 结合git分支实现项目维度隔离进一步使用时我发现一个问题同一个项目下可能有不同功能分支比如版本V1和V2。如果上下文只有一个分支之间的信息会交叉污染。解决办法是让上下文名称包含分支信息或者用一个符号链接自动指向当前git分支对应的上下文。我在cm switch里增加了一个--auto选项cm switch --auto它会检测当前目录的git分支名比如feature/priv-role然后自动切换到名为priv-role的上下文如果不存在就自动创建。这样切换git分支的同时AI上下文也跟着切完美对齐。这个操作背后的逻辑是上下文本质上和“当前工作主题”强相关而代码分支通常就代表了工作主题。当代码分支切换时工作主题必然变了AI的上下文也必须跟着变。把这两者绑定在一起能省下很多手动管理成本。4. 常见问题与排查技巧实录4.1 上下文文件越来越大导致AI响应变慢用了一段时间后context.md越写越长尤其对话摘要一直累积。AI读取的上下文一多响应速度明显下降而且会开始“过度关注”那些琐碎的旧摘要。我的解决方式是引入大小上限。在cm save时如果文件超过8KB就把最老的对话摘要移到archive.md只保留最近3轮。这样当前上下文始终保持精简历史信息不丢但不会干扰AI。你可能会问为什么不保留所有历史因为模型对近因效应很敏感前面塞一万字的旧日志反而会把最新的目标淹没。上下文管理的本质是“懂得舍弃”不是把所有东西都堆在一起。4.2 多设备同步冲突我在公司和家里都会使用同一套上下文早期用坚果云同步整个~/.context-mode目录但偶尔会出现两边都修改同一个文件的情况markdown内容直接乱掉。后来我把上下文目录本身做成一个git仓库在cm save后自动git add和git commitcd ~/.context-mode git add -A git commit -m update context: $CM_CONTEXT如果冲突git会标出来。因为上下文文件是文本格式冲突合并很直观用编辑器手动解决一下就行。这个方案比各种云同步稳定很多而且所有历史版本都有记录误删也能恢复。4.3 敏感信息泄露风险上下文里经常会有API密钥、数据库密码、内部系统地址。如果你把上下文目录提交到公开仓库或者传给AI服务商时这就是很大风险。我的习惯是用环境变量存密钥上下文里只写变量名比如DB_PASSWORD${DB_PASSWORD}。在.gitignore里忽略secrets.md单独特权访问。配置一个检查脚本在cm save前扫描上下文文件如果发现类似password或api_key的行自动警告。这个检查逻辑很简单但能在关键时刻拉你一把。有一次我差点把生产环境的AK写进摘要里工具弹了个警告我当场删掉了。自用工具也一样要重视安全别因为“只有我自己用”就放松警惕。4.4 忘记保存上下文怎么办最尴尬的是当你正切换上下文时突然意识到上个上下文的重要进展还没保存。这个情况太常见了。我后来加了一个机制在shell的PROMPT_COMMAND里检测当前目录的.cm/config.yml被修改过但上下文没有及时保存就自动打印提醒if [[ -f .cm/config.yml -z $CM_UP_TO_DATE ]]; then echo cm changes not saved, run cm save fi当然更保险的做法是定期自动保存。我写了一个后台脚本每隔10分钟把当前的context.md的修改时间戳写入版本文件。检测到当前上下文在半小时内被编辑过且未cm save就自动保存并commit。虽然有点“过度工程”但确实解决了健忘问题。4.5 与其他AI工具的兼容性different AI工具读上下文的方式都不一样。有些支持--system-file有些不支持有些人喜欢JSON格式的上下文。为了兼容我把命令设计成多种输出格式cm cat --format md直接输出Markdown正文。cm cat --format text去掉YAML头只输出给AI看的提示文本。cm cat --format json输出包含元数据和正文的JSON对象方便程序处理。如果你用的AI客户端接收JSON就可以直接用cm cat --format json作为输入。如果客户端只支持纯文本就选text。关键是不要在某个单一客户端里绑死把格式转换放在输出层。5. 核心代码参考一个可用的最小实现受篇幅限制我提炼一个最精简版本。它实现了new、switch、save、cat四个核心命令够日常使用。完整工程还包含init、list、clear、自动提交和校验逻辑大家可以根据下面的框架继续扩展。#!/usr/bin/env python3 context-mode minimal implementation. import click import yaml import os import shutil from pathlib import Path from datetime import datetime CONTEXT_ROOT Path.home() / .context-mode CONTEXTS_DIR CONTEXT_ROOT / contexts CURRENT_LINK CONTEXT_ROOT / current.md ARCHIVE_DIR CONTEXT_ROOT / archives def ensure_dirs(): CONTEXTS_DIR.mkdir(parentsTrue, exist_okTrue) ARCHIVE_DIR.mkdir(parentsTrue, exist_okTrue) def get_context_path(name: str) - Path: return CONTEXTS_DIR / name / context.md def make_context(name: str): ctx_dir CONTEXTS_DIR / name ctx_dir.mkdir(parentsTrue, exist_okTrue) ctx_file ctx_dir / context.md if not ctx_file.exists(): now datetime.now().isoformat(timespecminutes) template f--- name: {name} created: {now} updated: {now} --- # 当前目标 # 关键决策 # 技术约束 # 当前进展 # 对话摘要 ctx_file.write_text(template, encodingutf-8) return ctx_file def switch_context(name: str): ctx_path make_context(name) if CURRENT_LINK.is_symlink() or CURRENT_LINK.exists(): CURRENT_LINK.unlink() os.symlink(ctx_path, CURRENT_LINK) return ctx_path click.group() def cli(): context-mode: manage AI contexts. ensure_dirs() click.command() click.argument(name) def new(name: str): Create a new context and switch to it. path switch_context(name) click.echo(fcreated and switched to {name} ({path})) click.echo(fexport CM_CONTEXT{name}) click.command() click.argument(name) def switch(name: str): Switch to an existing context (create if missing). path switch_context(name) click.echo(fswitched to {name}) click.echo(fexport CM_CONTEXT{name}) click.command() click.option(--summary, defaultNone, helpNew summary text.) click.option(--max-size, default8192, helpMax context size.) def save(summary: str, max_size: int): Save current context. if not CURRENT_LINK.exists(): click.echo(no current context, errTrue) raise SystemExit(1) current CURRENT_LINK.resolve() data current.read_text(encodingutf-8) if summary: new_data replace_summary(data, summary) current.write_text(new_data, encodingutf-8) size current.stat().st_size if size max_size: archive_and_trim(current, max_size) click.echo(fcontext trimmed to {max_size} bytes) click.echo(context saved) click.command() click.option(--format, fmt, defaultmd, typeclick.Choice([md, text, json])) def cat(fmt: str): Print current context. if not CURRENT_LINK.exists(): click.echo(no current context, errTrue) raise SystemExit(1) data CURRENT_LINK.read_text(encodingutf-8) if fmt md: click.echo(data) elif fmt text: lines data.splitlines() start False for line in lines: if line.startswith(---): if start: continue else: start True continue if start: continue click.echo(line) elif fmt json: parts data.split(---, 2) meta yaml.safe_load(parts[1]) if len(parts) 3 else {} body parts[2].strip() if len(parts) 3 else data click.echo(Path(json_dumps({name: meta.get(name), content: body})))这里有两个要点值得说明。replace_summary函数建议用正则定位# 对话摘要部分并替换简单实现即可核心是避免手动拼接出错。archive_and_trim是把最旧的对话摘要移到archive.md。我在裁剪时会把原文件的内容备份到ARCHIVE_DIR/context_name-date.md然后删除对话摘要下所有的列表项只保留最近4行。这样既保留了可追溯性又不让当前上下文膨胀。如果只想快速使用你甚至可以把上面这段代码保存为cm.py放到PATH里加上chmod x然后配合eval $(cm switch xxx)就能跑起来。完整的复杂逻辑自动提交、敏感词检测、git分支绑定刚才也都有思路可以作为进阶练习自己加。6. 再聊聊我对上下文管理的体会我最初写 context-mode 只是为了让AI少犯蠢但用了一年多之后我发现它真正改变的是我自己的思维方式。以前我经常在项目切换时靠大脑硬记陷入“好像有个限制条件但我忘了”的状态。现在每次保存上下文我都会下意识把当前目标、关键决策、技术约束想一遍这本身就是对工作非常有效的梳理。给大家一个小建议不要一上来就追求完美的上下文模板。先随便用一段文字记录“这个项目在干嘛、下一步做什么”然后配合AI的对话摘要慢慢沉淀。等到用顺手了再逐步补充像“技术约束”、“已废弃方案”这些结构。工具是死的使用习惯才是核心。如果你也在被多项目切换困扰不妨先试一下这套机制告别每次开新对话都要重新教一遍AI的原始生活。最后再说一个细节这类工具一定要让你的“切换成本”趋于零。如果切换一个上下文要敲五条命令那你早晚会放弃。用别名、用shell函数、用git分支绑定怎么方便怎么来。把复杂留给工具把简单留给自己这才是个人生产力工具的终极目标。