最近我在给一个多文件项目接入 AI 编码代理时,遇到了一个非常典型的问题:会话一长,代理就开始“失忆”——前面定好的接口签名它转头就忘,改了 A 文件又拿旧逻辑去写 B 文件,你气得想摔键盘,它还在那儿一本正经地胡说八道。
解决方案大家其实都知道:把上下文管理起来。但真正动手做,就会掉进一连串细节坑里——上下文窗口到底开多大?哪些历史必须留?工具调用时的上下文怎么裁剪?为什么 MCP 火了这么久,接进来反而让上下文更乱?这篇文章,我想把这段时间折腾出来的经验做个完整复盘,从 ChatMemory 的滑动窗口机制讲起,一直聊到 Context-mode 模式下的 MCP 上下文优化,全程都是可落地的东西,适合正在用 Cursor、Claude Code、Codex 这类编码代理,又被上下文问题反复折磨的同学。
1. 上下文工程的底层逻辑:AI 编码代理为什么比聊天机器人更容易“断片”
1.1 核心矛盾:上下文窗口是有限的,项目是无限的
先放下工具,理解一个最本质的问题:AI 编码代理本质上是个大语言模型,模型一开场的上下文窗口大小是写死的,比如 Claude 200K、GPT-4o 128K。但人类的代码项目是长在硬盘上的,动辄几千个文件、几十万行代码。要让 AI 帮你改代码,你必须把“项目的近况”塞进那个有限窗口里,否则它就是一台没有硬盘的电脑,每次开机都等于重新做人。
这就形成了上下文工程的第一重矛盾:窗口有限,项目无穷,你不可能把整个代码库都喂进对话框。
很多人第一反应是“那我每次把所有相关文件都粘进去不就行了”。在 50 个文件以内的玩具项目里,这招确实能跑。但到了真实项目里,你很快会发现三个问题:第一,Token 成本飞速上涨,一次全量带上下文的长对话可能烧掉十几万 Token,长期下来费用根本扛不住;第二,无关代码会严重污染模型的注意力,把真正关键的文件淹没在噪声里,效果反而不如少带几个文件;第三,旧的对话历史会持续占着窗口,最新的指令反而挤不进去,代理开始“只顾尾巴忘开头”。这三条,基本就是编码代理“越用越笨”的元凶。
所以上下文工程的第一性原则其实是:窗口预算纪律。你得像一个项目经理一样,给每一类信息分配预算——项目背景占多少、当前会话目标占多少、相关代码片段占多少、历史决策记录占多少。超预算的信息,要么压缩、要么淘汰、要么外置到检索系统里,绝不能无脑堆进软上下文。
1.2 编码场景特有的三类上下文:任务级、会话级、项目级
通用聊天机器人需要的上下文,基本就是“刚才聊了什么”,建模简单很多。但编码代理面临的是三层上下文的叠加,复杂度完全不在一个量级。
第一层是任务级上下文,也就是用户当下的那条指令、正在改的那个函数、报错信息的那几行日志。这一层精度要求最高,错一个字都可能跑偏。
第二层是会话级上下文,覆盖从本次会话开始到现在,用户提过哪些需求、确认过哪些细节、否决过哪些方案。ChatMemory 这类机制处理的主要就是这一层,因为会话越久,这一层的量越大,不淘汰迟早爆窗。
第三层是项目级上下文,也就是代码库的整体结构、关键模块的职责、依赖关系、编码规范。这一层信息量最大,通常不可能全靠对话窗口承载,需要借助 MCP 这类工具协议按需拉取。
关键是这三层之间还会互相干扰。我吃过一个很实在的亏:连续改了 40 多分钟后,代理为了迎合我在会话早期说的一句“尽量用 Python 原生的方式”,硬是在一个性能敏感模块里用纯 Python 实现了一个本应该调用 C 扩展的功能。它没有坏心,它就是单纯地被几十分钟前的会话级上下文带偏了。这种问题,单靠“加长窗口”永远无解,必须靠精密的裁剪和淘汰策略才能压住。
1.3 上下文管理的三个技术路线:淘汰、压缩、按需拉取
既然窗口有限,能做的就是三件事:淘汰低价值历史、压缩高价值历史、对项目级信息按需拉取。
淘汰对应着滑动窗口,最古老也最直接——保留最近 N 轮对话,再往前直接丢弃。代价是你会丢长程依赖,比如“两小时前定好的命名规范”。压缩对应摘要机制,把早期对话提炼成几句话的纪要,既保留了关键约束,又不占太多空间。代价是细节损耗,摘要不会替你记得每行代码。按需拉取对应的是 MCP(Model Context Protocol)这类工具协议,让代理在需要时自己去找代码库、查文档、取数据,而不是一股脑全带进窗口。
很多团队在做上下文工程时只盯着其中一条路线,比如疯狂调滑动窗口大小,或者迷信 MCP 万能。我自己的体会是:这三条路线必须配合使用,淘汰保底、压缩保鲜、按需拉取补齐项目级信息。后面我会分别拆开讲,再给一套组合落地的方案。
2. ChatMemory 与滑动窗口机制:怎么淘汰历史才不伤记忆
2.1 滑动窗口的核心原则:近热远冷、近存远汰
ChatMemory 这个名字听起来很高大上,本质上它是一套“该记什么、该忘什么”的记忆调度策略。而滑动窗口,是这套策略最常用的执行机制。
你可以把对话历史想象成一条传送带,最新的消息永远在最右边,窗口就像一个固定宽度的取景框,只显示最近的内容。随着对话推进,左边的内容被推出框外,新内容从右边进入。这个机制背后的假设非常直接:离当前 moment 越近的信息,越可能是当前任务需要的。在大多数编码场景里,这个假设是成立的——你刚让代理重构了parse_config,它大概率接下来还在这几个函数附近工作。
但工程上有个关键细节:窗口的计量单位到底是什么?我见过三种主流设计。
第一种是消息条数窗口,实现最简单,直接数对话轮次,保留最近 40 条,超出就丢。问题是聊天消息长短不一,有时候一条消息顶十条的 Token 量,按条数分配极不均匀。
第二种是Token 数量窗口,严格按预算控制,比如窗口上限 100K Token,塞满了就把最旧的丢出去。这样更接近真实成本约束,但在 API 实作里需要每次请求前动态裁剪,实现复杂度高不少。
第三种是混合窗口,先按消息条数粗筛,再按 Token 精算,外层再加摘要兜底。这也是我认为最实用的方案。
实际编码代理产品里,OpenAI 的 Codex 早期用的就是简化版的条数窗口,Claude Code 则倾向于让模型在长上下文里做“自我摘取”。了解这些区别不是为了评测谁好谁坏,而是为了让你在调参时知道自己在动哪个旋钮。
2.2 不只是“最近 N 条”:单调队列思想给窗口淘汰的启发
滑动窗口在算法领域是个成熟话题,热词里提到的“单调队列-滑动窗口”就是典型例子。滑动窗口的最大值/最小值问题里,最经典的解法是用单调队列在 O(n) 时间内维护窗口内最值。我认真想过,这套思路在上下文工程里有一个非常漂亮的映射:我们不能只按时间近远淘汰对话,还得评估每条历史的信息价值。
单调队列维护最值的本质是:当一个旧元素的“值”永远不可能再成为某个窗口的最值时,它就可以放心出队了。对应到对话历史,所谓“值”就是这条历史对当前任务还有没有指导意义。有个很典型的例子:会话早期用户说“这里先别管性能,跑通再说”,到后期开始做性能优化时,这条旧指令不仅没用,还会误导模型。正确的做法是,当新的、更高优先级的指令出现时,旧的低优先级约束应该更早被淘汰,而不是死等它滑出时间窗口。
ChatMemory 真正该做的,就是给每条消息打上“优先级标签”——硬性约束、已确认决策、临时讨论、寒暄——然后按照优先级和时效两个维度共同决定淘汰顺序。我把它称之为“带权重的滑动窗口”:时间衰减是主排序键,优先级是次排序键。这条技巧不是从哪篇论文里抄来的,是实打实被代理的“蠢回复”逼出来的。
2.3 ChatMemory 的参数调优实战:窗口大小与摘要触发时机
纸上谈兵没意思,直接给一组可以落地的参数经验。
先说窗口大小。如果你是单人小项目,每次会话只处理一两个文件的改动,那么 Token 窗口设在 20K 到 30K 之间就非常舒服,既能装下近期对话和相关文件,又不会让模型注意力过度发散。如果你在用 Cursor 做跨模块重构,或者和 Claude Code 一起梳理整个目录,窗口得放大到 50K 以上。我的习惯是:初始窗口设置为项目预估复杂度的 1.5 倍,再根据错误率动态调整——如果代理频繁忘信息,就加大窗口;如果它开始东拉西扯、答非所问,说明窗口太大被污染了,优先做裁剪而不是继续加法。
再说摘要触发时机。很多人设为固定步长,比如每 20 轮做一次摘要。但真正的触发时机应该看两个指标:上下文占用率超过 70%,同时过去 10 轮对话中没有产生新的硬性约束。在这个节点上把更早的历史压缩成摘要,基本不会损失决策质量。摘要的格式我建议固定为三块:已完成事项、待办事项、硬性约束。每次摘要覆盖前一次摘要,形成一个“洋葱式”套叠,这样模型永远能读到最新的决策纪要,而旧细节只要没进硬性约束,丢掉也无妨。
我在实际项目中还测过一个控制参数叫摘要阈值 Token 数,默认 400 Token。如果早期历史压成摘要后超过这个数,就把摘要再删减一轮,优先保留涉及文件路径、变量名、接口签名的硬信息。那些泛泛而谈的“我们讨论了设计方案的取舍”是摘要里最该删掉的东西,留着纯属浪费窗口。
2.4 滑动窗口在实践中带出的经典翻车现场
讲完原理和参数,聊几个我实际踩过的坑,给大家省点学费。
第一个坑:窗口裁剪发生在请求的前置阶段,不是后置阶段。有段时间我在自己写的工具里偷懒,只在对话返回后把最旧的消息标记为“已过期”,想着反正下次请求前再删。结果带进服务端的上下文还是被撑爆了,API 直接报超限错误。教训是:裁剪逻辑必须挂在构建请求前,确保发出去的每一份上下文都是裁剪后的。
第二个坑:滑动窗口把内置的 System Prompt 一起滑出去了。很多编码代理会内置一套“你是一个资深工程师”之类的系统提示词,里面还带着代码规范、输出格式要求。有些实现会把系统提示词也算在窗口长度里,导致窗口明明还剩 20K,实际装满再塞几条用户消息就爆了。所以窗口尺寸一定要算上各类系统提示的固定开销,我一般会额外预留 15% 的 Buffer。
第三个坑:消息元数据比消息本体还占空间。当你的上下文结构里每条消息带着工具调用记录、结构化附件、时间戳、状态标记时,这些元数据消耗的 Token 往往超过正文。用 Token 计费的服务特别容易被这个坑到。优化方式是把元数据精简到最小集,只保留“谁、何时、作用于哪个文件、结论是啥”,其它全部丢掉。
3. Context-mode 与 MCP:把“按需拉取”做到极致
3.1 MCP 到底是干嘛的:一个给 AI 加外设的协议
聊到 Context-mode,绕不开 MCP(Model Context Protocol)。很多人一听到“协议”就发怵,其实它的逻辑很简单:以前你想让 AI 代理读取本地文件、操作数据库、调用代码搜索,每个工具都得单独定制一套接入方式,相当于每个外设都得自己焊线。MCP 做的事,是把“AI 与外部工具对话”的方式统一成一个标准接口,就像给电脑装上了 USB 口,什么设备都能插。
MCP 架构里有两个关键角色:MCP Server和MCP Client。Server 负责暴露能力,比如“提供一个读取文件的接口”“提供一个搜索代码的接口”;Client 运行在编码代理那侧,负责发现这些接口、按需调用。协议走 JSON-RPC,定义了initialize、tools/list、tools/call这些标准方法。
为什么上下文工程里要专门谈 MCP?因为它是一种比“全文塞入”更高级的上下文供给方式。代理面对一个问题时,不再被迫从对话历史里翻找线索,而是可以主动向 MCP Server 发请求,精准获取某个文件里的某段代码、某份文档里的某个章节。换个说法:上下文从“广播式推送”变成了“点播式拉取”。
3.2 Context-mode 是什么:上下文供给的“垂直切片”模式
Context-mode 这个概念,可以理解为 MCP 实践里的一套最佳配置路径,核心思想是:在请求工具获取上下文时,明确传递上下文的工作模式,让提供方据此优化返回内容的粒度和范围。
举例来说,a16z 生态里的 AI 编码原语里,一个关于“加载项目上下文”的工具签名是这样的:load_context(path, mode),而 mode 的取值通常包括cost-optimized、extended、complete、minimal之类。用中文直白翻译就是:代理向 MCP Server 说“我要加载/src/main.rs”,同时告诉它“我现在只想看 public API 的列表,不要实现细节”,于是 Server 返回的内容就只会是短短几行,而不是把 500 行源码全塞回来。
这个机制解决了什么?解决了 MCP 普及之后新产生的“上下文通胀”问题。工具调用越来越方便,代理就越喜欢一次性拉一大堆数据,结果拉完根本用不上,还把窗口挤爆。Context-mode 的核心价值不是提高技术上限,而是增加一层“产量纪律”:每一次上下文请求都要带着明确目的,目的越聚焦,返回的数据越精简,窗口里的信息密度就越高。
这里也回应很多人热炒的问题:browser-use MCP 和 Playwright MCP 有什么区别。它俩表面上看都是浏览器自动化工具,但侧重点完全不同。Playwright MCP 更偏向稳定的结构化操作——精确点击选择器、等待元素、执行脚本,适合做确定性的 E2E 测试流程。browser-use MCP 则更像一个“看得懂页面”的 AI 浏览器代理,它主打大模型自主理解网页内容、自己规划点击路径,更适合让代理完成非结构化的调研类任务。放到 Context-mode 框架里看,差别就更清楚了:Playwright 适合把“特定操作结果”精确返回给模型,而 browser-use 适合把“网页语义”概括后喂给模型做判断。选哪个不是看谁更“先进”,而是看你想要的是结构化反馈还是语义型反馈。
3.3 按需拉取的颗粒度控制:文件级、函数级、版本级
Context-mode 落到具体编码场景,需要解决一个更实际的问题:把“按需拉取”的颗粒度定到哪一层。
我见过最简单粗暴的 MCP 工具实现是read_file(path),一次把整个文件读给模型。文件小的时候没问题,但一个 800 行的核心模块这样搞就是灾难。更合理的模式是给工具增加粒度参数:
- 文件级:
read_file(path, target_version=..., limit_lines=...),只读某个版本里的指定行区间。 - 符号级:
get_symbol(path, symbol_name),直接返回函数/类/接口的定义和签名,不包含实现体。 - 定义级:
find_references(symbol),返回所有引用位置,让代理自己判断影响面,而不是把每个引用的内容全读出来。
我自己写过一组 RUOYI-VUE-PRO 项目的 MCP 增强工具,踩过一遍坑后把读取函数固定成了两个层级:第一层是“摘要+签名列表”,用于代理快速了解模块结构;第二层是“精确函数体”,代理明确点菜后才会返回完整实现。两层的 Token 开销差别大概有 5 倍,但信息获取的成功率并没有下降。这其实就是 Context-mode 在实操层面的落地:先用粗粒度做筛选,再用细粒度做读取。
3.4 MCP 服务端选型与授权的小经验
热词里反复出现“codex 接入 figma mcp 怎么授权”“codex 接入蓝湖 MCP”“idea 插件通义灵码怎么使用 mcp 链接 oracle”,这些都是 MCP 在真实生态里遇到的痛。简单聊一下选型和授权。
先讲选型。做一个 MCP Server,无非是选 SDK。从生态成熟度看,我推荐按语言分:Python 项目用mcp官方 Python SDK,TypeScript 项目用@modelcontextprotocol/sdk。两者底层都是一套协议,区别主要在异步支持和类型定义的手感。如果服务器端本身是 Go 写的,也有mcp-go这类第三方库,但成熟度差一截,建议商用慎选。
再讲授权。Figma 这类云端工具的 MCP 授权,本质是 OAuth 2.0,也就是 MCP Server 拿到授权后替代理去访问 Figma 的 API。你在 Codex 里接 Figma MCP 时,通常需要两步:先在 Figma 开发者后台创建应用拿到 Client ID/Secret,再在 MCP Server 配置里填入授权跳转地址,跑起来后按提示打开浏览器完成 OAuth 确认。蓝湖的 MCP 逻辑类似,但它的文件权限模型比较特殊,一个团队文件的人员权限如果不匹配,你会遇到“明明 MCP 连接成功但拿不到数据”的诡异问题。排查思路一般是三步:确认 MCP Server 进程起来且协议握手成功,确认授权 Token 对应的账号在蓝湖/Figam 项目里有权限,确认工具调用的参数里是否带了正确的文件 key。
还有一条经验很重要:不要给一个 MCP Server 塞太多工具。hackathon 里有人喜欢把所有数据源全塞进一个 Server,工具列表几百个,模型光是决策调用哪个工具就要消耗大量 Token,而且选择越多越容易选错。我的做法是“一个数据域一个 Server”,代码库一个、设计稿一个、数据库一个,每个 Server 的工具数控制在 15 个以内。这能让模型在 Context-mode 下做工具选择时又快又准。
4. 组合实战:从 ChatMemory 滑动窗口升级到 Context-mode MCP 的落地配置
4.1 一套能直接抄作业的双层上下文架构
前面分别讲了滑动窗口和 Context-mode MCP,现在把它们组合成一套完整架构。我目前在项目里跑这套配置,效果很稳。
第一层是会话记忆层,由 ChatMemory 负责。这一层坐落客户端侧,处理所有对话历史的淘汰与压缩。配置上我采用混合窗口:消息条数 60 条以内按原始保留,超过后启动摘要压缩;Token 上限 45K,超过后强制裁剪最旧的非硬性约束消息。摘要的更新和存储挂在每次请求结束后异步完成,绝不阻塞主链路。
第二层是项目知识层,由 MCP Server 负责。我分别起了三个 Server:repo-files管代码文件读取,repo-search管代码搜索和符号定位,docs-db管项目文档和数据库表结构查询。每个 Server 的工具都按 Context-mode 思想做了“粗读+细读”分级。代理在处理任务时,默认只会看到工具清单和粗粒度摘要,只有它自己觉得需要细看某个实现,才会触发细读工具。
这两层之间有一条明确的职责边界:ChatMemory 只回答“之前说过什么”,MCP 只回答“项目里有什么”。一个管会话纵向的时间轴,一个管项目横向的空间分布。边界清晰之后,代理的行为会肉眼可见地变稳,因为它不再需要从那坨历史里翻代码片段,也不用为了回忆某个决定把整个项目重读一遍。
4.2 配置范例:一个 Token 预算分配的表格
为了让方案更好理解,我把一次典型重构任务的 Token 预算分配做成了一张表,照这个比例去分配窗口,基本不会出大问题。
| 上下文来源 | 预算占比 | 典型内容 | 淘汰/裁剪策略 |
|---|---|---|---|
| 系统提示词与工具定义 | 10% | 角色设定、MCP 工具列表 | 固定开销,不参与淘汰 |
| 会话级历史(近端) | 20% | 最近 15 轮对话、当前任务指令 | 滑动窗口保留,硬性约束置顶 |
| 会话级历史(远端) | 10% | 早期决策摘要、已完成事项 | 摘要压缩,按 Token 上限裁剪 |
| 项目级上下文(拉取) | 35% | 当前文件细读、相关函数体 | 按需拉取,用后即弃 |
| 候选结果与工具返回 | 20% | 搜索结果、报错信息、测试输出 | 只保留结论,过程性输出做摘要 |
| 弹性 Buffer | 5% | 模型中间推理过程 | 预留 |
这个表最大的价值不是精确数字,而是它强制你思考每一份信息的必要性。配置完以后,我明显感觉同样的模型,回答质量高了不止一档,Token 成本反而降了三成左右。
4.3 不同 AI 编码代理的接入差异:Cursor、Claude Code、Codex
同样是双击 Ctrl+Enter,不同编码代理对上下文的处理诉求差异极大,接入时必须分开调。
Cursor最让人省心的地方在于它有可视化的 @Codebase、@Docs 引用机制,相当于内置了一层上下文快捷方式。但它的问题在于,默认情况下它会把大量文件自动塞进上下文,导致“上下文恐怖膨胀”。我在 Cursor 里的配置原则是:关闭自动全库文件索引,改成手动 @ 引用+MCP 按需拉取。这样能让模型的注意力集中在窄带上,准确性反而更高。
Claude Code对长上下文更友善,它的 CLI 设计就是为长时间运行的多文件会话准备的。它支持 CLAUDE.md 这类持久化记忆文件,天然适合放硬性约束。但 Claude Code 的 ChatMemory 策略里,摘要触发偏谨慎,长会话跑到后期窗口占用率极高。我通常会主动在 CLAUDE.md 里写明高频项目路径和通用约定,减少它反复搜索项目的次数。
Codex对项目级上下文的依赖比较重,特别是它那个由 OpenAI 维护的仓库解析链,天然会把大量代码结构带进上下文。用 Codex 时我反而会减少 MCP 工具的个数,因为工具列表本身也是上下文开销。只需要保留最核心的代码搜索工具,其余全部拿掉;又因为它对沙箱安全比较严格,授权类的 MCP 一定要提前在配置阶段测好。
4.4 实测效果:两轮迭代后代理的“记忆力”和“专注力”
这套架构搭完以后,我用一个真实的 CRUD 模块重构任务做了两轮 A/B 对比。
第一轮裸奔,没有任何上下文管理,直接把整个模块的 20 个文件路径丢给编码代理,让它“自己看着办”。结果代理在改第 3 个文件时就忘了第 1 个文件里约定的字段命名,等到第 7 个文件时错误率明显上升,同一段逻辑它重复实现了两次,还振振有词地给了两个接口名。会话进行到第 30 分钟,我基本就是在帮它擦屁股。
第二轮启用双层上下文架构。同样是 20 个文件的模块,ChatMemory 滑动窗口保住了“字段命名用下划线、状态机枚举放在common/status.py”这些硬性约束,MCP 则让代理在需要读某个函数时,精准拿到函数体和直接依赖的局部类型定义。结果整个重构过程中,代理没有再出现一次接口命名前后不一致的问题,对文件间依赖的判断也准确很多。总 Token 消耗大概比第一轮少了 22%,因为废对话少了,来回纠正的轮次大幅下降。
这组对比给我的结论很清晰:上下文管理的收益不在“省 Token”,而在“让模型的每一点能力都用在正确的地方”。
5. 常见问题与排查技巧实录
5.1 MCP 连接不上的排查顺序:先分清是协议层还是业务层
热词里“codex 无法找到 mcp”这类问题出现频率极高。我遇过的类似问题分两类。
第一类是协议层失败。现象是 MCP Server 启动时报错、Client 找不到 Server、tools/list调用超时。排查第一步永远是用原始工具直接测协议握手:用mcp-run或 Postman 模拟一次 JSON-RPC 请求,确认 Server 能不能正常响应。如果这一步就挂,问题在部署环境,检查 Python/Node 版本、端口占用、依赖缺失。
第二类是业务层失败。协议握手正常,但工具调用返回一堆看不懂的错误码。这时候别死磕协议,直接看 MCP Server 的日志。我遇到过最隐蔽的一次是数据库连接串里密码含特殊字符,被 JSON 序列化时转义坑了,结果工具明明存在,一调用就报连接失败,日志里不细看根本发现不了。
排查的先后顺序必须固定:环境部署 → 协议握手 → 工具注册 → 业务调用。倒着查只会浪费时间。
5.2 滑动窗口明明没满,代理还是“遗忘”了
这是最迷惑人的问题:你算了 Token 没超限,消息条数也没到底,但代理就是在某个早期环节犯迷糊。
后来我想明白了,问题不是窗口容量,而是信息的可检索性。大语言模型是自回归的,它对越靠后的上下文注意力越强,哪怕前文信息没被物理丢弃,它的“有效注意力”其实已经飘走了。就好比一本书没撕页,但你不小心把最重要的一页放在目录前面,读者翻书时压根不会再看它一眼。
解法有两个。一是关键信息重复强化,把硬性约束在每条用户消息前重复一遍,别嫌啰嗦,模型记不住就是记不住。二是在滑动窗口机制里加入“重要信息置顶”逻辑,让 ChatMemory 把带优先级标签的消息从时间顺序里抽出来,放到窗口的稳定前缀区。这两种都相当于在窗口里做了“注意力插桩”,比单纯放大窗口有效得多。
5.3 摘要层层套叠后的信息走样问题
摘要压缩用多了,会有一个延迟显现的坑:信息经过多层摘要后逐步失真,最后变成一句面目全非的总结。
举个例子,第一层摘要记录“用户要求性能优先,允许用 C 扩展”。第二层摘要把它压缩成“性能优先”。第三层再压缩就变成“快一点”。三层以内还算可控,超过三层,细节损失非常明显。
我的规避方案是禁止对摘要做二次摘要。摘要一旦生成,就作为不可变的历史快照保存,新摘要永远从原始消息重新生成,而不是从旧摘要里再压缩。每次生成摘要时都保留三层结构(已完成/待办/硬约束),让模型做信息“合并”,不做“缩写”。这样虽然 Token 开销略高,但能保住关键细节不丢。
5.4 工具的过度调用:MCP 反而变成上下文杀手
接入 MCP 后最常见的副作用,不是连接失败,而是模型变成了“工具狂魔”。它只要有疑问就 call 工具,一次任务调用十几次,每次工具返回的 JSON 又有一大坨,上下文就这样被工具结果撑爆了。
对付这种“查询成瘾”,我目前的方案是三层设限:第一,在 MCP Server 的工具描述里明确标注“仅当本地片段缺失时才调用”;第二,在客户端加一层“工具结果缓存”,同一参数同一工具在一段时间内只允许调用一次;第三,对工具结果做后处理,用关键词抽取和格式压缩把返回体减小 70%。这套组合拳打下来,工具调用次数能降一半,上下文健康度大幅回升。
结尾
这套方案从 ChatMemory 滑动窗口到 Context-mode MCP,我大概调了两周才跑顺。说实话,上下文工程不像写业务代码那样有“把功能做出来”的明确边界,它更像是在跟模型的认知特性做博弈,看不见摸不着,但你投进去的每一个优化,最后都会反映在代理输出质量的稳步提升上。
如果只让我留一句话给还在被上下文问题折磨的朋友:先别急着换更大的模型或者堆无限长的窗口,静下心梳理一下自己的对话历史里有哪些是“必须记住的事”、哪些是“看过就该忘的事”。把这条线划清楚了,后面的一切手段——滑动窗口、摘要压缩、MCP 按需拉取——才真正有了着力点。下一期我打算聊聊怎么给不同类型的项目制定上下文预算表,需要的话我也可以把这次用的配置文件和 MCP Server 样例整理出来,到时候直接在评论区见。