Agent长上下文处理中Context Compact与Memory协同机制
时间:2026-07-21 | 作者:风起客 | 阅读:0前言
很多人刚开始做 Agent,会下意识追求一件事:
尽量保留完整历史
这个想法很自然。毕竟用户需求、工具输出、文件内容、测试日志、错误堆栈,看起来都可能对后续有用。
但真实系统里,Agent 可不是只回答一轮问题。它会反复读文件、跑命令、修错误、再测试。messages 会不断变长,里面既有关键状态,也有大量中间噪音。
如果不处理,问题就来了,而且是三个:
- 成本变高:每一轮都要把完整上下文重新发给模型。
- 噪音变多:旧日志、临时搜索结果、已经修复的错误会一直赖在窗口里不走。
- 注意力被干扰:模型很可能被过时的计划、陈旧的失败记录、无关的输出带偏。
所以说,长会话 Agent 真正需要的,压根不是“无限记忆”,而是两种核心能力:
Context Compact:让当前上下文保持可用Memory:让重要状态离开 messages,变成可恢复的外部状态
一、为什么必须有 Context Compact
messages 是模型下一轮真正能看到的工作上下文。它最理想的状态,是尽量保留三类信息:
- 用户当前的目标
- 最近的决策和当前状态
- 下一步必须依赖的代码、错误和工具结果
但它绝不适合长期背着所有原始历史。
举个例子,一次 coding agent 任务里,模型可能运行过:
lsrg "retry"cat tests/test_checkout.pypytest tests/test_checkout.pynpm run build
这里面,有些结果很重要,比如当前失败用例和刚读过的源码;有些结果呢,只在当时有点用,比如旧的 ls 输出或已经过时的测试日志。如果全部原样保留,模型每一轮都要重新“路过”这些内容。它不光慢,还可能把旧信息当成当前事实。
所以 Context Compact 的必要性就出来了:
当前上下文窗口有限,必须把低价值历史折叠掉,把高价值状态留下来。
记住,这不是删除记忆,而是重写工作记忆。
二、Context Compact 的两层实现
这份教学实现把 compact 分成了两层:
micro_compact:每轮请求前做轻量清理auto_compact:上下文超过阈值后生成连续性摘要
1. 轻量压缩:先清理旧工具输出
每次请求模型前,agent_loop() 会先调用 micro_compact(messages)。它干三件事:
- 找出历史里的
tool消息 - 保留最近
KEEP_RECENT条工具结果 - 把更旧、很长、价值较低的工具输出替换成占位文本
比如一段很长的 shell 输出,可能会被替换成 [Previous: used bash]。这么做的好处是立竿见影的:模型不用继续背完整日志了。
但这个占位文本信息量确实很低。它只能说明“之前用过 bash”,但具体执行了什么命令、成功还是失败、关键错误是什么,一概不知。所以这只是教学版的减负占位,不是理想的生产级做法。
更好的方式应该是这样的:
当前 messages 里放短摘要完整 stdout / stderr 单独落盘摘要里保留命令、退出码、关键错误、日志路径需要细节时再按日志路径取回
比如旧日志可以压成:[Previous bash: pytest tests/test_checkout.py, exit=1, failed=tests/test_checkout.py::test_retry, full_log=.logs/tool_42.txt]。这样既减轻上下文,又保留恢复线索。
这里还有一个有趣的取舍:代码会尽量保留 read_file 的结果。原因是文件内容通常比旧 shell 输出更有后续价值。模型刚读过的源码、配置、文档,可能直接影响下一步编辑;如果太早折叠,模型就可能丢掉函数签名、类名、参数细节。
所以教学版的策略可以概括为:
旧 shell 输出优先压缩旧 read_file 内容谨慎保留
2. 完整压缩:超过阈值后生成连续性摘要
轻量压缩只能处理局部噪音。如果整个 messages 还是太长,那就要触发完整压缩了。
代码里的判断是 estimate_tokens(messages) > THRESHOLD,意思是当前会话历史估算出来的 token 数,超过了预设的安全线。这里:
messages是当前会话历史,包括用户输入、模型回复、工具结果。THRESHOLD是压缩阈值,表示上下文长到什么程度以后必须整理。estimate_tokens(messages)是粗略估算,不追求精确计费,只判断“是不是大概太长了”。
超过阈值后,系统调用 auto_compact(messages)。它的流程很清晰:
保存完整 transcript→让模型生成摘要→用摘要替换旧 messages
注意,完整压缩不是直接丢掉历史,而是先把原始记录写到 .transcripts/,再用摘要替换当前上下文。
看一个简化例子。压缩前,messages 可能是:
[{"role": "user","content": "帮我修复 checkout 重试失败的问题"},{"role": "assistant","content": "我先看一下相关测试和实现。"},{"role": "tool","name": "read_file","content": "tests/test_checkout.py 的完整内容..."},{"role": "tool","name": "bash","content": "pytest tests/test_checkout.pyn...nFAILED test_retry ..."},{"role": "assistant","content": "失败点在 retry_count 没有递增,我会修改 checkout.py。"},{"role": "tool","name": "edit_file","content": "已修改 src/checkout.py"}]
触发 auto_compact(messages) 后,系统先保存完整历史 .transcripts/transcript_1720000000.jsonl,然后把当前 messages 替换成一条连续性摘要:
[{"role": "user","content": "[Conversation compressed. Transcript: .transcripts/transcript_1720000000.jsonl]nn已完成:读取了 tests/test_checkout.py,运行了 pytest tests/test_checkout.py,并确认失败用例是 test_retry。nn当前状态:失败原因初步判断为 src/checkout.py 中 retry_count 没有在重试路径正确递增。已经修改了 src/checkout.py,但还没有重新跑完整测试。nn关键文件:tests/test_checkout.py、src/checkout.py。nn关键决策:保留现有 checkout 接口,不改测试语义,只修复重试计数逻辑。nn下一步:重新运行 pytest tests/test_checkout.py;如果通过,再运行更大范围测试。"}]
这段摘要保留了“继续工作所需的信息”:用户目标、已读文件、失败测试、已修改文件、当前决策、下一步动作、原始 transcript 路径。这,就是连续性摘要的意义——它不是普通总结,而是让 Agent 在压缩后还能接着干活。
另外,这份代码还暴露了一个 compact 工具。模型如果觉得上下文已经很乱,可以主动调用它,并传入 focus,告诉系统摘要应该重点保留什么。比如修 bug 时,focus 可以强调:保留失败命令、错误栈、已修改文件和下一步验证命令。这说明 compact 的目标不是单纯变短,而是围绕当前任务保留连续性。
三、为什么必须有 Memory
Compact 解决的是“当前上下文太长”的问题。但它不能解决另一个问题:有些状态不应该只存在于 messages 里。
原因很简单:
messages会被压缩messages会被上下文窗口挤掉- 会话结束后,内存里的历史可能消失
- 摘要不适合维护结构化状态,比如任务依赖和定时规则
所以需要 Memory。这里的 Memory 不要理解成“模型脑子里记住了什么”。在这份教学代码里,它更具体:把需要长期保留的状态,从 messages 移到文件系统。
所以,Memory 的必要性在于:不是所有重要信息都应该留在上下文里;有些信息应该变成可恢复、可查询、可更新的外部状态。
四、Memory 的实现:过去、现在与未来状态
这套 harness 里没有一个单独的 memory.py。Memory 是分散在几个模块里的外部状态机制。我们可以按时间维度来理解:
过去发生过什么:.transcripts/现在还有什么任务:.tasks/未来什么时候再做:.scheduled_tasks.json
1. .transcripts/:保存过去的完整记录
auto_compact(messages) 生成摘要前,会先把完整历史写到 .transcripts/transcript_1720000000.jsonl。这解决的是:摘要可以变短,但原始过程不能完全消失。
压缩后的摘要可能只写“pytest tests/test_checkout.py 失败,失败用例是 test_retry”,但如果后面需要追溯完整 stdout、stderr、工具调用顺序,就可以回到 transcript。所以 .transcripts/ 不是模型每轮都看的上下文,而是系统保留的可追溯记录。
2. .tasks/:保存当前任务状态
第 06 节里的 TaskManager 会把任务写成 JSON,比如 .tasks/task_1.json。一个任务大致是:
{"id": 2,"subject": "补 checkout retry 测试","description": "覆盖失败重试和成功重试两种路径","status": "pending","blockedBy": [1],"owner": ""}
这类信息不适合只放在对话里。因为任务状态需要跨会话存在,还要能维护依赖关系。写进 .tasks/ 后,它就不再依赖当前 messages:会话结束后还在,compact 之后还在,程序重启后还能读回来,任务依赖也能结构化维护。
3. .scheduled_tasks.json:保存未来唤醒规则
第 08 节里的 CronScheduler 会把 durable cron job 写进 .scheduled_tasks.json。比如:
[{"id": "cron_102314","cron": "0 9 * * 1","prompt": "检查项目状态并汇总未完成任务","recurring": true,"durable": true}]
这类状态描述的是:未来什么时候,把什么任务重新交给 Agent。如果只放在对话历史里,程序一重启就可能丢掉。持久化以后,Agent 才能在未来重新醒来。
所以,这份代码里的 Memory 雏形可以总结为:.transcripts/ 保存过去,.tasks/ 保存现在,.scheduled_tasks.json 保存未来。
它还不是完整产品级 Memory,还缺少:自动抽取用户偏好、用户级/项目级/任务级记忆分层、检索索引、权限/过期/删除管理、每轮请求前自动选择相关 memory 注入上下文。但它已经说明了最核心的一点:Memory 的第一步不是让模型多背一点,而是让关键状态离开 messages。
五、Compact 和 Memory 如何协同
现在可以把两者关系说清楚了。
Context Compact 解决的是当前窗口问题:当前 messages 太长、太乱,必须压缩。Memory 解决的是状态持久化问题:有些信息不能只靠 messages 活着,必须外置保存。
所以它们不是替代关系,而是协同关系:
短期噪音:用 compact 折叠当前状态:用摘要保留长期状态:用 memory 外置完整记录:用 transcript 追溯
如果没有 compact,当前上下文会越来越慢、越来越吵。如果没有 memory,压缩之后就容易丢掉长期状态。
一个更接近真实系统的链路应该是:
工具输出很长→提取关键信息放回 messages→完整日志落盘→任务状态写入 .tasks/→上下文过长时生成连续性摘要→未来需要细节时,再通过日志、任务文件或 transcript 找回
这才是长会话 Agent 能持续工作的基础。
六、从教学实现到产品级 Agent
这份教学实现跑通了主线:旧工具输出可以折叠,长历史可以压成摘要,重要状态可以写到文件系统。但如果对齐 Claude Code、Codex 这类产品级 coding agent,还要继续补几层。下面不是说它们内部一定按这个结构实现,而是从公开资料和产品能力里,可以看到产品级系统通常会把问题拆得更细。
1. 状态分层,而不是全靠 messages
教学版里,状态大致分成 messages(当前工作上下文)、.transcripts/(历史记录)、.tasks/(任务状态)、.scheduled_tasks.json(定时唤醒规则)。产品级系统会继续细分。
以 Codex 为例,公开文档里能看到多种控制面:AGENTS.md 保存仓库级长期规则和项目约定,Memories 保存跨线程可复用的偏好、工作流、技术栈和已知坑点,skills 和 MCP 按需加载专业能力或外部数据,hooks 在关键生命周期事件中执行外部逻辑,automations 保存后台任务、运行记录和 triage 结果。Claude Code 也有类似分层,比如 CLAUDE.md、auto memory、hooks、subagents 等。
这说明产品级系统不会把长期规则、用户偏好、项目状态、工具结果都塞进 messages,而是按用途拆成不同层:
- 规则层:项目约定、团队规范
- 记忆层:跨线程稳定信息
- 任务层:当前目标、依赖、进度
- 工具层:命令、文件、API 调用结果
- 审计层:完整 transcript / trace
这样做的好处是,每层都有自己的生命周期。规则可能长期存在,工具结果可能很快过期,trace 主要用于追溯,任务状态需要可更新。
2. 工具结果清理,不只是简单占位
教学版的 micro_compact 会把旧工具输出替换成 [Previous: used bash]。这个实现足够说明原理,但信息量太低。产品级系统通常会把“工具结果清理”做得更像一个可配置策略。
比如策略可以是:超过 50,000 tokens 时开始清理,保留最近 5 组工具调用结果,清理旧的 bash / search / API response,保留 read_file / memory / task 这类关键工具,每次至少清出 5,000 tokens。
Claude 的 Context Editing 就是一个参照:它不只是摘要,还会清理旧工具结果和 thinking block,并支持配置触发阈值和保留最近多少组工具调用。
清理前,模型可能看到的是:
tool: bash("pytest tests/test_checkout.py")result: 3000 行测试日志...tool: bash("npm run build")result: 2000 行构建输出...tool: read_file("src/checkout.py")result: checkout.py 的源码...
清理后,旧的工具结果会被移出当前上下文,但工具调用轨迹和关键结果仍然保留:
tool: bash("pytest tests/test_checkout.py")result: [cleared old result: exit=1, failed=test_retry, full_log=.logs/tool_42.txt]tool: bash("npm run build")result: [cleared old result: exit=0, full_log=.logs/tool_43.txt]tool: read_file("src/checkout.py")result: checkout.py 的源码...
这里的关键不是“删掉旧结果”,而是删掉大块原文,保留恢复线索,必要时能重新取回。
这和完整 compaction 不一样。完整 compaction 是“把一大段历史总结成摘要”;Context Editing 更像“按类型清理上下文里的低价值块”。它的目标不是生成总结,而是在不打断会话结构的前提下,把最占空间、又可重新获取的旧工具结果清掉。
3. 完整 compaction 要有结构化摘要
教学版的 auto_compact 会让模型生成一段自然语言摘要。产品级系统通常会要求摘要更结构化。比如不是只写“修复了 checkout retry 的问题,接下来要跑测试”,而是拆成:
goal:修复 checkout 重试失败current_state:已修改 src/checkout.py,尚未重新跑完整测试files:- tests/test_checkout.py- src/checkout.pydecisions:- 不修改 checkout 对外接口- 不改测试语义,只修重试计数逻辑next_steps:- pytest tests/test_checkout.py- 通过后运行更大范围测试trace:.transcripts/transcript_1720000000.jsonl
这样做有两个好处:模型下一轮更容易接着执行,系统也更容易检查摘要是否漏掉关键状态。Claude Code 的 compaction 公开说明里也强调,压缩要保留架构决策、未解决 bug 和实现细节,同时丢弃冗余工具输出。也就是说,产品级 compaction 的目标不是“短”,而是“短到还能继续干活”。
4. Memory 不是一份文件,而是一套注入机制
教学版的 Memory 雏形是 .transcripts/、.tasks/、.scheduled_tasks.json。它们能保存状态,但还没有解决一个关键问题:下一轮模型请求时,应该把哪些 memory 放回上下文?
产品级 Memory 通常会多一层选择机制。比如一次新任务来了,系统可能会先判断:当前仓库是什么?用户在做什么任务?哪些历史偏好相关?哪些项目规则必须注入?哪些旧任务状态需要恢复?然后只注入相关内容,而不是把所有 memory 都塞进去。
Codex memories 就体现了这个方向:把有长期价值的信息整理成本地 memory 文件,未来按需带入上下文。AGENTS.md 则承担更稳定的项目规则角色。两者定位不同,不能混用。
所以产品级 Memory 至少要回答三个问题:写什么(哪些信息值得长期保存)、存哪里(用户级、项目级、任务级分别放哪)、取什么(当前请求应该注入哪些 memory)。教学版主要解决了“存哪里”的雏形,后续还可以继续补“写什么”和“取什么”。
5. transcript / trace 不只是备份
教学版把完整 transcript 存到 .transcripts/,已经有了可追溯的基础。产品级系统通常还会继续接:检索(需要时找回旧记录)、审计(知道 compact 前后丢了什么)、评估(检查摘要有没有漏掉关键状态)、回放(复盘 Agent 当时为什么这样决策)、调试(定位某次上下文清理是否过于激进)。
举个例子,如果一次 compact 后 Agent 忘了“不能修改 checkout 接口”这个决策,产品级系统应该能追溯:compact 前原始 transcript 是什么?summary 里有没有保留这个决策?如果没保留,是 prompt 问题还是策略问题?下一版 compact prompt 应该怎么改?
这就把 transcript 从“备份文件”升级成了“可观测、可评估、可调试的 trace”。
把这些做法抽象成一句话:
messages:只放当前工作所需tool log:保存可重新取回的工具细节memory:保存跨会话稳定信息rules:保存团队和项目约定trace:保留原始过程和压缩前后差异hooks / compact:在关键时机触发整理
小结
长会话 Agent 的核心,不是把所有历史都塞进上下文,而是给信息找到合适的位置。
当前要用的,留在 messages只需要连续性的,压成摘要需要跨会话恢复的,写入 memory需要追溯细节的,保存到 transcript / trace
Context Compact 解决当前窗口的负担,Memory 解决长期状态的归宿。两者配合起来,Agent 才能既不被旧日志拖慢,也不会在压缩之后丢掉关键状态。这也是它从“能跑几轮”走向“能持续工作”的关键一步。
免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。
相关文章
更多-
- 深入理解Kotlin协程中withContext与launch的真正区别
- 时间:2026-08-27
-
- 谷歌浏览器WebGL崩溃排查与图形硬件加速诊断
- 时间:2026-08-23
-
- React Context 使用详解:组件通信的原理与实践
- 时间:2026-08-21
-
- Pi 如何统一 Provider 与上下文交接实现多模型共享 Agent
- 时间:2026-08-16
-
-
- 年突发AI上下文编译器开始替代提示工程
- 时间:2026-07-28
-
- 微软开源FastContext轻量级代码仓库探索模型
- 时间:2026-07-27
-
- LLM应用四层嵌套:Prompt、Context、Harness、Agentic,你卡在哪一层?
- 时间:2026-07-27
精选合集
更多大家都在玩
大家都在看
更多-
- 2026年9月17日小鸡庄园答案
- 时间:2026-09-16
-
- 蚂蚁庄园今日答案2026年9月17日
- 时间:2026-09-16
-
- 蚂蚁庄园小课堂今日最新答案2026年9月17日
- 时间:2026-09-16
-
- 蚂蚁庄园小鸡答题今日答案2026年9月17日
- 时间:2026-09-16
-
- 褪黑素主要由人体哪个器官分泌 蚂蚁庄园今日答案9.17
- 时间:2026-09-16
-
- 蚂蚁庄园今天答题答案2026年9月17日
- 时间:2026-09-16
-
- 蚂蚁庄园答题今日答案2026年9月17日
- 时间:2026-09-16
-
- 研学旅游指导师的核心服务对象是 蚂蚁新村今日答案2026.9.16
- 时间:2026-09-16
