如何设计 Agent 上下文管理系统:从踩坑到体系化的实践指南
做多 Agent 系统时,最先遇到的不是模型能力问题,而是上下文管理问题------聊几轮就"失忆"、工具结果撑爆窗口、每次新会话都要从头解释项目背景。本文从实际开发中遇到的问题出发,结合对 Claude Code、Codex CLI、OpenClaw 等开源项目的源码研究,总结出一套 Agent 上下文管理的设计方法论。
为什么上下文管理是 Agent 的核心基础设施
大多数团队对"上下文不够用"的反应是选更大窗口的模型或写更短的 Prompt。但实际做下来会发现,上下文管理不是提示词工程,而是运行时基础设施------它涉及缓存策略、压缩协议、记忆持久化、消息规范化、并发调度等多个工程子系统的协作。
我在做多 Agent 视频生产系统时,最初的架构只关注了 Agent 编排和工具链设计,上下文管理几乎是"能跑就行"。结果上线后遇到一系列问题:
- ScriptAgent 产出的分镜表传到下游 ClipAgent 时,中间轮次的工具调用结果已经把窗口撑满了
- 每次新会话都要重新注入项目的资产库结构、标签体系等背景信息
- 多个 Agent 并行执行工具时,结果回灌的顺序不确定,导致上下文混乱
这些问题迫使我系统性地研究了业界方案。以下是提炼出的设计框架。
一、上下文组装:分层缓存比"一股脑塞进去"省一半钱
问题
每次 API 调用都要把系统指令、用户偏好、项目规范、对话历史、工具定义打包发送。这些信息的变化频率完全不同------系统指令几乎不变,用户偏好每人不同但会话内稳定,工具执行结果每轮都变。如果揉在一起发送,任何一处变化都会导致整段 Prompt Cache 失效。
设计方案:三路分离
核心思路是按变化频率拆分上下文,让每路独立缓存:
三路上下文分离
System Prompt
(几乎不变)
全局共享缓存
User Context
(每用户不同,会话内稳定)
用户级缓存
Session Context
(每轮可能变化)
体量最小,不缓存
API 调用
实现要点:
- System Prompt 全局共享:工具定义、角色设定、输出格式约束放在这里。所有用户的首次调用都能命中同一份缓存。关键约束:这一层的内容一旦上线就不要频繁改动,任何改动都会导致全量缓存失效。
- User Context 用户级缓存:用户偏好、项目规范(比如 CLAUDE.md 这类文件)放在这里。同一用户的会话内保持稳定。
- Session Context 不缓存但控制体量:当前轮次的 git diff、工具执行结果等放在这里。因为每轮都变所以不缓存,但要严格控制体量。
成本收益:以 Anthropic 的 Prompt Cache 定价为例,缓存命中的 token 成本是未命中的 1/10。如果 System Prompt 占输入 token 的 60%(这在工具定义多的 Agent 中很常见),三路分离后仅这一项就能降低约 50% 的输入成本。
对比参考
- Codex CLI 做了基线 diff(
reference_context_item),避免每轮全量重发,但上下文整块发送,没有显式区分缓存边界 - OpenClaw 的
ContextEngine接口可插拔性好,对 Anthropic 提供商会自动启用 Prompt Cache,但更像是副效果而非设计目标
踩坑提醒
System Prompt 的"稳定性"需要制度保障。建议在 Code Review 流程中加一条检查:这次改动是否影响了 System Prompt 的缓存稳定部分。可以用命名约定(如
STABLE_前缀)标记不应频繁修改的配置段。
二、上下文压缩:先轻后重,别上来就调 AI 做摘要
问题
长对话一定会撑爆上下文窗口。关键问题不是"要不要压缩",而是用多重的代价压缩。很多系统只有一档压缩------调 AI 生成摘要,既慢(额外一次 API 调用)又贵(摘要本身消耗 token),而且摘要质量不可控。
设计方案:四级渐进式压缩
从最轻到最重依次尝试,只有当前级别不够用时才升级到下一级:
不够用
不够用
不够用
Level 1
Snip 裁剪
直接删除最旧的消息
Level 2
Micro Compact
清空旧工具结果
但保留调用记录
Level 3
Context Collapse
折叠上下文视图
保留结构骨架
Level 4
Auto Compact
调 AI 生成摘要
信息保留最好但最贵
其中 ROI 最高的是 Level 2:Micro Compact。
它不是删消息,而是只清空旧的工具返回结果内容,但保留工具调用记录。效果是:模型仍然知道"我之前读过 config.yaml 这个文件",只是看不到具体内容了。如果后续需要,模型会主动再次调用工具读取。
这种"保留骨架、清空内容"的策略,在信息保留和空间回收之间取得了非常好的平衡。在我的实践中,仅这一步就能回收 40-60% 的窗口空间,因为工具返回结果(尤其是文件读取)往往是上下文中最大的部分。
Level 4 的熔断保护也很重要:如果 AI 摘要连续失败(比如 3 次),应该熔断而不是无限重试。Claude Code 的数据显示,不加熔断的情况下,异常会话可能触发数千次无效的摘要请求。
对比参考
- Codex CLI 有
TruncationPolicy控制工具输出截断,但只有"截断/不截断"两档,没有渐进中间态 - OpenClaw 有一个值得学习的细节:摘要时的标识符保留策略------UUID、哈希、URL 在摘要过程中原样保留。这很实用,因为标识符一旦被摘要改写,后续引用就会断链
踩坑提醒
压缩触发的时机很关键。不要等到窗口真的满了才压缩------那时候已经来不及了(AI 摘要本身也需要上下文空间)。建议在窗口使用率达到 70-80% 时就开始 Level 1,留出足够的缓冲区。
三、记忆体系:让 Agent 不再是"每次都失忆的实习生"
问题
上下文窗口只管当前会话。会话结束或被压缩后,之前积累的知识就丢了。用户下次开会话,又要从头解释项目结构、编码规范、测试框架。没有记忆的 Agent 用一万次还是第一次。
设计方案:按生命周期分层的记忆体系
记忆生命周期(从短到长)
瞬时记忆
Tool Result
可被压缩回收
会话级记忆
Session Memory
自动提取结构化笔记
项目级记忆
Auto Memory
后台 Agent 提取持久知识
团队级记忆
Team Memory
多人共享,服务端同步
永久级记忆
配置文件(如 CLAUDE.md)
用户手动维护,版本控制
优先实现的是 Session Memory,它的 ROI 最高:
每轮结束后,用轻量模型自动提取结构化笔记------当前在做什么、关键文件路径、遇到了什么错误。当对话被压缩时,这些笔记注入摘要中,确保关键信息不因压缩丢失。
这是记忆体系和压缩体系的交汇点------Session Memory 的存在让 Level 4 压缩的信息损失大幅降低。
Auto Memory(项目级记忆)的设计约束:
不是什么都值得记住。建议用封闭类型约束"什么该存":
- 用户角色:偏好、习惯
- 方法论偏好:测试框架选择、代码风格
- 进行中的工作:当前任务上下文
- 外部系统指针:文档链接、API 地址
一个重要原则:可从代码或 git 推导的信息不应保存 。比如"项目用了 React 18"------这从 package.json 就能看到,存到记忆里只会随版本升级而过时。
记忆的召回也需要设计:
不是把所有记忆都塞进上下文,而是按相关性选择:
- 配置文件全文始终注入(体量小、价值高)
- 编辑文件时沿目录树触发相关记忆
- 每轮用轻量模型做语义相关性筛选(比关键词匹配更准,能理解用户意图做跨领域关联)
对比参考
- Codex CLI 在记忆维度几乎空白------只有 JSONL 消息历史和
AGENTS.md项目指令,没有自动提取和跨会话知识管理 - OpenClaw 的
memory-lancedb插件提供了向量搜索的召回路径,可插拔设计是正确方向,但缺少自动整合机制
踩坑提醒
记忆和压缩的交互需要特别注意:压缩清除旧附件后,记忆的去重状态应该重置,让同一份记忆文件可以在后续轮次被重新召回。否则会出现"记忆存在但永远不会被注入"的 bug。
四、消息规范化:模型比你想象的更在意消息格式
问题
从本地消息到 API 调用,中间的转换远不是 JSON.stringify 那么简单。Agent 的消息流里可能混着系统注入的元信息、孤立的 thinking 块、重复的 user 消息、无效的工具引用。模型对消息顺序和格式是敏感的------一条放错位置的系统消息,可能比写错一段 Prompt 更影响输出质量。
设计方案:标准化管线 + 统一附件协议
消息规范化不需要很复杂,但需要覆盖以下检查点:
原始消息流
去除孤立 thinking 块
合并连续同角色消息
修复工具调用/结果配对
确保首条是 user 消息
注入附件
token 预算检查
干净的 API 请求
附件系统是一个容易被忽视但很有价值的设计。把额外上下文(文件内容、记忆、状态信息、Hook 结果)通过统一协议注入当前轮次:
统一附件协议
文件附件
@提及 / IDE 选中
记忆附件
配置文件 / 语义召回
状态附件
Agent 列表变化 / 工具断连
Hook 附件
pre-commit 结果
统一注入接口
当前轮次完整上下文
好处是:任何子系统都可以通过同一接口向当前轮次补充上下文,而不是各自找一个"缝隙"塞信息。这让系统的可扩展性好得多。
五、工具执行并行:把延迟藏在模型输出的空隙里
问题
Agent 的一个典型轮次是:模型输出 → 执行工具 → 结果回灌 → 模型继续输出。如果工具串行执行,用户会感觉到明显的"卡顿-输出-卡顿-输出"节奏。
设计方案:按工具语义决定并发策略
关键判断:并发策略不由模型决定,而由工具语义决定。
流式并行时序
边输出边执行
边输出边执行
模型流式输出...
Read A ⬆
Grep B ⬆
等待 A,B 完成
Edit C(独占)
结果回灌
工具并发分类
只读工具
Read / Grep / Glob
可互相并行
修改型工具
Edit / Write / Bash
必须独占执行
更进一步,可以把 memory prefetch 和相关文件预加载藏在流式输出和工具执行的空隙里------不是每轮同步阻塞等待,而是利用空闲时间预取下一步大概率需要的信息。
感知延迟比绝对延迟更重要。 用户看到模型在打字时,工具已经在后台跑了------这种体验差距不是靠模型速度能弥补的。
六、关键参数调优:先埋点,后调参
上下文管理中有很多需要选择的参数:压缩触发阈值、输出 token 上限、续写次数限制等。
不要凭直觉定参数,要用数据。 建议至少埋以下四个点:
| 埋点 | 用途 |
|---|---|
| 压缩触发次数 / 会话 | 判断窗口大小是否匹配业务场景 |
| 压缩失败次数 | 决定熔断阈值 |
| 输出 token 分布(p50/p95/p99) | 决定默认 Cap 和升级阈值 |
| 续写次数分布 | 决定最大续写次数和递减收益检测 |
输出 token 管理的两阶段策略:
- 默认 Cap 设低(如 8K):绝大多数请求在这个范围内完成,节省 slot 资源
- 截断后静默升级:如果 8K 不够,自动升级到更大限制(如 64K),对模型透明
- 递减收益检测:如果连续 3 次续写,每次增量不足 500 token,说明模型已经没有实质性内容了,停止续写
七、子系统协作:不是各做各的
前面讲的每一层都不是孤立的。真正形成体系的关键,是这些子系统之间的协作:
上下文生命周期
下一轮构建时
召回相关记忆
压缩后记忆去重重置
同一记忆可再次召回
Session Memory
注入压缩摘要
Memory prefetch
藏在工具执行空隙
- 构建
三路汇聚 + 附件注入
2. 执行
流式推理 + 工具并行
3. 压缩
四级渐进 + 边界标记
4. 持久化
五层记忆 + 语义召回
三条关键协作链路:
- 压缩后记忆重生:压缩清除旧附件后,记忆去重状态重置,同一份记忆可以再次被召回
- Session Memory 辅助压缩:压缩前获取结构化会话笔记,注入摘要,确保关键信息不因压缩丢失
- 预取藏在空隙里:Memory prefetch 叠在流式输出和工具执行期间执行,不阻塞主流程
总结:上下文管理的优先级路线图
如果你正在做自己的 Agent 系统,建议按以下优先级逐步建设:
P0:消息规范化
- 三路缓存分离
(上线前必须有)
P1:Micro Compact
- Snip 裁剪
(长对话必须有)
P2:Session Memory
- 压缩联动
(体验质变)
P3:Auto Compact
- 熔断保护
(极端场景兜底)
P4:完整记忆体系
- 工具并行
(精细化运营)
核心观点:上下文管理不是"以后优化"的事。 消息格式、缓存结构、压缩协议一旦跑起来就很难改。模型会越来越强,但上下文运行时的工程质量决定了 Agent 能不能在长会话、多工具、复杂任务场景下稳定交付。