Agent 为什么会忘记前文?拆解 Codex、pi、Claude Code 的上下文与记忆
让 Agent 连续工作,关键是保存可恢复的历史,并为每次推理构建合适的上下文。 摘要负责概括旧进展,近期原文保留正在处理的细节,历史查询负责找回证据,长期记忆负责跨任务复用。四者各有职责。
目录
本文比较 Codex、OpenAI Responses、pi、Claude Code、ChatGPT 和 Meta Muse,随后给出一套可落地的参考架构,并展开 usage 增量计数与多次摘要管理。资料核对日期为 2026-10-10;源码分析固定到文中列出的提交。产品功能与不同版本可能继续变化。
先区分:压缩、裁剪、检索和记忆
“聊天记录还在”和“模型这次看到了全部聊天记录”是两件事。理解上下文管理,可以先把系统拆成四层:
| 层次 | 保存或处理什么 | 使用方式 |
|---|---|---|
| 原始历史 | 用户消息、Agent 回复、工具调用与结果、附件引用、修订信息 | 在授权和保留期内提供原文回查与恢复 |
| 工作上下文 | 当前规则、任务状态、摘要、近期原文、检索片段 | 本次实际送入模型,有明确 token 预算 |
| 长期记忆 | 经筛选的偏好、稳定事实、经验和项目知识 | 按作用域、时效和当前任务选择载入 |
| 执行状态 | 已提交的操作、任务进度、回调状态、幂等键 | 由程序维护,保障任务恢复与正确执行 |
下面是已公开压缩机制的简化对照。图中各路径是不同实现,不能直接组合成某一产品的真实内部流程;ChatGPT 和 Muse 没有足够公开信息支持绘制其压缩算法。
这几个概念会产生不同效果:
- 裁剪直接把某些内容移出工作上下文,例如仅保留最近 N 轮。若没有摘要或回查,旧信息就无法参与当前推理。
- 摘要压缩把旧内容转成更短的状态描述。它可以保留任务进展,但属于有损表达。
- 检索从历史或外部资料中按需补入片段。它保留取回精确证据的机会,也增加查询延迟和选择难度。
- 长期记忆提炼可以跨任务复用的信息,需要纠错、更新和删除机制。
- Prompt cache复用相同前缀的计算。它影响成本和延迟;缓存命中内容仍占本次上下文空间。
最后一项容易混淆。OpenAI 对 Codex 的工程说明强调保持工具与前缀的稳定顺序以提高缓存命中,同时另行通过压缩管理上下文窗口。这是两种资源管理。Codex agent loop
六种实践:机制、状态和设计取舍
Codex:阈值触发,但压缩路径有区别
Codex 配置提供 model_auto_compact_token_limit,未显式设置时使用模型默认值。当前参考文档还区分整个输入与携带前缀之后的增长量作为阈值统计范围。因此,不能把某个固定 token 数或窗口比例视为所有 Codex 会话的通用默认值。配置参考
分析固定到 openai/codex 提交 806d973。其中至少有三种不同语义:
| 代码路径 | 如何缩减 | 阅读代码时应注意 |
|---|---|---|
本地 compact.rs |
生成文本摘要,并重建有界历史 | 保留的用户原文不等于完整的最近若干轮 |
远端 compact_remote_v2.rs |
获取压缩项,再按客户端规则保留部分消息、重建上下文 | 保留形态受预算与功能开关影响 |
compact_token_budget.rs |
按 token 预算开启新上下文窗口 | 此路径跳过模型摘要,不能仅凭“compaction”名称推断行为 |
本地实现为保留的用户消息文本设置 20,000 token 上限,从较新的消息向前选择;必要时截断选中范围中较旧的文本。摘要成功后才替换活动历史,并记录来源信息、重算用量、重新加入当前上下文。压缩请求自身超限时,也存在移除候选历史最旧项后重试的处理。这说明它有明确的容量约束,不能据此宣称任意长历史都被无损覆盖。本地压缩源码
远端 v2 路径在该提交中使用 64,000 token 的保留消息预算,过滤消息后追加新的压缩项,并重新构建当前运行上下文。这个数是特定实现的保留预算,不是模型窗口大小,也不是所有客户端的统一阈值。远端 v2 源码
另一个预算策略直接启动新窗口,仍执行压缩生命周期钩子,却跳过模型和服务端摘要。部署时需要确认实际选择的策略。预算策略源码
**工程启发:**将“何时触发”“怎样缩减”“保留什么”“如何提交新状态”分开设计。模型支持原生压缩时,可以使用其能力;应用仍应掌握自己的历史、任务状态与恢复边界。
OpenAI Responses:压缩状态成为 API 协议的一部分
Responses 官方公开了两种使用方式。共同点是返回不透明、加密的压缩项;应用不能把它当成可编辑的 Markdown 摘要,也不能从字段外观推断内部编码。
| 方式 | 触发与输入 | 续聊规则 |
|---|---|---|
| 服务端压缩 | context_management 中配置 compact_threshold,按渲染后的 token 量触发 |
无状态模式保留返回项;可清理最新压缩项之前的历史 |
使用 previous_response_id 的服务端模式 |
服务管理上下文状态 | 传新输入,按文档约定由服务管理历史 |
独立 /responses/compact |
显式提交当前窗口;输入仍必须放得进模型窗口 | 将返回的完整新窗口原样作为后续输入基础,再追加新内容 |
独立接口返回值可能还包含保留消息,因此“只拿压缩项、丢掉其他返回项”并不是通用的正确接法。也不能一直等到历史已经超过可接受输入上限才首次压缩。Responses compaction 文档
**工程启发:**Provider 的返回窗口应由独立适配层处理。跨模型迁移时保留可读历史和应用任务状态,不把某个 Provider 的不透明状态当作唯一恢复来源。
OpenAI Agents SDK Cookbook 还给出了轮次裁剪与“摘要+最近 N 轮”的参考示例。这些示例展示应用侧策略,并非 ChatGPT 的生产实现。固定轮数适合解释边界,实际容量仍应按渲染后的 token 量判断。Session memory 示例
pi:摘要与原文保留边界一起持久化
pi 的实现提供了清晰的状态模型。分析固定到 earendil-works/pi 提交 42a3497。
其默认自动压缩条件为:
contextTokens > contextWindow - reserveTokens
reserveTokens = 16,384
keepRecentTokens = 20,000
这些值是该版本默认设置,可以调整;keepRecentTokens 以 token 为单位。它从近期内容向前寻找保留边界,通常按用户轮次切分;单轮过大时可以在合适的 assistant 边界拆分,同时避免拆散工具调用与对应结果。pi 压缩文档
关键状态是 CompactionEntry:包括摘要、firstKeptEntryId、压缩前 token 量和相关元数据。Session 采用追加式 JSONL 与父子 entry 关系;上下文投影读取压缩状态,将摘要与保留边界之后的消息组合起来。原始记录与实际模型输入由此分开。Session manager 源码
后续压缩会使用已有摘要与此前未覆盖的前缀。源码也根据上下文编辑后的投影校正估计,避免继续沿用已过时的 usage。与此同时,启发式 token 估算、摘要前工具结果截短等处理仍可能损失精度;保存边界并不保证摘要记住所有语义。Compaction 源码
**工程启发:**摘要正文和覆盖位置必须组成同一个检查点。重启后才能确定哪些历史已经被表示、哪些原文仍需载入。分支切换的摘要则需要额外记录分支关系,不能当作普通线性会话压缩。
Claude Code:先降低噪声,再压缩,持久规则独立加载
官方说明中,Claude Code 接近上下文限制时先清理较旧的工具输出,必要时再摘要。可用 /compact 指定重点;早期仅存在于聊天中的细节可能在摘要中丢失。单次巨大输入导致反复压缩仍无空间时,系统会停止这种循环并报告问题。Claude Code 工作机制
持久信息另有入口:项目根目录 CLAUDE.md 在压缩后重新读取并注入;auto memory 通过简短 MEMORY.md 索引和按需读取的主题文件组织。当前文档规定启动时读取索引前 200 行或 25KB,以先达到者为准。这些文件提供上下文,严格的执行限制仍需代码或钩子实施。Claude Code memory
Anthropic 的工程文章将压缩、结构化笔记和按需检索视为互补方法,建议先检查摘要对关键信息的召回,再优化简洁程度。Effective context engineering
**工程启发:**重复文件、完整日志和失效工具输出不必持续占据工作上下文。长期规则、任务状态与历史摘要也需要独立来源,避免所有信息都依赖一次摘要生成。
ChatGPT:公开记忆能力,压缩算法仍缺少证据
ChatGPT 官方说明记忆可利用保存的信息和过往聊天,但不会保留每一个细节;用户可以查看、纠正和删除记忆。删除聊天与删除已保存记忆也需要分别处理。
这些资料没有足够细节确认同一聊天内的压缩阈值、保留窗口和摘要淘汰算法。不能由 ChatGPT Memory 推断它采用 pi 的边界策略,或直接套用 Responses 的内部状态格式。Memory in ChatGPT
**工程启发:**面向用户的“记住”需要可解释、可纠正和可删除。界面显示完整聊天记录,也不意味着每次模型推理读取全部原文。
Meta Muse:持久文件与长期工作空间,压缩细节未公开
Meta Muse 的公开设计强调主对话、侧对话,以及可以跨聊天使用、由用户查看和编辑的记忆文件。它公开了连续交互的产品方向,但没有给出可核实的 token 阈值、摘要结构或原文保留算法。How We Designed Muse
Meta 的安全架构说明进一步披露专属隔离 Linux 环境,并将该虚拟机视为记录系统;用户可以检查、编辑或下载记忆与其他文件。这能支持对持久状态和运行隔离的分析,不能作为压缩准确率的证据。Meta Muse 安全架构
**工程启发:**长期 Agent 的可靠性越来越依赖可检查的外部工作空间。对话是入口,文件、任务状态和记忆共同承载连续工作。
已知效果:有收益证据,缺少统一排名依据
上述资料能说明实现与设计取舍,但没有提供这六种实践在同一模型、任务集和预算下的可比较成绩。不能据此判断哪款产品“最不丢上下文”。
Anthropic 在 2025-09-29 发布的 Claude Developer Platform 评估称:
| 官方实验 | 报告结果 | 解读范围 |
|---|---|---|
| 内部 agentic search 评估:记忆工具+context editing | 相对基线性能提升 39% | 两种能力组合的结果 |
| 同类评估:仅 context editing | 提升 29% | 清理旧工具上下文也可能有收益 |
| 100 轮网页搜索评估 | token 消耗降低 84%,完成原本因窗口耗尽而失败的流程 | 特定长工具链实验 |
这里的 39% 不是增加 39 个百分点,也不是 Claude Code 摘要的准确率。Context editing 主要清理旧工具调用与结果,不等于本文提出的完整摘要策略;公开文章没有给出足以复现全面比较的样本、方差与实验细节。官方评估说明
可以据此提出一个待验证的工程判断:有价值的上下文管理同时减少无用输入,并保留继续完成任务所需的信息。 只比较压缩率,会奖励丢信息;只比较信息召回,又可能忽视延迟、成本和工具协议完整性。
有了摘要,还需要近期原文吗?
建议保留,但将其视为按预算选择的原文尾部,而不是每轮强制淘汰的固定 N 轮。
摘要与近期原文承担不同任务:
| 内容 | 为什么摘要仍可能不够 |
|---|---|
| 用户刚补充的限定条件 | 否定、条件顺序和指代容易被概括得过宽 |
| 订单号、路径、代码、金额、日期 | 一处字符差异就可能改变操作对象 |
| 当前工具调用与结果 | 需要精确关联,摘要不能替代协议消息 |
| 失败尝试与修正 | 只保留“尝试过”会丢失失败原因与下一步依据 |
“摘要+近期原文”仍然有风险:摘要可能失真,原文尾部可能单轮超大,检索也可能找不到所需内容。因此,它需要持久历史、结构化关键事实与回查能力共同支撑。
批量压缩可以不漏区间,前提是提交顺序正确
用编号表示已完成的轮次,当前输入为:
活动检查点:摘要覆盖 1–40
原文尾部:41–81
候选压缩输入:旧摘要 + 原文 41–65
候选保留尾部:66–81
生成候选摘要期间,41–65 仍然属于活动原文尾部。 它们不能因为已经排进摘要任务就被移出输入预算。候选结果通过检查且提交成功后,才切换为:
活动检查点:摘要覆盖 1–65
原文尾部:66–81
这样可以通过程序保证覆盖区间没有断档。它保证的是“这些记录参与了摘要或继续保留原文”,不保证“摘要完整保存了每个事实”。原文回查解决后者的部分风险。
若压缩期间又追加了 82–83,成功提交时保留尾部应为 66–83。若 41–65 中的历史被修订、删除或改变可见权限,候选摘要必须重新验证或废弃。
异步预压缩可以减少前台等待,但只能提前生成候选结果。空间不足时,前台仍需要等待有效检查点;失败后应重新准备上下文、调整输入或明确报错,不能默默丢掉待摘要原文继续回答。
摘要是否应立即更新?
无需每一轮都调用模型更新摘要。更合理的触发依据是实际预算:
- 将新消息和完整工具交互加入历史。
- 构建真实待发送上下文,评估剩余空间。
- 空间充足时沿用旧摘要,并继续保留全部未覆盖尾部。
- 达到触发条件时,压缩合适的历史前缀。
- 成功提交新检查点后,才移出被覆盖的原文。
近期原文预算决定一次压缩后保留多少细节;触发阈值决定何时值得支付压缩成本。二者分开后,批量压缩和连续性可以兼顾。
参考架构:持久历史、可恢复检查点、按需证据
以下是综合公开实践提出的应用侧设计,不是对某个闭源产品内部实现的描述。
1. 用稳定会话标识归属历史,运行 Session 标识单次执行
原始记录应有稳定的会话归属和确定顺序。运行 Session、模型调用、连接和执行实例可以多次变化;恢复上下文时按稳定会话读取历史。
建议记录 conversation_id、event_id、顺序、角色、工具调用关联、内容或附件引用、修订与可见性信息。会话隔离规则仍要包含租户、访问主体与分支作用域;标识相同本身不能替代授权检查。
这一步首先解决“历史属于谁”。摘要解决“本次怎样表示这些历史”。两者分开后,运行实例重启或切换才不会造成上下文归属混乱。
2. 检查点记录摘要,也记录可验证的覆盖边界
一个可恢复检查点至少应包含:
| 字段 | 用途 |
|---|---|
| 会话与分支标识 | 确定适用范围 |
covered_through_event_id 或 first_kept_event_id |
区分已覆盖历史与原文尾部 |
source_revision、必要的范围哈希 |
检测历史修订与候选结果过期 |
generation、父检查点 |
防止旧候选覆盖较新的状态 |
summary 或 Provider 状态 |
保存新的上下文表示 |
| 摘要格式与生成模型版本 | 解释结果,支持升级与重新构建 |
| 关键事实的来源引用 | 支持精确回查与用户纠错 |
线性会话可以从简单的覆盖位置开始。只有出现分支、消息排除或历史编辑,才增加相应范围表达;不要一开始构建通用事件图。
摘要和边界在一次原子提交中更新。后台生成时读取固定快照;提交时检查被摘要前缀仍有效、活动检查点代际未被替换、当前权限允许继续使用。追加新消息不必天然使候选失效,前缀修订则需要重新处理。多实例可用单任务租约配合版本比较,最终以数据库提交条件防止过期写入。压缩和恢复重试应有次数与时间上限;超过上限时明确报告准备失败,避免在压缩循环中持续消耗资源。
3. 预算使用真实投影,并给下一步留空间
容量约束可以表达为:
规则 + 工具定义 + 任务状态 + 记忆
+ 摘要 + 未覆盖原文 + 本次检索内容
+ 输出预留 + 输入增长安全余量
<= 模型上下文容量
这是预算模型,不是统一默认值。初始阈值应依据实际模型、工具结果大小和延迟目标选择,再用观测调整。
计数需遵循 Provider 的语义:缓存输入通常仍占上下文容量,已包含在 input 或 total 中的缓存量不能再次累加。压缩、工具结果替换或历史过滤后,应对新的投影重新估算,避免用上次请求的 usage 判断已经改变的输入。
中文、图片、音频、附件和工具协议都有额外成本。优先使用 Provider 可用的计数能力;估计器应说明适用范围,并根据实际 usage 校准。工具输出也需限制首轮进入上下文的体量,避免一个超大结果刚返回就再次填满窗口。
压缩完成后的目标容量应低于触发容量,留下足够空间继续工作。对于无法按轮次完整保留的巨大输入,可保存原件、生成内容索引并按需取片段;完整未完成工具调用仍需满足接口协议。
4. 摘要保存任务状态,关键事实保存结构与来源
建议摘要包含当前目标、有效约束、已完成动作与结果、失败尝试、关键决策及原因、未解决问题、下一步,以及需要回查的来源。
金额、标识符、时间、地址、授权状态等影响具体操作的信息,优先进入带来源的结构化记录。用户修正应显式标记替代关系;不确定信息标记为未确认。
摘要文字不能改变权限,也不能证明某个外部动作已经提交。付款、发信、创建资源等操作状态应从业务记录和幂等机制读取;恢复时不会因为摘要里写着“下一步执行”而重复产生副作用。
滚动更新“旧摘要+新前缀”效率较高,却会累积语义漂移。应保留原文和来源,允许按需重建摘要;是否定期重建以及重建频率,交由误差和成本数据决定。
5. 历史查询补精确证据,摘要帮助定位
查询可以先从确定性过滤开始:按会话、事件、时间区间、关键词、工具类型或实体标识检索。返回有边界的结果页,包含来源、时间、是否还有下一页及明确的截断信息。
“找回一条旧订单号”和“理解整段长期任务进展”需要不同输入。让摘要提供进展导航,再按问题取回原文,通常比每轮扫描全部历史更可控。
只有确定性查询的召回不足时,再引入语义检索或分层主题摘要。检索出的用户文本和工具结果应维持原始来源与信任等级,不能因为进入记忆就获得更高指令权限。
6. 长期记忆有自己的更新与失效规则
从会话中提炼长期记忆时,应记录作用域、来源、更新时间、可信程度,以及需要时的失效条件。稳定偏好可以跨任务复用;临时进度应留在任务状态中;过期业务事实需重新核验。
用户删除历史、撤销权限或纠正事实后,相关摘要、检索索引和长期记忆都需要失效或重建。备份和审计记录也应遵循适用的保留与访问政策。“原文已删除,旧摘要仍泄露内容”属于数据生命周期错误。
原始历史在保留期内应可恢复,但保存全部历史也不是无限期保留的理由。连续性设计与删除能力应一起建设。
usage 怎么增量计数:先分清上下文占用和累计消耗
压缩会减少当前输入,但不会减少已经发生的 token 消耗。 实现时应分别维护下面三种数据:
| 数据 | 表示什么 | 更新方式 |
|---|---|---|
| 当前上下文估计 | 下一次推理的输入占用 | 沿用有效基线并计入新增内容;压缩或编辑后重新估算 |
| 单次调用 usage | 一次模型调用实际处理的输入与输出 | 按 Provider 协议归一化,流式阶段暂存,结束后确认 |
| 会话或任务账本 | 多次回答、摘要、重试等累计消耗和费用 | 汇总不同实际调用;重复事件只更新同一条记录 |
累计账本不能用于判断窗口是否已满。否则,一次长会话即使压缩成很小的输入,也会被历史总消耗持续判定为超限。
Provider 字段先归一化,缓存部分只计一次
相同字段名在不同协议中可能有不同含义。以下是当前官方接口的输入计数方式:
| 接口 | 总输入 token | 缓存字段的关系 |
|---|---|---|
| OpenAI Responses | input_tokens |
input_tokens_details.cached_tokens 和支持时的 cache_write_tokens 属于输入总数的组成部分 |
| Claude 原生 Messages | input_tokens + cache_read_input_tokens + cache_creation_input_tokens |
三个字段分别表示普通输入、缓存读取和缓存写入 |
| SDK 已归一化的 usage | 遵循该 SDK 的字段契约 | 先确认 SDK 是否已经合并缓存,避免再次相加 |
OpenAI 官方费用示例从总输入中减去缓存读、写,再按不同费率计算;Claude 官方文档则明确将三个输入字段相加。二者不能复用同一个未经适配的求和公式。OpenAI prompt caching、Claude prompt caching
例如,同样是 10,000 输入 token,其中 8,000 来自缓存:OpenAI 的总输入仍是 10,000;Claude 可以报告普通输入 2,000、缓存读取 8,000、缓存写入 0。两者的上下文占用都按 10,000 处理。缓存价格单独计算,不能把折扣后的“费用等价 token”当成窗口占用。
建议适配层保留原始 usage,同时输出明确的总输入、普通输入、缓存读取、缓存写入和输出字段。计费还需匹配实际模型、当时费率及其他收费项。缺失数据应标记未知,不能一律补成零。
用最近一次有效输入作基线,只计算新增部分
pi 在固定提交中使用最近一次有效 assistant usage,加上其后的消息估计;遇到之后发生的上下文编辑或压缩,会放弃旧基线,按当前投影重新估算。其基线含该次输出,因此不能再把同一回复加一遍。pi 计数源码
下面给出另一种应用侧参考实现:基线只保存上次请求的总输入。在旧输入前缀保持有效、Provider 没有改变其表示的条件下:
下一次输入估计 =
最近一次有效请求的总输入 token
+ 该请求输入边界之后、实际保留内容的 token 估计
+ 估算余量
这里的新增内容包括上次回复中实际保留的文本或协议块、新工具结果、新用户消息,以及其他追加项。必须按完整渲染形式计数,包含角色、工具定义和消息封装。已计费的输出 token 未必等于下一次实际保留的输出,尤其涉及推理状态、内容过滤或不透明协议项时。
**示例,以下数字仅用于演示:**上次输入 18,000;随后保留 assistant 内容 700、工具结果 2,300、新用户消息 500,则下一次输入估计为 21,500,再加估算余量。输出预留另计。若采用含上次输出的基线,700 就不能重复相加。
为支持这个快速路径,计数状态至少需要记录:
| 状态 | 用途 |
|---|---|
| 会话、分支与 Provider/模型标识 | 防止使用其他作用域的计数 |
projection_epoch,即上下文视图版本 |
旧前缀发生结构变化时递增 |
input_through_item_id |
明确真实输入已经覆盖到哪里 |
measured_input_tokens 与对应 response ID |
保存输入基线及其来源 |
| 渲染器和协议版本、计数完整性 | 判断基线能否继续使用 |
仅追加内容不必使视图版本变化。压缩提交、历史编辑或删除、分支切换、旧检索片段替换、规则或工具定义变化,以及模型或渲染协议变化,都需要检查基线有效性。无法证明旧前缀仍一致时,重新估算当前工作上下文,无需扫描会话全部原文。
可以按“模型/渲染版本+消息 ID+内容修订”缓存单项估计,并维护新增尾部的累计值。正常追加只处理变化项;失效后重算有预算上限的工作视图。单项 token 数相加仍是估计,协议封装、分词边界和多模态表示都会产生误差,应以实际 usage 校准;接近窗口限制时,使用可用的 Provider 计数接口或更早触发压缩。
压缩模型的 usage 只能进入消耗账本,不能成为主对话的容量基线。 两次请求的提示词不同。新摘要提交后先重新估算“摘要+尾部”,再用下一次有效主对话调用校准。并发旧请求晚到的 usage 同样可以入账,但不能覆盖已经更新的视图基线。
流式 usage 按协议合并,不能把累计快照当增量相加
Claude 官方明确说明,message_delta 中的 usage 是累计值。假设同一响应先后报告输出 30、80、120,最终输出是 120,而不是 230。Claude streaming
建议按 Provider 账户作用域和 response ID 记录单次消耗;无可用 response ID 时使用本地 attempt ID,并在获得响应标识后关联。每条记录保存逻辑任务、调用用途、原始字段、归一化结果和完整/部分/未知状态。更新规则如下:
- 按协议区分累计快照和真正增量。累计字段更新已有值;事件中缺失的字段不清零。按事件顺序或版本拒绝过期更新,防止晚到的中间事件覆盖终态。结束后以确认结果替换暂存结果。
- 重复终止事件更新同一条记录,不再增加一次费用。真正重新发起并产生消耗的调用单独入账,包括失败的摘要尝试。
- 中断或失败仍可能产生费用。保留已知部分并标记不完整,后续与 Provider 账单核对;不要用部分 usage 建立容量基线。
- 会话累计消耗按实际调用记录汇总。压缩成功不清空账本;账单更正则更新对应记录,并重新汇总。
若需要实时更新累计统计,采用以下参考规则:
本次入账差额 = 同一次调用的新已知值 - 该调用上次已入账值
会话累计值 += 本次入账差额
调用记录和累计值在同一事务中更新,或通过版本比较实现幂等汇总。重复事件的差额为零;从暂存值改为最终值时只补差额;后续账单更正的差额可以为负。未知字段不参与确定值的累加,界面区分已确认与暂存金额。这样也避免“流式期间计一次,结束后又全量计一次”。
这样可以同时解释“窗口缩小了多少”和“为了缩小窗口花了多少”。摘要改变前缀还可能影响缓存复用,评估收益时应比较回答、压缩和检索的总成本,而不只看某一次输入变短。
多次摘要怎么管理:一个活动摘要,旧版本按需保留
同一会话的同一分支,每次推理默认只载入一个有界的活动摘要,再接上未覆盖原文。 多次压缩产生的是摘要版本;它们可以留在存储中,却不应全部进入提示词。
社区实现怎样避免摘要越积越多
| 实现 | 与本问题直接相关的机制 |
|---|---|
| pi | 上下文投影只让最新 compaction 记录贡献压缩摘要;旧记录仍可留在 Session 历史中 |
| LangMem | 将新的 RunningSummary 替换旧状态;模型输入使用摘要与剩余消息,完整历史可放在另一个状态字段 |
| LangGraph | Checkpointer 保存运行快照以支持恢复;官方提醒检查点会持续增长,需要清理或保留策略 |
这些机制分别体现了“存储历史”和“本次模型输入”的分工。pi 投影源码、LangMem 示例、LangGraph persistence
LangMem 的 RunningSummary 保存正文、已摘要消息 ID 集合和最后摘要位置。本文源码核对固定到 提交 48e3c11。对严格有序、始终覆盖连续前缀的应用,可以采用前文的覆盖游标与修订信息,避免元数据保存越来越大的 ID 集合;存在分支、跳过或不连续覆盖时,单个游标不足,需要范围或 ID 记录。
使用框架也要核对失败边界:LangMem 文档中的 max_summary_tokens 用于预算估计,不会自动传给生成模型限制输出;待摘要输入超出其限制时,还可能只保留选定输入的末段。要求连续覆盖的应用需要在调用前分批控制输入,并校验实际输出,不能仅凭一个配置参数宣称所有历史都被处理。LangMem API 边界说明
活动摘要、历史版本和分段摘要,分别存什么
以下是应用侧参考设计,具体保留数量与期限由恢复要求、数据政策和成本决定:
| 记录 | 保存内容 | 正常推理是否载入 |
|---|---|---|
| 活动摘要 | 当前任务状态、覆盖边界、来源修订、实际 token 数、格式版本 | 载入一个,并计入预算 |
| 历史摘要版本 | 曾经生效的完整摘要及生成信息 | 不载入;用于回溯、诊断或重建 |
| 分段摘要,可选 | 独立历史区段的索引、事实与来源范围 | 仅在相关查询命中且预算允许时载入 |
例如,S1 覆盖 1–40,S2 覆盖 1–65,S3 覆盖 1–90。当前输入应使用 S3+原文 91 之后的尾部。S1、S2、S3 是互相重叠的版本;拼接它们会重复表达旧信息,并重新制造容量压力。
建议为会话分支保存一个 active_checkpoint_id。摘要记录保留前文所述的覆盖边界、来源修订、代际和格式,再增加状态、实际长度、创建时间和必要的保留标记。活动指针与摘要记录在一次原子提交中更新,同时使旧容量基线失效。每个新活动摘要应自包含其职责范围内的任务状态;普通恢复无需沿父版本链重新拼接全文。
候选结果验证通过后替换,旧版本进入保留流程
摘要记录可以采用下面的生命周期:
| 状态变化 | 条件与处理 |
|---|---|
| 候选 → 活动 | 来源前缀仍有效、父代际一致、内容检查通过且实际长度合格;比较版本后提交 |
| 旧活动 → 已替代 | 同一次提交切换活动指针;按保留策略保存旧版本 |
| 候选 → 失败或过期 | 生成失败、超出重试限额或版本过期;不移除原文尾部,不替换仍有效的活动状态 |
| 活动/历史版本 → 失效 | 来源被修订、删除或权限变化;禁止继续加载受影响内容并安排重建 |
| 已替代/失败 → 归档或删除 | 保留期已到,且没有恢复、分支、运行中任务或合法审计依赖 |
生成时同时设置输出上限并检查实际摘要长度、结构完整性和来源有效性。超预算或被截断的候选不能靠截掉末尾强行提交;应在有限重试内重新压缩,仍失败就保留旧检查点,并暂停依赖新容量的推理。关键事实有自己的来源记录,不能让“摘要更短”成为覆盖它们的理由。
存储上可以保留当前活动版本和少量近期完整版本,其余按策略归档或删除;诊断元数据也需要保留上限。索引应支持按会话分支直接读取活动指针与边界之后的历史,而不是每轮遍历所有摘要记录。后台清理失败候选,防止重试产生大量无用记录。
框架检查点和摘要正文不是同一层数据。 即使最新摘要是完整快照,框架的恢复状态仍可能依赖父记录、待完成写入或其他存储结构。清理应使用存储后端支持的保留与删除机制,保留被依赖的状态;不能给所有检查点行统一加 TTL 后直接删除。原始历史、摘要、检索索引和备份也必须执行一致的删除与权限政策。
长历史需要回查时,增加分段索引,不把旧版本堆回来
一个固定长度的摘要无法保存无限增长历史中的全部细节。若关键事实测试表明滚动摘要与原文查询仍不足,可以增加按独立历史区段生成的摘要索引:
滚动活动摘要:描述当前目标、约束、进度和证据入口
区段 A:原文 1–100 的索引与来源
区段 B:原文 101–200 的索引与来源
区段 C:原文 201–300 的索引与来源
查询旧问题 → 选择相关区段 → 取回有预算的摘要或原文证据
每个区段记录来源范围、修订和可见性;查询按相关性选取,合并重复来源,并明确截断。这里的 A、B、C 是不同区段,S1、S2、S3 则是同一覆盖范围逐步扩大的版本,不能混用。
区段数量继续增长时,可按主题或时间增加有界的上层索引。每次只读取必要层级与相关子节点,各层仍受查询和输入预算限制。多层摘要会增加构建成本和误差,因此先以任务召回结果证明需要,再引入;不必一开始建设完整摘要树或向量数据库。
滚动摘要出现事实漂移时,应从授权原文按有界批次重建,并核验关键来源。把失真的旧摘要再摘要一遍,无法自动找回已经丢失的事实。
怎样判断方案真的有效
评价对象应是完整上下文方案,而不是一段摘要文字。建议用同一批长会话、同一模型与工具行为重放比较:
- 能装入窗口的完整历史,作为短会话参考基线。
- 仅保留最近 N 轮,观察直接裁剪的损失。
- 仅保留摘要,观察精确细节和最新交互损失。
- 摘要+近期原文。
- 摘要+近期原文+授权历史查询。
长到无法装入窗口的会话不能继续使用“全量输入”作为可执行基线,可用独立事实答案、分段证据和任务成功标准评价。每种策略都统计全部模型调用与查询成本,不能只统计最终回答。
| 指标 | 建议观察内容 |
|---|---|
| 任务完成率 | 是否完成真实目标,是否重复无效尝试或已完成动作 |
| 关键事实与修正召回 | 旧标识符、否定条件、后续更正、来源一致性 |
| 覆盖连续性 | 检查点边界前后是否遗漏或重复纳入记录 |
| 协议完整性 | 工具调用与结果是否正确配对 |
| 成本与延迟 | 回答、摘要、检索总成本,准备时间和首响应 p50/p95 |
| usage 正确性 | 缓存不重复计数,累计流式字段不重复求和,重复事件与真实重试分别处理 |
| 摘要数量与容量 | 多次压缩后仍只加载当前摘要,提示词长度有界,历史版本执行保留策略 |
| 长期漂移 | 多次压缩后任务约束与事实是否逐步改变 |
| 恢复能力 | 重启、并发追加、压缩失败、模型切换后的状态 |
| 数据生命周期 | 删除、权限变更后,派生状态是否失效 |
测试应包含长历史早期的关键事实、相互矛盾的新旧信息、单次巨大工具结果、压缩期间追加或修订消息、多次连续压缩,以及重启恢复。另需验证压缩后旧 usage 基线失效、并发旧响应晚到、流式事件重复到达,以及大量摘要版本下的读取与清理。对有随机性的回答重复试验,报告样本数量与波动范围。
“区间无遗漏”可以通过确定性检查证明;“关键事实不丢”需要事实测试;“任务效果变好”需要任务评估。三种结论不能互相代替。
对未来 Agent 工程的意义与实施建议
上下文将成为受预算约束的历史视图
一个稳定会话可以拥有完整可查询历史,同时使用多个推理窗口。模型调用只是读取工作视图,持久历史和检查点承载连续性。工程上可以把上下文管理看作有版本、可重建的历史视图,而不是不断增长的 messages 数组。
这种设计也让连续性不必绑定某个进程、连接、超时时间或 Provider。即使某次运行结束,下一次仍可恢复目标和证据。运行生命周期与历史生命周期因此可以分别优化。
更大的窗口仍需要信息选择与来源治理
扩大窗口能推迟容量压力,不能自动清除过期信息、冲突事实、重复日志和低价值工具输出。应持续优化工具返回格式、按需加载规则与证据选择,再评估是否需要更多摘要层级。
Provider 原生压缩有使用便利性,应用可读摘要有可检查与迁移优势。应通过适配层支持两者,并对模型切换后的恢复效果单独验证。
先完成可靠闭环,再增加复杂记忆能力
建议按四个阶段实施:
| 阶段 | 建设内容 | 进入下一阶段的依据 |
|---|---|---|
| 1. 明确历史与预算 | 稳定会话归属、原文回查、Provider 用量归一化、可重放用例 | 能解释输入来源与上下文增长 |
| 2. 完成压缩闭环 | 摘要+原文尾部、持久边界、原子提交、失败和重启恢复 | 覆盖检查通过,任务效果与成本达标 |
| 3. 补充精确回查 | 来源引用、分页历史查询、必要的结构化事实 | 关键事实与修正召回达到任务要求 |
| 4. 建设跨任务记忆 | 记忆作用域、编辑与删除、按需载入;必要时语义检索与预压缩 | 跨任务收益超过维护成本 |
最先值得投入的是历史身份、覆盖边界、真实预算、恢复流程和效果评估。每轮摘要、向量数据库、多级摘要树或异步预压缩都可以后续按证据增加。
持续工作的 Agent 需要同时满足两类要求:用户能够延续原来的目标;系统能够解释它依据了哪些历史,并在失败、纠错和权限变化后恢复正确状态。上下文压缩只是其中一个部件,完整工程方案应围绕这两类要求建设。
评论公开保存在 GitHub Discussions。