“context-mode”这个名字,乍看像某个编辑器插件,其实是我给自己日常 AI 辅助开发工作流写的一组小脚本。最早只是因为受够了在几个项目之间切换时,AI 助手总是把上一个项目的约定和依赖信息带到下一个项目里,导致代码建议经常“串味”。后来我干脆做了一个统一的上下文切换机制,把每个项目的背景、技术栈、常见命令、编码规范都拆成独立的上下文档案,需要时一键加载。用了一段时间之后,效果比想象中好很多,所以这篇博客就把这套 context-mode 的完整思路、实现代码和踩过的坑都整理出来,给同样被上下文混乱困扰的朋友一个参考。
这套东西适合谁?如果你经常用 AI 编程助手写代码,同时手上又有多个项目在维护,或者你需要在不同的技术栈之间来回切换,那么 context-mode 的工作方式就能帮你把每次对话的“记忆背景”梳理干净。它不依赖某个特定的 AI 工具,本质上是一种管理思维加少量脚本,完全可以照着自己的习惯改造。
1. context-mode 的由来与设计思路
1.1 痛点:上下文碎片化
我最初的状态是:打开终端,跑一个 AI 编程助手,把项目里的几个关键文件路径贴给它,然后开始提问。这个做法在单一项目里还算能用,但一旦涉及多个项目,问题就来了。比如我上午在搞一个 Go 后端服务,下午切到 Vue 前端项目,AI 助手如果还带着上午的上下文,就会用 Go 的习惯去写 TypeScript,甚至会把前端项目里根本不存在的依赖告诉你去安装。
更麻烦的是,有些项目的背景信息非常长,比如内部组件的命名规范、数据库表结构、第三方接口的鉴权方式。每次新开会话都要重新粘贴这些内容,粘贴完往往已经占用了大量上下文窗口,真正用来分析代码的空间就变小了。我曾经仔细观察过几次,一个带完整背景描述的会话,到后期经常出现“记不清你刚才说的文件内容”的情况,就是因为上下文被背景资料塞满了。
context-mode 的出发点很直白:把“项目背景”和“实时代码”分成两个维度管理。项目背景是相对静态的,一个项目在几个月内基本不会变,所以应该单独存成文件,需要时自动加载;实时代码是动态的,应该由用户按需提供,而不是让 AI 自己去翻整个仓库。这样一来,每次对话都有一个清晰的“上下文层”,既保留了项目记忆,又不会把上下文窗口浪费在重复的背景描述上。
1.2 设计目标
在设计 context-mode 时,我给自己定了几个硬指标:
- 切换成本要低:一条命令完成上下文切换,不需要打开文件去改内容。
- 上下文档案要做成纯文本:方便直接用 git 管理,也能用任意编辑器查看。
- 对 AI 助手要透明:切换之后,AI 助手能感知到当前处于哪个上下文,而不是靠我口头告诉它。
- 不能入侵项目本身:所有上下文档案都存在项目目录之外,不往仓库里塞多余文件。
这几个指标很关键。切换成本低,人才会愿意持续用;纯文本,意味着可追溯、可 diff;对 AI 透明,意味着 AI 的回答会自动贴合当前项目;不入侵项目,则避免了对团队协作造成额外负担。现在市面上的很多 AI 编程工具都推出了项目级记忆功能,但大部分都绑定在特定编辑器或特定平台里。我想要的是一套通用的、可以在终端里自由组合的方案,context-mode 就是为了满足这种自由度而生。
2. 核心功能拆解与实现
2.1 上下文档案(context file)
上下文档案是整个机制的心脏。它本质上是一个 Markdown 文件,里面记录了这个项目所有值得“记住”的信息。我没有发明任何新的格式,就是用最自然的方式写,AI 能直接读懂。
一个典型的 context file 长这样:
# 项目:用户中心服务 ## 技术栈 - Vite + Vue 3 + TypeScript - 后端接口为 REST,前缀 /api/v1 - 组件库使用 Element Plus ## 项目结构 - src/api -> 接口请求封装 - src/views/user -> 用户相关页面 - src/components/common -> 通用组件 ## 编码约定 - 所有用户列表接口,返回格式为 { list, total } - 请求封装统一用 request 函数,不要直接使用 axios - 路由命名使用小驼峰,页面组件使用大驼峰 ## 当前任务背景 - 正在迁移旧的用户名登录逻辑到手机号登录 - 后端接口联调已完成,前端需要补充错误码处理写出这样的档案之后,切换到该项目的 context 时,脚本会把这份内容注入到 AI 助手的系统提示词或者首条消息里。AI 会根据这些描述调整它的回答风格、命名约定和技术选型,整体上就像换了一个熟悉这个项目的“专家”。
有人会问,上下文档案应该写多详细?我的经验是:只写长时间内不会变的事实,不要写短期任务描述。技术栈、项目结构、接口规范,这些能保持几个月;而“正在修 bug”“准备上线”这类信息属于短时状态,不适合放在档案里,否则每次任务变化都要改档案,反而增加了维护成本。如果真的需要短期记忆,可以在切换时通过命令行参数临时追加。
2.2 快速切换命令
context-mode 的核心命令是cm(我给它起了一个简短的别名),用法大概是:
cm . # 切换到当前目录对应的上下文 cm <项目名> # 切换到指定项目 cm --list # 列出所有已保存的上下文 cm --edit <项目名> # 编辑指定项目的上下文档案切换动作做了什么事情呢?简单来说,它做了两步:第一步,读取当前终端所处的目录,反向匹配出项目名;第二步,把对应的上下文档案内容,写入一个临时的“活动上下文”文件里。这个活动上下文文件,就是给 AI 助手用的入口。
这么做的好处是,项目上下文变得像“环境变量”一样透明。无论你在哪个目录下打开终端,只要执行一次cm .,当前会话就绑定了正确的项目记忆。配合 shell 的自动切换钩子,甚至可以做到进入一个目录就自动切换上下文,彻底不需要手动敲命令。
这里有一个设计细节值得说明:我故意没有把所有历史上下文全部塞给 AI,而是只保留“活动上下文”一份。某个项目一个月没碰,它的档案还是原来的内容,但你切过去时,它会成为当前唯一的上下文。这样做的好处是,AI 不会被多个项目的混杂记忆干扰,每次对话都只面对一个项目的背景,上下文窗口也能省出更多空间给具体代码。
2.3 自动注入机制
有了档案和切换命令,还差最后一步:怎么把这些内容真正送到 AI 手里。自动注入的机制不难,但位置选错了,效果会差很多。
对于支持自定义系统提示词的 AI 编程助手,我会把活动上下文文件的内容追加到系统提示词的末尾。系统提示词是每轮对话都会保留的高优先级信息,放在这里最合适,AI 在回答任何问题时都会先看到这些背景。对于不支持系统提示词的助手,也可以把上下文内容放在首条消息里,效果接近,但可能会和后续的追问产生一定干扰。
我还做了一个更轻量的做法:在 shell 里定义一个预置变量,记录当前上下文文件路径。当需要手动贴给 AI 时,只需要执行:
cat "$CM_CONTEXT_FILE"然后把输出贴入对话。这样做虽然不够自动,但胜在兼容一切工具,用过的都知道,很多时候“能方便地复制”比“自动但偶尔出错”更重要。后来我把两种方式都保留了,默认走自动注入,遇到没法自动注入的工具时,就走手动粘贴,自由度很高。
3. 实操:Linux/macOS 下的完整实现
3.1 目录结构与初始化
我选择了完全基于 shell 脚本的实现,没有引入 Python 或 Node 依赖,这样在任何 Linux/macOS 终端都能直接跑。整个 context-mode 的目录结构大概是这样:
~/.context-mode/ ├── contexts/ # 存放所有项目的上下文档案 │ ├── user-center.md │ ├── blog-frontend.md │ └── admin-system.md ├── active_context.md # 当前活动上下文(软链接指向实际档案) └── cm.sh # 主脚本简单解释一下:contexts目录是用来存储所有项目档案的地方,active_context.md则是一个软链接,永远指向当前激活的那个档案。这样设计的话,AI 助手只需要固定读取active_context.md,不管用户切换了哪个项目,它读到的都是最新内容,不用修改 AI 工具端的配置。
初始化也很简单,在~/.bashrc或~/.zshrc里加几行:
export CM_HOME="$HOME/.context-mode" export CM_CONTEXT_FILE="$CM_HOME/active_context.md" alias cm="$HOME/.context-mode/cm.sh"然后执行source ~/.bashrc。第一次用的时候,先创建目录结构,再新建一个项目档案即可。
3.2 核心脚本代码
下面是我实际在用的cm.sh,做了简化,去掉了一些花哨的交互,保留最核心的功能。
#!/usr/bin/env bash # context-mode - switch project context for AI assistants CM_HOME="${CM_HOME:-$HOME/.context-mode}" CONTEXTS_DIR="$CM_HOME/contexts" ACTIVE_LINK="$CM_HOME/active_context.md" # 确保目录存在 mkdir -p "$CONTEXTS_DIR" # 从路径中提取项目名:最后一级目录名 project_name() { basename "$(cd "$1" && pwd)" } # 列出所有上下文 list_contexts() { echo "Available contexts:" for file in "$CONTEXTS_DIR"/*.md; do name=$(basename "$file" .md) if [ "$(readlink "$ACTIVE_LINK")" = "$file" ]; then echo " * $name (active)" else echo " - $name" fi done } # 切换到指定项目 switch_context() { local target="$1" local ctx_file="$CONTEXTS_DIR/$target.md" if [ ! -f "$ctx_file" ]; then echo "Context '$target' not found." echo "Create it with: cm --edit $target" return 1 fi ln -sf "$ctx_file" "$ACTIVE_LINK" echo "Switched context to '$target'." echo "Active context: $ctx_file" } # 编辑(或新建)上下文档案 edit_context() { local target="$1" local ctx_file="$CONTEXTS_DIR/$target.md" if [ ! -f "$ctx_file" ]; then echo "# $target" > "$ctx_file" echo "Context '$target' created." fi ${EDITOR:-vim} "$ctx_file" ln -sf "$ctx_file" "$ACTIVE_LINK" } case "$1" in --list) list_contexts ;; --edit) edit_context "$2" ;; "") switch_context "$(project_name "$PWD")" ;; *) switch_context "$1" ;; esac这个脚本的核心逻辑就两条:ln -sf更新软链接;cat "$CM_CONTEXT_FILE"读取活动档案。其他都只是辅助。你可能会说,这也太简单了?对,这就是我刻意想要的。越简单的机制,越不容易出错,也越好维护。
我来解释一下几个关键选择:
- 用软链接而不是复制文件,是为了让
$CM_CONTEXT_FILE始终保持对当前档案的引用。如果复制,就需要在每次切换时同步两份文件,多了一步就多了一个出错机会。 - 项目名直接取当前目录的 basename,所以一个项目最好都放在同一个目录下。如果你希望一个项目支持多个不同的上下文(比如“开发模式”和“代码审查模式”),可以允许
.md前再加后缀,但基础版本够用了。 - 使用
${EDITOR:-vim},这样如果你平时用 VS Code,可以设EDITOR="code --wait",编辑档案时就会打开 VS Code,保存完才返回终端。
3.3 与 AI 编程助手的集成
脚本本身只是把上下文档案切来切去,真正发挥威力的是把它接到 AI 助手的读取链路上。
如果你用的是一个支持自定义系统提示词的 AI 助手,可以在它的设置里加一条:
请先阅读我的项目上下文,路径为 ~/.context-mode/active_context.md 根据上下文内容了解项目背景,然后回答我的问题。这样每次打开会话,AI 会自动读取当前档案。如果你用的编程助手不支持从文件读取,那也简单,我一般会在会话开头手动执行:
cm .然后说:“我的项目上下文如下:”再把cat "$CM_CONTEXT_FILE"的输出贴进去,下面开始提问。虽然多了一步粘贴,但效果和自动注入是等价的。
另外一个我常用的玩法是:结合 shell 钩子实现目录自动切换。在zsh里,可以加一个chpwd钩子:
chpwd() { if [ "$PWD" != "$HOME" ]; then "$HOME/.context-mode/cm.sh" . > /dev/null 2>&1 fi }这样我只要cd进入项目目录,上下文档案就会自动切到对应项目,根本不用主动执行命令。有一段时间我甚至忘了它的存在,只感觉到 AI 助手在哪个项目里都很“懂我”,这就是好的工具该有的体验。
4. 使用中的常见问题与避坑
4.1 上下文过期与更新
用了几个星期之后,我发现最大的问题其实是自己偷懒,不更新上下文档案。比如项目已经换了新的接口前缀,但档案里还写着旧地址,AI 就会一本正经地把错误信息告诉你。这不怪 AI,责任在我。
所以我现在养成了一个习惯:每当项目发生结构性变化,比如新增了目录、改变了请求封装方式、引入了新的依赖,就会顺手跑一下cm --edit <项目名>把对应内容改掉。频率大概一两周一次,不会占用太多时间,但可以确保档案始终站在“当前版本”上。
如果你觉得纯手动更新太容易忘,可以在档案的头部加一个last_updated字段,然后用 cron 定期提醒自己检查。不过说实话,对于个人项目,保持轻量手动维护就够了,没必要做过度设计。
4.2 多项目交叉
还有一种情况是,两个项目本身就有依赖关系,比如一个前端项目连着两个后端项目。这时候如果只用一个上下文档案,AI 往往会搞混“当前在改哪个服务”。我的建议是:档案只描述当前仓库本身,不要试图把所有关联项目都写进一个档案里。需要联调时,把另一个项目的接口文档或者关键的代码摘出来,在提问时临时补充,这样比全部塞进档案要可靠得多。
还有一个多项目交叉的坑就是同名目录。如果你的两个项目都叫web,它们就没办法用 basename 来区分了。我后来给 context-mode 加了一个环境变量覆盖机制,在项目目录下不放配置文件,但允许你在 shell 里手动指定:
export CM_PROJECT_NAME="user-center-web" cm这个小改动解决了 90% 的同名目录冲突。剩下 10% 的情况,比如两个项目同名且你会频繁切换,那就建议给其中一个改目录名,长痛不如短痛。
4.3 隐私与安全
上下文档案里可能会写一些不便于公开的信息,比如内部服务的地址、数据库表名、API 密钥提示等。如果你用 git 管理~/.context-mode,一定要记得这个目录里存的可能就是敏感信息。我的做法是,把contexts目录加入.gitignore,只把脚本本身提交到仓库。如果确实需要多人共享上下文档案,可以考虑单独建一个私有仓库,并且不要在档案里写明文密码,只写“从环境变量读取”之类的说明。
另外,当你把一个公开的上下文档案发给别人的时候,要留意里面有没有包含你得当前路径、用户名等环境信息。context-mode 本身不抓取环境变量,但你在写档案时可能会无意识地把个人路径写进去。这个靠自觉,养成检查习惯就好。
5. 从 context-mode 到团队协作的扩展思路
context-mode 现在只是个人工具,但它的基本思想完全可以扩展到团队协作。我们曾经在团队内部讨论过这个方案,发现只要统一约定上下文档案的格式,就能让不同成员对同一个项目的 AI 助手保持一致的“背景理解”。
做法并不复杂:在项目仓库里建一个.context/目录,放一份经过脱敏的project.md,内容就是公共的项目背景。团队成员各自本地维护自己的~/.context-mode/contexts,把仓库里的project.md复制过来,或者索性在切换脚本里加一条“优先读取项目内 .context/project.md,不存在再读本地档案”的逻辑。
这样的好处是,新成员加入时,只需要导入仓库里的背景档案,AI 助手就能立刻按照团队规范来辅助编码,而不需要每个新人都通过口口相传去了解项目结构。当然,项目内档案需要更谨慎地控制信息粒度,和代码强相关的结构描述可以写,但涉及安全的内容就不要放了。
如果你想走得更远,还可以把 context-mode 的上下文档案与代码搜索工具结合。比如在切换上下文时,同时用ctags或者rg生成一份当前项目关键符号清单,追加到档案末尾。不过我实测下来,这个功能对小型项目没什么必要,对大型项目倒是有些帮助。关键符号清单可以让 AI 在回答问题时快速定位到具体模块,减少它“瞎猜文件名”的次数。
最后的最后,我个人的体会是:context-mode 这类工具的价值,不在于脚本本身写得多优雅,而在于它逼着你去认真思考“AI 要了解哪些关于我这个项目的事实”才能给出好答案。哪怕你不写一行脚本,只是养成了把项目背景存成纯文本、每次切换时主动修改的习惯,AI 辅助开发的质量也会肉眼可见地提升。先把上下文档案维护好,再谈用什么工具去注入,这才是这个标题背后真正的核心。