资讯动态

AI编码代理上下文工程实战:ChatMemory滑动窗口与Context-mode MCP优化

发布时间:2026/10/3 23:47:48 来源:尧图企业网站定制
1. 为什么上下文工程成了 AI 编码代理的胜负手1.1 从一次真实的翻车现场说起去年冬天我接手了一个遗留的 Java 微服务项目代码量大概在 12 万行左右模块之间耦合严重注释覆盖率不到 15%。当时我信心满满地把整个项目丢给一个主流的 AI 编码代理想着让它帮我梳理一下订单模块的调用链路。结果呢代理在第三轮对话之后就开始胡言乱语把OrderService和PaymentService的方法签名混在一起甚至凭空捏造了一个根本不存在的RefundHandler类。我一开始以为是模型能力不行换了个更强的模型问题依旧。后来我把对话历史打印出来一看傻眼了——上下文窗口里塞满了前几轮的文件内容、工具调用结果和我的追问真正关键的当前任务描述被挤到了最边缘的位置模型根本看不见。这就是典型的上下文污染问题。AI 编码代理和普通聊天机器人最大的区别在于它需要在一个持续的任务流中反复读写代码、调用工具、维护状态上下文不是一次性的而是像滚雪球一样越滚越大。如果不做工程化的管理再强的模型也会被自己的历史拖垮。上下文工程Context Engineering这个词这两年才逐渐被行业重视但它的核心思想其实很朴素在有限的上下文窗口里动态地、有策略地放置此刻最该被模型看到的信息。它和提示词工程Prompt Engineering不是一回事——提示词工程关注的是怎么问上下文工程关注的是给模型看什么、看多少、什么时候看、什么时候忘。这篇文章我会围绕两个具体的抓手展开一个是ChatMemory 的滑动窗口机制另一个是Context-mode MCP 的上下文优化策略。前者解决的是对话历史怎么裁剪后者解决的是工具返回的内容怎么压缩和路由。两者配合起来基本能覆盖一个 AI 编码代理 80% 的上下文管理需求。适合谁看如果你正在做 AI 编码代理的开发、正在用 MCP 协议对接各种工具、或者单纯被代理聊着聊着就失忆的问题困扰过这篇内容应该能给你一些可以直接抄作业的思路。1.2 上下文窗口不是越大越好很多人有个误区既然模型支持 128K 甚至 200K 的上下文那我全塞进去不就行了我实测过在一个 128K 窗口的模型上当上下文填充到 60% 以上时模型对中间位置信息的召回率会明显下降。这个现象在业界被称为迷失在中间Lost in the Middle。也就是说上下文长度和有效信息密度是两回事。你塞了 10 万 token 进去模型真正能稳定利用的可能只有前 2 万和后 1 万。更现实的问题是成本和延迟。每一次工具调用都要把完整上下文重新送进模型token 消耗是线性增长的而响应延迟会随着上下文变长而显著上升。在一个需要几十轮工具调用的编码任务里不做上下文管理的代理token 成本可能是优化后的 5 到 10 倍。所以上下文工程的目标从来不是塞满窗口而是在正确的时间把正确的信息以正确的粒度放到正确的位置。这句话听起来像废话但真正落地的时候每一个正确都需要具体的机制来支撑。2. ChatMemory 滑动窗口对话历史的裁剪艺术2.1 ChatMemory 到底在管什么ChatMemory 这个概念在不同的框架里叫法不太一样有的叫 ConversationBuffer、有的叫 MessageHistory但本质都是同一件事维护一个有序的消息列表并在每次调用模型前决定哪些消息进入上下文。一个典型的 ChatMemory 里存的消息类型包括System Message系统提示词定义代理的角色、能力边界、输出格式要求Human Message用户的输入包括原始任务描述和后续追问AI Message模型的回复可能包含思考过程、工具调用请求Tool Message工具执行的结果比如读文件返回的内容、命令执行的输出这四类消息的生命周期是完全不同的。System Message 通常全程保留Human Message 里的原始任务描述需要长期保留而 Tool Message 里的文件内容往往只在当前这一步有用用完就该丢。如果只是简单地按时间顺序保留最近 N 条消息会出现一个很尴尬的情况一次读大文件的操作可能产生一条 5000 token 的 Tool Message它会把前面好几轮有价值的对话全部挤出窗口。这就是为什么朴素的滑动窗口不够用需要更细粒度的策略。2.2 滑动窗口的三种裁剪策略与取舍我在实际项目里试过三种滑动窗口的实现方式各有适用场景下面这张表是我自己的对比总结策略核心逻辑优点缺点适用场景固定条数窗口保留最近 N 条消息实现简单行为可预测忽略消息长度差异大消息会挤爆窗口短对话、工具返回内容稳定的场景Token 预算窗口按 token 数从后往前填充精确控制上下文大小可能截断一条消息的中间部分需要严格控制成本的场景分层保留窗口System 首条 Human 永久保留其余按 token 预算裁剪关键信息不丢失实现复杂度较高长任务、多轮工具调用的编码代理我最终在编码代理里用的是分层保留窗口因为它解决了一个核心痛点原始任务描述不能丢。你想想一个编码任务可能要进行 30 轮工具调用如果第 25 轮的时候模型已经忘了用户最初要求用 Python 3.11 的语法特性那前面 24 轮的工作可能都要返工。分层保留的具体做法是这样的class LayeredChatMemory: def __init__(self, max_tokens8000, reserve_ratio0.3): self.max_tokens max_tokens self.reserve_ratio reserve_ratio # 为最新消息预留的比例 self.system_message None self.first_human_message None self.recent_messages [] def build_context(self): # 第一层System Message 永远保留 context [self.system_message] # 第二层首条 Human Message 永远保留原始任务 context.append(self.first_human_message) # 第三层从最近的消息往前填充直到用完预算 budget self.max_tokens - self._count_tokens(context) recent [] for msg in reversed(self.recent_messages): msg_tokens self._count_tokens([msg]) if budget - msg_tokens 0: break recent.insert(0, msg) budget - msg_tokens return context recent这段代码里有个细节值得说reserve_ratio这个参数我一般设成 0.3意思是给最新消息预留 30% 的预算。为什么要预留因为最新的工具返回结果往往是当前决策的直接依据如果它被裁掉了模型就会基于过时信息做判断。这个比例不是拍脑袋定的我试过 0.2、0.3、0.4 三档0.3 在大多数编码任务里表现最稳。2.3 消息摘要让被裁掉的历史留个念想滑动窗口有个天然的缺陷被裁掉的消息就彻底消失了。但有些信息虽然不需要原文却需要存在过的痕迹。比如代理前面已经读过pom.xml知道项目用的是 Spring Boot 3.2这个结论比文件原文重要得多。我的做法是在裁剪之前先对即将被裁掉的消息做一次摘要压缩。具体来说把连续的 Tool Message 和 AI Message 打包让模型生成一段 100 到 200 字的摘要然后作为一条特殊的 System Message 插回上下文。def summarize_and_evict(self, messages_to_evict): summary_prompt 请用不超过150字总结以下对话的关键信息 重点保留已确认的技术栈、已修改的文件、已发现的问题、待办事项。 不要保留具体的代码内容。 summary self.llm.invoke(summary_prompt format_messages(messages_to_evict)) # 作为一条带标记的系统消息插回 self.recent_messages.insert(0, SystemMessage( contentf[历史摘要] {summary}, metadata{type: summary, evicted_count: len(messages_to_evict)} ))这里有个坑我踩过摘要本身也会消耗 token。如果每裁一次就生成一次摘要摘要会越积越多最后反而占满窗口。我的解决办法是给摘要也设一个上限比如最多保留 3 条摘要超出的旧摘要再合并成一条。这个摘要的摘要机制听起来有点绕但实测下来能把长任务的上下文稳定控制在预算内。提示摘要生成最好用便宜的小模型来做不要用主模型。摘要质量差一点没关系它只是记忆锚点不是决策依据。2.4 滑动窗口的触发时机别等到满了才动手很多人实现滑动窗口是被动触发的——每次调用模型前检查一下超了就裁。这个思路能用但不够好。我现在的做法是主动预判在每次工具调用返回之后先估算一下这条 Tool Message 会占多少 token如果加上它之后会超过预算的 80%就提前触发一次裁剪和摘要。这样做的好处是避免临时抱佛脚式的裁剪——被动裁剪往往只能粗暴地砍掉最近的消息而主动裁剪可以从容地做摘要、做分层。估算 token 有个简单的经验公式英文大约 4 个字符 1 个 token中文大约 1.5 个字符 1 个 token代码介于两者之间大约 3 个字符 1 个 token。这个精度对于预算控制足够了不需要上真正的 tokenizer那样反而拖慢速度。3. Context-mode MCP工具返回内容的压缩与路由3.1 MCP 协议为什么让上下文问题雪上加霜MCPModel Context Protocol这两年在 AI 编码代理圈子里火得不行它把各种工具文件系统、浏览器、数据库、IDE统一成一套标准接口代理可以通过 MCP Server 调用它们。但便利的背后是新的上下文挑战每个 MCP 工具返回的内容格式、粒度、大小都不可控。我举个例子。用 Playwright MCP 让代理去抓一个网页返回的可能是完整的 DOM 树动辄几万 token用文件系统 MCP 读一个日志文件可能返回几千行用数据库 MCP 查一次可能返回一个几百行的结果集。这些内容如果原封不动塞进上下文几轮下来窗口就爆了。更麻烦的是MCP 工具返回的内容里大量是噪音。DOM 树里 90% 是样式和布局信息日志文件里大部分是重复的 INFO 级别记录数据库结果集里可能只有几列是当前任务关心的。上下文工程在这里的核心任务就是在工具返回结果进入上下文之前先做一次提纯。3.2 Context-mode 的三种压缩手段我在自己的代理里实现了一套 Context-mode 处理层夹在 MCP 工具和 ChatMemory 之间专门负责对工具返回内容做压缩。核心手段有三种第一种是结构化裁剪。针对不同工具返回的格式用不同的规则提取关键字段。比如 Playwright 返回的 DOM我只保留可见元素的文本内容和交互属性把 style、class 里的样式类名全部剥掉。数据库结果集只保留前 50 行和列名超出部分用还有 N 行未显示代替。第二种是语义摘要。对于非结构化的长文本比如日志、报错堆栈用一个小模型做摘要只保留错误类型、关键行号、异常信息。这里的关键是摘要要保留可操作性——比如报错堆栈行号和异常类名必须保留因为代理后续可能要基于这些信息去定位代码。第三种是引用替代。对于大文件不把内容塞进上下文而是把内容存到一个临时的上下文存储里只在上下文里放一个引用 ID 和一段简短描述。代理需要看具体内容时再通过一个专门的工具去取。这个思路有点像操作系统的虚拟内存——物理内存上下文窗口里只放当前活跃的页其余的存在磁盘外部存储上。class ContextModeProcessor: def __init__(self, storage, summarizer, max_inline_tokens1500): self.storage storage self.summarizer summarizer self.max_inline_tokens max_inline_tokens def process(self, tool_name, raw_result): tokens estimate_tokens(raw_result) # 小结果直接内联 if tokens self.max_inline_tokens: return raw_result # 大结果走引用模式 ref_id self.storage.put(raw_result) summary self.summarizer.summarize(tool_name, raw_result) return f[工具 {tool_name} 返回内容已存储] 引用ID: {ref_id} 摘要: {summary} 如需查看完整内容请调用 fetch_context(ref_id{ref_id})这套机制实测下来能把工具返回内容的平均 token 占用降低 70% 以上而且代理的任务完成率没有明显下降——因为它需要细节的时候知道去哪里取。3.3 上下文路由让不同的信息去不同的地方Context-mode 的另一个核心能力是路由。不是所有信息都该进对话上下文有些信息更适合放在别的地方。我把代理运行时的信息分成了三个存储层对话上下文当前任务相关的、需要模型直接看到的信息容量最小最宝贵工作记忆任务执行过程中的中间状态比如已修改的文件列表、已执行的命令历史通过工具按需查询长期记忆跨任务的知识比如项目的技术栈约定、代码规范通过检索注入路由的规则我总结成一句话决策依据进上下文执行记录进工作记忆背景知识进长期记忆。举个例子代理在重构一个函数时这个函数当前的实现是决策依据要进上下文之前已经改过哪几个函数是执行记录进工作记忆这个项目的命名规范是背景知识进长期记忆。这样分层之后上下文里永远只有当前这一步真正需要的东西。注意路由规则不要写得太复杂我见过有人搞了七八层存储结果代理自己都搞不清信息在哪。三层足够了再多就是过度设计。3.4 与 ChatMemory 的协同谁先谁后Context-mode 和 ChatMemory 的调用顺序很关键。我的实践是MCP 工具返回 → Context-mode 压缩 → 写入 ChatMemory → ChatMemory 裁剪 → 送入模型。这个顺序不能反。如果先写 ChatMemory 再压缩那 ChatMemory 里存的就是原始大内容裁剪的时候会误伤如果先裁剪再压缩那压缩的时候可能已经丢失了关键信息。先压缩后裁剪能保证 ChatMemory 里存的都是提纯过的内容裁剪的粒度也更可控。还有一个细节Context-mode 压缩后的引用 ID 要能被 ChatMemory 识别为可回收的消息。当引用对应的内容被取用之后那条引用消息就可以标记为低优先级在裁剪时优先淘汰。这个机制能让上下文始终保持新鲜。4. 实操从零搭一套上下文优化流水线4.1 环境准备与依赖选型先说技术栈。我用的是 Python 3.11主要依赖这几个库pip install langchain-core langchain-openai mcp tiktokenlangchain-core提供消息类型和 ChatMemory 的基础抽象langchain-openai用于调用模型换成其他厂商的 SDK 也行mcp是 MCP 协议的官方 Python SDKtiktoken用于精确的 token 计数预算控制用估算用经验公式模型方面我建议主模型和辅助模型分开。主模型负责编码决策用能力强的辅助模型负责摘要、压缩、路由判断用便宜快的小模型。这样成本能降下来一大截而且辅助任务对模型能力要求不高。4.2 核心模块的代码骨架整个流水线分四个模块我按数据流的方向依次说。模块一MCP 工具调用层。这一层负责和 MCP Server 通信拿到原始返回结果。关键是给每个工具调用加上超时和大小限制避免一个工具返回把整个流程卡死。async def call_mcp_tool(server, tool_name, params, timeout30, max_size1_000_000): try: result await asyncio.wait_for( server.call_tool(tool_name, params), timeouttimeout ) if len(str(result)) max_size: result str(result)[:max_size] \n[内容已截断] return result except asyncio.TimeoutError: return f[工具 {tool_name} 调用超时]模块二Context-mode 处理器。前面已经给过骨架这里补充一下摘要提示词的设计。摘要提示词要针对不同工具类型定制不能一套提示词打天下。SUMMARIZE_PROMPTS { read_file: 总结这个文件的内容保留文件用途、关键类/函数名、依赖关系。不要保留具体代码。, run_command: 总结命令执行结果保留退出码、错误信息、关键输出行。, query_db: 总结查询结果保留列名、数据规模、异常值。, browse_page: 总结网页内容保留页面主题、主要交互元素、关键文本。 }模块三分层 ChatMemory。前面给过核心逻辑这里补充消息优先级标记的实现。每条消息写入时打一个优先级标签裁剪时按优先级从低到高淘汰。PRIORITY { system: 100, first_human: 90, summary: 70, recent_human: 60, recent_ai: 50, tool_result: 30, context_ref: 20 # 引用消息优先级最低可随时淘汰 }模块四上下文组装与调用。最后一步把 ChatMemory 组装成模型能接受的格式调用主模型。这里要注意消息顺序System 在前摘要其次然后是保留的历史最后是最近的消息。4.3 参数调优几个关键数字的来历这套流水线里有几个参数直接决定效果我把调优过程说一下。max_tokens这个取决于主模型的窗口大小。我的经验是只用窗口的 50% 到 60%。比如 128K 窗口的模型我设成 64K 到 76K。留出的空间是给模型输出和突发的大工具返回用的。设太满会导致模型输出被截断设太少又浪费能力。max_inline_tokens工具返回内容内联的阈值我设的是 1500。这个数字是试出来的——低于 1500 的内容做引用模式的收益省下的 token抵不上引用带来的额外工具调用开销高于 1500引用模式就开始划算了。reserve_ratio给最新消息预留的预算比例0.3。前面说过这个比例保证最新工具返回不会被裁掉。摘要触发阈值当被裁消息的总 token 超过 2000 时才触发摘要。太小的裁剪不值得摘要直接丢就行。这几个数字不是绝对的不同模型、不同任务类型会有差异。但作为起点它们能让你少走很多弯路。4.4 一次完整的任务执行记录我拿一个真实任务跑一遍让你看看这套流水线在实际中怎么工作。任务是给这个 Spring Boot 项目加一个订单导出 Excel 的接口。第 1 轮用户输入任务。ChatMemory 里只有 System Message 和这条 Human Message上下文占用约 800 token。第 2 轮代理调用文件系统 MCP 读取项目结构。返回一个 3000 token 的目录树。Context-mode 判断超过 1500 阈值做摘要压缩到 200 token原文存入引用存储。上下文占用约 1100 token。第 3 轮代理调用 MCP 读取OrderController.java。返回 2500 token。同样压缩摘要保留类名、方法列表、依赖注入的字段。上下文占用约 1400 token。第 4 到 10 轮代理陆续读取了 Service、Mapper、实体类每轮都走压缩。到第 10 轮时ChatMemory 里积累了 7 条摘要消息加上原始任务和最近的对话上下文占用约 5000 token。第 11 轮代理开始写代码调用文件写入 MCP。返回结果很短写入成功直接内联。此时 ChatMemory 触发了一次裁剪把第 2 到 5 轮的摘要合并成一条项目结构摘要上下文占用回落到 4200 token。第 12 到 20 轮代理反复修改代码、运行测试。每次测试失败返回的报错堆栈被压缩成错误类型 关键行号 异常信息。到第 20 轮上下文占用稳定在 6000 token 左右。任务结束整个任务 20 轮工具调用如果不用这套流水线上下文峰值会超过 40000 token用了之后峰值控制在 6000 token 以内token 成本降低约 85%任务完成质量没有下降。5. 常见问题与排查技巧实录5.1 代理失忆了怎么办这是最常见的问题。表现是代理突然问一个前面已经确认过的信息或者重复执行已经做过的操作。排查思路按这个顺序走先看 ChatMemory 的裁剪日志。是不是关键消息被裁掉了如果是检查优先级标记是否正确原始任务描述有没有被标记为first_human。再看摘要质量。如果关键信息被裁掉但摘要里应该有那就是摘要生成出了问题。检查摘要提示词是否覆盖了这类信息。最后看上下文组装顺序。有时候消息都在但顺序不对模型也会看不见。System 和摘要必须在前面最近消息在后面。我遇到过一次诡异的情况代理反复忘记项目用的是 Java 17。查了半天发现是摘要提示词里写了不要保留具体代码结果小模型把Java 17也当成代码细节给删了。后来把提示词改成保留技术栈版本信息问题解决。5.2 工具返回内容被压缩后代理无法操作这个问题的表现是代理说我需要查看完整内容但不知道怎么取或者取回来之后又不会用。根因通常是引用消息的格式不清晰。代理不知道fetch_context这个工具的存在或者不知道引用 ID 怎么用。解决办法是在 System Message 里明确说明引用机制当工具返回内容被压缩为引用时你会看到 [工具 X 返回内容已存储] 的标记。 如需查看完整内容调用 fetch_context 工具并传入引用 ID。另外fetch_context返回的内容也要走一遍 Context-mode 处理避免取回来一个大内容又把上下文撑爆。这里可以设一个取用后自动摘要的规则。5.3 摘要越积越多反而占满窗口前面提过这个问题这里给一个具体的解决参数。我设的规则是摘要消息最多保留 5 条超过 5 条时把最旧的 2 条合并成 1 条合并后的摘要标记为二级摘要优先级比普通摘要低二级摘要最多保留 2 条超过就再合并这样层层收敛摘要的总量始终有上限。实测在 50 轮以上的长任务里摘要占用的 token 稳定在 800 以内。5.4 常见问题速查表现象可能原因排查动作解决方向代理重复问已确认信息关键消息被裁查裁剪日志和优先级标记提升关键消息优先级代理说看不到完整内容引用机制未说明查 System Message补充引用工具说明上下文占用居高不下摘要未收敛查摘要数量和大小启用摘要合并机制工具调用超时MCP Server 响应慢查工具调用日志加超时和降级返回代理输出被截断max_tokens 设太大查上下文占用比例降到窗口的 50%-60%摘要丢失关键信息提示词覆盖不全查摘要提示词按工具类型定制提示词5.5 几个我踩过的坑坑一用主模型做摘要。一开始图省事摘要也用主模型结果成本没降下来延迟还上去了。后来换成小模型摘要质量略降但完全够用。坑二裁剪粒度太粗。早期我是按轮裁剪的一轮对话要么全留要么全丢。后来改成按消息裁剪灵活多了。一条大 Tool Message 可以单独被压缩不影响同一轮的其他消息。坑三忽略工具返回的元数据。MCP 工具返回的结果里往往带有元数据比如文件大小、查询耗时这些信息对代理决策有用压缩时不能丢。我现在会在摘要里专门保留一段元数据。坑四上下文路由规则写太死。一开始我给每类信息都写死了去哪一层结果遇到边界情况就卡住。后来改成默认进工作记忆显式标记才进上下文灵活多了。6. 上下文工程的边界与延伸6.1 什么情况下这套方案不适用说实话这套流水线不是万能的。如果你的任务很短少于 5 轮工具调用上这套机制反而是过度设计直接全量塞上下文更简单。如果任务对细节精度要求极高比如逐行代码审查压缩和摘要可能丢失关键信息这时候宁可牺牲成本也要保留原文。还有一种情况是工具返回内容本身就很小。如果你的 MCP 工具都是返回几十 token 的短结果那 Context-mode 的压缩层基本是空转可以关掉。判断标准很简单当上下文占用成为瓶颈时才需要上下文工程。没到瓶颈就上是给自己找麻烦。6.2 可以继续深挖的方向这套方案目前解决的是单代理、单任务的上下文管理。往深了走还有几个方向值得探索。多代理协作的上下文隔离。当多个代理协同工作时每个代理的上下文应该独立管理只通过明确的接口交换信息。这比共享一个大上下文要清晰得多也更容易调试。基于任务阶段的动态策略。编码任务的不同阶段探索、设计、实现、测试对上下文的需求是不一样的。探索阶段需要大量文件内容实现阶段需要精确的接口定义测试阶段需要报错信息。如果能根据阶段动态调整压缩策略效果会更好。上下文质量的度量。现在判断上下文管理好不好主要靠代理有没有出错这种间接指标。如果能有一个直接的度量比如关键信息召回率调优会更有方向。这个方向目前还没有成熟的方案但值得关注。6.3 我个人的一点体会做上下文工程这两年最大的感受是它更像是一门取舍的手艺而不是堆料的技术。很多人一上来就想把所有信息都留住结果反而什么都留不住。真正有效的做法是承认上下文的稀缺性然后逼着自己去判断什么才是此刻真正重要的。这个判断能力一部分靠机制优先级、摘要、路由一部分靠经验知道什么信息在什么阶段有用。机制可以抄经验只能自己攒。我建议你在自己的项目里先把这套流水线跑起来然后根据实际遇到的问题去调参数、改规则。跑上十几个真实任务之后你对什么该留什么该丢的直觉就会建立起来。最后分享一个小技巧给代理加一个上下文自省的能力。让它在每次决策前先输出一句我当前基于哪些信息做判断。这句话会逼着代理去检索上下文也能让你直观地看到它到底看见了什么。这个技巧帮我定位过好几次上下文丢失的问题成本几乎为零但效果立竿见影。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑