如何设计 Agent 上下文管理系统:从踩坑到体系化的实践指南

做多 Agent 系统时,最先遇到的不是模型能力问题,而是上下文管理问题——聊几轮就"失忆"、工具结果撑爆窗口、每次新会话都要从头解释项目背景。本文从实际开发中遇到的问题出发,结合对 Claude Code、Codex CLI、OpenClaw 等开源项目的源码研究,总结出一套 Agent 上下文管理的设计方法论。

为什么上下文管理是 Agent 的核心基础设施

大多数团队对"上下文不够用"的反应是选更大窗口的模型或写更短的 Prompt。但实际做下来会发现,上下文管理不是提示词工程,而是运行时基础设施——它涉及缓存策略、压缩协议、记忆持久化、消息规范化、并发调度等多个工程子系统的协作。


一、上下文组装:分层缓存比"一股脑塞进去"省一半钱

问题

每次 API 调用都要把系统指令、用户偏好、项目规范、对话历史、工具定义打包发送。这些信息的变化频率完全不同——系统指令几乎不变,用户偏好每人不同但会话内稳定,工具执行结果每轮都变。如果揉在一起发送,任何一处变化都会导致整段 Prompt Cache 失效。

设计方案:三路分离

核心思路是按变化频率拆分上下文,让每路独立缓存

三路上下文分离

System Prompt
(几乎不变)
全局共享缓存

User Context
(每用户不同,会话内稳定)
用户级缓存

Session Context
(每轮可能变化)
体量最小,不缓存

API 调用

实现要点:

  1. System Prompt 全局共享:工具定义、角色设定、输出格式约束放在这里。所有用户的首次调用都能命中同一份缓存。关键约束:这一层的内容一旦上线就不要频繁改动,任何改动都会导致全量缓存失效。
  2. User Context 用户级缓存:用户偏好、项目规范(比如 CLAUDE.md 这类文件)放在这里。同一用户的会话内保持稳定。
  3. Session Context 不缓存但控制体量:当前轮次的 git diff、工具执行结果等放在这里。因为每轮都变所以不缓存,但要严格控制体量。

成本收益:以 Anthropic 的 Prompt Cache 定价为例,缓存命中的 token 成本是未命中的 1/10。如果 System Prompt 占输入 token 的 60%(这在工具定义多的 Agent 中很常见),三路分离后仅这一项就能降低约 50% 的输入成本。

对比参考

  • Codex CLI 做了基线 diff(reference_context_item),避免每轮全量重发,但上下文整块发送,没有显式区分缓存边界
  • OpenClawContextEngine 接口可插拔性好,对 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 CLITruncationPolicy 控制工具输出截断,但只有"截断/不截断"两档,没有渐进中间态
  • 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(项目级记忆)的设计约束:

不是什么都值得记住。建议用封闭类型约束"什么该存":

  1. 用户角色:偏好、习惯
  2. 方法论偏好:测试框架选择、代码风格
  3. 进行中的工作:当前任务上下文
  4. 外部系统指针:文档链接、API 地址

一个重要原则:可从代码或 git 推导的信息不应保存。比如"项目用了 React 18"——这从 package.json 就能看到,存到记忆里只会随版本升级而过时。

记忆的召回也需要设计:

不是把所有记忆都塞进上下文,而是按相关性选择:

  1. 配置文件全文始终注入(体量小、价值高)
  2. 编辑文件时沿目录树触发相关记忆
  3. 每轮用轻量模型做语义相关性筛选(比关键词匹配更准,能理解用户意图做跨领域关联)

对比参考

  • Codex CLI 在记忆维度几乎空白——只有 JSONL 消息历史和 AGENTS.md 项目指令,没有自动提取和跨会话知识管理
  • OpenClawmemory-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 管理的两阶段策略:

  1. 默认 Cap 设低(如 8K):绝大多数请求在这个范围内完成,节省 slot 资源
  2. 截断后静默升级:如果 8K 不够,自动升级到更大限制(如 64K),对模型透明
  3. 递减收益检测:如果连续 3 次续写,每次增量不足 500 token,说明模型已经没有实质性内容了,停止续写

七、子系统协作:不是各做各的

前面讲的每一层都不是孤立的。真正形成体系的关键,是这些子系统之间的协作:

上下文生命周期

下一轮构建时
召回相关记忆

压缩后记忆去重重置
同一记忆可再次召回

Session Memory
注入压缩摘要

Memory prefetch
藏在工具执行空隙

1. 构建
三路汇聚 + 附件注入

2. 执行
流式推理 + 工具并行

3. 压缩
四级渐进 + 边界标记

4. 持久化
五层记忆 + 语义召回

三条关键协作链路:

  1. 压缩后记忆重生:压缩清除旧附件后,记忆去重状态重置,同一份记忆可以再次被召回
  2. Session Memory 辅助压缩:压缩前获取结构化会话笔记,注入摘要,确保关键信息不因压缩丢失
  3. 预取藏在空隙里:Memory prefetch 叠在流式输出和工具执行期间执行,不阻塞主流程

总结:上下文管理的优先级路线图

如果你正在做自己的 Agent 系统,建议按以下优先级逐步建设:

P0:消息规范化
+ 三路缓存分离
(上线前必须有)

P1:Micro Compact
+ Snip 裁剪
(长对话必须有)

P2:Session Memory
+ 压缩联动
(体验质变)

P3:Auto Compact
+ 熔断保护
(极端场景兜底)

P4:完整记忆体系
+ 工具并行
(精细化运营)

核心观点:上下文管理不是"以后优化"的事。 消息格式、缓存结构、压缩协议一旦跑起来就很难改。模型会越来越强,但上下文运行时的工程质量决定了 Agent 能不能在长会话、多工具、复杂任务场景下稳定交付。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐