你有没有遇到过这种情况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 switchimport 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(code1) 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, encodingutf-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工具去固化流程。如果你一上来就搭一堆自动化很容易被复杂度和不断增加的配置项劝退。先把“模式拆分交接单”这两件核心事坚持一周感受到上下文不再混乱之后自然会理解工具该做成什么样。