最近我在用AI编码代理做重构时碰上一个非常典型的问题:前半个小时它还思路清晰,代码改得又快又准,半小时后它突然开始重复问我已经回答过的问题,甚至把我刚删掉的函数又加了回来。盯着满屏的报错我意识到,不是模型变笨了,而是它脑子里的“上下文”已经成了一锅粥。Context Mode这个概念,正是为了解决这件事出现的。
我见过太多人把AI编码代理当普通聊天框用,认为上下文窗口越大越好,结果几千行代码和几十轮对话一起塞进去,模型的注意力被稀释得七零八落。Context Mode 不是某个神秘开关,而是一套“让AI知道该看什么、不该看什么”的工程实践。它会直接影响你每天写代码的速度、Token消耗和最终代码质量。这篇文章把我实际踩过的坑、设计过的方案和调试记录整理出来,希望能帮你少走弯路。
1. 上下文管理为什么成了AI编码代理的头号问题
1.1 单次问答与长期任务的本质差异
普通聊天机器人处理的是“一句话进来、一句话出去”的短交互,模型只需要关心最近几轮内容就够了。但AI编码代理的工作方式完全不同:它要在一个会话里连续完成需求理解、方案设计、文件修改、测试执行、报错修复等一系列操作,每一次动作都依赖前面所有状态。
举个例子,你让代理“把用户模块里的注册逻辑改成邮箱优先”,它会先定位相关文件,读懂现有实现,然后开始改。这个过程中,它需要同时记住的目标包括:你最初的需求是什么、已经改到哪一步了、哪些文件是核心、哪些文件只是参考、测试结果是否符合预期。这些信息放在一起,才叫“上下文”。
传统开发里,程序员靠自己的大脑、IDE的书签、Git的提交历史来维持这种状态。到了AI代理这里,所有这些都得通过Token的形式塞进模型窗口里。问题是窗口再大也有限度,一个中型项目动辄几百个文件、几十万行代码,根本不可能全部塞进去。于是上下文管理就成了一个必须面对的新问题。
1.2 上下文失控的典型症状
如果你连续用一个编码代理几个小时,出现下面几种情况,大概率就是上下文管理出了问题:
第一,记忆错乱。它把不同分支的逻辑搞混,或者引用了一个已经废弃的配置项。这是因为对话历史里保留了太多旧版本的信息,新信息没有足够的权重把旧信息顶掉。
第二,反复打转。它会重复问同一个问题,或者在你给出明确答复之后仍然按自己的理解去做事。这通常是因为关键决策记录在上下文里被淹没了,模型根本“看”不到它。
第三,改A伤B。修改一个文件时,它顺带改动了一个无关函数的行为,因为它无法准确判断哪些文件才是当前任务的作用范围。
第四,Token消耗急剧上升。上下文越长,每次请求的Token成本就越高。等到会话后半段,你每多说一句话,可能都要附带一万多个历史Token,其中大半已经过时。
这些症状背后有一个共同原因:我们默认把所有历史信息都当成同样重要的上下文,而没有做分层、筛选和压缩。Context Mode的核心,就是把这种“你全都要”式的信息处理方式,改成“按需加载,用完即走”的工程思维。
2. Context Mode的设计思路:给模型做信息降噪
2.1 持久层、工作层与瞬时层的拆分
我设计Context Mode时,最核心的思路是按照信息的生命周期,把上下文分成三个层级:持久层、工作层、瞬时层。
持久层是项目级的长期记忆,包括架构文档、模块职责说明、关键决策记录、命名规范、技术选型。这些信息在一个项目周期内基本不变,但它们决定了模型对整个代码库的基本认知。通常应该放在一个固定的项目说明文件里,比如ctx/arch.md,并在每次会话开始时注入。
工作层是当前任务相关的短期记忆,包括正在修改的文件、当前任务的目标和约束、已经完成和待办的事项。这部分信息是动态的,会随着任务推进不断更新。我会把它们单独放到ctx/current-task.md里,由代理在每次关键节点重写这个文件。
瞬时层就是普通的对话历史,包括用户最近几轮消息、代理最近几次操作和输出。瞬时层最容易被替代,也最容易无限膨胀。Context Mode的做法是给瞬时层设置一个滑动窗口窗口,只保留最近5到10轮对话,超出窗口的部分不再直接进入模型,而是折叠成一条极短的摘要。
这里有一个很自然的疑问:把历史折叠成摘要,模型不会丢掉细节吗?我的经验是,关键细节根本不需要留在历史里。如果一个信息足够重要,它应该出现在持久层或工作层。如果它只在某一次对话中有效,那丢掉了也不可惜。这就像团队开周会,上周的闲聊没人会复述,但重要的决策一定会写进会议纪要。
2.2 压缩与摘要的取舍原则
压缩不是简单的“用一句话概括整份文件”。无脑压缩会把代码细节和函数调用关系磨平,模型反而会编造接口。我在项目里用的取舍原则是:
文件路径和结构信息必须原样保留,一条都不能省。模型靠文件路径来理解代码的组织方式,路径删了,它就分不清哪个是入口、哪个是工具模块,出错率会明显上升。
关键代码片段不压缩。比如当前正在修改的核心函数、最近新增的公共方法、需要保持一致的接口签名,这些要按原始格式保留在上下文中,哪怕它们占不少Token也不能省。
“装饰性”内容尽量砍掉。包括大量的注释、模板代码、重复的配置块,在非当前任务相关文件中都可以用一行摘要代替,比如utils/date.ts:包含日期格式化与时区转换函数,共约200行。
对话历史里的代码块只保留最新版本。很多时候,用户和代理在一来一回之间反复修改同一段代码,历史里会留下七八个版本。上下文管理应该去重,只保留最终版本。
我还发现一个非常实用的技巧:在压缩时把“发生了什么”和“为什么这么做”分开。代码本身说明“发生了什么”,“为什么这么做”需要用自然语言补一条记录,这样模型在后续修改中才能保持设计意图,不会因为读过压缩后的纯代码摘要而误解思路。
2.3 系统提示词与“任务板”机制
很多人忽略了系统提示词在Context Mode中的位置。系统提示词不只是“你是AI助手”这类套话,它其实是一个稳定的头部上下文,在每次请求时都会出现。把它利用好,可以大大降低后续历史被裁剪带来的信息损失。
我在实践中会维护一个“任务板”(Task Board),放在系统提示词或上下文头部。任务板的内容很紧凑:当前目标一句话、完成状态列表、正在修改的三个文件路径、几条不能违反的硬约束。模型每一次生成代码前都会先看到这个任务板,哪怕它忘了之前某轮对话,也能靠这一小块信息迅速找回自己的位置。
你可以把任务板想象成驾驶台上的仪表盘。开车的人不需要记住一小时前每一个转弯,只要盯着仪表盘就能知道当前速度和油量。Context Mode里的任务板,就是给AI编码代理提供的实时仪表盘,让它在长任务中始终拥有一个“当前状态快照”。
3. 实操落地:为你的编码代理搭一套Context Mode
3.1 项目级上下文目录的设计
我这里把Context Mode当作一套可以上手配置的工程实践,并不绑定任何具体产品。实际落地时,我建议在仓库根目录下创建一个ctx/目录,用来存放所有上下文文件。目录结构大致是这个样子:
ctx/ arch.md # 架构说明,模块划分,技术选型 current-task.md # 当前任务板,动态更新 decisions.md # 关键决策记录 glossary.md # 术语表,领域概念统一口径 rules.md # 编码风格与硬约束这五个文件各有分工。arch.md适合长期存在,只在架构调整时更新;current-task.md是任务期间的“临时大脑”;decisions.md记录那些“我们为什么没选方案B”之类的历史决策。很多模型之所以在后期自相矛盾,就是因为在对话早期提到的决策没有被固化下来,后面被新内容覆盖了。
我会在项目启动时先写一版arch.md,把最核心的模块职责和依赖关系用三到五句话描述清楚。不用写成完整的研发文档,因为写到后面你会发现模型根本读不过来。关键是内容的优先级,不是内容的完整性。
3.2 定义上下文加载规则
光有文件还不够,你得让代理知道什么时候该读哪个文件,什么时候不读。我一般会在代理的配置里增加类似上下文选择器的规则,支持glob匹配和优先级设置。下面是简化版示例:
context_mode: workspace: src auto_load: - ctx/arch.md - ctx/rules.md - ctx/current-task.md include: - src/**/*.{ts,tsx} exclude: - src/generated/** - src/__tests__/fixtures/** priority: high: - ctx/current-task.md - src/features/auth/** low: - src/utils/legacy/**规则的核心逻辑是:默认只让模型看到它当前正在操作的目录和文件,而不是全仓库扫一遍。exclude尤其重要,像生成的代码、测试数据、临时文件这类内容,如果混入上下文,模型会分不清哪些是业务代码,甚至会在生产代码里引用测试文件里定义的mock数据。
不过要注意,配置不是设完就万事大吉。我通常会要求代理在每个任务开始时先执行一次“上下文准备”,让它明确说出自己当前要使用哪些文件、需要哪些额外参考信息。这一步看着繁琐,但能避免模型自作主张地加载一堆无关文件。
3.3 会话内上下文的滚动策略
Agent的对话历史是动态增长的,所以还需要一套滚动策略。我会在配置里规定一个最大消息数,比如保留最近12轮消息,超过的部分由代理自动生成一段不超过50字的摘要。
这里用一个伪代码来表示:
def build_context(history, task_board, files): # 头部:任务板 + 核心规则 blocks = [system_prompt, format_task_board(task_board)] # 文件区:按优先级加载相关文件 for priority_group in ordered_priority: blocks.extend(load_files(priority_group)) # 历史区:先放摘要,再放最近消息 if len(history) > MAX_HISTORY: blocks.append(summarize(history[:-MAX_HISTORY])) blocks.extend(history[-MAX_HISTORY:]) else: blocks.extend(history) return truncate_to_budget(blocks)这个函数的作用是在每次请求前,先组装上下文,再按Token预算截断。截断的时候不是从头砍,而是优先砍历史区,再看低优先级文件。任务板和系统提示词永远不砍,因为它们承担着“不要跑偏”的兜底责任。
实际执行时,每隔一段时间你就要提醒代理更新ctx/current-task.md。我会使用一个很简单的约定:完成一个子任务后,就调用一次代理内置的“更新任务板”工具,把已完成项、待办项和当前文件状态写进去。这样即使对话历史被压缩掉,任务板里的信息依然是新鲜的。
3.4 用快照脚本固化上下文状态
光在会话里管理上下文还不够,会话结束后也要留下痕迹。我习惯在每个任务开始和结束时跑一次上下文快照脚本,把当时的任务板、相关文件列表和关键决策保存为带时间戳的文件。这样下次再讨论同一个模块时,代理可以直接读取上一次的快照,而不是重新把整个代码库读一遍。
一个简单的bash脚本就能实现:
#!/usr/bin/env bash TS=$(date +%Y%m%d-%H%M%S) SNAPSHOT_DIR=".ctx/snapshots/$TS" mkdir -p "$SNAPSHOT_DIR" cp ctx/current-task.md "$SNAPSHOT_DIR/" cp ctx/decisions.md "$SNAPSHOT_DIR/" git diff --stat > "$SNAPSHOT_DIR/diff-stat.txt" echo "Context snapshot saved to $SNAPSHOT_DIR"这个脚本的价值在于:它让上下文管理从“内存”变成了“磁盘文件”。模型会话可以被反复重建,而项目状态不会被丢失。调试的时候尤其有用,如果上下文真的乱了,直接回滚到上一个快照,重新来一轮,比无穷无尽地“继续对话”高效得多。
4. 常见问题与排查技巧实录
4.1 代理总是忘掉你刚才的修改
遇到这种情况,我的第一反应是看current-task.md是否刷新了。很多代理不会自动记住你修改过的每一个文件,它只能记住上下文里面提到的内容。如果你在修改src/auth/login.ts之后,任务板里还写着“正在处理注册模块”,那它当然会跑偏。
排查路径很简单:先把任务板里“正在修改的文件”改成最新状态,再明确告诉代理“请重新读取src/auth/login.ts并更新你的上下文”。如果每次都要求你手动刷新,就在代理的规则里加一条:“每当用户提及文件路径或你修改过文件后,必须用最新的文件内容替换记忆中的旧版本,不要继续使用缓存信息”。
4.2 上下文越长,结果反而越蠢
这是一个很反直觉但经常发生的现象。上下文窗口从8K涨到128K,不是说你塞到120K也能跑得很好。模型的注意力是有限的,无关内容越多,它对关键信息的捕捉能力就越差。我在一次重构里塞了60多K代码和日志,结果模型连最基本的函数签名都开始臆造。
解决办法就是给上下文“瘦身”。优先删除那些你不希望模型看到的文件,比如测试夹具、构建产物、历史对话中的报错堆栈。报错堆栈这种信息,只在处理当前报错的那一轮有用,一旦解决,就应该立即从上下文里移除。可以用代理的“清除历史片段”功能,或者手动输入“忽略我们刚才讨论的XX文件,它已经无关紧要了”。
4.3 多个模块互相干扰
当你让一个代理同时处理前端、后端和数据库迁移脚本时,上下文里就会出现三个不同模块的内容互相污染的问题。典型表现是:模型在后端代码里使用了前端才有的变量命名,或者在SQL迁移里莫名其妙加了一段组件代码。
最有效的解法不是靠提示词让它“别搞混”,而是彻底做上下文隔离。一个会话只允许处理一个模块,或者使用支持子Agent的编码代理,让前端协作者和后端协作者各自维护独立的上下文。如果代理不支持子Agent,那就手动拆分任务:先集中处理前端,结束后再开新会话处理后端,不要在一个会话内反复横跳。
4.4 Token预算怎么算才心里有数
做Context Mode之前,我一直不看Token消耗,直到某个月账单吓我一跳。后来我养成一个习惯:把常用文件按Token量级分类测算。举例来说,一个200行的TypeScript文件大概1500到2500个Token,一个10K行的大模块全量加载大概消耗8000到12000个Token,而上下文窗口如果设为32K,那么最多容纳两三个大文件和一小段对话,就已经非常吃紧。
我建议你直接用Tokenizer脚本统计关键文件,把结果记录在快照里。我自己会定期跑一条命令:
python scripts/count_tokens.py ctx src 2>/dev/null输出对每个文件的Token估计。这样你在决定是否加载某个文件时,第一反应就是“它值多少Token”。如果一份文件不是当前任务必需的,哪怕只有50Token也尽量不加;如果一份文件是核心依赖,哪怕5000Token也值得留。
实际上,一个中型模块任务,我通常把预算控制在窗口的六成以内,留出三四成给模型思考和生成输出。一旦超过这个比例,就先做摘要、裁剪历史,再继续跑。别等上下文爆了才后悔。
4.5 其他避坑清单
还有几个容易被忽略的细节,我整理成了一张速查表:
| 问题 | 原因 | 处理方法 |
|---|---|---|
| 代理开始重复读同一个文件 | 上下文指令里没有去重 | 检查是否需要对该文件做缓存禁用,改为仓库级快照 |
| 代理突然用了不存在的配置项 | 旧版本的配置残留 | 在ctx/current-task.md里写明当前有效配置路径 |
| 代理把注释当需求 | 模板代码干扰 | 压低无关模板文件的加载优先级 |
| 代理生成大量重复代码 | 上下文里缺少“已实现功能”清单 | 在任务板上维护“已完成功能”列表 |
| Token消耗比自己预期高很多 | 忽略了每轮对话的隐式信息传递 | 观察请求日志,统计单轮有效Token增量 |
这些坑单独看起来都不难解决,但凑在一起就会让体验变得很糟糕。Context Mode的本质,就是给这些问题提供一套结构化的解决框架,而不是等问题出现后靠运气去哄模型。
5. 进阶:多代理协作与上下文隔离
5.1 按任务拆分代理,别让一个代理干所有事
当项目复杂度超过一定规模,单代理会越来越力不从心。我建议的做法是把任务按领域拆开,分别交给不同代理。比如代理A负责用户认证模块,代理B负责数据导入导出,代理C负责前端页面联调。每个代理拥有独立的会话和独立的工作目录,它们的核心上下文文件都放在各自对应的子目录里。
这样做有两个好处。第一是上下文串味的问题基本消失,因为代理A的窗口里根本不会出现代理B的代码;第二是性能提升,每个代理都能在自己小而精的上下文里保持高效,而不是在十万字的“全宇宙上下文”里挣扎。代价是需要一些协调工作,但对于一个相对复杂的迭代任务来说,值得。
实践中我会用Git分支做物理隔离,每个代理工作在自己的分支上,然后通过合并请求把改动集中起来。上下文管理也随之变成分支级别的管理,也就是说,ctx/current-task.md里的内容只描述当前分支的任务状态,不对全仓库负责。
5.2 代理之间如何共享关键上下文
多个代理之间需要共享一些公共背景,比如项目架构、规范、术语表。不要通过复制聊天记录的方式来共享,那样既浪费Token,还会让信息失真。正确做法是把公共上下文落到文件里,让每个代理在启动时统一读取同一份文件。
我通常的做法是维护一个ctx/shared/目录,里面放置所有代理都必须知道的公共信息。每个代理启动时会先加载共享文件,再加载自己的专属文件。如果某次架构调整导致共享信息过期,只需要更新一份文件,所有代理下次运行就能拿到新版本。在调试多代理系统时,这也是最省力的上下文同步方案。
5.3 未来的自适应上下文管理方向
随着模型能力提升,上下文管理的范式也在演进。我认为未来的Context Mode会从现在的“人工设计规则”走向“自适应管理”。比如模型可以根据当前任务的注意力分布,动态决定何时保留原始代码、何时切换成摘要;可以识别出用户到底在反复强调哪个需求,并把那个需求提升到任务板的更高优先级。
我现在会做一些简单的自动化尝试:用脚本监测代理的输出,如果检测到同一逻辑被重复实现了两次,就自动在任务板里加一条“勿重复实现”的提醒。这本质上就是把上下文管理从静态规则变成一个带反馈的闭环。虽然还很粗糙,但方向是对的。
对于大多数团队而言,现阶段先把前面讲的分层、快照、隔离做到位,体验提升已经非常明显。上下文管理不是一个能在某个配置项里一次设置完的东西,它更像一套保持清醒工作状态的方法论,需要持续维护。
6. 写在最后的一点经验
我在实际项目中使用这套Context Mode思路大概三个月,最明显的感受是:它让我和AI之间的关系从“反复沟通、不断重来”,变成了“给它一个清爽的起点,然后看着它高效推进”。问题很少出在模型不够聪明,大部分都出在它连“我们究竟做到哪一步了”都看不清。
最后分享一个我自己特别喜欢的小技巧:每次正式开工前,先不要急着让代理写代码,而是花两分钟让它“阅读ctx/current-task.md和ctx/arch.md,并用自己的话复述当前任务范围和已完成内容”。这一步听起来很傻,但它能逼着模型把上下文真正吸收进去,而不是在生成时临时乱翻。就这么一个小动作,能让后续的长任务稳定很多。如果你也经常被上下文问题折磨,强烈建议今天就在自己的项目里试一下。