最近半年我一直在折腾各种 AI 编码代理,从早期的简单对话补全,到现在的项目级代码生成、跨仓库重构、多工具联动。用下来的最大感受是:决定代理效率上限的,早就不是模型本身的代码能力,而是上下文喂得好不好。
这个认知是从一次真实事故开始的。我用一个编码代理做跨模块重构,前面十轮对话都还正常,结果到第十二轮,它突然忘了我们一开始定的命名规范,开始用另一种风格写代码,还把之前的接口签名全写错了。排查了半天,发现原因很简单——对话窗口被前面的报错日志和中间产物占满了,早期的核心约定被挤出了上下文。这就是 ChatMemory 和滑动窗口策略要解决的问题。再后来,我接入了一批 MCP 工具,又踩进了另一个坑:工具越来越多,上下文越来越乱,代理根本不知道该优先看哪个。一路折腾下来,我才把“上下文工程”这件事真正当成一个工程问题来做,而不是靠运气。
这篇文章就把我实战中的完整方案摊开讲,涵盖 ChatMemory 滑动窗口的演进、Context-mode MCP 的优化思路、以及两者怎么组合落地。适合正在调教 Codex、Cursor 这类 AI 编码代理,或者自己在做编码代理上层应用的开发者。不管你用的是商业工具还是自研框架,这套思路都能直接套。
1. 上下文工程:编码代理真正的性能瓶颈
先聊一个反直觉的事实:上下文窗口大,并不等于代理更聪明。市面上主流模型已经把窗口做到了几十万甚至百万 token,听起来很美好,但真在生产环境中把这么长的上下文一次性塞进去,你会发现推理变慢、成本飙升,而且输出质量反而可能下降。这不是玄学,背后有三个硬约束。
1.1 三个硬约束:成本、注意力、延迟
第一个是成本约束,可以直接算一笔账。假设你用的是按 token 计费的主流模型,输入大概是每百万 token 几美元到十几美元。一次覆盖全仓库的重构,光把相关文件读进来可能就要消耗几千个 token,如果再加上历史会话、工具返回结果,一次请求轻松突破 1 万 token。一天高强度开发下来,上下文开销可能就是很可观的一笔数字。更关键的是,这里面真正对生成结果有贡献的 token,可能只占 10% 到 20%,其余全是冗余。
第二个是注意力稀释。模型在处理超长上下文时,注意力会被均匀分散。我做过一个简单测试:在上下文里插入一段跟任务完全无关的日志,然后让代理在另一段代码里找一个 bug,它找到的效率比干净上下文里慢了将近一倍。这不是模型能力问题,而是信息噪声干扰了注意力聚焦。就像你在一个堆满杂物的桌面上找一把钥匙,桌面越大、杂物越多,反而越难找。
第三个是延迟。长上下文的 prefill 阶段(也就是模型读取并理解所有输入信息的过程)耗时明显增加,首 token 输出时间会从几百毫秒涨到几秒甚至更久。对于编码这种高交互场景,这种延迟非常致命,因为用户会频繁地调整需求、追问细节,每次都要等几秒,整个开发节奏全被打乱。
1.2 上下文工程的定义和三个原则
所以我把上下文工程定义为:在预算约束下,把当前任务所需的高价值信息,用尽可能高的密度组织进上下文窗口。它追求的目标不是“把尽可能多的信息塞进去”,而是“让代理每看到一个 token 都物有所值”。
做上下文工程,我总结出三个原则。
第一个是相关性优先。上下文里只放与当前任务相关的信息。做前端样式修改时,就不需要把后端缓存的实现细节全部塞进去;改数据库迁移脚本时,也不需要让代理反复读取完整的商品详情页模板。相关性判断依赖我们对任务的拆解能力,这也是为什么现在做编码代理不能完全甩手,人需要在关键节点上做信息筛选。
第二个是时效性优先。上下文里应该是最新的状态,而不是历史快照。很多代理方案喜欢把完整的会话历史一字不差地喂给模型,这其实是最偷懒也最低效的做法。正确的做法是维护一个“当前状态摘要”,每次对话结束后更新摘要,历史细节除了被明确点名引用,否则不再装入上下文。
第三个是结构化优先。同一段信息,用结构化描述比用原始文本有高得多的利用效率。比如“用户权限逻辑在 src/auth/permission.ts 中,核心函数是 checkPermission(user, resourceId),返回布尔值”,就比直接把整个文件源码贴进去高效得多。因为模型可以快速定位函数间的关系,而不是在成千上万行代码里自行搜索。
这三个原则听上去简单,但真正实现时,每一步都对应着具体的系统设计,也就是下文要说的 ChatMemory 和 MCP 上下文优化。
2. ChatMemory 滑动窗口:从朴素实现到智能裁剪
ChatMemory 这个概念在 AI 编码代理领域并不新鲜,本质上是给对话系统装一个记忆组件。它要做的事情只有一件:决定哪些对话内容应该留在上下文中,哪些应该被压缩,哪些应该直接丢弃。
2.1 朴素滑动窗口的核心思想与缺陷
最简单的实现就是滑动窗口。设定一个固定长度 N,永远只保留最近 N 轮对话。这个方案的优点是极端简单,实现只需要一个队列数据结构,新消息进来就把最旧的消息弹出去,时间复杂度 O(1)。但它有两个致命缺陷,我在实际项目中都踩过。
第一个缺陷是信息无差别淘汰。滑窗只关心时间,不关心内容。早期讨论中确定的架构决策,跟早期一段无关紧要的闲聊,会被完全一样地对待。一旦窗口被短平快的问答刷过去,核心约定就丢了。我开头提到的命名规范事故,就是这么来的。
第二个缺陷是单一下文不可复用。滑动窗口是绑死在单一会话上的,开一个新窗口处理另一个任务,前面的记忆完全用不上。做编码代理时,你往往希望它记住跨会话的偏好——比如“错误处理统一用自定义 Err 类型,不要用 panic”“测试文件放在 tests/ 目录下且命名为 xxx_test.go”。这些项目级约束如果只存在会话窗口里,每次开新会话都要重新强调,非常低效。
2.2 三种滑窗策略对比与选型
在实践中,滑动窗口通常有三种策略可选,我逐个说适用场景。
第一种是时间轮次窗口。也就是固定保留最近 N 轮对话,适合简单问答和临时的代码解释型任务。实现成本最低,但信息密度不可控,因为每一轮的 token 消耗差别很大——有可能一轮对话里贴了 3000 行代码,另外一轮只说了个“好的”。
第二种是 Token 预算窗口。以 token 数为单位切窗口,比如设定 8K token 上限。每次插入新对话前,计算当前总 token 数,超出就淘汰最旧的消息。这种策略能精确控制上下文长度,避免了“留得多但全是水”的问题。核心数据结构是一个带权重的滑动窗口,我一般用双端队列维护消息序列,配合前缀和判断淘汰哪一段。
下面是一个简化版的 Token 预算滑窗实现思路,用 Python 描述,方便理解核心逻辑:
from collections import deque class TokenBudgetWindow: def __init__(self, max_tokens=8000): self.max_tokens = max_tokens self.messages = deque() # 按时间顺序存消息 self.token_usage = deque() # 每条消息的token数 self.total_tokens = 0 def add_message(self, message, token_count): # 先塞进去,再判断是否超预算 self.messages.append(message) self.token_usage.append(token_count) self.total_tokens += token_count # 淘汰最旧的消息,直到低于预算 while self.total_tokens > self.max_tokens and self.messages: oldest_tokens = self.token_usage.popleft() self.messages.popleft() self.total_tokens -= oldest_tokens # 关键:即使淘汰后,也要在队首留一个摘要消息 # 避免窗口全是被淘汰后的“无记忆”状态这里有一个非常关键的细节:纯 Token 预算滑窗仍然会把最早的意图和决策丢掉。所以我在实现中加了一个机制——在淘汰旧消息的同时,把被淘汰区域的要点压缩成一个摘要对象,插到窗口开头。这个“卡住窗口防止关键信息流走”的思路,很接近单调队列在滑窗最大/最小值问题里的做法:你不必保留窗口内所有值,只需要保留极值候选。信息记忆也一样,不必保留所有历史,只保留“重要度最高的那些候选”。
第三种是语义摘要窗口。不按轮次也不按 token 数切,而是对消息做语义摘要,逐轮压缩。这个方案最符合上下文工程的三原则,但实现复杂度最高,一般需要额外调用模型做 summarization,还会引入摘要本身的 token 开销和事实扭曲风险。我通常在两种场景下用:一是会话开场时,把上一轮会话总结作为“背景种子”注入;二是当某一轮的代码 diff 长到离谱时,强制把完整 diff 压缩成删改文件列表和关键函数变更说明。
三种策略各有适合的位置,对比如下:
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 时间轮次窗口 | 实现极简 | 信息密度不可控 | 简单问答、临时解释 |
| Token 预算窗口 | 精确控制长度 | 仍需处理重点信息流失 | 常规编码会话、工具调用密集场景 |
| 语义摘要窗口 | 信息密度高 | 有额外开销和失真风险 | 长会话续接、跨会话移植 |
2.3 提高滑窗记忆密度的三个手段
单纯的滑窗不解决“记什么”的问题,所以我后来重点改造的是窗口内的内容组织。这里分享三个我验证过的手段。
第一是给消息标记优先级。每条消息入窗时打一个分类标签,比如USER_INTENT(用户明确意图)、TOOL_RESULT(工具返回结果)、CODE_SNIPPET(代码片段)、CHITCHAT(闲聊)、SYSTEM_CONSTRAINT(系统约束)。淘汰消息时,先扔CHITCHAT,再扔TOOL_RESULT中的低价值日志,SYSTEM_CONSTRAINT和USER_INTENT永远转移到摘要区,不直接丢。这样滑窗的“淘汰”就从盲目裁剪变成了分级裁剪。
第二是窗口内的消息合并。工具调用产生的结构化输出,往往可以合并成一张表,而不是逐条堆日志。比如代理连续调用了三次代码搜索,三次返回了三个文件路径和相关函数签名,就可以合并为一段索引块,看起来像这样:
[代码搜索索引] - src/service/order.go: CreateOrder(orderReq) error - src/service/payment.go: CreatePayment(orderID, amount) (paymentID, error) - src/repository/order_repo.go: SaveOrder(ctx, order) (int64, error)这个索引块占用的 token 远小于三次完整搜索结果的拼接,而且对模型来说更好用,因为它直接把“文件路径 + 函数签名 + 职责”的关系给了出来,不需要再自己梳理。
第三是定期做会话滚动摘要。当窗口长度快满的时候,触发一次摘要任务,把窗口中段的对话压缩成同语言的简报,插回队首。我刚才说过它有失真风险,所以我的做法是:摘要只记录“已经不活跃但后续可能被引用”的内容,比如已完成的接口定义、确定的目录结构、项目经理临时口头补充的边界条件;而所有仍处于活跃修改状态的文件内容,绝对不摘要,必须原文保留。摘要对象本身遵循统一的格式,而不是让模型自由发挥,这样后续解析时不会出乱子。
3. Context-mode MCP:把上下文管理交给协议
滑动窗口解决的是“会话内部怎么留”的问题,但很快我发现,还有一个更大的上下文黑洞在等着我——MCP 工具。
3.1 从模型上下文到 MCP 接口,新一轮上下文失控
MCP(Model Context Protocol)本质上是一个工具接入标准,它解决了以往每个 AI 应用都要为每个工具写一套专用集成的问题。接入 MCP 的编码代理可以通过统一协议调用本地的文件系统、数据库、浏览器,或者是 Figma、蓝湖这类设计协作平台。
但 MCP 恰恰是上下文失控的重灾区,原因有三。
其一,每个工具连接都需要占用上下文来声明自身。MCP 的工具描述、输入输出 schema、连接参数这些东西,并不是免费的,它们最终都会被序列化进系统提示或工具列表中。工具接得越多,上下文被“工具说明书”吃掉的量就越大。有些重构类 MCP 工具,光 schema 就有上千 token。
其二,工具返回结果经常是成片的原始数据,但当前任务只关心其中一小块。比如浏览器自动化 MCP 返回了一整页 DOM 结构,代理其实只需要某个按钮的定位信息。这个“搬运费”非常昂贵。
其三,不同工具之间缺乏协同记忆。MCP 服务器通常是无状态的,每次请求进来都要重新加载文件路径、连接信息、过滤条件。A 工具的返回结果,无法直接作为 B 工具的输入,中间需要代理做数据转换,转换过程又消耗上下文和推理时间。
3.2 标准 MCP 的高频调用是隐形杀手
我用一个真实场景来说明问题。假设接入了一个浏览器 MCP 和一个数据库 MCP,让代理去完成一个“前端页面展示数据库用户列表”的改动。
代理先调用数据库 MCP 执行查询,返回 500 行 JSON。紧接着又调用浏览器 MCP 打开页面,返回 DOM 结构。这两份结果如果原样都塞进上下文,后续的代码生成过程就会被大量无关数据污染。而实际上,代理真正需要的只是“用户表有 id、name、email 三列”,以及“页面列表容器 id 是 user-list”。
这就是标准 MCP 的隐性开销:接口按需返回了完整数据,但缺乏上下文层的裁剪、合并与优先级调度。工具层提供的是数据,上下文工程要做的是把数据变成信息。这两层之间缺了一个中间环节。
3.3 Context-mode MCP 的设计思路与实战改造
我想说的 Context-mode MCP,就是在 MCP 原有协议之上,增加一个面向上下文优化的运行模式。它不是要改协议本身,而是在连接和调用两个层面做一层封装。核心思想是:把上下文看作一种需要被生产、调度、回收的资源,而不是一个被动接收的容器。
具体改造落到四个层面。
第一层是上下文片段化。任何 MCP 工具的数据返回,在进入上下文之前,先被拆成独立片段,每个片段带上元数据。元数据包括片段类型、相对任务的优先级、时效标签(永久 / 会话 / 一次性)、预估 token 数。这一步相当于给信息贴标签,为后续调度做准备。
第二层是片段级裁剪和路由。根据当前任务的意图,系统决定哪些片段需要进入主窗口,哪些只保留摘要,哪些直接丢弃。注意,这里的“丢弃”不代表信息真的消失,而是可以落在本地缓存里,等代理主动发起查找时再取出来。这样既减少了无效 token,又保住了信息的可追溯性。
第三层是多个 MCP 服务器的上下文合并。把多次调用的返回结果按任务模型做归并,合并为结构化摘要。比如上面说的数据库查询和浏览器 DOM 抓取,最终合并成一个高密度片段:
[task model: 前端页面用户列表] - data source: SELECT id, name, email FROM users; 行数 500 - ui anchor: <div id="user-list"/> in src/pages/user/list.tsx - required output: 渲染为表格,支持按 name 搜索 - relation: 无额外关联对比一下,原始数据进上下文可能要 3000 token,这个合并后的片段只需要 200 token,而且信息编排方式更接近“人脑中的任务认知”,模型处理起来不需要再自己拼装理解。
第四层是跨会话的上下文持久化。Context-mode 下的上下文片段,可以携带“会话身份”,在多个会话间共享。比如项目结构摘要和编码规范片段,在项目启动时注入一次,之后每个会话都能复用。这正好补上了我在 ChatMemory 部分提到的跨会话记忆缺失问题。
这四个层面落在工程实现上,核心是要区分三种上下文角色的代码形态。下面是我的一个简化实现建议,不依赖特定框架,你们可以直接套用到自己的调度层:
class ContextFragment: def __init__(self, content, priority, lifetime, mcp_server=None): self.content = content # 文本内容 self.priority = priority # 1-5,数字越大越优先 self.lifetime = lifetime # "session" / "permanent" / "one-shot" self.mcp_server = mcp_server # 来源 MCP 服务器标识 self.tokens = estimate_tokens(content) class ContextScheduler: def __init__(self, budget=8000): self.budget = budget self.fragments = [] def inject_mcp_result(self, result, task_intent): """把 MCP 返回结果转为上下文片段,并做裁剪路由""" fragment = mcp_result_to_fragment(result) # 按任务意图决定优先级 if matches_current_intent(fragment, task_intent): fragment.priority = 5 else: fragment.priority = 1 # 低优先级片段只保留摘要,原文存缓存 fragment.content = summarize(fragment.content) self.fragments.append(fragment) self.enforce_budget() def enforce_budget(self): # 先按 priority 降序,再按时间升序(旧的先淘汰) self.fragments.sort(key=lambda f: (-f.priority, f.timestamp)) while sum(f.tokens for f in self.fragments) > self.budget: released = self.fragments.pop() # 淘汰最低优先级且最旧的 cache.store(released)这段代码思路的优点是通用,你可以把它接在任何 MCP 客户端和编码代理之间。实际项目中我还会加一层“意图理解”,用一次小模型调用判断当前任务到底需要哪些类型的上下文片段,避免把全部历史数据都塞给调度器。
4. 完整落地:从会话记忆到 MCP 上下文的分层架构
单独做 ChatMemory 滑动窗口,或者单独做 Context-mode MCP,都只能解决一半问题。真正稳定的方案是分层组合。我的线上方案拆成四个上下文层,每层有自己的预算、生命周期和调度策略。
4.1 四层上下文模型与预算分配
第一层是系统约束层,内容为项目级规范、目录约定、编码风格、测试要求。这层优先级最高,生命周期为 permanent,通常在会话开始时注入。预算大概占 5% 到 10%,因为内容高度凝练。它的核心来源是仓库根目录下的 AGENTS.md 或者 CLAUDE.md 这类代理专用说明文件。我建议每个人都在项目里维护一份这样的文件,它比在每次对话里反复敲偏好高效得多。
第二层是任务上下文层,内容为当前任务的目标描述、涉及的核心文件路径、关键函数签名、验收标准。这层由用户在创建会话时提供,或者在对话中由系统自动提取。优先级次高,生命周期为 session,占 15% 到 20% 预算。这个层是 Context-mode 的主战场,因为任务上下文经常来自多个 MCP 工具的联合输出,比如设计稿的图信息、代码仓库的文件索引、测试报告等,都需要经过合并裁剪再进入这层。
第三层是历史摘要层,内容为过去对话的关键决策和已解决事项,由 ChatMemory 的语义摘要模块产出,占 10% 到 15% 预算。需要注意的是,历史摘要是“尽量不影响当前工作、但防止意外遗忘”的存在,所以它的优先级低于任务上下文。编码代理变笨的最大隐患,往往是这一层与第二层冲突,让代理同时看到“用户说 A”和“历史决策记录说 B”,然后无所适从。
第四层是参考材料层,包含所有临时拉取但当前尚未用到的代码片段、日志、文档。这层实行按需加载,不进常驻上下文,只在代理主动查找时临时载入。通常占 0% 到 30% 的弹性预算。
四层模型在会话运行中的效果,是一次 token 开销的重新分配。以前我使用的所有上下文都挤在一个大聊天窗口里,历史、任务、工具结果互相污染;现在它们各司其职,信息和指令不再打架,编码代理的逻辑清晰度提升明显。
4.2 真实项目配置示例与选型对比
理论讲完,看两个我实际配过的项目。
第一个是用 Codex 接入设计稿协作场景。任务是让代理根据 Figma 设计稿实现前端页面。这里涉及两个 MCP:Figma MCP 和蓝湖 MCP。一开始直接让 Codex 同时连接两个 MCP,结果代理经常分不清该从哪个取图,给的 CSS 尺寸逻辑也混乱不堪。问题就出在两个 MCP 的返回没有经过 Context-mode 合并,设计稿标注和前端代码上下文互相抢占。
后来我在 Context 调度层做了统一封装:Figma MCP 和蓝湖 MCP 的返回统一转为“设计信息片段”,优先级设为同等,同时在片段上标注来源服务器名。这样代理看到的是一份统一的“设计上下文”,而不是两个互相独立的工具输出。注意授权问题,Figma MCP 接入需要生成访问令牌,在 Codex 的 MCP 配置里把它作为环境变量注入即可。我在这一块踩过一个坑:Codex 无法找到 MCP 服务器,排查半天发现是令牌里的特殊字符没有 URL 编码,导致握手失败。换成 base64 编码传输后问题解决。
第二个场景是 IDEA 插件通义灵码接入 Oracle 数据库的 MCP。Oracle 驱动和数据库连接串都很重,直接让 MCP 服务器把所有表结构和数据字典返回给代理,上下文瞬间暴涨。我的做法是在 Context 调度层实现了一个“字典裁剪器”:数据字典先落本地缓存,只把与当前 SQL 操作相关的表结构片段送入上下文。比如代理要优化一条订单查询 SQL,就只把 orders 表、order_items 表和关联索引放进上下文,而不是把整个 schema dump 出来。
这两个例子共同说明了选型关键:MCP 工具选型,不只是选“这个工具能接”,还要看它的上下文友好度。下面是对比表格:
| 场景 | 推荐的 MCP 类型 | 上下文特点 | 不推荐的场景 |
|---|---|---|---|
| 浏览器自动化 | Playwright MCP | DOM 结构化,可通过 locator 精确定位,上下文较轻 | 需要模拟复杂用户操作时,优先用更底层的 CDP 工具 |
| 浏览器信息抓取 | Browser-use MCP | 偏向“让 AI Agent 自主浏览操作”,需要配合行为描述,逻辑完整但上下文占用更大 | 纯页面元素级操作,用 Playwright MCP 更轻 |
| 设计稿转前端 | Figma MCP / 蓝湖 MCP | 输出需二次裁剪 | 不要让多个设计协作 MCP 同时常驻 |
| 数据库操作 | Oracle / MySQL MCP | 元数据重,必须做字典裁剪 | 不要直接暴露库级 list tables 权限 |
另外,像 RuoYi-Vue-Pro 这类全栈脚手架项目,现在也有人在做合并 MCP 功能。它的思路是把代码生成、测试执行、文档更新统一收敛到项目级 MCP 服务器里,这样代理只需要连一个项目服务器,就能拿到项目内部的全部能力。这也是一种减少上下文消耗的策略:把多个细粒度工具合并成一个粗粒度工具,减少上下文协议开销。工具数量少了,代理做工具选型的负担也就轻了。
4.3 任务上下文合并的具体实现
在四层模型中,任务上下文层的合并逻辑最复杂,也最值得打磨。我总结出一个可复用的操作流程。
第一步,明确任务意图。在会话开始后先让代理从用户自然语言中提取三个要素:改动目标、涉及模块、验收标准。这三个要素会作为后续裁剪 MCP 结果的过滤器。
第二步,决定上下文组装顺序。系统约束在前,任务上下文在后,历史摘要只做补充。组装顺序对模型影响很大,因为注意力通常是按照系统提示中最靠前的指令优先执行的。我实测下来,把验收标准放在历史摘要前面,代理会更重视最终的输出检查,而不是先入为主地对历史细节进行复述。
第三步,做冲突消解。当历史摘要和当前任务上下文冲突时,应该以当前任务上下文优先,并显式标注“以下以最新指示为准”,避免模型纠结是否要遵循旧的项目约定。原理是模型对令牌级冲突的处理常常不可预测,但明确的优先级指示可以降低它摇摆的概率。
第四步,执行最终预算审计。下发请求前估算所有片段的 token 总数,如果超过预算,就把参考材料层的片段全部移到缓存,并把任务上下文层的低优先级片段压缩为“待查证摘要”。
这套流程执行起来并不复杂,但它需要你能在编码代理的请求链路上插入一层拦截逻辑,也就是前面 ContextScheduler 干的事。
5. 问题排查实录:我踩过的坑和调参心得
最后分享一些实战中反反复复遇到的问题和排查方法。这些内容是我一张截图一张截图试出来的,希望能让你少走弯路。
5.1 常见问题速查表
| 问题现象 | 可能原因 | 排查与解决 |
|---|---|---|
| Codex 无法找到 MCP 服务器 | 环境变量未注入、令牌未编码、服务器地址不可达 | 检查 MCP 配置 JSON 中的 server 路径和 env 字段;令牌尝试 URL encode |
| MCP 调用总是超时 | 工具返回数据体量过大,序列化耗时 | 在 Context 调度层做数据裁剪,减少返回 payload |
| 编码代理偶尔忘掉早期确定的架构约定 | 滑动窗口把早期关键信息淘汰了 | 建立摘要区,把架构决策从普通消息中分离保护 |
| 代理同时参考 MCP 数据和对话框中的代码片段,导致逻辑冲突 | 上下文层没有做优先级隔离 | 在上下文组装时显式声明“MCP 数据高于对话历史片段” |
| 数据库 MCP 返回 500 行数据后,代理变迟钝 | 低价值数据直接进了主上下文 | 实现字典裁剪器,只保留当前任务相关表结构 |
| 两个设计协作 MCP 同时连接时,代理拿错数据 | 工具输出没有统一归并 | 把两类返回统一转为“设计信息片段”,并标注来源 |
5.2 滑动窗口调参的三条实证经验
第一个是 Token 预算窗口的上限不要顶着模型窗口设,我试过把预算设成模型上下文窗口的 80%,结果代理在长任务中还是明显变笨。合理的预算应该是模型窗口的 50% 到 60%,剩下空间留给工具返回和生成输出。这个比例不是拍脑袋,我做了一个对照实验:同一个 32K 窗口模型,上下文预算 16K 和 25K 各跑一轮完整 Code Review,16K 的那轮定位到的问题更集中,也更准确。
第二个是摘要触发阈值建议设在 90%,不要等满了再处理。因为摘要本身也有开销,处理过程也会产生新的上下文。提前触发可以预留充分的缓冲,避免在请求中途强制压缩导致信息丢失。实际复现中,90% 阈值下摘要质量最稳定。
第三个是低优先级消息淘汰时,优先淘汰工具完整输出,保留工具摘要,这也是 Context-mode 和 ChatMemory 结合最紧密的地方。工具输出的本质是结构化数据,摘要可以用 JSON Schema 统一表示,大大降低后续解析成本。我维护过一个只消耗 300 token 就能描述 80% 工具行为的数据结构,基本就是前面提到的“任务模型”。
5.3 关于上下文工程后续还能做什么
我目前正在做的方向,是把上述逻辑进一步产品化。具体来说,是做一个项目级的“Context 浏览器”——可视化展示当前编码代理的上下文窗口状态,哪些片段占了多少 token,优先级分布如何,哪些信息即将被淘汰。有了这个工具,你就能像给代码做性能剖析一样,给代理的上下文做剖析。
此外,多人协作场景下,我还在考虑让所有开发者的编码代理共享同一份项目规范摘要,这样无论谁开新会话,代理都能立即理解项目的“世界观”,而不是由每个人重复解释。
最后留一句我自己最深的体会:上下文工程不是单纯的技术优化,它是一种全新的编程交互范式。早期我们写代码是对着编译器说话,后来是对着 IDE 说话,现在是对着一个能理解语言的长上下文模型说话。让它听话的关键,不在于越来越大的窗口,而在于你能不能用最小的信息量,准确表达你的意图和约束。这才是上下文工程真正值得下功夫的地方。