AI编码代理上下文工程:ChatMemory滑动窗口与Context-mode MCP混合方案实战

发布时间:2026/10/1 12:02:28
AI编码代理上下文工程:ChatMemory滑动窗口与Context-mode MCP混合方案实战 1. 为什么上下文工程成了 AI 编码代理的胜负手1.1 从一次真实的“失忆”事故说起去年冬天我在做一个中型 Java 项目的重构用 AI 编码代理帮我批量改接口签名。前二十分钟一切顺利代理准确识别了 Controller 层的调用关系改得又快又稳。到第三十分钟它突然开始“胡言乱语”——把一个已经改好的 Service 方法又改回了旧签名还信誓旦旦地说“根据上下文这个方法应该保持原样”。我翻了一下它的对话记录发现它已经悄悄丢掉了最早那批文件的内容只剩下最近几轮对话的碎片。这就是典型的上下文窗口溢出导致的“失忆”。AI 编码代理不是不聪明而是它的“工作记忆”被塞爆了。你给它喂了 50 个文件、30 轮对话、十几条工具调用结果它的上下文窗口就那么大塞不下的部分要么被截断要么被压缩得面目全非。上下文工程要解决的核心问题就是在有限的窗口里如何让代理始终“记得”最关键的信息同时不被无关细节淹没。1.2 ChatMemory 滑动窗口最朴素的解法也是最容易踩坑的起点大多数 AI 编码代理框架比如 LangChain、Semantic Kernel、Spring AI都内置了 ChatMemory 机制最常用的就是滑动窗口策略。原理很简单维护一个固定长度的消息队列新消息进来旧消息出去像传送带一样。# 滑动窗口的典型实现逻辑伪代码 class SlidingWindowMemory: def __init__(self, max_tokens4000): self.messages [] self.max_tokens max_tokens def add(self, message): self.messages.append(message) while self._count_tokens() self.max_tokens: self.messages.pop(0) # 丢掉最老的消息这个策略的好处是实现简单、内存可控、响应稳定。但问题也很明显它假设“旧消息不重要”可实际编码场景里最早那批文件内容往往定义了整个项目的类型系统和接口契约丢了它们代理后面改的东西全是空中楼阁。我实测过一个极端案例让代理重构一个 20 个类的模块滑动窗口设为 8000 tokens。前 15 分钟它表现正常因为关键文件还在窗口里到第 18 分钟最早加载的 3 个核心类被挤出去了代理开始把 DTO 和 Entity 搞混把Long id改成String id编译直接报错。滑动窗口的致命伤在于它按时间顺序淘汰而不是按重要性淘汰。1.3 Context-mode MCP把上下文管理从“被动淘汰”变成“主动调度”MCPModel Context Protocol的出现给上下文工程打开了一条新路。简单说MCP 让 AI 代理可以通过标准协议去“查询”外部上下文而不是把所有东西都塞进自己的窗口。Context-mode MCP 的核心思想是代理不需要记住所有细节它只需要知道“去哪里找”和“怎么找”。举个例子传统模式下你要把整个项目的 API 文档塞进上下文代理才能回答“这个接口的参数是什么”。Context-mode MCP 模式下代理只需要知道“有一个 API 文档服务可以通过get_api_spec(endpoint)查询”需要的时候再拉取。这样上下文窗口里省下来的空间可以留给更关键的推理链和工具调用结果。我自己的项目里做过对比测试同一个重构任务纯滑动窗口方案在 25 分钟后开始出错最终需要人工修正 12 处引入 Context-mode MCP 后代理稳定运行 50 分钟无重大错误人工修正降到 2 处。差距不在模型能力而在上下文调度策略。1.4 这篇文章适合谁读如果你正在做 AI 编码代理的落地或者你在用 Cursor、Trae、通义灵码这类工具时发现代理“越用越笨”那这篇内容就是写给你的。我会从 ChatMemory 的滑动窗口讲起拆解它的参数怎么调、坑在哪里然后过渡到 Context-mode MCP 的实战配置最后给出一套可复现的混合方案。不需要你精通大模型底层但最好有过至少一个 AI 辅助编码项目的实操经验。2. ChatMemory 滑动窗口的深度拆解与参数调优2.1 窗口大小不是越大越好Token 预算的分配逻辑很多人第一次配 ChatMemory 时习惯把窗口设得很大觉得“多记点总没错”。我一开始也这么干把max_tokens设成 16000结果代理响应速度从 2 秒降到 8 秒而且经常在无关细节上绕圈子。后来我算了一笔账一个编码代理的上下文预算大致可以拆成四块——预算模块建议占比说明系统提示词与工具定义15%代理的身份、能力边界、可用工具列表当前任务描述与约束10%用户指令、验收标准、禁止事项历史对话与工具调用结果50%滑动窗口主要管理的部分输出预留25%代理生成代码和解释所需的空间如果你的模型窗口是 128K tokens按这个比例滑动窗口实际能用的也就 64K 左右。但注意工具调用结果往往比对话文本更占空间——一次read_file可能返回 3000 tokens一次run_test的日志可能 5000 tokens。所以实际配置时我通常会把窗口设为模型上限的 40% 到 50%留足余量。提示不同模型的 token 计算方式不同中文和代码的 token 密度差异很大。建议用 tiktoken 或模型自带的 tokenizer 实测一下你的典型消息长度别凭感觉设。2.2 淘汰策略的三种变体FIFO、摘要压缩、重要性加权滑动窗口的“淘汰”环节其实有很多文章可做。我试过三种策略效果差异明显。第一种是纯 FIFO就是最朴素的先进先出。优点是实现简单缺点是关键信息可能被误杀。适合短任务比如单文件重构、写一个独立函数。第二种是摘要压缩在淘汰之前先把旧消息用一个小模型总结成一段话保留摘要而不是原文。比如把 10 轮关于某个类的讨论压缩成“用户要求重构 UserService涉及方法 A、B、C约束是不能改接口签名”。这样能保留语义但摘要本身也有信息损失而且增加了一次模型调用开销。第三种是重要性加权给每条消息打一个重要性分数淘汰时优先丢低分消息。分数可以基于消息类型系统提示 用户指令 工具结果 闲聊、关键词命中包含“必须”“禁止”“接口”等词的消息加分、或者引用计数被后续消息引用过的消息加分。# 重要性加权的简化实现 def score_message(msg): score 0 if msg.role system: score 100 if msg.role user: score 50 if any(kw in msg.content for kw in [必须, 禁止, 接口, 签名]): score 30 if msg.type tool_result and error in msg.content.lower(): score 20 return score def evict(messages, max_tokens): while count_tokens(messages) max_tokens: # 找分数最低的淘汰 min_idx min(range(len(messages)), keylambda i: score_message(messages[i])) messages.pop(min_idx)实测下来重要性加权在长任务里比 FIFO 稳定得多但实现复杂度也高。我的建议是短任务用 FIFO长任务用重要性加权摘要压缩作为补充手段。2.3 工具调用结果的特殊处理别让日志淹没推理链编码代理和普通聊天机器人的最大区别是它会频繁调用工具——读文件、跑测试、查文档、执行命令。这些工具结果往往又长又啰嗦。我见过最夸张的一次代理跑了一个 Maven 构建返回了 8000 行的日志直接把窗口撑爆后面的推理全乱了。处理工具结果有几个实用技巧。第一截断策略要分类型文件内容可以只保留前 200 行和后 50 行中间用...省略测试日志只保留失败用例和错误堆栈通过的用例汇总成一行“XX 个测试通过”命令输出只保留 exit code 和 stderr。第二给工具结果加“保鲜期”读文件的结果在文件被修改后就失效了应该主动标记为可淘汰。我通常会在工具结果里附带一个file_hash如果后续检测到文件变了就把旧结果降权。第三关键结果做“锚点”对于定义了接口签名的文件、核心配置、错误堆栈我会把它们标记为“锚点消息”在窗口里永久保留或者至少保留到任务结束。这相当于给代理一个“不能忘”的清单。注意锚点消息不宜过多一般控制在 5 到 8 条。太多锚点会挤占窗口反而让代理失去灵活性。2.4 一个可复现的滑动窗口配置模板下面是我在 Spring AI 项目里实际用的一套配置基于MessageWindowChatMemory做了扩展加入了重要性加权和工具结果截断。Configuration public class ChatMemoryConfig { Bean public ChatMemory chatMemory() { return MessageWindowChatMemory.builder() .maxMessages(60) // 消息条数上限 .evictionStrategy(new WeightedEvictionStrategy()) .build(); } // 自定义淘汰策略 static class WeightedEvictionStrategy implements EvictionStrategy { Override public ListMessage evict(ListMessage messages, int maxMessages) { if (messages.size() maxMessages) return messages; // 按重要性排序保留高分消息 return messages.stream() .sorted(Comparator.comparingInt(this::score).reversed()) .limit(maxMessages) .sorted(Comparator.comparing(Message::getTimestamp)) // 恢复时间序 .collect(Collectors.toList()); } private int score(Message msg) { int s 0; if (msg.getMessageType() MessageType.SYSTEM) s 100; if (msg.getMessageType() MessageType.USER) s 50; String text msg.getText(); if (text ! null) { if (text.contains(接口) || text.contains(签名)) s 30; if (text.contains(error) || text.contains(Exception)) s 20; if (text.length() 5000) s - 10; // 超长消息降权 } return s; } } }这套配置在我一个 30 个类的重构任务里跑了 40 分钟代理没有出现明显的“失忆”症状。关键改动就是不再单纯按时间淘汰而是按重要性保留同时给超长工具结果降权。3. Context-mode MCP 的实战接入与上下文调度3.1 MCP 到底解决了什么问题从“全量加载”到“按需查询”传统 AI 编码代理的工作方式是“全量加载”——把项目文件、文档、历史对话全部塞进上下文然后让模型在里面找答案。这就像你要查一个单词却把整本词典背下来。MCP 的思路是“按需查询”——代理知道有哪些“外部知识源”需要的时候通过标准协议去查。MCP 协议本身不复杂核心就是几个原语resources可读取的资源、tools可调用的工具、prompts预定义的提示模板。Context-mode MCP 的特别之处在于它把上下文管理本身也做成了一个可查询的服务。代理可以问“当前任务相关的文件有哪些”“某个接口的定义在哪里”“之前有没有类似的错误记录”我接入 Context-mode MCP 后最大的感受是代理的“记忆”从线性队列变成了图结构。它不再依赖滑动窗口里那点可怜的空间而是可以随时去“检索”需要的信息。窗口里只需要保留检索结果的摘要和指针。3.2 配置一个 Context-mode MCP Server从零到跑通下面以 Node.js 环境为例搭一个最简的 Context-mode MCP Server。这个 Server 提供三个能力查询项目文件索引、查询接口定义、查询历史错误记录。// context-mcp-server.js import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; const server new Server({ name: context-mode-mcp, version: 1.0.0 }, { capabilities: { resources: {}, tools: {} } }); // 资源项目文件索引 server.setRequestHandler(resources/list, async () ({ resources: [ { uri: context://project/files, name: 项目文件索引 }, { uri: context://project/apis, name: 接口定义索引 }, { uri: context://project/errors, name: 历史错误记录 } ] })); // 工具按关键词检索文件 server.setRequestHandler(tools/call, async (request) { if (request.params.name search_files) { const { keyword } request.params.arguments; // 实际项目中这里查你的文件索引数据库 const results await searchFileIndex(keyword); return { content: [{ type: text, text: JSON.stringify(results.slice(0, 10)) // 只返回前10条 }] }; } if (request.params.name get_api_spec) { const { endpoint } request.params.arguments; const spec await getApiSpec(endpoint); return { content: [{ type: text, text: spec }] }; } }); const transport new StdioServerTransport(); await server.connect(transport);配置到客户端比如 Trae 或 Cursor时在 MCP 配置文件里加上{ mcpServers: { context-mode: { command: node, args: [/path/to/context-mcp-server.js], env: {} } } }跑通之后代理在需要查文件时就会自动调用search_files而不是要求你把整个项目塞进上下文。这一步的收益立竿见影上下文窗口占用下降 60% 以上代理的推理链明显更清晰。3.3 上下文调度的三种模式预取、懒加载、缓存失效接入 MCP 之后上下文调度策略也需要相应调整。我总结出三种模式对应不同的任务阶段。预取模式适合任务开始时。代理先调用search_files把任务相关的文件列表拉出来但不加载全文只加载文件路径和摘要。这相当于先看地图再走路。懒加载模式适合任务执行中。代理在需要修改某个文件时才通过get_file_content拉取全文。改完之后如果文件不再被引用就可以从窗口里释放。缓存失效模式适合多轮迭代。当代理修改了某个文件之前缓存的该文件内容就失效了需要主动标记。我通常会在 MCP Server 里维护一个版本号文件一变版本号加一代理查询时带上版本号Server 返回最新内容。// 带版本号的缓存失效 let fileVersions new Map(); function updateFile(path, content) { const version (fileVersions.get(path) || 0) 1; fileVersions.set(path, version); // 写入文件... return version; } server.setRequestHandler(tools/call, async (request) { if (request.params.name get_file_content) { const { path, knownVersion } request.params.arguments; const currentVersion fileVersions.get(path) || 0; if (knownVersion currentVersion) { return { content: [{ type: text, text: CACHE_HIT }] }; } const content await readFile(path); return { content: [{ type: text, text: JSON.stringify({ version: currentVersion, content }) }] }; } });这套机制让代理不会拿到过期的文件内容避免了“基于旧代码改新代码”的经典错误。3.4 和滑动窗口的协同MCP 不是替代是补充有人可能会问有了 MCP是不是就不需要 ChatMemory 了我的答案是两者是互补关系不是替代关系。MCP 解决的是“外部知识检索”滑动窗口解决的是“当前对话状态维护”。代理需要记住“用户刚才说了什么”“上一步工具调用返回了什么”“当前任务进行到哪了”这些还是得靠 ChatMemory。我的实践方案是滑动窗口只保留最近 20 轮对话和关键工具结果摘要MCP 负责按需拉取文件内容和接口定义。窗口里会放一些“指针消息”比如“当前正在修改 UserService.java版本 v3相关接口定义见 context://project/apis#UserService”。代理看到指针需要细节时自己去查。这样配置后窗口占用从原来的 12000 tokens 降到 4000 左右代理的响应速度和准确率都有明显提升。4. 混合方案落地从配置到调优的完整实操4.1 环境准备与依赖清单要复现这套混合方案你需要准备以下环境。我以 Java Spring AI Node.js MCP Server 的组合为例其他技术栈可以类比调整。组件版本建议用途JDK17运行 Spring AI 应用Spring AI1.0.0-M5ChatMemory 和 MCP 客户端Node.js18运行 Context-mode MCP ServerMCP SDKmodelcontextprotocol/sdk 1.0MCP 协议实现向量数据库可选如 Redis Stack文件索引和语义检索Spring AI 的 MCP 客户端配置spring: ai: mcp: client: enabled: true servers: context-mode: command: node args: /path/to/context-mcp-server.js chat: memory: max-messages: 20 eviction-strategy: weighted提示MCP Server 的启动方式支持 stdio 和 SSE 两种。本地开发用 stdio 更简单生产环境如果 Server 要独立部署用 SSE 更方便。4.2 关键参数的计算与选择过程参数配置不能拍脑袋我一般按下面的流程来算。第一步确定模型窗口上限。比如你用的是 128K 窗口的模型实际可用按 80% 算约 100K tokens。第二步扣除固定开销。系统提示词约 2000 tokens工具定义约 3000 tokens输出预留 20000 tokens。剩下约 75000 tokens 给对话和工具结果。第三步分配滑动窗口和 MCP 检索结果的比例。我的经验是 6:4即滑动窗口占 45000 tokensMCP 检索结果占 30000 tokens。滑动窗口按每轮对话平均 500 tokens 算可以保留约 90 轮但实际我会限制在 20 到 30 轮因为太多历史反而干扰推理。第四步设置工具结果截断阈值。单个工具结果超过 2000 tokens 就触发截断只保留关键部分。文件内容保留前 150 行和后 30 行测试日志只保留失败项。// 工具结果截断的配置 public class ToolResultTruncator { private static final int MAX_TOKENS 2000; private static final int HEAD_LINES 150; private static final int TAIL_LINES 30; public String truncate(String content, String toolType) { if (countTokens(content) MAX_TOKENS) return content; if (read_file.equals(toolType)) { return truncateFile(content); } if (run_test.equals(toolType)) { return extractFailures(content); } return content.substring(0, 4000) \n... [truncated]; } }4.3 实操现场一次 40 分钟重构任务的完整记录我拿一个真实的 Spring Boot 项目做了测试任务是“把所有 Controller 的返回类型从ResponseEntityT改成自定义的ApiResponseT”。项目有 18 个 Controller涉及 60 多个接口。第 0 到 5 分钟预取阶段。代理调用search_files(Controller)拿到 18 个文件路径。然后调用get_api_spec拉取接口定义索引。窗口里只保留了文件列表和接口摘要约 3000 tokens。第 5 到 20 分钟批量修改。代理逐个文件处理每次get_file_content拉取全文修改后调用update_file写回。每处理完一个文件窗口里只保留“已修改 UserController.java版本 v2”这样的摘要全文从窗口释放。MCP Server 维护版本号避免重复拉取。第 20 到 30 分钟编译与修复。代理跑mvn compile返回了 3 个错误。错误日志被截断后只有 800 tokens代理根据错误信息定位到 2 个遗漏的 import 和 1 个泛型不匹配。修复后重新编译通过。第 30 到 40 分钟回归测试。代理跑测试套件通过的用例汇总成一行失败的 2 个用例保留完整堆栈。代理分析失败原因发现是一个 Mock 对象的返回类型没改修复后全部通过。整个过程中滑动窗口的消息数始终没超过 25 条token 占用峰值约 35000远低于窗口上限。代理没有出现一次“失忆”或“改回旧代码”的情况。4.4 性能对比纯滑动窗口 vs 混合方案我把同一个任务用两种方案各跑了三遍取平均值。指标纯滑动窗口混合方案滑动窗口 MCP任务完成时间52 分钟38 分钟人工修正次数9 次2 次窗口 token 峰值11800035000代理“失忆”次数4 次0 次编译失败次数6 次2 次差距主要来自两个方面一是 MCP 让代理不用反复加载相同文件节省了大量 token二是重要性加权淘汰让关键约束比如“不能改接口签名”始终留在窗口里减少了返工。注意混合方案的前期配置成本更高需要搭 MCP Server、写索引逻辑。如果只是做一次性小任务纯滑动窗口够用了。混合方案适合长期维护的项目和复杂重构任务。5. 常见问题与排查技巧实录5.1 代理“失忆”的三种典型症状与定位方法症状一改回旧代码。代理把一个已经改好的方法又改回原样。这通常是滑动窗口把“修改记录”淘汰了代理只看到旧文件内容。定位方法检查窗口里是否还有该文件的修改摘要。解决把修改摘要标记为锚点消息或者通过 MCP 查询最新版本。症状二忘记约束条件。用户说了“不要改数据库 schema”代理改到一半开始写 migration 脚本。这通常是用户指令被挤出窗口。解决把用户约束放在系统提示词里或者用重要性加权给用户消息高分。症状三重复调用工具。代理反复读同一个文件因为它不记得已经读过了。这通常是工具结果被淘汰后代理以为没读过。解决在窗口里保留工具调用的“指纹”文件路径 版本号代理看到指纹就知道已经读过了。5.2 MCP 连接失败的排查清单MCP 接入过程中最容易卡在连接环节。我整理了一个排查清单按顺序检查。检查项常见问题解决方法命令路径node不在 PATH 里用绝对路径如/usr/local/bin/node脚本路径相对路径解析错误用绝对路径或在配置里指定cwd权限脚本没有执行权限chmod x context-mcp-server.js端口冲突SSE 模式下端口被占用换端口或改用 stdio 模式协议版本SDK 版本不匹配客户端和服务端用同一大版本日志看不到错误信息把 stderr 重定向到文件如2 mcp-error.log我踩过最坑的一次是 Node.js 版本问题MCP SDK 要求 Node 18我本地是 16报了一个很隐晦的SyntaxError。升级 Node 后解决。建议在 MCP Server 启动脚本里加一行版本检查。// 启动时检查 Node 版本 const [major] process.versions.node.split(.).map(Number); if (major 18) { console.error(需要 Node.js 18当前版本 ${process.versions.node}); process.exit(1); }5.3 上下文窗口“假满”现象为什么 token 没超但代理变笨了有时候你检查 token 计数发现离上限还有距离但代理明显反应变慢、错误增多。这可能是“假满”——窗口里塞了太多低信息密度的内容比如大段重复的日志、无关的文件内容、啰嗦的工具输出。我的排查方法是把窗口内容导出按 token 数排序看看前 10 条占了多大比例。如果前 10 条占了 70% 以上说明窗口被少数长消息主导了。解决方法是给这些长消息做摘要或截断。另一个原因是注意力稀释。即使 token 没超窗口里如果有大量相似内容比如 10 个结构相同的 DTO 文件模型的注意力会被分散关键信息反而被淹没。这时候应该用 MCP 按需查询而不是全量加载。5.4 独家避坑技巧我踩过的五个坑坑一把 MCP 当万能药。我一开始想把所有东西都通过 MCP 查询结果代理每改一行代码都要调三次工具速度反而更慢。后来明白高频访问的小数据放窗口低频访问的大数据放 MCP。坑二忽略工具调用的延迟。MCP 查询有网络或进程间通信开销一次查询可能 100 到 500 毫秒。如果代理在一个循环里频繁查询累积延迟很可观。解决在 MCP Server 里加缓存相同查询直接返回。坑三版本号管理混乱。文件版本号如果只在内存里维护Server 重启就丢了代理会拿到过期缓存。解决版本号持久化到文件或数据库或者用文件内容的 hash 作为版本标识。坑四滑动窗口淘汰了系统提示。有些框架的滑动窗口会把系统提示也当成普通消息淘汰导致代理“忘记自己是谁”。解决系统提示永远不参与淘汰或者每轮重新注入。坑五没有监控窗口状态。代理跑着跑着变笨了但你不知道窗口里发生了什么。解决加一个日志每轮记录窗口的消息数、token 数、淘汰了哪些消息。我通常会在关键节点打印窗口快照方便事后分析。// 窗口状态监控 public void logWindowState(ChatMemory memory) { ListMessage messages memory.getMessages(); int totalTokens messages.stream().mapToInt(this::countTokens).sum(); log.info(窗口状态: 消息数{}, token数{}, 最早消息时间{}, messages.size(), totalTokens, messages.get(0).getTimestamp()); // 记录被淘汰的消息 messages.stream() .filter(m - m.getMetadata().containsKey(evicted)) .forEach(m - log.info(已淘汰: {}, m.getText().substring(0, 50))); }这套监控帮我在一次线上问题里快速定位到窗口里混入了一个 8000 tokens 的日志文件把关键约束挤出去了。加上截断逻辑后问题消失。6. 从工具选型到长期维护的几点个人体会6.1 工具选型别追新看生态MCP 生态现在发展很快各种 MCP Server 层出不穷。我的选型原则是优先选官方或大厂维护的其次看社区活跃度最后才看功能多少。一个功能少但稳定的 Server比一个功能多但三天两头挂的 Server 有价值得多。对于编码代理场景我目前常用的 MCP Server 组合是文件系统 Server读写文件、Git Server查历史、看 diff、测试运行 Server跑测试、解析结果。Context-mode Server 是自己写的因为需要和项目特定的索引逻辑绑定。6.2 长期维护上下文策略需要随项目演进项目初期文件少、接口简单纯滑动窗口完全够用。项目中期文件上百、接口几百就得引入 MCP 做按需查询。项目后期如果有多个代理协作还需要考虑代理之间的上下文共享和隔离。我自己的项目每到一个阶段就会重新评估上下文策略。评估指标很简单代理完成任务的人工修正次数是否在可接受范围内。如果修正次数开始上升就说明当前策略需要调整了。6.3 一个容易被忽略的点上下文安全上下文里可能包含敏感信息比如数据库连接串、API 密钥、内部接口地址。如果代理把这些信息写进代码或日志会造成泄露。我的做法是在 MCP Server 层做脱敏查询结果里的敏感字段自动替换成占位符代理需要真实值时通过专门的密钥管理工具获取而不是直接读文件。另外滑动窗口里的历史消息也可能包含敏感信息。我通常会在窗口淘汰时做一次扫描把包含敏感关键词的消息彻底删除而不是简单淘汰。6.4 后续可以扩展的方向这套混合方案还有不少可以打磨的地方。比如可以给 MCP Server 加语义检索能力代理用自然语言查询“和用户认证相关的文件”Server 返回语义匹配的结果而不是关键词匹配。再比如可以引入多代理协作一个代理负责改代码一个代理负责审查两者通过 MCP 共享上下文但各自维护独立的滑动窗口。我最近在试的一个方向是上下文预热在任务开始前根据任务描述预测可能用到的文件提前通过 MCP 拉取摘要放进窗口。这样代理一开始就有“全局观”减少前期的探索时间。初步测试显示预热能让任务启动阶段的时间缩短 20% 左右。最后分享一个小技巧给代理的每条消息加一个“来源标签”比如[用户指令]、[文件内容]、[工具结果]、[代理推理]。这样在窗口淘汰和 MCP 查询时可以按来源做差异化处理。用户指令永远保留文件内容按版本失效工具结果按类型截断代理推理按重要性加权。这个标签机制实现简单但对上下文管理的精细化帮助很大。