1. "context-mode" 到底在解决什么问题:先讲一个我连续犯了两周的错
“context-mode”这个词,我第一次认真对待它,不是在看某本系统设计书,而是因为自己连续两周在终端里“串台”。
当时我同时维护三个项目:一个是公司的生产微服务,一个是我自己的开源库,还有一个是客户现场排障环境。三套环境各自有自己的kubectl集群配置、不同的.env文件、不同版本的依赖。结果是什么呢?我在开源项目目录里激活了公司的虚拟环境,在自己的账号下跑出了生产环境的数据库连接串,还差点把一条测试配置apply到一个不该碰的集群上。那一周我删了两天日志,跟客户道了三次歉。
问题的本质不是“我不小心”,而是我的工具没有把上下文当作一个需要显式切换的对象。终端只是把环境变量、配置文件、历史命令、当前目录全部混在一起,我靠肉眼和记忆去区分它们。人的工作记忆本来就有限,一旦同时打开十几个标签页、好几个窗口,出错只是时间问题。
context-mode 的核心思路其实特别朴素:把“我现在在哪个项目 / 哪套环境 / 哪个集群 / 哪个分支 / 哪种推理模式下工作”这一整组状态,做成一个可以显式进入、显式退出的胶囊。进入一个 context,就是同时切换一组关联配置;退出一个 context,就是把环境恢复到进入之前的状态。你不再一个个手动记着“这里该用哪个变量、那里该切哪个 cluster”,而是直接说“我要去生产环境工作”,系统帮你把该带的东西全部带上,不该带的东西全部隔离掉。
这个设计在不同工具里有不同名字:有的叫 context,有的叫 profile,有的叫 workspace,有的干脆叫 mode。但背后的思路是同一个:上下文是一种资源,必须显式管理。
1.1 同一个思想,三种叫法,其实都是同一件事
我整理了一下自己日常用的工具,发现所有做得好的工具,几乎都实现了某种 context-mode,只是叫法不同:
| 工具/场景 | 叫法 | 切换的内容 | 隔离边界 |
|---|---|---|---|
| kubectl | context | 集群、用户、命名空间 | 一组 kubeconfig 配置 |
| git worktree | worktree | 工作目录、分支、index | 独立目录 + 独立 checkout |
| tmux | session | 窗口布局、shell 环境 | 独立进程组 |
| Python | context manager | 资源状态、全局变量 | with块内的执行范围 |
| 大模型应用 | system prompt / context | 角色、任务背景、提供的事实 | 模型接收到的 token 集合 |
你发现没有,不管哪个领域,context-mode 都在做三件事:定义状态边界、提供进入机制、提供退出机制。kubectl 通过--context参数指定进入哪个组合;git worktree 通过目录空间隔离不同分支;Python 通过with语句确保退出时自动恢复。
理解了这一点,你会突然看明白很多设计:为什么很多 CLI 工具要加一个--profile参数,为什么 IDE 里要搞 workspace,为什么 AI 应用里大家都在强调“system prompt 要结构化”。本质上都是在管理上下文这个资源。
1.2 人脑的“工作记忆”就是一块很贵的缓冲区
我一直觉得,context-mode 对人和对机器同样重要。人的工作记忆大概只能同时装下三到五个概念组块,你要是把六个项目的细节全堆在脑子里,任何一个新信息进来,都可能挤掉更重要的东西。
我用一个比喻来理解这件事:人脑的上下文就像一台内存很小的机器。你以为你开了很多后台进程还都能秒切,但真正做决定的时候,只有最前面那几条数据是有效。所以各种 context-mode 工具,本质上是在帮你做上下文换页:你要处理哪个项目,就把哪一页换进内存;换页的代价由工具承担,而不是靠你硬记。
这也是为什么我认为 context-mode 不是“高级功能”,而是基本功。
2. 命令行世界里的 context-mode:kubectl、git worktree、tmux 还有 Python 里的典型玩法
既然思路清楚了,我就把自己平时用到的 command-line 里的 context-mode 实践一条条列出来。这部分偏实战,你可以直接照着配置。
2.1 kubectl 的 context:教科书级别的实现
kubectl 是我见过的把 context-mode 做得最标准的命令行工具。它的 context 不是一个简单的“某个集群”,而是(集群,用户,命名空间)的三元组。也就是说,切换一个 context,等于一次性告诉 kubectl:连哪个集群、用哪个账号、默认看哪个 namespace。
查看当前可用的 context:
kubectl config get-contexts查看当前正在用的 context:
kubectl config current-context切换 context:
kubectl config use-context prod但是这里我要强调一个很多人容易踩的误区:use-context是修改全局配置,它影响的是你本机所有新开的终端窗口。如果你在 A 终端切到 prod,B 终端里再敲 kubectl 也会变成 prod。更安全的做法是一次性命令指定 context,不改全局状态:
kubectl --context=prod get pods -n order-service这个用法我后来养成了习惯:能临时指定的,绝不去改全局。因为全局 context 一旦固定在某个危险环境里,下一个忘记检查的人就会出事。
如果项目切换非常频繁,我还会在.bashrc里加一个极简函数:
kctx() { kubectl config use-context "$1" export KUBE_PS1_CONTEXT=true export KUBE_PS1_ENABLED=on }配合kube-ps1之类的提示工具,把当前 context 直接显示在命令行提示符上。你永远不会在终端里问自己“我现在到底在哪个环境”。
Kubectl 的 context-mode 价值在于:你把“集群+账号+命名空间”打包成了一个名称,不用分别去记三件事。但它的缺陷也很明显:它只管 kubectl 自己的“世界”,管不到你的数据库连接串、你的 SSH 跳板机、你的 Redis 地址。所以我后来才意识到,真正的 context-mode 不只是某个工具自带的切换,而是一整套跨工具的协调。
2.2 git worktree:用目录空间代替大脑切换分支
再说一个非常实用的 context 隔离:git worktree。
以前我在一个仓库里切分支,每次git checkout feature/a到git checkout feature/b,未提交的修改得先 stash,IDE 里的索引要刷新,编译缓存也变了。切换一次成本不小,切换到错分支的概率也高。
git worktree 的思路是:同一个仓库的多个分支,各自有一个独立的工作目录。你在app-aaa目录里是 feature-a,在app-bbb目录里是 feature-b,两台“工作台”互不干扰。
git worktree add ../order-service-bugfix -b bugfix/order-001 main这样你就多了一个../order-service-bugfix目录,里面 checked out 的是bugfix/order-001分支。想看旧分支代码,不用把当前目录搞乱,直接开一个新 worktree 就行。
这其实是 context-mode 里特别典型的一种设计:用物理位置隔离物理状态。你人走进哪个目录,就是在哪个上下文里。目录本身就是你的上下文提示器。我现在基本上不再用git stash来搬运未提交改动,而是直接开一个 worktree,各自提交、各自测试,最后再 merge。对大脑来说,这是最舒服的。
2.3 tmux session:一个 session 就是一个完整的工作现场
tmux 对 context-mode 的抽象特别直观:一个 session 就是一套窗口、面板、环境变量和历史。我在做客户现场排障时,会专门开一个 session 叫customer-a,在里面起三个面板:一个连跳板机,一个看日志,一个写命令。这个 session 的所有状态都独立于其他工作,我离开一周再回来,tmux attach -t customer-a就直接回到当时的现场。
就算你需要临时切到别的工作,直接tmux detach,然后再tmux new -s personal起新 session。两个 session 之间不会互相污染 shell 变量,也不会在历史命令里混入上一个项目的密码。对我这种容易疲劳的人,这种“物理隔离”比贴便利贴有效得多。
2.4 Python 的 context manager:现代编程里的“进入/退出”原语
编程语言里,最接近 context-mode 的抽象就是 Python 的 context manager。with open(...) as f:这种写法,本质上就是在说:进入一个上下文,结束时不管有没有异常都退出。
from contextlib import contextmanager @contextmanager def connect(host: str): print(f"connect to {host}") try: yield finally: print(f"disconnect from {host}") with connect("prod-db"): run_migration()这段代码里,connect就相当于一个 context-mode 的开关:进入时建立连接,退出时清理资源。关键是finally保证即使中途抛异常,上下文也会退干净。
后来我在写命令行脚本时,也把这种思想搬到了 Bash 里。Bash 没有with,但可以用trap:
work_in_env() { local old_kube=$KUBECONFIG export KUBECONFIG="$HOME/.kube/customer-a.yaml" trap 'export KUBECONFIG="$old_kube"' EXIT # do things... }这个脚本一执行,就只在这个函数的生命周期内使用 customer-a 的 kubeconfig,函数结束自动恢复原来的配置。这就是最小可用的 context-mode。
3. 大模型应用里的 context-mode:难的不是存储,是注意力
前面讲的是传统命令行和编程里的 context,现在说说另一个我最近花了很多时间的场景:大模型应用。
很多人第一次接触“context-mode”这个说法,可能就是从 AI 产品里来的。比如某个对话系统支持“上下文模式”,或者某个 Agent 框架强调“把上下文传给模型”。这里面的坑比命令行世界深得多,因为 LLM 的 context 不是一个简单的状态文件,而是模型接收到的所有 token。你给它什么,它就从什么里面找答案。
3.1 token 预算不是越多越好
现在大模型动不动就支持 128k、200k 的上下文窗口,很多人的第一反应是:那我是不是可以把整个项目代码一股脑全塞进去?
我的经验是:别这么干。
上下文窗口只是“装得下”,不代表“效果好”。模型对输入不同位置的注意力并不均匀,中间部分长达几万字的内容经常会被“淹没”。我实测过多次,把你想要的结论放在一段很长的上下文正中间,模型经常找不到它;而你把它放在开头或结尾,结果明显更稳。所以有效上下文窗口其实远小于理论窗口。
更现实的问题是 token 费用和延迟。每次请求都要把整个 context 重新处理一遍,context 越长,响应越慢越贵。把 200k 的代码全塞进去,一次请求可能多花好几倍的钱,最后效果还更差。这就是典型的“伪 context-mode”:看起来把所有上下文都带上了,实际是把噪音也全带上了。
3.2 把上下文分层,而不是全量灌入
我在自己维护的一个 AI 辅助工具里,把输入给模型的上下文分成了四层:
| 层次 | 内容 | 典型体积 | 更新频率 |
|---|---|---|---|
| 固定上下文 | system prompt、角色定义、项目规则 | 很小 | 几乎不变 |
| 事实上下文 | 当前要用的 API 文档、数据结构、相关代码片段 | 中等 | 按照任务变化 |
| 过程上下文 | 最近操作记录、工具返回结果、用户的临时输入 | 小 | 持续追加 |
| 会话摘要 | 前面很多轮对话的浓缩结论 | 小 | 定期压缩 |
每次请求,我先把固定上下文放进去,再按当前任务挑选相关的事实上下文,最后加上最近的过程记录和摘要。绝不把整个项目的 1 万行代码一次性塞进去,而是只塞“当前文件 + 相关模块的接口摘要 + 测试用例”这些真正有用的东西。
这样模型拥有的不是“全部上下文”,而是“有效上下文”。它做判断时,注意力不会被无关代码稀释。对我来讲,这套分层办法带来的提升非常明显:回答准确率上来了,上下文费用下降了,响应速度也快了。
3.3 摘要到底该谁来写?什么时候写?
会话越来越长之后,总是要把历史对话压缩成摘要。这里有个特别容易踩的坑:摘要不是随便写一句话塞进去就行。
我一开始的做法是,每当历史超过限制,就调用一次模型“请把前面的关键信息总结一下”,然后把总结文本直接追加到对话里。结果模型会开始“模仿”总结口吻,回答变成列表式、概述式,完全不像一个执行任务的助手。
后来改成把摘要放到一个独立的上下文槽位,明确传入“这是对之前对话的压缩记忆,不是你当前回复的一部分”。换句话说,摘要要作为事实上下文,而不是作为对话历史继续。这样做之后,模型依然能引用摘要里的关键信息,但不会把摘要格式当成回复模板。
还有一个细节:摘要的时机。我在每次会话长度超过大约 1 万 token 之后才做压缩,太早压缩反而丢失细节。压缩时我会保留三个东西:用户的核心目标、已经确认的技术决策、还没解决的前置问题。至于中间那些来回讨论的过程,统统丢掉。
4. 想在项目里落地一套自己的 context-mode?直接抄这个设计框架
讲了那么多工具,最后还是要回到实践:如果你想在自己的项目、脚本、或者团队工作流里,落地一套 context-mode,该怎么做?
我自己在多个项目里反复迭代后,沉淀出一套五要素框架,任何项目都可以照着裁剪。
4.1 五要素:路由、存储、切换、可见性、审计
一个完整的 context-mode 设计,至少要有以下五个部分:
- 路由:你靠什么触发 context 切换?是手动命令、进入某个目录,还是识别到某个关键词?
- 存储:每个 context 的配置存在哪里?是独立的配置文件,还是环境变量,还是数据库记录?
- 切换:进入和退出 context 时,具体要执行哪些操作?必须先停旧再起新。
- 可见性:用户能否随时看到自己当前处于哪个 context?提示符、日志前缀、面板颜色都算。
- 审计:每次切换有没有记录?在哪个 context 下执行了什么关键操作?这是出事之后救命的线索。
缺了任何一个,这套机制都会在使用过程中慢慢变形。
4.2 一个可以抄的 Bash 实现:.contextrc+cs函数
我给自己的日常终端做过一个 30 行左右的 context switcher。你不一定直接用,但可以照这个思路改。
先写一个.contextrc,每行一个 context 配置:
# ~/.contextrc [work] workdir=~/projects/work-app kubeconfig=~/.kube/work-prod.yaml tmux_session=work-main prompt=work [oss] workdir=~/projects/my-lib kubeconfig= tmux_session=oss prompt=oss [customer-a] workdir=~/projects/cust-a kubeconfig=~/.kube/customer-a.yaml tmux_session=cust-a prompt=cust-a再写一个cs函数,放在.bashrc里:
cs() { case "$1" in use) local ctx="$2" # 读取配置 local wd kb ts wd=$(sed -n "/^\[$ctx\]/,/^\[/p" ~/.contextrc | awk -F= '/workdir/{print $2}' | head -n1) kb=$(sed -n "/^\[$ctx\]/,/^\[/p" ~/.contextrc | awk -F= '/kubeconfig/{print $2}' | head -n1) ts=$(sed -n "/^\[$ctx\]/,/^\[/p" ~/.contextrc | awk -F= '/tmux_session/{print $2}' | head -n1) # 进入 context export CU_CONTEXT="$ctx" export KUBECONFIG="$kb" cd "$wd" || return 1 [ -n "$ts" ] && tmux new-session -d -s "$ts" 2>/dev/null # 设置提示符 PS1="[$ctx] $PS1" echo ">>> switched to context: $ctx" ;; exit) unset CU_CONTEXT KUBECONFIG PS1=${PS1#\[$ctx\] } echo ">>> left context" ;; *) echo "usage: cs use <ctx> | cs exit" ;; esac }这段代码很糙,但思路是对的:用环境变量承载 context,而不是直接改一堆配置文件;进入 context 时把工作目录、Kubeconfig、tmux 会话一次配好;退出时把环境变量清掉。实际使用中,我还会加上把当前 context 写入一个临时文件,供其他脚本读取。
为什么用环境变量而不是直接改.kube/config?因为环境变量的作用域只在当前 shell,不会污染其他终端。改全局配置文件则会把副作用扩散到整个机器,这是 context-mode 里最忌讳的事。
4.3 默认要刁钻:拿不到明确 context 时,宁可拒绝执行
另一条重要经验是:默认配置应该保守,默认动作应该拒绝。
我见过很多事故,是因为脚本里写了“如果没有指定环境,就默认连测试环境”,结果测试环境和生产环境之间只隔了一个 if 判断的疏忽。更安全的做法是:如果脚本没有拿到明确的 context 上下文,直接退出并报错,而不是猜一个默认值。
from dataclasses import dataclass @dataclass(frozen=True) class Context: name: str kube_context: str | None env_file: str | None allowed_hosts: tuple[str, ...] def ensure_allowed(self, host: str) -> None: if host not in self.allowed_hosts: raise PermissionError( f"host {host} not allowed in context {self.name}" )这里的关键是allowed_hosts。在生产 context 里,只允许连已经登记过的主机;连错一台机器,直接抛异常。宁可让流程停住,也不能让它带着错误的上下文继续跑到最后。
5. 落地 context-mode 最容易踩的坑,我一个个都踩过
最后这部分,是我最想说的。因为设计一套 context-mode 不难,难的是使用过程中那些隐蔽的坑。我把踩过的六个比较典型的坑列出来,希望你能跳过。
5.1 坑一:只切了提示符,没切真实配置
我第一版 context switcher 做出来之后,提示符确实显示[prod]了,但脚本里忘了设置KUBECONFIG。结果是,界面显示我在生产环境,实际 kubectl 还是指向旧配置。这种“视觉上下文”和“真实上下文”不一致,比没有 context-mode 更危险。它让人产生了一种虚假的安全感。
从那以后,我给自己定了一条硬规则:提示符显示的内容,必须来自一个已经被环境变量确认过的值。也就是说,PS1里的 context 名称和KUBECONFIG必须同时被设置,否则提示符不做任何显示。
5.2 坑二:只记了进入,没记退出
很多 context 切换脚本只管进入,不管退出。比如一个函数里export KUBECONFIG=...之后,函数结束了,这个变量却留在了当前 shell。之后你在同一个终端里做别的事,所有命令都会带上旧的生产配置。
正确解法就是前面提到的trap,或者使用子 shell:
( export KUBECONFIG=~/.kube/customer-a.yaml kubectl get pods )括号把小 shell 和外面隔开,里面的环境变量不会泄漏出来。这比手动记着“用完再切回去”可靠得多。
5.3 坑三:在 context 切换里删改配置,而不是叠加环境变量
有人会为了“切换 context”,直接覆写.kube/config或.env文件。这个做法在单机单用户时也许能跑通,但它有一个致命问题:没法回滚。你把配置改成 A 之后,原来的配置已经没有了,想切回 B 就没法切。
环境变量是更好的叠加层。它不改动底层文件,只改变当前 shell 看到的配置结果,退出时天然恢复。我后来也慢慢把数据库连接、云服务凭证这些敏感信息都改成从环境变量读取,context 切换脚本只负责设置环境变量,不负责重写配置文件。这样切换成本最低,风险也最小。
5.4 坑四:上下文缓存没有失效机制
在 AI 应用里,这个坑特别明显:模型第一次读取了一个文件的内容,把它放进了上下文,但文件后来变了,模型还在用旧内容做判断。我在一个代码生成工具里就遇到过:我改了接口参数,模型却在生成的代码里继续用旧参数,因为它加载的上下文还是几轮之前的快照。
解决办法是给每个进入上下文的资源加一个mtime校验,或者干脆规定:文件只要发生变更,对应上下文就要重新加载。上下文缓存一定要有失效机制,不能只做一次性加载。
5.5 坑五:没有审计,出事之后只能靠猜
有一次我的脚本误删了某个环境的数据,第一反应是“是不是我切错了 context”。但翻日志发现,我的脚本根本没有记录当时用了哪个 kubeconfig、哪个环境变量。最后只能靠终端历史命令人工反推,折腾了很久。
之后我给所有会执行危险动作的脚本都加了审计输出,至少记录三样东西:当前 context 名称、执行命令的用户、目标资源标识。格式很简单,就是几行 JSON:
{ "time": "2025-06-20T10:00:00Z", "context": "prod", "user": "zhang", "action": "delete", "target": "jobs/payment-worker" }不需要复杂的日志系统,关键是出事时能回答一个问题:这个操作是在哪个上下文下发生的。
5.6 坑六:把摘要当成普通对话继续,而不是当作上下文
这一点在 AI 场景里说过一次,但我还想再强调:摘要进入模型的方式不同,结果完全不同。如果摘要只是追加在对话历史里,模型会“继承”摘要的语态和格式;如果摘要被当作独立的一条上下文记录传给模型,模型就知道“这是对我之前记忆的压缩,不需要模仿”。我在实践里会把摘要通过 system prompt 注入,并明确标注“memory”。
我在实际项目里折腾了快一年,才算把这些坑基本填平。现在每次换上新的环境,我都会先问自己一个问题:“这个环境切换,进入和退出是否对称?退出后能不能回到和之前完全一样的状态?”如果答案是否定的,那这个 context-mode 就是不完整的。
最后分享一个小技巧:无论做什么类型的 context-mode,都要让它的状态可见可查可回滚。不可见的 context 是陷阱,不可查的 context 是黑盒,不可回滚的 context 是定时炸弹。把这三点守住,你在任何项目里落地 context-mode,基本都不会翻车。