
如果你平时用 AI 编码代理干活大概率遇到过这种血压升高的时刻任务刚开始时它反应飞快记得你定下的命名规范、架构约束甚至能主动提醒你“上次说过这个接口不能动”。可聊到三四十轮之后它就开始和稀泥——把已经冻结的方案又拿出来改重新提出你否决过无数回的库选型甚至在某次重构里顺手改坏一个它自己刚写的函数。很多人第一反应是换更大的上下文窗口但实测下来你会发现问题往往不是窗口不够大而是该进窗口的东西没进去不该占位置的垃圾一直赖着不走。这篇文章想聊的就是上下文工程这件事。围绕 AI 编码代理我会拆解两套核心手段一套是 ChatMemory 这类对话记忆组件里最常用的滑动窗口策略另一套是 MCP 体系下 Context-mode 这种按需加载的上下文优化方案。它们不是二选一的对立关系而是分别管理短期会话记忆和长期项目知识的两种武器。如果你在做 Agent 工作流、AI 应用开发或者只是想让自己的编码代理干活更稳这篇内容应该能给你一份可以直接照抄的配置思路顺带几颗我提前踩过的雷。1. 上下文窗口不是“够大”就够用编码代理为什么会突然失忆1.1 从一次真实的“改坏”事故说起前阵子我让代理做一个跨模块的重构任务把某个支付服务里的回调逻辑统一收敛到新的PaymentCallbackDispatcher。前 15 轮它表现很好按我给的接口清单逐文件迁移还在提交说明里写清楚了变更点。结果在第 32 轮我让它顺手优化一下异常处理它居然把第 6 轮已经确认过的回调顺序又给改了回来导致两个渠道的签名验签全部错位。我当时的第一反应是“模型又发疯了”但事后翻聊天记录才发现第 6 轮的信息早就被滑动窗口排挤出去了模型只看到了最近 20 轮的内容根本不知道那个顺序是甲方签字画押过的约束。这种“失忆”不是模型智力问题而是上下文管理问题。AI 编码代理和聊天机器人不一样它的每一轮动作都要依赖之前所有轮次里隐含的代码依赖、命名约束和架构决策。这些信息一旦被挤出窗口轻则重复提问重则把稳定代码改成一坨新 bug。1.2 注意力衰减与 Token 预算上下文管理的两个底层约束Transformer 模型对输入序列中每个 token 的注意力不是均匀的。即便是超长窗口模型也存在明显的“远端遗忘”距离当前对话较远的信息在注意力权重上天然处于劣势。更关键的是每个模型都有硬性的 token 上限和实际可用的“有效上下文长度”。你给代理塞了 100k token 的代码文件它虽然能“看到”但回答时真正参考的往往只有离当前位置最近的那几段以及和你要求最相关的检索片段。这带来的工程推论非常直接上下文管理不能只盯着“总量有没有超”还要看“每一层信息的位置是否合理”。滑动窗口要解决的就是让最重要的信息尽量待在模型注意力最容易覆盖到的区间而 Context-mode MCP 要解决的是让最相关的外部知识以结构化方式出现在正确的位置而不是靠大海捞针。1.3 编码任务独有的上下文依赖特征编码代理的上下文依赖有三个特点决定了它比通用对话更考验上下文工程。第一个特点是隐性约束密度高。一个项目里大量的约束不是写在需求文档里的而是散落在历史对话中“这个函数不能直接返回空数组”“线上环境不允许写日志到 stdout”“数据库连接必须走统一代理”。这些约束一旦被滑出窗口代理就会凭训练数据里的“一般做法”自由发挥结果必然是风格漂移甚至安全事故。第二个特点是工具调用记录占据大量 token。编码代理每轮可能要执行 3~5 次工具调用包括 grep、读文件、跑测试、查报错。每条工具结果动辄几百上千 token很容易把窗口塞满。ChatMemory 滑动窗口在裁剪时最先挤掉的往往就是这些看似不重要的工具输出但问题的关键在于工具输出里可能藏着那次测试失败的真实报错信息。第三个特点是长任务的时间跨度长。一个中型重构任务可能要 30 轮、50 轮甚至上百轮交互才能完成远超滑动窗口能覆盖的轮次范围。所以你必须给代理设计一套“记忆外置”机制让关键信息从易失的会话记忆转移到持久的外部存储里。这正好引出了后面说的 Context-mode MCP。2. 滑动窗口的工程本质用“最近 N 轮”换确定性2.1 滑动窗口不是新东西它是计算机系统的通用手法先把这个概念放到更大的谱系里看滑动窗口几乎是计算机系统里最经典的一类策略。TCP 的流量控制用滑动窗口做可靠传输信号处理里用滑动窗口滤波做局部平滑算法题里的单调队列专门求滑动窗口最大值最小值。到 AI 上下文工程里它被翻译成了同样一句话只保留最近的一段数据旧数据要么丢弃要么被压缩成摘要。ChatMemory 这类对话记忆模块核心就是一个带容量的消息队列新消息进来队列满了最老的消息要么出队要么浓缩一下再放进一个“摘要区”。这个思路的好处是确定性强、实现简单、token 消耗可控坏处也很明显它默认“最近的就是最重要的”但真实编码场景里这个假设经常不成立。2.2 ChatMemory 滑动窗口的消息结构与裁剪策略我按照工程实现的方式给你拆一版。ChatMemory 里的消息通常分三类系统指令、用户请求、助手响应包括工具调用记录。滑动窗口裁剪的对象主要是后两类。常见做法不是一个一个轮次地丢而是按“轮”为单位管理——一轮指的是“一次用户请求 对应助手响应 期间的全部工具调用”。内存结构大致长这样class ChatMemory: def __init__(self, max_tokens48000, summary_tokens4000, min_keep_rounds6): self.rounds [] # 每一轮是 (user_msg, assistant_turns) self.max_tokens max_tokens # 窗口记忆预算 self.summary_tokens summary_tokens self.min_keep_rounds min_keep_rounds # 无论如何保留最近 N 轮 self.summary # 被挤出窗口的旧内容摘要 def append(self, user_msg, assistant_turns): self.rounds.append((user_msg, assistant_turns)) self._trim() def _trim(self): while self._count_tokens(self.rounds) self.max_tokens \ and len(self.rounds) self.min_keep_rounds: oldest_user, oldest_assistant self.rounds.pop(0) self.summary self._compress(f{self.summary}\n f用户要求{oldest_user}\n f代理决定{oldest_assistant})这里的_compress可以是调用摘要模型的入口也可以简单截断关键字段比如只保留用户请求里带“不能/必须/确认”这类约束词的句子。更讲究的做法是维护一个“结构化解约清单”把被挤出轮次中的决策项、否决项、文件修改记录单独抽出来存成结构化条目而不是让摘要模型自由发挥。2.3 Token 预算分配窗口大小不是拍脑袋定的滑动窗口的边界必须由 token 预算倒推出来而不是随便选个“最近 20 轮”。我常用的预算拆分思路是这样的固定开销系统提示、工具定义、角色设定。这部分通常占 10k~15k token几乎不波动。任务载荷当前正在操作的文件内容、diff、报错堆栈。这部分我会分两种情况对待只读文件给 15k~20k涉及大文件修改时再追加 10k。窗口记忆预算总上下文上限减去前两项再留 20% 的余量。比如模型上下文 128k固定开销 15k任务载荷 30k那么窗口记忆就是128k - 15k - 30k再乘以 0.8大约 66k。平均每轮 token包含一次用户输入、助手回复和两三次工具调用我实测一轮大概 2k~3k。所以窗口轮次不一定越大越好。按上面这个预算窗口稳定在 22~33 轮之间够用但不会把任务载荷挤掉。我还特意设了一个min_keep_rounds保护位不管 token 多紧张最近 6 轮完整保留因为最近几轮通常包含正在执行的修改指令丢了比上下文溢出更致命。2.4 滑动窗口的硬伤没有摘要机制旧知识必丢纯滑动窗口的最大缺陷是“硬丢失”。我做过一次很典型的对照实验一个 20 轮就完成的小型重构任务窗口设在 18 轮结果代理在最后两轮把第 3 轮定的类型定义名UserId改成了UserID。原因不用猜——旧的类型名被挤出去了模型按自己训练数据里的常见写法做了“合理化”修改。要缓解这个硬伤必须配合摘要或结构化记忆。但摘要又会引入第二个问题摘要压缩会抹掉否定性约束。让摘要模型概括上一段对话时它天然倾向于保留“做了什么事”而不是“明确不要做什么事”。所以我的经验是摘要里一定要单独开一个“禁令清单”字段专门记录被挤出窗口对话里的否定判断。否则你的代理会反复去尝试同一个你早就否决过的方案还觉得自己特别积极。3. Context-mode MCP把上下文从“流水账”变成“可寻址资产”3.1 什么是 MCP以及 Context-mode 到底在优化什么MCPModel Context Protocol模型上下文协议定义了一套标准化的方式让 AI 应用通过服务端暴露工具Tools、资源Resources和提示Prompts。你可以把它理解成 AI 领域的“USB-C 接口”不同的数据源和服务只要实现了 MCP就能被同一个代理统一访问。Context-mode 是 MCP 体系里一个非常实用的细分方向它不是把整个仓库、整个文档一股脑塞给模型而是让服务端按“上下文模式”动态组装并返回当前任务真正需要的那一小块信息。这里的“模式”可以理解为视角比如项目概览模式、符号索引模式、变更影响模式、任务快照模式。代理在进入某个阶段时先声明要什么模式MCP server 再针对性地生成上下文。这和滑动窗口是两种层面的优化ChatMemory 管的是“已发生的对话怎么留”Context-mode MCP 管的是“任务需要的外部知识怎么给”。前者是记忆管理后者是知识供给。3.2 上下文读取模式项目地图、符号索引、任务快照在实际的 AI 编码代理集成里我建议把 Context-mode 拆成三类资源来设计。第一类是项目地图project map。小于 500 字的项目结构描述包含核心模块职责、目录边界、构建入口、关键配置路径。代理启动任务时先读这张地图就有了全局坐标系没有它代理会凭经验猜项目结构然后灰头土脸地 grep 半天。第二类是符号索引symbol index。不是全语言的 AST 转储而是当前任务涉及文件的导出符号、类型定义、公共函数签名。MCP server 负责维护增量索引代理在需要读某个文件之前先拉取符号索引判断这个文件是不是真的相关。这一步能省掉大量无意义的整文件读取。第三类是任务快照task snapshot。每完成一个子任务代理把变更状态写成结构化记录改了哪些文件、接口变更是啥、遗留风险是什么。任务快照既可以存到本地文件也可以由 MCP server 暴露给下游代理。它相当于把“当前轮次的短期记忆”固化成了“可查询的长期记忆”正好补上滑动窗口会丢东西的短板。3.3 一个最小可用的 MCP 资源配置示例假设我们要给编码代理接一个负责仓库上下文的 MCP server配置可以长这样{ mcpServers: { repo-context: { command: npx, args: [-y, context-mcp-server], env: { REPO_ROOT: /workspace/my-service, INDEX_MODE: symbolcallgraph, MAX_OVERVIEW_TOKENS: 800, MAX_SYMBOL_TOKENS: 4000 } } } }这个context-mcp-server是我自己搭的示例实现不是某个现成包。它对外暴露三类资源地址repo://overview返回项目地图repo://symbols/{file_path}返回指定文件的符号签名repo://task/{task_id}读写任务快照代理在系统提示里被告知“访问外部上下文前先尝试从 repo-context 获取”然后它运行时会按需发起请求。这样做的收益是项目知识不再占用会话记忆的窗口预算而是通过 MCP 按需拉取用完即走。窗口里留下的只是代理对这些知识的“理解”而不是知识本身。3.4 为什么说 Context-mode 解决的是滑动窗口解决不了的问题滑动窗口解决问题的角度是“删”Context-mode 的角度是“寻址”。项目里的知识不是线性对话它有天然的结构模块、函数、数据结构、依赖关系。用滑动窗口硬塞这些结构化信息等于把一本字典按出现顺序记在脑海里既不经济也不利于检索。MCP 的 URI 寻址机制让代理能精确拉到“第 X 个函数签名”或者“模块 Y 的接口变更历史”这是任何轮次记忆都替代不了的。我个人的体会是如果只有滑动窗口代理像是一个“记性好的短工”干久了开始自作主张配上了 Context-mode MCP代理更像是“有档案室查询权限的员工”遇到不确定就翻档案而不是凭印象乱猜。编码代理的长稳运行本质上靠的是后者。4. 混合方案落地短期记忆用滑动窗口长期知识用 MCP Context4.1 上下文分层设计热层、温层、冷层真正实用的上下文工程不是只选一个机制而是分层。我把代理运行时的上下文分成三层热层对话记忆由 ChatMemory 管理滑动窗口 摘要只保留最近 N 轮完整会话和禁令清单。这一层对应“现在正在做什么”。温层项目知识由 Context-mode MCP 管理按需拉取的仓库结构、符号签名、架构说明。这一层对应“这个系统长什么样”。冷层外部索引文档、历史提交、测试报告等不直接进上下文的资料通过检索引擎或 MCP 工具按需查询。这一层对应“如果要深挖从哪里找证据”。代理每次运作的流程是先拉热层的最近记忆确定当前目标再向温层请求项目地图和符号索引定位改动点遇到不确定的决策时再去冷层搜索历史依据。三层各自有独立预算不互相挤占这是混合方案最重要的收益。4.2 一套可复制的配置模板含关键参数下面是我的默认配置你可以按自己的模型和项目规模调整层级组件预算/参数说明热层ChatMemorymax_tokens48000, min_keep_rounds6窗口记忆约 20~24 轮热层摘要模块summary_tokens4000含禁令清单旧轮次压缩后的结构化摘要温层repo-context MCPoverview 800tsymbol 4000t按资源 URI 按需读取温层文件读取工具单文件最多 12k token超大文件走分片读取冷层查询工具无固定预算限制返回条数grep/搜索最多返回 20 条结果在这个配置下我给代理的启动系统提示会加一句固定的话“对话历史只保留近 N 轮被压缩的决策和约束见记忆摘要项目结构、接口签名、架构说明等请优先从 repo-context 资源获取不要凭经验猜测。”4.3 上下文失效与回收什么时候手动清空窗口混合方案也不是设了就能一直跑。上下文是会“过期”的尤其是任务切换的时候。这里我吃过几次亏总结出三个必须手动清空或重建上下文的时机。第一个时机是切换任务主题时。上一个任务结束后的窗口里全是旧代码的上下文直接开新任务会导致代理把旧逻辑套到新代码上。我会先触发一次显式摘要把上一个任务的快照写入任务文件然后清空 ChatMemory再加载新的项目地图。第二个时机是大规模重构中间点上。重构进行到一半代码库里到处都是中间态文件此时 MCP 返回的符号索引可能已经和磁盘真实状态不一致。我会要求代理重新建立符号索引而不是继续信任旧的缓存。第三个时机是代理开始重复问同一个问题时。这通常意味着记忆摘要已经失真继续在旧窗口上补丁只会越补越乱。宁可清空重开把关键约束从任务快照里重新声明一遍也不要让代理带着错误的记忆瞎跑。4.4 实测对比纯滑动窗口 vs 混合方案的任务差异我挑了一个中型 Node.js 服务做了对照测试任务包含跨 6 个文件的接口改造和异常处理收敛。纯滑动窗口方案在第 21 轮开始出现约束遗忘重复确认同一个接口名最终耗时 38 轮完成中间改坏了一次回调顺序混合方案在同样任务里耗时 26 轮完成没有出现约束遗忘但中间多出了 7 次 MCP 上下文请求。额外开销是真实存在的。MCP 每次请求都会产生工具调用记录这些记录本身也吃窗口 token。所以混合方案的意义不是“省 token”而是“省思考路径”——把项目知识从对话历史里剥离出去让窗口里留下的都是决策和判断而不是一段段文件内容。对长任务来说这个交换非常划算。5. 调参与避坑记录这三类问题最容易被低估5.1 压缩摘要会吃掉“否定性约束”而且毫无声息这是我在所有踩坑里遇到最多的一次。摘要模型在压缩对话时天然偏向“保留做了什么”比如“将 AuthService 中的 token 校验改为异步调用”但它很容易漏掉一句同样关键的否定判断“不要删除 Redis 缓存因为降级方案依赖它”。我现在的对策是给摘要模块加一个强制结构摘要必须输出三个字段——完成事项、当前设计、禁令清单。禁令清单里的每一条都必须包含“主体 否定动作 原因”例如“Redis 缓存不得删除因为降级方案依赖”。实测加上这个结构后代理在长任务里重复发起已否决方案的概率下降了一半以上。5.2 MCP 调用本身也在吃 Token小心“上下文放大器”MCP 的按需加载不是免费的。每次代理调用 repo-context 工具都会产生一次完整的工具调用记录包括请求参数和返回内容这些都会被计入对话历史。我自己统计过一次 MCP 符号索引查询平均能产出 800~2000 token 的调用日志。如果代理在一个任务里频繁调用十几次光是“查询上下文”这件事本身就消耗了 20k token。应对办法是给代理设定更严格的工具调用策略。一是批量获取不要为单个符号多次调用一次请求整个文件或整个模块的符号表二是结果缓存同一个文件的索引在同一任务里只拉一次三是给 MCP 查询工具加“返回条数上限”宁可让代理多读一次文件也不要让它拿回一个 50k token 的超大 JSON。5.3 滑动窗口与工具调用记录互相挤占ChatMemory 滑动窗口裁剪时最容易忽略的就是工具调用日志的体量。代理每执行一次 grep、一次测试、一次文件读取都会在历史里留下痕迹。假设一轮对话包含 4 次工具调用、每次平均 800 token那一轮的 token 可能就冲到 4k 以上。这直接压缩了窗口能容纳的真实对话轮数。我现在的参数设计里强制要求工具调用结果做“摘要化落盘”完整结果写进本地日志文件窗口里只保留“工具名 返回结果的前 200 个字符 关键状态码”。这样窗口轮次能多保留将近一倍。代价是排查问题时需要翻本地日志文件但对代理本体来说记忆密度高了很多。5.4 我现在的默认配置与日常工作流经过这半年多的调参我现在的默认工作流是这样固定的启动新任务前先通过 MCP 拉项目地图确认代理对项目结构有基本认知然后把任务约束写进系统提示的“硬约束”段防止后续窗口滑动把约束滑掉会话过程中由 ChatMemory 自动管理最近 20~24 轮被挤出窗口的旧轮次生成摘要其中禁令清单单独成段遇到跨大文件改动时让代理通过 MCP 拉符号索引判断影响面再决定要不要整文件读取任务完成前用 MCP 的 task snapshot 把关键决策和遗留问题写回项目仓库。这套流程不是万能的但在面对 40 轮以上的复杂编码任务时代理的“记忆衰退”速度明显变慢了。上下文工程说到底是一门取舍的手艺滑动窗口给了你确定性和可控的 token 预算Context-mode MCP 给了你结构化的知识供给能力。把记忆留给窗口把知识交给 MCP最后留一份禁令清单兜底这三件事做好编码代理就能从“偶尔惊艳”变成“长期靠谱”。