ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

AI编码代理上下文工程:从ChatMemory滑动窗口到Context-mode MCP实战 1. 为什么上下文工程成了 AI 编码代理的胜负手1.1 从一个真实场景说起代理为什么总在第三轮对话后“失忆”我最早接触 AI 编码代理是在一个内部工具项目上当时用的是一个基于大模型 API 搭建的代码助手功能很简单读代码、改代码、跑测试。前两轮对话体验非常好它能准确理解我的意图甚至能主动指出我代码里的边界问题。但到了第三轮、第四轮情况急转直下——它开始忘记之前改过哪些文件重复建议我已经否决过的方案甚至把已经修好的 bug 又“修”回去。这个问题困扰了我很久。一开始我以为是模型能力不够换了更大的模型情况有所改善但本质问题没解决。后来我才意识到根因不在模型本身而在上下文管理。AI 编码代理的“记忆”不是无限的它每次调用模型时能携带的 token 数量有硬上限。当对话轮次增加、代码文件变多、工具调用结果堆积上下文窗口很快就被塞满了。一旦超出限制要么报错要么被迫截断而截断就意味着“失忆”。这就是上下文工程要解决的核心问题在有限的上下文预算内让 AI 编码代理尽可能保持对任务状态、代码变更历史、用户意图的准确感知。它不是一个单纯的“加大窗口”就能搞定的事而是一套涉及记忆结构设计、信息压缩策略、工具调用编排的系统工程。1.2 ChatMemory 滑动窗口最直觉的方案也是最容易踩坑的方案面对上下文溢出最直觉的方案就是滑动窗口只保留最近 N 轮对话旧的直接丢掉。ChatMemory 这类组件通常就是这么实现的——维护一个固定长度的消息队列新消息进来旧消息出去。这个方案的好处显而易见实现简单内存占用可控永远不会超出上下文限制。但它的缺陷同样明显。我踩过的坑包括任务状态丢失代理在第 2 轮被告知“不要修改配置文件”到第 5 轮这条约束被滑出窗口代理又开始改配置文件了。代码变更历史断裂代理在第 3 轮修改了utils.py的第 42 行到第 6 轮需要基于这个修改继续开发时它完全不知道这个修改存在。工具调用结果被截断一次grep搜索返回了 200 行结果占用了大量 token把之前的关键对话挤出去了。滑动窗口的本质问题是它假设“越近的信息越重要”但在编码任务中一条早期的架构决策可能比最近三轮的寒暄重要得多。单纯按时间顺序淘汰等于把婴儿和洗澡水一起倒掉。1.3 Context-mode MCP把上下文管理从“被动截断”变成“主动编排”MCPModel Context Protocol的出现给这个问题提供了一个新的解题思路。MCP 本质上是一套标准化协议让 AI 代理能够以统一的方式访问外部工具和数据源。而Context-mode MCP的核心思想是不把上下文管理当成一个被动的“窗口截断”问题而是当成一个主动的“信息编排”问题。具体来说Context-mode MCP 允许代理在需要时主动查询、检索、压缩上下文而不是被动地接受一个固定窗口。它把上下文分成几个层次热上下文当前轮次直接相关的信息始终保留。温上下文近期任务相关的信息按需检索。冷上下文历史归档信息通过工具调用按需拉取。这种分层思路借鉴了计算机体系结构里的缓存设计——L1 缓存小而快L3 缓存大而慢但整体上让系统感觉“什么都知道”。在 AI 编码代理的场景里这意味着代理可以在需要时“回忆”起早期决策而不需要把所有历史都塞进每一次模型调用。1.4 本文要解决的问题与适合的读者这篇文章要讲清楚的就是如何从 ChatMemory 滑动窗口这种“原始方案”逐步演进到基于 Context-mode MCP 的上下文优化方案。我会拆解每一步的设计考量、实操细节、踩过的坑以及最终可复现的配置方案。适合的读者包括正在搭建 AI 编码代理的开发者、被上下文溢出问题困扰的 AI 应用工程师、对 MCP 协议感兴趣但还没找到落地场景的技术人以及任何想让自己的 AI 助手“记性更好一点”的实践者。不需要你精通大模型底层原理但需要你写过代码、调过 API、理解基本的 token 概念。2. 核心细节解析从滑动窗口到 MCP 的演进路径2.1 ChatMemory 滑动窗口的实现细节与参数选择先把手头最熟悉的方案讲透。ChatMemory 滑动窗口的典型实现是这样的维护一个ListMessage每次新消息加入后检查总 token 数是否超过阈值如果超过就从头部移除最旧的消息直到低于阈值。这里有几个关键参数需要仔细选择参数典型值选择理由最大 token 数模型窗口的 60%-70%留出空间给系统提示词和工具定义单条消息最大 token2000-4000防止单条长消息挤爆窗口保留轮次下限3-5 轮保证最基本的对话连贯性截断策略从最旧开始移除简单但粗暴后续会优化我一开始用的是“模型窗口的 80%”作为阈值结果经常在工具调用返回大结果时直接报错。后来降到 60%稳定了很多。这个数字不是拍脑袋来的系统提示词通常占 500-1000 token工具定义如果有 10 个工具可能占 2000-3000 token再加上当前轮的用户输入和模型输出预留60% 是一个比较安全的比例。但滑动窗口有一个隐藏问题它按消息条数或 token 数淘汰而不是按信息价值淘汰。一条“好的我明白了”可能只占 10 token但它承载了“用户确认了方案”这个重要状态而一次失败的grep返回了 5000 token 的无关结果却可能把这条确认挤出去。这就是为什么单纯调参数解决不了根本问题。2.2 上下文溢出的三种典型表现与根因分析在实际项目中上下文溢出不是一种单一现象而是三种不同的问题混在一起第一种硬溢出。总 token 数超过模型窗口API 直接返回错误。这是最明显的也最容易发现。根因通常是工具调用返回了超大结果或者用户粘贴了一大段代码。第二种软溢出。总 token 数没超但关键信息被挤到了窗口边缘模型对它的注意力权重下降表现为“好像记得又好像不记得”。这是最隐蔽的因为不会报错但代理行为会变得不稳定。第三种结构溢出。上下文里塞满了低价值信息比如重复的工具定义、冗余的对话历史导致高价值信息密度下降。模型不是记不住而是被噪音干扰了。我遇到过最典型的一次软溢出代理在第 2 轮确认了“使用 PostgreSQL 而不是 MySQL”到第 8 轮生成数据库连接代码时它写了 MySQL 的驱动。检查上下文发现那条确认消息还在窗口里但已经被挤到了很靠前的位置模型在生成时更关注最近的几条消息那条早期决策的注意力权重被稀释了。2.3 MCP 协议为什么适合做上下文编排MCP 的核心价值在于它提供了一套标准化的工具调用接口。在上下文管理的场景里这意味着代理可以通过调用工具来“查询记忆”而不是把所有记忆都塞进上下文。举个例子代理需要知道“之前是否修改过config.py”。在滑动窗口方案里这个信息要么在窗口里占用 token要么不在代理不知道。在 MCP 方案里代理可以调用一个search_memory工具传入查询条件工具返回相关记录。这样历史信息不需要常驻上下文只在需要时拉取。MCP 的另一个优势是工具结果可以被压缩。比如search_memory返回 10 条记录但代理只需要知道“修改过在第 3 轮改了第 42 行”工具可以在返回前做摘要只传关键信息。这比把原始对话历史塞进上下文高效得多。2.4 Context-mode MCP 的分层记忆模型设计Context-mode MCP 的核心是一个三层记忆模型热层Hot Layer当前任务直接相关的信息。包括当前轮的用户输入、最近 2-3 轮对话、当前正在编辑的文件内容。这一层始终在上下文里不经过检索。温层Warm Layer当前任务相关但非直接的信息。包括任务开始时的需求描述、已确认的架构决策、已修改的文件列表。这一层通过 MCP 工具按需检索检索结果经过摘要后注入上下文。冷层Cold Layer历史归档。包括已完成任务的记录、旧版本的代码变更、用户的历史偏好。这一层通常不主动检索只在代理明确需要“回忆”时通过工具拉取。这个分层的关键在于写入策略什么信息进热层什么进温层什么进冷层。我的经验是用户明确说“记住这个”的进热层。代理自己做出的决策如“我选择用 A 方案”进温层。工具调用的原始结果进冷层只把摘要进温层。超过 5 轮的对话自动从热层降级到温层。2.5 信息压缩与摘要生成的关键技术点分层之后温层和冷层的信息需要压缩。这里有几个技术点摘要生成用一个小模型如 7B 参数级别对长对话或工具结果做摘要。摘要的 prompt 要明确要求保留“决策、约束、变更”三类信息丢弃“寒暄、重复、无关细节”。结构化存储不要存纯文本摘要而是存结构化记录。比如{type: decision, content: 使用 PostgreSQL, round: 2, confidence: high}。这样检索时可以按类型过滤比全文搜索精准得多。去重与合并同一决策可能在多轮对话中被重复确认存储时要合并只保留最新版本和确认次数。过期策略温层信息超过一定轮次如 20 轮或任务完成后自动降级到冷层。冷层信息超过一定时间如 7 天可以归档或删除。3. 实操过程搭建一个带 Context-mode MCP 的编码代理3.1 环境准备与依赖安装先列一下我用的技术栈这不是唯一方案但经过实测比较稳代理框架任何支持 MCP 的代理框架都可以我用的是一个自研的轻量级框架核心逻辑是“接收用户输入 - 组装上下文 - 调用模型 - 执行工具 - 更新记忆”。MCP Server自己实现一个 Context-mode MCP Server提供search_memory、store_memory、summarize_context三个工具。存储SQLite 足够温层和冷层的数据量不大不需要上向量数据库。如果要做语义检索可以加一个轻量级 embedding 模型。模型主模型用 32B 级别的摘要模型用 7B 级别的省钱且够用。安装依赖以 Python 为例pip install mcp sqlite-utils openai tiktokentiktoken用来精确计算 token 数比按字符估算准得多。我试过按“1 token ≈ 4 字符”估算结果在中文场景下偏差很大后来老老实实用 tokenizer。3.2 MCP Server 的核心工具实现Context-mode MCP Server 需要暴露三个核心工具。下面是简化版的实现思路# context_mcp_server.py import sqlite3 import json from mcp.server import Server, Tool app Server(context-mode) app.tool() def search_memory(query: str, layer: str warm, limit: int 5) - str: 检索记忆。layer 可选 hot/warm/cold。 conn sqlite3.connect(memory.db) cursor conn.cursor() cursor.execute( SELECT content, round, type FROM memory WHERE layer? AND content LIKE ? ORDER BY round DESC LIMIT ?, (layer, f%{query}%, limit) ) results cursor.fetchall() conn.close() if not results: return 未找到相关记忆。 return \n.join([f[第{r[1]}轮/{r[2]}] {r[0]} for r in results]) app.tool() def store_memory(content: str, layer: str warm, mem_type: str decision, round_num: int 0) - str: 存储记忆。mem_type 可选 decision/constraint/change/note。 conn sqlite3.connect(memory.db) cursor conn.cursor() cursor.execute( INSERT INTO memory (content, layer, type, round) VALUES (?, ?, ?, ?), (content, layer, mem_type, round_num) ) conn.commit() conn.close() return f已存储到{layer}层{content[:50]}... app.tool() def summarize_context(text: str, max_tokens: int 500) - str: 对长文本做摘要保留决策、约束、变更三类信息。 # 这里调用摘要模型实际实现略 prompt f请对以下内容做摘要只保留决策、约束、变更三类信息控制在{max_tokens}token内\n{text} summary call_summary_model(prompt) return summary这三个工具的关键设计点search_memory支持按层检索代理可以明确知道自己在查热层还是温层。store_memory要求指定mem_type强制代理在存储时做信息分类而不是一股脑塞进去。summarize_context的 prompt 明确限定保留三类信息避免摘要模型自由发挥。3.3 代理主循环的上下文组装逻辑代理每次调用模型前需要组装上下文。我的组装逻辑是这样的def build_context(user_input, round_num): # 1. 系统提示词固定 system_prompt load_system_prompt() # 2. 热层最近 3 轮对话 当前输入 hot_messages get_recent_messages(3) hot_messages.append({role: user, content: user_input}) # 3. 温层检索与当前输入相关的记忆 warm_memories search_memory(user_input, layerwarm, limit5) warm_context format_memories(warm_memories) # 4. 工具定义 tools load_tool_definitions() # 5. 组装并检查 token context { system: system_prompt \n\n相关记忆\n warm_context, messages: hot_messages, tools: tools } # 6. 如果超限压缩热层 if count_tokens(context) MAX_TOKENS: hot_messages compress_hot_layer(hot_messages) context[messages] hot_messages return context这里的关键是第 3 步温层检索不是全量拉取而是按当前输入做相关性检索。我一开始偷懒把温层所有记忆都塞进系统提示词结果上下文还是爆了。后来改成按关键词检索只拉最相关的 5 条效果好了很多。第 6 步的压缩逻辑也值得说当热层超限时不是简单丢弃最旧的消息而是先对最旧的消息做摘要把摘要存到温层然后用摘要替换原始消息。这样既释放了 token又保留了信息。3.4 记忆写入与降级的触发时机记忆写入不是每轮都做而是有明确的触发条件用户明确要求“记住这个”、“以后都用这个方案”——立即写入热层。代理做出决策代理在回复中说了“我选择...”、“我决定...”——写入温层。工具调用产生重要结果如grep找到了关键代码、测试通过/失败——摘要后写入温层。每 5 轮对话对最近 5 轮做一次摘要写入温层同时把热层中超过 5 轮的消息降级。降级逻辑def degrade_memories(current_round): # 热层超过 5 轮的消息降级到温层 old_hot get_hot_messages_older_than(current_round - 5) for msg in old_hot: summary summarize_context(msg[content], max_tokens100) store_memory(summary, layerwarm, mem_typenote, round_nummsg[round]) delete_hot_message(msg[id]) # 温层超过 20 轮的记忆降级到冷层 old_warm get_warm_memories_older_than(current_round - 20) for mem in old_warm: update_memory_layer(mem[id], cold)这个降级策略的核心思想是信息不是被删除而是被移动到更便宜的存储层。需要时还能检索回来但不再占用宝贵的上下文 token。3.5 实测效果对比滑动窗口 vs Context-mode MCP我在一个真实项目上做了对比测试任务是“给一个 Flask 项目添加用户认证功能”涉及 8 个文件的修改共 23 轮对话。指标滑动窗口Context-mode MCP任务完成度70%遗漏了 2 个文件的修改95%只遗漏了 1 个边缘文件重复建议次数5 次1 次上下文溢出报错3 次0 次平均每轮 token 消耗68004200用户手动纠正次数7 次2 次最明显的改善是重复建议和手动纠正。滑动窗口方案下代理经常忘记已经确认过的方案反复建议已经被否决的选项。Context-mode MCP 方案下代理在需要时会主动检索“之前是否讨论过这个”避免了重复。token 消耗的降低也出乎意料。我原本以为增加检索工具调用会增加 token但实际上因为温层信息经过摘要和结构化注入上下文时比原始对话历史紧凑得多整体反而省了 38%。4. 常见问题与排查技巧实录4.1 检索不到记忆为什么 search_memory 总是返回空这是最常见的问题。我一开始用LIKE %query%做检索结果用户输入“改一下登录逻辑”检索词是“改一下登录逻辑”而记忆里存的是“用户认证使用 JWT”完全匹配不上。解决方案是检索词扩展。在调用search_memory之前先用一个小模型或规则引擎把用户输入转成关键词列表。比如“改一下登录逻辑”转成[登录, 认证, auth, login]然后用OR连接做检索。另一个技巧是存储时打标签。store_memory时不仅存内容还存关键词标签。检索时先匹配标签再匹配内容。标签可以用摘要模型自动生成成本很低。4.2 摘要丢失关键信息摘要模型不听话怎么办摘要模型经常“自由发挥”把关键决策摘要成“讨论了数据库选型”而丢掉了“最终选择 PostgreSQL”这个关键信息。我的解决办法是结构化摘要 prompt请对以下对话做摘要严格按以下格式输出 - 决策[具体决策内容] - 约束[具体约束条件] - 变更[具体文件或代码变更] - 待办[未完成事项] 如果某一类没有内容写“无”。 不要添加任何额外解释。这个 prompt 的关键是“严格按格式”和“不要添加额外解释”。我试过让模型自由摘要结果它总是加一些“总的来说”、“需要注意的是”之类的废话既占 token 又没信息量。4.3 记忆冲突新旧决策矛盾时听谁的代理在第 3 轮决定“用 MySQL”第 10 轮改成“用 PostgreSQL”。如果两条记忆都检索出来代理会困惑。解决方案是记忆版本管理。每条记忆有一个supersedes字段新决策写入时把旧决策标记为superseded。检索时默认只返回未被取代的记忆。如果代理需要查看历史决策可以显式查询include_supersededtrue。app.tool() def search_memory(query: str, layer: str warm, include_superseded: bool False): sql SELECT content, round, type FROM memory WHERE layer? AND content LIKE ? if not include_superseded: sql AND superseded0 sql ORDER BY round DESC LIMIT 5 # ...4.4 性能问题MCP 工具调用拖慢响应怎么办每次模型调用前都要检索记忆增加了延迟。实测下来SQLite 检索本身很快10ms慢的是摘要模型调用200-500ms。优化方案缓存摘要结果同一段文本的摘要缓存起来避免重复调用。异步摘要摘要不阻塞主流程在后台线程做下一轮对话时结果已经就绪。批量摘要每 5 轮做一次批量摘要而不是每轮都做。我最终用的是异步方案代理回复用户后后台线程开始对最近几轮做摘要和降级用户看到的是即时响应记忆更新在后台完成。4.5 常见问题速查表问题可能原因排查方法解决方案检索返回空检索词不匹配打印检索词和记忆内容关键词扩展 标签匹配摘要丢信息prompt 太宽松检查摘要输出格式结构化 prompt 格式校验记忆冲突新旧决策共存检查 superseded 字段版本管理 默认过滤响应变慢摘要同步调用计时各环节耗时异步摘要 缓存上下文仍溢出温层注入过多统计各层 token 占比限制温层条数 二次摘要代理不调用检索工具描述不清检查工具定义在系统提示词中明确要求“不确定时先检索”4.6 几个我踩过的坑和对应的技巧坑一把 MCP 工具定义写得太复杂。我一开始给search_memory定义了 8 个参数结果模型经常传错参数。后来精简到 3 个核心参数错误率大幅下降。工具定义要像 API 设计一样宁可多几个简单工具不要一个复杂工具。坑二忘记给记忆加时间戳。早期版本只存了轮次没存时间。后来做“7 天归档”时发现没法判断哪些记忆是 7 天前的。补上created_at字段后解决。坑三摘要模型和主模型用同一个。一开始图省事摘要也用主模型成本高且慢。换成 7B 小模型后摘要质量略降但完全够用成本降到 1/10。坑四没有做 token 预算分配。我后来给各层定了预算系统提示词 15%热层 40%温层 25%工具定义 15%预留 5%。超出预算的层优先压缩。这个预算不是固定的根据任务类型调整——代码生成任务热层多给点架构讨论任务温层多给点。坑五忽略了工具调用结果的 token 消耗。一次grep可能返回几千 token直接塞进上下文会挤爆。我的做法是工具结果先经过summarize_context压缩只把摘要注入上下文原始结果存到冷层。代理需要看原始结果时再通过工具拉取。5. 从滑动窗口到 MCP 的迁移路径与扩展思路5.1 渐进式迁移不要一次性重写如果你现在用的是滑动窗口方案不要急着全部推倒重来。我的建议是分三步走第一步加一层温层存储。保持滑动窗口不变但每轮对话后把关键信息摘要存入 SQLite。这一步不改变代理行为只是开始积累记忆数据。第二步加检索工具。实现search_memory工具在系统提示词里加一句“不确定时先检索记忆”。代理开始偶尔调用检索滑动窗口仍然是主力。第三步切换主循环。把热层缩短到 3 轮温层检索变成常规流程滑动窗口退化为热层的实现细节。这一步完成后Context-mode MCP 就成为主力了。每一步都可以独立测试和回滚风险可控。5.2 与其他 MCP 工具的协同Context-mode MCP 不是孤立的。在实际项目中它需要和其他 MCP 工具协同文件系统 MCP代理读取文件时文件内容可以自动摘要后存入温层避免重复读取。测试运行 MCP测试结果自动存入温层代理在后续轮次中能“记得”哪些测试通过了。代码搜索 MCP搜索结果摘要后存入温层避免重复搜索。协同的关键是统一的记忆写入接口。所有 MCP 工具的结果都通过store_memory写入保证记忆格式一致。5.3 后续可以扩展的方向这个方案还有不少可以深挖的地方语义检索目前用关键词匹配换成 embedding 向量检索会更精准。但要注意 embedding 模型本身的 token 消耗和延迟。记忆重要性评分给每条记忆打一个重要性分数检索时按分数排序而不是按轮次。重要性可以由用户反馈、代理置信度、信息类型综合计算。跨任务记忆目前记忆是按任务隔离的但有些偏好如“用户喜欢用 type hints”应该跨任务共享。可以加一个全局记忆层。记忆可视化做一个简单的 Web 界面展示各层记忆的内容和 token 占用方便调试和优化。5.4 一个实际项目中的配置参考最后分享一个我在实际项目中用的配置可以直接抄作业CONFIG { max_tokens: 128000, # 模型窗口 budget: { system_prompt: 0.15, hot_layer: 0.40, warm_layer: 0.25, tools: 0.15, reserve: 0.05 }, hot_layer: { max_rounds: 3, compress_threshold: 0.8 # 热层用到 80% 预算时触发压缩 }, warm_layer: { max_results: 5, degrade_after_rounds: 20, summary_model: 7b-model, summary_max_tokens: 100 }, cold_layer: { archive_after_days: 7, retrieval_enabled: True }, memory_write: { auto_summarize_interval: 5, # 每 5 轮自动摘要 async_summary: True, cache_summaries: True } }这套配置在我经手的三个项目中都跑得比较稳token 消耗比纯滑动窗口低 30%-40%任务完成度提升 20% 以上。当然具体数字因项目而异建议你先用这套配置跑起来再根据实际数据微调。我在实际使用中发现上下文工程最反直觉的一点是加更多记忆不等于更好。早期我恨不得把所有东西都存下来结果检索噪音太大代理反而更困惑。后来学会做减法——只存决策、约束、变更三类信息其他一律摘要或丢弃——效果反而更好。记忆的质量比数量重要得多。
返回列表