你有没有遇到过这种情况:AI编程助手用了半天后突然变傻——你让它改A函数的返回值,它却给你改了B函数,还振振有词地引用一段你已经明确废弃的需求?我最近就碰上了。折腾了两小时,最后发现问题不在模型,在于我压根没有管理“上下文模式”(context-mode)的能力。
所谓context-mode,就是AI在某一时刻读取什么、遵循什么、忽略什么的状态。我以前一直觉得上下文窗口是无限的、对话历史是“记忆”,后来才发现这是个天大的误会。这篇文章记录了我是如何把这种抽象状态变成一套显式可管理的模式,并顺手做了一个极简CLI工具来落地这套思路。如果你每天都要和AI助手打交道,尤其是写代码、改代码、评审代码的高频用户,这个方案值得你花十分钟看完。
1. 为什么AI编程助手越用越“傻”:上下文污染的根源
想解决context-mode问题,先得明白它为什么会失控。我最初以为是自己用的AI助手“不够聪明”,后来翻了一些底层机制的资料,才意识到问题出在我的使用方式上。
1.1 窗口不是硬盘:所有token都被平等对待
上下文窗口的本质是:模型每次推理时,会把窗口内的所有内容重新读一遍,然后基于这些内容生成下一个token。这里有两个关键特性:
第一,窗口有容量上限。超过上限的部分会被直接截断,最古老的内容在物理上就“消失”了。你可以把窗口想象成会议室桌面,所有文件摊在桌面上,新文件进来,旧文件就会被推到地上——地上的文件不是“暂时看不到”,是彻底不被读取。
第二,窗口内所有内容对模型来说没有“优先级”之分。它不会因为你凌晨1点发的那句“必须用async,不要用同步IO”就在回复时额外重视。所有token都平等地参与注意力计算,而注意力是有限的。
所以问题就来了:你和一个AI助手连续聊两小时,早期明确的需求会被中间大段讨论、调试输出、报错信息稀释。模型不是“忘了需求”,它只是在一大堆内容里找不出哪个才是真正的“当前最高优先级指令”。
1.2 跨任务串味:同一个会话承载了太多任务
我犯过的最大错误,是让一个会话同时承担多个任务。比如上午用它查了一个部署脚本的问题,中午让它帮忙重构用户模块,下午又让它给一段营销文案润色。每个任务单独看都完成得不错,但到了隔天继续时,问题开始出现。
最典型的表现是“串味”:我明明在聊重构需求,AI给出的方案却带着部署脚本里的假设;我让它在A模块里做改造,它却把B模块里已经讨论过的改动思路套了过来。原因不复杂——模型会认真阅读整个会话历史,它会觉得所有历史内容都和当前任务有关系,于是不同任务的细节混在一起,互相污染。
我后来想明白一个事实:看起来AI“记住了所有对话”,实际上它是把全部内容原封不动地重新读了一遍。它没有“删除键”,也没有“我明确告诉你这条作废”的能力。消息发出去就一直在那个窗口里,直到被截断。这就像你把一屋子人的会议纪要全部摊在桌上,然后问其中一个人“今天下午我们要干什么”——它只能尽力从纪要里猜。
1.3 指令优先级丢失:原始约束被中间讨论淹没
另一个隐蔽问题是位置偏置。模型对会话开头和结尾的内容会更敏感,中间段落的内容往往容易被弱化。这意味着:
- 你在会话开头交代的“本项目禁止使用requests库”——大概率中间会被淡化
- 你费了很大力气解释的业务规则,夹在一堆调试信息中间——后续很容易被忽略
- 你临时补的一句“等等,不是这个方案”——如果它在一个较长的讨论段之后,模型可能接不住
这解释了为什么AI助手“用久了就不听话”。不是你把它宠坏了,是它窗口里的历史越来越长、越来越乱,原始约束被挤到了注意力的边缘。
想明白这三点之后,我开始意识到:与其抱怨模型“记不住”,不如主动管理它“能看到什么”。这就是context-mode的核心理念——显式控制AI在某个时刻读取的信息范围和行为边界。
2. 设计思路:把上下文当作一门显式管理的资源
既然问题出在上下文组织方式,那解决方案就清楚了:把一次大而无边的连续对话,拆成若干个有明确边界、有明确模式的短会话。每个短会话就像一次干净的会议,只讨论一件事,并且开头就说清“我们现在处于什么阶段、要做什么、已经知道了什么”。
2.1 核心转变:从“无限对话”到“模式化会话”
传统使用方式是一个线程聊到底,agent自己决定往哪个方向走。我改成了一种更结构化的方式:每次任务开始前,先定义一个模式。模式是一份行为契约,告诉AI“你现在应该干什么、不应该干什么、用什么格式输出”。
为什么要这么做?因为AI在没有边界时会倾向于“泛泛而谈”。你让它“看看这段代码”,它可以给你讲十个方向;你让它“改成异步的”,它可能把文件里所有可疑的点全动了。模式把任务收敛住,相当于戴上手套干活,不让它四处乱摸。
在新流程里,每个阶段都对应独立的会话文件,阶段之间通过一份“交接单”传递必要信息。AI每次只看到一个阶段的对话历史,不会看到上一个阶段的大段讨论——那些不需要记住的东西,压根就不出现在窗口里。
2.2 四种典型模式:explore / plan / implement / review
我把软件开发的主链路收敛成四个模式,覆盖了日常绝大多数场景:
| 模式 | 适用场景 | 行为约束 | 期望输出 |
|---|---|---|---|
| explore | 理解代码库、查找定义、梳理调用链 | 只允许读代码和搜索,禁止修改文件 | 结论必须附文件路径和行号 |
| plan | 制定实现方案、拆解步骤、评估风险 | 不写完整代码,只做思路与方案 | 输出步骤清单、风险点、测试策略 |
| implement | 写代码、改代码、修Bug | 只修改当前任务相关的文件,不做无关重构 | 每次改动附diff摘要和原因 |
| review | 检查diff、找问题、提优化建议 | 不直接改代码,只做审查 | 输出问题清单,按严重程度排序 |
选择这四种而不是更多,是因为增加模式会带来切换成本。模式越多,你就越懒得切,最后又回到混着用的老路上。四种模式基本对应“先看懂、再想清楚、然后动手、最后验收”这条完整链路,足够覆盖绝大多数个人项目。
每个模式我都会在配置里写死行为约束,并在每次进入模式时完整贴给AI。比如explore模式下,我明确写“禁止修改任何文件,哪怕你觉得那里有Bug,也只能在结论里说明”。有了这条约束,AI“自作主张改代码”的频率大幅下降。
2.3 切换协议:上下文交接单
模式拆分之后,新的问题出现了:信息如何跨模式传递?我总不能每次切换模式就重新把所有背景讲一遍。
这就需要一个交接单(handoff note)。它的作用不是把上一阶段的讨论内容全量复制过去,而是提取出“下一阶段必须知道的东西”。我设计了一个固定模板,限制在200字以内:
**当前目标**:一句话,说清楚这次任务最终要达成的结果 **已完成事实**:最多3条,每条一行 **当前阻塞**:无,或具体的待解决问题 **下一步**:明确的下一个动作为什么严格限制200字?因为交接单的目的是给下一个模式一个干净起点,不是写工作总结。写多了,AI又要在一堆内容里找重点,等于又回到污染的老路。
切换模式的操作流程是这样的:模式A结束时,我先让AI按照模板输出交接摘要,确认无误后写入handoff.md;然后开一个新的会话文件,进入模式B,把交接单作为会话的第一条内容发给AI。所有多余讨论都留在旧的会话文件里,不进入新会话。
这套流程刚开始纯手工执行,跑了三周,效果很好。但它有一个明显缺点:步骤琐碎、容易忘。于是我做了一个小工具,把模式状态、交接单、预算检查这些事自动化。
3. context-mode 的落地实现:一个极简CLI工具
工具做出来不是为了炫技,是为了把流程固化。它的核心作用有三个:管理当前任务处于哪个模式、在模式切换时强制生成交接单、监测上下文预算并自动裁剪。我用Python 3.10加Typer写的,代码量不大,结构也简单,适合按自己习惯改造。
3.1 工程结构与初始化
安装依赖后,执行ctx init初始化一个项目:
pip install typer pyyaml tiktoken ctx init my-project初始化会在~/.context-mode/下建立目录结构:
~/.context-mode/my-project/ ├── tasks/ │ └── (task-id)/ │ ├── handoff.md # 当前交接单 │ ├── sessions/ │ │ ├── explore.md # 各模式的独立会话文件 │ │ ├── plan.md │ │ ├── implement.md │ │ └── review.md │ └── config.yaml # 项目配置每个任务一个目录,session文件按模式分开。这样做的好处是:任何一个模式的会话历史都不会干扰其他模式,因为AI每次读到的只是当前模式对应的那个文件。
config.yaml是最核心的配置文件,里面定义了模式的行为规则和窗口预算:
project: my-project model_window: 8192 current_task: "refactor-auth-flow" modes: explore: behavior: "只允许阅读代码、搜索信息,禁止修改任何文件" output: "结论须附文件路径与行号" plan: behavior: "不写完整代码,只输出方案和步骤" output: "步骤须带风险提示" implement: behavior: "只修改与当前任务相关的文件,不做无关重构" output: "每次改动附diff摘要" review: behavior: "逐文件审查,不直接改代码" output: "输出问题清单并按严重程度排序" budget: max_session_tokens: 2048 keep_messages: 83.2 模式状态机与切换命令
我实现了一个简单的状态机,核心切换命令是ctx switch:
import typer from pathlib import Path app = typer.Typer() @app.command() def switch(mode: str, task_id: str = typer.Option(..., "--task", "-t")): """切换当前任务的工作模式""" allowed = {"explore", "plan", "implement", "review"} if mode not in allowed: typer.echo(f"非法模式: {mode},可选: {', '.join(allowed)}") raise typer.Exit(code=1) task_dir = get_task_dir(task_id) cfg = load_config(task_dir) current = read_current_mode(task_dir) # 非首入模式,且模式发生变化时,强制生成交接单 if current and current != mode: summary = ask_model_summary(cfg["modes"][current]) write_handoff(task_dir, summary) typer.echo(f"[context-mode] 已从 {current} 生成交接单") write_current_mode(task_dir, mode) typer.echo(f"[context-mode] 已切换到 {mode} 模式")这里最关键的一行是:只要模式发生变化,就先调用一次模型总结当前模式的产出,生成交接单,然后才允许切换。哪怕你只是想从implement退回explore再确认一段逻辑,也必须先交代清楚你正在做什么、卡在哪。这个“强制动作”杜绝了“懒得写交接直接切”的偷懒行为。
我允许的回跳规则很简单:可以从任何模式回退到explore,但必须在交接单里写明“为什么要重新探索”。比如实现到一半发现接口定义和计划不一致,回到explore确认——这时候交接单的“当前阻塞”会写清楚具体是什么不一致,避免重新探索时把整个背景都忘光。
3.3 上下文预算:token估算与自动裁剪
管理上下文的核心是管理token。我引入tiktoken做估算,这样不用等AI提示“上下文超长”,自己就能提前知道当前会话文件占了多大空间。
import tiktoken enc = tiktoken.get_encoding("cl100k_base") def estimate_tokens(text: str) -> int: return len(enc.encode(text)) def check_budget(session_file: str, limit: int = 2048): used = estimate_tokens(open(session_file, encoding="utf-8").read()) if used > limit: print(f"警告: 当前会话已使用约 {used} tokens, 超过预算 {limit}") print("请执行 ctx trim 进行裁剪,或切换到新的会话文件") else: print(f"当前会话约 {used} tokens, 预算 {limit}, 剩余约 {limit - used}")预算规则是:会话文件里的内容,最多占模型窗口的四分之一。以8k窗口为例,我把单个会话文件限制在2048 tokens以内,剩下的空间留给当前对话的原文、AI回复以及新增讨论。实测下来,这个比例能保证AI始终有足够的“余量”处理新的对话,不会因为历史太长而失去对最近输入的重心。
当预算超限时,我会执行ctx trim。它的裁剪逻辑设计过两版,最终采用的是“保护三段+压缩中间”的结构:
- 保留文件顶部的模式声明区和硬性约束区
- 保留最近keep_messages条消息(默认8条),保证最近上下文连续
- 把中间的历史消息压缩成一段摘要,放在约束区之后
- 旧消息不删除,移动到
.archive/目录备查
用一句话概括:顶部约束必须完整,最近对话必须保留,中间过程只留摘要。这比单纯“保留最近N条”可靠得多,因为它同时保住了指令的最高优先级和对话的连续性。
3.4 与AI助手对接的模式声明模板
有了工具之后,真正参与对话的其实是一个模式声明块。每次开新会话,我要做的第一件事就是把当前模式的声明粘贴给AI。这个声明块是手工拼出来的,内容来自config.yaml、handoff.md和任务文件列表:
[mode: implement] 项目: my-project 当前任务: refactor-auth-flow 任务目标: 把认证模块从同步IO改为异步IO,保持对外接口不变。 已完成事实: - 已梳理 auth.py 中所有用到阻塞IO的位置(共9处) - 已确认 token 刷新逻辑无需改动 当前阻塞: 无 下一步: 按 plan.md 第3步改造 session_store 模块 硬性约束: - 禁止引入新的第三方依赖 - 所有改动必须保持向后兼容 - 不要使用 requests 库,统一用 aiohttp这个声明块让AI在一开始就拿到所有必要信息,并且明确当前模式的边界。实测中有一个明显感受:贴上声明块之后,AI的第一次回答就基本贴合需求,不用再靠后续两三轮对话来“校准目标”。而在我没有声明块的时候,前几轮输出经常是泛泛的方案枚举,完全偏的也有。
配合声明块的收尾动作是:当前模式结束时,让AI按交接单模板输出摘要。我一般把模板直接贴在对话末尾:
请按照以下格式输出交接摘要,不超过200字: **当前目标**:... **已完成事实**:... **当前阻塞**:... **下一步**:...然后我把这段内容黏贴到handoff.md,等着下次切换模式时提取使用。这个“声明块开始、交接单结束”的闭环,是整个context-mode能跑起来的关键。
4. 实测效果与踩坑记录:数据不会说谎
工具写了三周,跑了两个项目,前后对比非常明显。先说结论:对话轮次少了,出错的次数少了,最明显的感受是“AI不用我反复重申需求了”。
4.1 没有context-mode时的翻车现场
没有这套机制之前,我记录过两次典型翻车:
第一次是重构用户登录模块。当时同一个会话里还残留着几天前讨论部署脚本的内容。我明明已确认要保留旧的令牌刷新逻辑,但AI在后续改动里把这个逻辑删掉了,原因是在早期对话里有人提过“简化令牌刷新流程”——那是我准备放弃的一个想法,但模型无法识别哪条消息是最终决定。
第二次是修改一个函数。AI给了我一个看起来合理的修改方案,里面却引用了另一个模块中已经被废弃的接口,因为那次对话里我们花了很长篇幅讨论过那个废弃接口的迁移方案。它误以为那是当前任务的一部分。
这两次翻车都不是“模型笨”,而是历史消息里的噪声最终以更高的“上下文权重”压过了真正的需求。缺少显式管理模式时,AI无法自动判断哪些内容已经作废、哪些仍在生效。
4.2 引入context-mode后的变化
引入context-mode后的数据,我是按周统计的:
| 指标 | 改造前(无限混合对话) | 改造后(context-mode) |
|---|---|---|
| 单任务完成所需对话轮次 | 18-25轮 | 8-12轮 |
| 需要重复说明需求的次数 | 每2-3轮一次 | 几乎为0 |
| 改错文件或改了不该改的代码 | 每周4次左右 | 偶尔1次 |
| 上下文超长被迫截断 | 每周2-3次 | 0次 |
样本不大,但趋势非常明显。最值得留意的是“重复说明需求”这一项。以前我经常说“不是这个意思,我再说一遍”,现在开头的声明块里已经写明了目标,AI第一轮就直奔主题,这类对话基本消失了。
4.3 踩坑一:交接单变成了“小作文”
第一版交接单设计时,我恨不得把所有讨论过的细节都写进去,生怕遗漏。结果交接单动辄500字以上,AI在新会话里反而抓不住重点,开始纠结一些已经不重要的过程细节。
后来我把交接单砍到四段:目标一句话、已完成事实三条、阻塞一条、下一步一条。强迫自己用最精简的语言表达。这里有个技巧:写“已完成事实”时,不要写过程,只写结论。比如“排查了session_store的三个候选方案,选定方案B”是对的;而“先测试了方案A发现连接池不够,又看了方案C发现依赖太重,最后觉得B比较稳妥”这种过程,直接删掉。AI需要的是“选定了B”,不是你的排除过程。
4.4 踩坑二:裁掉的是最不该裁的约束
trim功能刚上线时,裁减逻辑是“保留最近N条消息+压缩中间”。用了几天后发现一个严重问题:会话早期的硬性约束被压缩进摘要后,AI对约束的执行力度明显下降。比如我在第一次对话时明确说“不要用requests库,统一用aiohttp”,但这条约束处于会话中段,trim之后摘要只提到“网络请求需要使用特定库”,具体约束对象丢了,AI又开始写requests。
修复方案是双重保险:第一,在模式声明块里设一个固定的约束区,写死在config.yaml的modes配置里,任何trim操作都不会碰它;第二,给会话中需要长期生效的消息手动加[keep]标记,trim时遇到标记直接跳过。我写死了两条规则:[keep]标记表示“这条消息永远保留”,[mode]标记表示“这段是模式声明,优先级最高”。
这里建议读者注意:所有裁剪逻辑都必须有“不可裁剪区”的概念。没有保护机制的自动裁剪,本质上只是换了一种方式制造新污染。
5. 把context-mode嵌入日常开发流程
工具和流程都有了,最后一步是让它变成日常习惯。我目前的使用模式是下面这样,供你参考。
5.1 任务启动:固定句式写任务卡片
每天开工时,我不会直接打开AI开聊,而是先写一个任务卡片,相当于给整个任务定基调。格式是固定的几行:
任务: refactor-auth-flow 目标: 认证模块从同步IO改异步,对外接口不变 相关文件: auth.py, session_store.py, token_refresh.py 验收标准: 所有接口测试通过,无新增依赖这个卡片会写进任务目录下的task.md,同时作为explore模式的第一条消息发给AI。它和交接单的差别是:任务卡片在第一个模式之前就确定了整体方向,交接单则用于模式之间的信息传递。一始一终,闭环才完整。
5.2 与Git分支联动
我把context-mode和Git分支做了绑定。每个功能分支对应一个任务ID,切分支时自动切换上下文空间。实现方式是在.git/hooks/post-checkout里加一行:
ctx switch explore --task $(git branch --show-current)这样每次切换分支,我都自动坠入对应任务的explore模式,当前任务和大盘任务互不干扰。这个小联动特别适合“手头同时有几个任务在并行”的情况,不需要每次手动指定任务ID。
5.3 多项目并行时的隔离策略
多项目隔离的原则很简单:项目目录是不同项目会话的硬性边界。我在init项目时会把项目根目录与一个任务ID绑定,ctx命令在读取上下文时只读当前项目目录下的会话文件。这样就不会出现“上周做别的项目时讨论过的技术选型,这周突然被AI引用”的情况。
更重要的是,不要在项目A的会话里让AI帮你思考项目B的问题,即使只是随口一句话。这句话会留在项目A的会话文件里,污染项目A的上下文。
5.4 单人使用和团队协作的差别
单人使用时,交接单只需要满足“明天的AI能看懂”这个最低标准,可以写得很快。但团队协作时,交接单还承担知识传递的功能——它会成为其他人理解你工作进度的重要入口。我用团队方式跑过一次小项目,把交接单放在任务目录里共享给同事后,大家可以直接从交接单开始提问,不需要从头看会话历史。
团队版本我在模板里加了一行决策记录:这个任务中哪些决定是被明确推翻过的。它用来防止“这个方案明明讨论过不行,怎么又出现了”的经典问题。
最后再分享一个小技巧:不要一上来就追求完全自动化。我第一周是纯手工维护目录和交接单的,跑顺之后才写CLI工具去固化流程。如果你一上来就搭一堆自动化,很容易被复杂度和不断增加的配置项劝退。先把“模式拆分+交接单”这两件核心事坚持一周,感受到上下文不再混乱之后,自然会理解工具该做成什么样。