不管是那个AI IDE,不管LLM上下文是否1M
长任务你必然会看到类似“上下文已自动压缩”的提示
我认为这是上下文工程中最核心的一个模块
上下文全生命周期的管理和状态保持、长任务的维系都在这
那问题是大家有没有深入思考:
压缩到底该压缩什么,是压缩文本,还是压缩状态。
这也是在看DeerFlow源码时的核心问题:
它的上下文压缩不在上层 prompt 里打补丁,而是落到 summarization_middleware.py 里做 Runtime 层处理。
我们今天就从源码拆解入手,看看一套生产级 Context Compaction 到底是怎么设计的。
现有上下文压缩走到哪一步了
在看DeerFlow的具体实现逻辑前,先看看现有上下文压缩方案有哪些技术路线
第一条是 Prompt Compression,典型代表是 LLMLingua 和 LongLLMLingua,目标是把输入 Token 里的冗余信息直接压缩掉,适合 RAG 和长文档问答,但并不适合 Agent State。 第二条是 Semantic / Token Compression,代表是 UltraGist 这类工作,目标不是摘要,而是学习一个更紧凑的上下文表示,让模型直接基于压缩表示继续推理。 第三条是 Memory + Retrieval,代表是 MemGPT / Letta、Zep 这类方案,核心思路是把 Context Window 拆成 Working Memory 和 External Memory,用检索代替全量记忆。 第四条才是 Agent Runtime Compaction,代表包括 Claude Code Compact、DeerFlow 和 OpenHands 里的类似机制,核心目标是 Agent State 压缩后继续执行。
DeerFlow 没有提出一种新的压缩算法,也没有把问题简化成纯文本摘要,而是把传统 Summary Compression 放进 LangGraph State、Middleware Lifecycle、Summary Channel 和 Durable Context Projection 里协同工作。
换句话说, DeerFlow 的创新在于工程化组合的设计技巧:
它让“历史消息 -> 摘要 -> 替换旧消息”这件事,变成 Agent Runtime 里可持续恢复的状态管理。
DeerFlow 上下文压缩的四阶段流程
DeerFlow 的上下文压缩执行核心在summarization_middleware.py。
有两种触发模式:手动命令/compact 和 Lead Agent 构建时注入的中间件通过Hook自动触发
summarization_middleware.py 的核心处理逻辑如下:
每轮 before_model 钩子
→ _maybe_summarize(force=False)
→ _prepare_compaction
→ _messages_for_trigger_count(把 summary_text 也纳入 token 计算)
→ _should_summarize(父类方法,检查 trigger 阈值)
↓ 未达阈值 → return None(本轮不压缩)
↓ 达到阈值 → _determine_cutoff_index → _partition_messages → _preserve_dynamic_context_reminders
→ _asummarize_with(多模型回退生成摘要)
→ _fire_hooks(压缩前钩子:记忆冲刷)
→ 返回 {messages: [RemoveAll, ...preserved], summary_text: new_summary}
它先走 before_model,再进 _prepare_compaction,然后执行 _asummarize_with,触发 _fire_hooks,再落到 _amaybe_summarize,最后把结果写回 State。
从源码角度,可以把这套流程抽象成 Trigger、Partition、Summarization、Commit 四阶段。
Trigger 阶段解决的是“现在该不该压缩”。代码会先算当前 Context 大小,再判断是不是超过了该压缩的阈值。这里最容易被忽略的一个细节是:已有 Summary 也要算进 Token 预算。很多实现只算原始消息,结果 Summary 越攒越大,本来想省 Token,最后反而把省出来的空间又吃回去了。 DeerFlow 从一开始就把 Summary 当成 Context 的一部分,不是额外附件。
Partition 阶段解决的是“哪些内容该进摘要,哪些必须留下”。 DeerFlow 的做法是把消息分成两区:压缩区放早期讨论、搜索过程、工具结果、中间推理;保留区放最近用户需求、当前任务状态、最近代码修改。但这里要特别说明,它的分区依据主要是 cutoff index 和 keep 策略,而不是复杂语义评分。也就是说, DeerFlow 不是用模型去理解“哪条是代码修改”,而是先用时间窗口和保留策略保最近上下文连续性,再让 Summary 承担历史语义保留。这是一种工程上的稳定选择,不是语义理解式 Partition。
Summarization 阶段解决的是“摘要本身怎么生成更稳”。 DeerFlow 不会默认拿最强模型去干摘要活。它支持先走配置里的 Summary Model,再 fallback 到运行模型,实在不行就不压缩。这个顺序很实际:摘要本来就是个高吞吐、低决策密度的任务,用最贵的模型并不划算。
DeerFlow 在摘要 prompt 上做了两道安全边界。
第一道是预算切分:如果已有 Summary,新旧内容按预算分开;如果没有,就把全部预算留给新消息。
第二道是结构防御:待摘要内容被包进<existing_summary>和<new_messages>块之前会做html.escape。这防的不是普通用户输入,而是用户内容里夹带标签,把 prompt 结构打乱。
对于 Runtime 来说,这一步和摘要质量一样重要。Commit 阶段解决的是“压缩结果怎么写回状态,才不会把 Agent 写崩”。
DeerFlow 这里没有做温和的逐条删除,而是做原子替换:旧 Messages 被整体移除,保留消息和新的 Summary 一次性写回。
这个设计体现了类似数据库事务的思想:状态更新尽量保持原子性,避免压缩过程中出现中间态不一致。
动态上下文抢救: DeerFlow 最该细看的设计
如果只把 DeerFlow 的压缩理解成“摘要 + 分区 + 替换”,其实还是少看了一层。
DeerFlow 源码里还有一个很容易被略过的机制:动态上下文抢救。
动态上下文抢救的本质是:压缩可以删旧消息,但 ID-Swap 产生的消息三元组必须原子性地保留或删除——要么三条全留,要么三条全走。半留半删会导致提醒丢失、用户问题消失、重复注入等一系列连锁故障。
DeerFlow 的解法是关联 ID 保留机制:原始框架数据和用户数据会成对保留关联 ID,压缩时不是只救某一个 reminder,而是沿着 Base ID 把伙伴消息一起保护下来。
用户原始输入不会被误压缩,系统提醒也不会被孤立保留。
这个设计说明一件事:
生产级压缩不能只看消息内容本身,还要理解 Context 里各个消息之间的关系。
只做内容摘要,不关系拓扑,早晚会在动态注入这种场景里翻车。
另外也要注意,这种 Middleware 协同设计也说明 Agent Runtime 的复杂性:Context 压缩不是单个模块的问题,而是多个 Middleware 状态协议的问题。
DeerFlow 社区也曾出现过 DynamicContextMiddleware ID 递归膨胀导致历史消息被重复注入的情况,这说明这类机制虽然关键,但并不天然完备。
DeerFlow 上下文压缩还有哪些限制
第一,Summary 仍然依赖 LLM,本质存在信息损失。摘要模型很难稳定保留决策细节、隐含约束和代码上下文,压缩后 Agent 表现下降时,你往往很难区分是摘要写得太差,还是本来就不该把这部分内容压掉。
第二,Partition 仍然偏规则驱动。目前主要还是靠时间窗口和保留策略,不是语义重要性评分,也没有显式的任务相关性或依赖图判断。未来如果要更精细,可能需要引入 semantic importance、task relevance 或 dependency graph,但这会显著提高实现复杂度。
第三,Context Compression 缺少通用评估体系。现在最麻烦的问题不是做不做压缩,而是怎么判断压缩后的 Agent 没有变差。
这个领域还缺一个大家公认的 Context Compression Benchmark。
我的一些思考
DeerFlow 的源码拆解到最后,有几点思考:
一个长周期 Agent 至少需要五层能力:
Context Router 决定什么该进上下文
Compactor 负责状态压缩
Memory Manager 管理长期知识
Workspace 保存大对象和文件级内容
Safety Layer 负责注入边界和权限隔离。Summary 和 Memory 不能混为一谈。 DeerFlow 里,Summary 属于当前任务的 Durable Context,生命周期跟着任务走;Memory 是长期知识,生命周期独立于单次 Agent Run。 失败策略必须分层。压缩、摘要、安全、预算,这四个域对稳定性的要求不一样。
安全异常应该直接阻断,预算耗尽也应该硬停,但摘要失败不该把整个 Agent 打崩。
该 fail-closed 的地方要关门,该 fail-open 的地方要能恢复,不是全局一刀切。
DeerFlow 在这里给了一个很朴素的示范:不同关注点用不同失败语义,复杂系统靠分层取舍撑起来,而不是靠某一层全知全能。
DeerFlow 它把已有技术工程化组合成了 Runtime 能力,不管是单独的上下文压缩还是整个上下文工程都值得深入学习、参考。
DeerFlow:
https://github.com/bytedance/deer-flow/tree/main
"Extending Context Window of Large Language Models via Semantic Compression" :
https://arxiv.org/abs/2312.09571
Compressing Lengthy Context With UltraGist:
https://arxiv.org/abs/2405.16635
夜雨聆风