做 AI 编码代理的人,基本都会撞上同一堵墙:上下文。不管是基于 Claude、GPT 还是开源模型做 Code Agent,前期跑通 Demo 都很快,一旦进入真实项目——几千个文件、几十次迭代、多轮任务穿插——模型就开始“失忆”,改 A 文件忘了 B 文件的约束,明明项目里写好的规范就是不遵守,甚至把旧需求当新需求继续做。我最近半年几乎把所有时间都花在解决这个问题上,最后收敛到的答案,就是标题里这个 Context Mode。它不是一个模型,不是一个 Prompt 技巧,而是一套专门给编码代理设计的上下文管理范式——把“模型能看多少”从被动承受变成主动治理。这篇文章我会把它的设计思路、分层结构、参数调优和落地实现从头拆到尾,适合正在做 AI 编程工具、Agent 框架,或者被 Cursor / Claude Code 这类工具的长上下文问题折磨过的同学参考。
1. Context Mode 是什么:先从一个最现实的痛点说起
1.1 上下文窗口不是越大越好
很多人第一反应是:上下文不够,换大窗口模型不就行了?我一开始也这么想,128K、200K、1M token 的模型轮着试,结果发现窗口越大,问题越隐蔽。大窗口确实装得下更多内容,但模型对海量信息的注意力是稀疏的,中间段落经常被“漏读”。更麻烦的是,塞进去的内容如果相互矛盾,模型往往会采信离当前对话更近的那段,这就导致一种非常诡异的现象:项目文档明明写着“统一使用 pnpm”,你对话里早先说过一句“先凑合能用 npm”,模型后续真的就用 npm 了。
这不是模型笨,是信息架构的问题。把一万条信息平铺给模型,等于让它在噪音里找信号。Context Mode 要解决的,不是“能不能装下”,而是“该装什么、按什么顺序装、装不下的怎么处理”。
1.2 Context Mode 的定义与边界
我给 Context Mode 的定义是:一套显式的上下文治理机制,由代理在运行期间动态决定哪些信息进入模型视野、以什么形态进入、以及什么时候被清除或归档。它通常包含三个层次——工作内存(当前任务相关的短期信息)、项目记忆(代码库和文档的可检索索引)、长期档案(跨会话沉淀的决策与结论),外加一个路由模块负责在三层之间做切换。
注意区分两个概念:Context Mode 不是模型的 system prompt,也不是 RAG 的简单翻版。系统提示词是静态的规则,RAG 是“问一次查一次”的检索补充,而 Context Mode 是一整套状态管理——它知道当前任务做到哪一步、哪些文件被改过、哪些结论已经验证过、下一步最可能需要什么。它是编码代理的“工作台”,不是“图书馆”。
2. 为什么编码代理必须换一套上下文管理范式
2.1 会话问答与自主代理的本质差异
如果你只写过一个聊天机器人,可能感受不到上下文管理的压力。聊天是一次性的:用户问完,对话结束,历史记录只是背景资料。编码代理不一样,它要在一个长期任务里连续做决策:先读代码,再改文件,再跑测试,再根据报错修复,每一轮的输出都成为下一轮的输入,而且这个过程可能持续几十分钟,横跨十几个文件。
这就产生了一个会话模型没有的硬约束:状态一致性。聊天答错了可以重来,代理改坏了代码是要付代价的。代理必须记得自己刚才写了什么、为什么这么写、哪些约束是不能碰的。Session 型上下文管理根本承载不了这种需求,它只会让对话越滚越长,直到把模型“灌晕”。
2.2 我见过的五种典型失效现场
这些场景我在实际项目里都真实遇到过,列出来大家对照一下:
| 失效场景 | 现象描述 | 根因 |
|---|---|---|
| 需求漂移 | 代理改了 30 分钟后突然按照旧需求继续开发 | 早期对话被挤出窗口,只留下最新片段 |
| 约束冲突 | 项目规范要求 RESTful,代理却用 RPC 风格写接口 | 全局规范没有被检索到,局部示例占了上风 |
| 重复劳动 | 代理反复生成一个已经写好的工具函数 | 索引缺失,代理不知道代码里已存在实现 |
| 幻觉引用 | 代理引用了一个不存在的 API 或配置文件 | 长文本中位置编码衰减,模型记忆模糊 |
| 越改越乱 | 修复一个 bug 却破坏了另一个模块 | 上下文里有 A 模块信息,但缺失与 B 模块的关联 |
这五类问题的共同点,都不是模型能力不够,而是“该看到的信息没看到,不该看的倒是一直在”。Context Mode 的设计目标,就是把这五类问题一次性收敛掉。
2.3 四种管理范式的对比与取舍
目前市面上常见的做法大概有四类:全量注入、滑动窗口、分层摘要、Context Mode。前三种我都试过,各有各的死穴。
全量注入最简单,把整个仓库塞进上下文,适合文件总数少于 50 个的玩具项目;滑动窗口只保留最近 N 轮对话,实现成本低,但必然导致需求漂移;分层摘要(把历史对话递归压缩成摘要)比滑动窗口好一些,至少不会彻底失忆,但摘要会丢掉细节,比如具体的文件路径、精确的函数名。Context Mode 跟前三者的本质区别是:它把“哪些信息优先级最高”从隐式变成显式,并通过检索和路由,让代理始终看到的是“当前任务的信息快照”,而不是“对话历史的连续切片”。
3. 拆解 Context Mode 的分层结构与切换逻辑
3.1 工作内存:只保留当下要用的
工作内存对应的是“此刻模型视野里应该有什么”。它不是对话历史,而是一份结构化的任务快照。我在实现里把它设计成四块:
- 任务描述:当前目标的精炼表述,每次任务切换时重写,绝不自增。
- 相关文件内容:做过相关性排序后的文件,按重要程度截断,不是整文件硬塞。
- 变更记录:本次会话里已经改过的文件,以及每次改动的动机,用于防止越改越乱。
- 活动约束:当前必须遵守的规则,比如“不要动数据库迁移脚本”“测试必须通过”。
工作内存的容量一般控制在 8K 到 16K token 之间。超过这个量,与其硬塞,不如把信息降级到下一层。很多人在这一步舍不得截断,结果就是前 30 分钟好好的,后面整个上下文变成一个垃圾场。
3.2 项目记忆:让代理学会“翻代码”
项目记忆层解决的是“代理不知道代码库长什么样”的问题。核心是一个可检索索引,我通常用向量数据库存代码块的语义向量,同时保留一个倒排索引用于精确匹配文件名和符号名。为什么不用纯向量?因为函数名、变量名这种高精度信息,向量检索常常匹配不准,倒排索引的命中率反而更高。双路召回之后再合并排序,效果比单路好得多。
这一层的触发时机很关键:不是每一轮都检索,而是当代理需要了解某个模块、某个符号、或者判断某个改动的影响范围时,才触发检索。触发信号可以是显式的工具调用,也可以由路由模块根据当前任务状态预测。检索结果会写回工作内存,但只保留摘要,完整内容在需要用的时候再展开。
3.3 长期档案:把结论沉淀下来
跨会话的长期记忆是很多 Agent 框架做得最差的部分。大多数工具关闭会话之后,上下文就清了,下次打开又是从零开始。Context Mode 要求把每次会话的关键结论固化到长期档案里,内容至少包括:改动了哪些文件、为什么这么改、验证结果是什么、有哪些遗留问题、有哪些被否决的方案及否决原因。
这些内容平时不进入模型视野,只在三种情况下被调取:新任务涉及同一批文件、代理准备修改曾经改过的代码、或者路由模块发现当前任务与历史任务相似度很高。长期档案的存储格式我推荐用 Markdown 配合结构化字段,方便用文本检索,也方便模型直接阅读。
3.4 路由与切换:Context Mode 的灵魂
三层结构本身不稀奇,真正难的是路由逻辑——什么时候该把工作内存里的某段内容降级到项目记忆,什么时候该把长期档案里的结论提升到工作内存。我见过不少实现,把三层做成三个独立工具,让模型自己选。结果模型经常选错,该查索引的时候不查,不该查的时候猛查。
我的做法是把路由写成规则与预测混合:显式规则负责强约束(比如文件变更后必须刷新该文件在工作内存中的缓存),预测模型负责弱信号(比如根据任务描述预测下一步可能需要的模块)。切换动作本身必须是原子的——也就是说,切换期间模型不会读到半新半旧的状态,要么切完再生成,要么就不切。
4. 核心参数与调优:把 token 预算花在刀刃上
4.1 三层 token 预算怎么分
Context Mode 落地时最先要确定的,就是每一层的 token 预算。我的默认分配方案如下:
| 层级 | 预算占比 | 典型容量(按 128K 窗口) | 说明 |
|---|---|---|---|
| 工作内存 | 40% | 约 50K | 任务快照、相关文件、变更记录 |
| 项目记忆索引 | 10% | 约 12K | 当前轮检索召回的内容 |
| 长期档案摘要 | 5% | 约 6K | 跨会话结论的压缩呈现 |
| 系统指令与工具定义 | 10% | 约 12K | 规则、工具 schema、安全边界 |
| 预留缓冲 | 35% | 约 48K | 模型输出、测试报错、临时内容 |
这个比例不是拍脑袋定的。预留 35% 缓冲非常重要,因为测试命令的输出、编译报错、模型长输出经常会瞬间吃掉大量 token。预算不足导致模型输出被截断,是整个流程里最隐蔽的失败模式,报错信息后半段丢了,模型拿着半截信息去修复,越修越错。
4.2 检索参数:Top-K、相似度阈值、重排序
项目记忆的检索质量,比模型本身更影响最终效果。我常用的参数:向量 Top-K 取 20,倒排索引 Top-K 取 10,合并去重之后用重排序模型压到 5 到 8 条。相似度阈值设置在 0.65 到 0.75 之间,低于阈值的宁可不召回,避免噪音污染工作内存。
很多人会把 Top-K 调大,觉得多召回总比漏掉强。实测下来完全不是这么回事,召回越多,无关代码占用工作内存的比例越高,真正相关的代码反而被稀释。宁可漏,不可滥,这是检索型上下文管理的基本原则。漏了可以靠路由模块补一次针对性检索,滥了则会把模型注意力带偏,代价更大。
4.3 压缩策略:摘要不是简单截断
对超长文件、超长历史做压缩时,最忌讳的就是截断。截断会让信息在任意位置消失,模型甚至不知道自己错过了什么。我采用的是“结构化抽取 + 递归摘要”两步:先把文件里的关键信息抽出来——函数签名、导出符号、对外依赖、重要注释——形成一份紧凑的“代码卡片”;如果代码卡片仍然太长,再对它做分层摘要,按目录树自底向上逐层压缩。
这套策略对历史对话同样适用:每一轮对话结束时,把这一轮的关键决策抽成结构化记录,包括结论、依据、涉及文件、下一步行动。十几轮的对话压下来可能只剩 1K token,而关键信息一个都不会丢。
4.4 一组实测参考值
我在一个约 800 文件的中型后端项目上做过一组对比实验,基线是“全量注入最近对话 + 相关文件”,实验组是完整 Context Mode。同样 10 个开发任务,基线组完成 4 个,Context Mode 完成 8 个;基线组平均每任务消耗 86K token,Context Mode 是 41K。最明显的变化出现在跨文件修改任务:基线组经常把新逻辑写到错误的位置,而 Context Mode 能把“该文件依赖了哪些模块”正确地送进上下文。
5. 落地实现:搭一个最小可用的 Context Mode
5.1 存储层设计
整个系统可以拆成三块:存储、检索、路由。存储层我用一个简单的 JSON 结构作为工作内存的载体,长期档案则落到本地目录,每个项目一个文件夹。
{ "task": { "goal": "为订单模块增加取消接口", "status": "implementing", "constraints": ["不修改支付流程", "保持现有异常处理风格"] }, "files": [ { "path": "src/order/service.py", "relevance": 0.92, "mode": "full", "digest": "订单核心服务,包含 create_order 与 update_status" } ], "changes": [ { "path": "src/order/service.py", "action": "add cancel_order", "reason": "需求要求支持取消未支付订单", "status": "pending_test" } ], "archive_refs": ["2025-11-03-order-refactor.md"] }这里的 key 点在于:files 里每项都有 mode 字段,标记是 full(完整内容进上下文)还是 digest(仅摘要进上下文)。路由模块根据当前阶段动态调整,比如在实现阶段 service.py 是 full,在写测试阶段可能降级为 digest。
5.2 检索与路由实现
检索层我用了一个非常轻量的方案:代码文件先按函数和类切块,每个块生成两路索引——向量索引存语义,倒排索引存符号名。路由是一个纯函数,输入是当前任务状态和模型上一次的工具调用记录,输出是三个动作之一:keep(保持现状)、promote(把某条项目记忆提升到工作内存)、demote(把某条工作内存降级为摘要)。
def route(state: TaskState) -> list[RoutingAction]: actions = [] for f in state.working_memory.files: if f.mode == "full" and f.last_access_turns > 6: actions.append(RoutingAction("demote", f.path)) # 若代理连续两次检索同一模块,说明该模块应常驻工作内存 repeated = state.memory.recent_retrievals.most_common(1) if repeated and repeated[0][1] >= 2: actions.append(RoutingAction("promote", repeated[0][0])) return actions这个路由逻辑虽然简单,但解决了“切换震荡”问题:demote 有延滞(6 轮未访问才降级),promote 有确认(重复检索才提升)。实际跑起来,工作内存会稳定收敛到当前任务真正需要的文件集合上。
5.3 与代理框架的接入点
接入点放在两个位置:工具层和对话层。工具层给代理暴露三个新工具:recall(检索项目记忆)、commit_note(把当前结论写入长期档案)、compact(触发一次工作内存压缩)。对话层在每轮生成之前,插入一个 context_manager 钩子,执行路由函数并更新工作内存。
我踩过一个坑:把路由放在模型生成之后。这样会导致模型已经基于旧上下文生成了内容,路由再切也没意义,还得重新生成,浪费一轮。正确的顺序一定是先切上下文,再让模型生成。
5.4 一次真实运行演示
拿一个真实场景演示:代理接到任务“给用户模块增加导出 CSV 功能”。第一轮,工作内存里只有任务描述和用户模块的入口文件。代理调用 recall 工具,检索到 export 相关工具函数和一个类似的 Excel 导出实现。路由模块检测到代理连续两次检索 user_service 模块,把它提升为 full 模式。第二轮,代理基于完整上下文写出实现,调用 commit_note 把“导出逻辑复用 utils/exporter.py 的流式写法”记入长期档案。整个任务耗时三轮,token 消耗 18K,而之前用全量注入方式做类似任务要 60K 起步。
6. 常见问题与排查实录
6.1 检索不到关键代码
这是最常被问的问题。根源通常在索引切块:代码块切得太粗,一个文件一个块,语义模糊,召回自然不准;切得太细,一个函数一个块,丢失了上下文,召回了也不知道这个函数属于哪个模块。我的经验是:以函数和类为基本单位,同时给每个块附带文件路径、模块名、函数调用关系等元信息,检索时把元信息也参与匹配。
还有一个容易被忽略的点:倒排索引的分词器。拼音风格的变量名、驼峰命名的函数,在中文分词下经常被拆得七零八落。我会额外维护一份符号表,把完整符号名当作不可切分的整体,精确匹配永远优先。
6.2 上下文漂移
上下文漂移的表现是:代理越往后做,越偏离初始需求。排查看下来多半是工作内存里的任务描述被挤出去了,或者被中间生成的临时结论污染了。解决方案有两个,缺一不可:任务描述必须锚定在最顶部,且不允许被追加;每次变更记录只保留动机,不保留过程细节。过程细节是上下文漂移的最大温床,模型看到自己十分钟前说过的一句试探性话,往往会当真。
6.3 切换震荡
如果路由策略切换太频繁,模型会表现出“状态不安定”——一会儿认为某个模块已经实现,一会儿又认为它还没实现。这通常是因为 promote 和 demote 的阈值太接近,模型一检索就提升,两轮没用就降级,反复横跳。我的解法是给两个动作都加滞回区间:promote 需要连续两次检索命中,demote 需要超过 6 轮未被访问。这跟硬件设计里的滞回比较器是一个思路,目的就是消除抖动。
6.4 超时与 token 超限
运行中期的突然失败,大概率不是模型问题,而是某一轮测试输出异常巨大,把预留缓冲吃光了。我在代码里加了一道保险:工具输出的最大字节数写死在工具 schema 里,超过 8000 token 的输出强制截断并提示“结果过长已截断,可针对性查看文件”。宁可让代理多调一次工具,也不要让一次超长输出毁掉整个上下文预算。
6.5 问题速查表
| 症状 | 可能原因 | 处理办法 |
|---|---|---|
| 代理找不到已有实现 | 索引缺失或切块过粗 | 重建索引,按函数切块并增加元信息 |
| 改 A 坏 B | 工作内存缺 B 模块引用 | 路由增加“变更影响分析”触发检索 |
| 上下文越用越乱 | 工作内存未做降级 | 调低 demote 轮数窗口,强制压缩 |
| 代理重复生成相同代码 | 长期档案未覆盖该模块 | 检查 commit_note 是否真正写入 |
| 模型输出被截断 | 缓冲预留不足 | 增加缓冲占比,限制工具输出长度 |
7. 几个踩过坑之后才懂的经验
最后说几个不在教科书里、但真实决定成败的细节。第一个是“上下文管理要防呆”:不要把完整文件路径交给模型拼接,路径经常被拼错,直接在文件卡片里给出标准化路径,让代理照着用。第二个是“不要迷信 embeddings”——对代码语义检索来说,符号名和调用关系的权重应该高于向量相似度,我会在召回后做一个简单的规则重排,凡是命中符号表的候选,直接提到向量候选前面。第三个是“压缩要可追踪”:每次压缩都在档案里留一条记录,写清楚“原始内容在哪个文件可以找到”,模型如果需要细节,可以回源查找,而不是靠压缩后的摘要硬猜。
我个人现在的习惯是,每做完一个任务,花三十秒浏览一下长期档案,看看 commit_note 有没有记下真正值得留的东西。很多实现把精力花在“更聪明的路由”上,但其实更关键的是数据质量——工作内存里每条信息的来源、时效、置信度都清晰可查,路由才有得可切。Context Mode 从来不是一个单一算法,它是一套关于“信息什么时候该被看见”的纪律,把这个纪律执行到位,编码代理的能力才会真正稳定发挥出来。