DeerFlow 会话摘要机制深度解析:summarization 配置、触发阈值与上下文压缩实现
DeerFlow 内置的自动会话摘要(Summarization)是其长时程 Agent 能稳定处理数小时级任务的关键上下文治理机制。本文以 后端摘要设计文档 为主体,结合 配置模型、摘要中间件实现 与 Lead Agent 组装链,完整讲清 summarization 配置块的每个参数、触发/保留策略的解析规则、summary_text 的状态流转,以及与 DurableContextMiddleware 协同的"持久化上下文通道"设计,帮助你在自己的部署中正确配置并调优上下文压缩。
一、功能定位:为什么长会话需要摘要
DeerFlow 面向"研究、编码、创建"类长时程任务,一次会话的消息历史会迅速逼近模型的输入 token 上限。启用摘要后,系统会在模型每次调用前监控消息历史的 token 量,当达到配置阈值时自动将较早的消息压缩为一段摘要文本,同时保留最近上下文。设计文档给出的核心行为有五点:
- 实时监控消息 token 计数;
- 阈值命中即触发摘要;
- 保留近期消息原文,仅压缩较早的交互;
- 保证 AI 消息与其对应的 Tool 消息成对完整,不被切断;
- 摘要存入
ThreadState.summary_text,后续通过持久化上下文数据(durable context)以临时方式投影回模型请求。
一个值得注意的架构演进(见 summarization.md 开篇):新的 checkpoint 不再用原始的任务结果或技能读取(skill-read)转录内容来推导持久上下文。捕获路径消费的是打在对应 ToolMessage.additional_kwargs 上的有界结构化元数据;转录文本只作为展示/模型内容存在,而非状态捕获协议。这一点在后文中 skill_context 通道部分会具体展开。
二、配置详解:config.yaml 的 summarization 块
摘要功能在 config.yaml 的 summarization 键下配置。仓库根目录的 config.example.yaml 中给出了生产参考值(trigger: tokens 32000、keep: messages 10、trim_tokens_to_summarize: 15564),而设计文档给出的通用示例如下:
summarization:
enabled: true
model_name: null # null = 用运行自身的模型做摘要(见下文);或指定一个轻量模型
# 触发条件(OR 逻辑 - 任一条件命中即触发摘要)
trigger:
- type: tokens
value: 4000
# 可叠加其他触发器(可选)
# - type: messages
# value: 50
# - type: fraction
# value: 0.8 # 模型最大输入 token 的 80%
# 上下文保留策略
keep:
type: messages
value: 20
# 摘要调用本身的 token 裁剪上限
trim_tokens_to_summarize: 4000
# 自定义摘要 prompt(可选)
summary_prompt: null
# 视为"技能文件读取"的工具名,用于 durable skill_context 通道
skill_file_read_tool_names:
- read_file
- read
- view
- cat
2.1 enabled
- 类型:布尔;默认
false。 - 关闭时 create_summarization_middleware 工厂 直接返回
None,摘要中间件不进入中间件链。
2.2 model_name:模型所有权(model ownership)
- 类型:字符串或
null;默认null。 null(模型所有权):用该次运行实际执行的模型做摘要——即 Lead 运行解析出的模型、子代理自身的模型、或线程自定义 Agent 的模型,而不是config.models[0]。这样即使models[0]的 provider 故障(密钥过期、配额耗尽、服务中断),只要运行模型健康,压缩依然可用。- 设为具体模型名:该模型负责生成摘要;若其 provider 失败,压缩会回退到运行自身模型,确保一个损坏的摘要 provider 不会在还有可用模型时禁用压缩。官方建议使用
gpt-4o-mini或等价的轻量高性价比模型。 - 所有权规则覆盖全部三条路径:Lead 自动压缩、子代理压缩、手动
/compact。手动/compact按与正常运行相同的优先级解析模型:请求体model_name(前端从编辑器当前模型发送,走POST /api/threads/{id}/compact)→ 线程自定义 Agent 模型 → 默认值。
源码侧,这一语义由 summarization_middleware.py 的候选名逻辑实现:_generation_candidate_names() 在有显式摘要模型时返回"配置模型在前、运行模型在后"的去重候选序列;_summarize_with() / _asummarize_with() 按序尝试每个候选,任一候选在构建、调用、文本提取、非空校验任一阶段失败都落入下一候选,全部失败时保持压缩状态不变。此外,纯空白(whitespace-only)的摘要响应被视为生成失败,永远不会被提交为"有效的空摘要"——这一点由 _nonempty_summary() 保证。
2.3 trigger:触发阈值
- 类型:单个
ContextSize或ContextSize列表;启用时必须至少指定一个。 - OR 逻辑:任一阈值满足即运行摘要。
三种 ContextSize 类型:
-
Token 触发(推荐大多数场景):
trigger: type: tokens value: 4000 -
消息数触发:
trigger: type: messages value: 50 -
比例触发:达到模型最大输入 token 的指定比例时触发。
trigger: type: fraction value: 0.8 # 最大输入 token 的 80%比例的分母取自摘要锚定模型声明的
context_window——即生成摘要的模型:summarization.model_name若已设置则用它,否则用运行自身模型。需要在config.yaml的对应 models 条目上声明context_window。第三方 OpenAI 兼容模型没有内建容量档案,未声明context_window时,fraction 条款会在 Agent 构建时被丢弃并打警告,其余绝对条款(tokens/messages)继续生效。注意一个陷阱:若配置了独立的大窗口摘要模型(如运行模型 64k 配摘要模型 128k),fraction: 0.8会解析为约 102k token,自动摘要永远赶不上运行模型溢出——这种组合下应优先使用按运行模型窗口尺寸调整的绝对tokens阈值。源码印证:summarization_middleware.py 中的
_drop_unusable_fraction_clauses()在锚定模型没有可用profile["max_input_tokens"]时丢弃 fraction 触发条款(绝对条款存活)、把 fraction 型keep回退到共享常量DEFAULT_KEEP(messages/20),并在所有触发条款都被丢弃时以trigger=None继续构建——此时自动压缩永不触发,但手动压缩(force=True,从不读取 trigger 条款)依然可用。
多触发器叠加:
trigger:
- type: tokens
value: 4000
- type: messages
value: 50
2.4 keep:保留策略
- 类型:
ContextSize对象;默认{type: messages, value: 20}(summarization_config.py 中的DEFAULT_KEEP常量,且与 fraction-keep 降级回退路径共享,防止两者漂移)。 - 指定摘要后保留多少近期历史。
# 保留最近 20 条消息
keep:
type: messages
value: 20
# 保留最近 3000 token
keep:
type: tokens
value: 3000
# 保留模型最大输入 token 的最近 30%
keep:
type: fraction
value: 0.3
2.5 trim_tokens_to_summarize
- 类型:整数或
null;默认4000。 - 准备摘要调用材料时的最大 token 数。设为
null跳过裁剪(对超长会话不推荐)。
源码中该参数有一个精细的预算分配:_build_summary_input_text() 在存在上一轮 summary_text 时,把 trim_tokens_to_summarize 的一半分给新消息(strategy="first",保留最早的旧消息),另一半给既有摘要(strategy="last",保留最新尾部);两块内容分别经 html.escape 转义后嵌入 <existing_summary> / <new_messages> 标签块——这是对"内容闭合标签伪造权威区段"的块逃逸(block-breakout)防御。
2.6 summary_prompt
- 类型:字符串或
null;默认null(使用 LangChain 默认 prompt)。 - 自定义 prompt 模板应引导模型提取最关键上下文。LangChain 默认 prompt 的取向是:提取最高质量/最相关的上下文、聚焦对总体目标关键的信息、避免重复已完成动作、只返回提取出的上下文。
2.7 skill_file_read_tool_names
- 类型:字符串列表;默认
["read_file", "read", "view", "cat"](summarization_config.py 中的DEFAULT_SKILL_FILE_READ_TOOL_NAMES)。 - 当
DurableContextMiddleware把已加载技能捕获进 checkpoint 的skill_context通道时,视为"技能文件读取"的工具名。仅当工具调用名在此列表中、且目标路径位于skills.container_path之下时才捕获;设为[]可关闭持久技能引用捕获。 - 旧的
preserve_recent_skill_*配置已废弃:技能保留改由 durableskill_context引用通道承担,而不是把原始技能读取消息留在摘要窗口内。
2.8 配置加载与校验(源码补充)
summarization_config.py 用 Pydantic 建模,ContextSize 的 _validate_value_range 校验器把一批"静默失效"的配置错误变成加载期报错:
- 非有限值:YAML 的
.nan/.inf能通过 pydantic 浮点解析,但count >= nan恒为 False,等价于永不触发的死阈值; - 百分号风格的比例:
value: 80(应为0.8)会解析成int(max_input_tokens * 80),上下文永远到不了,摘要静默永不触发——校验器要求 fraction 值落在(0, 1]; - messages 值必须是整数:LangChain 用
messages[-keep:]切片,浮点数下标会在压缩中途抛TypeError。
加载入口为 load_summarization_config_from_dict(),全局单例通过 get_summarization_config() / set_summarization_config() 访问。
三、工作原理
3.1 摘要流程(Summarization Flow)
- 监控:每次模型调用前,中间件统计消息历史的 token 数,并把既有
summary_text一并计入——因为两者都会被投影进下一次模型请求(实现见_prepare_compaction()中的_messages_for_trigger_count(),它会把summary_text包装成一条name="summary"的 HumanMessage 参与计数); - 触发检查:任一配置阈值满足则触发;
- 消息切分:消息被分为两部分——
keep阈值之外的旧消息(待摘要)与keep阈值内的近期消息(保留); - 摘要生成:模型对旧消息生成简洁摘要;
- 上下文替换:消息历史被更新——旧消息全部移除、近期消息保留、生成的文字摘要存入
summary_text。实现上返回的是[RemoveMessage(REMOVE_ALL_MESSAGES), *preserved_messages]加上summary_text字段(见_maybe_summarize()); - AI/Tool 成对保护:确保 AI 消息与对应 Tool 消息不被拆散;
- 技能上下文通道:对话中读取的技能文件(工具名在
skill_file_read_tool_names内、路径位于skills.container_path下、收窄到.../SKILL.md)在读取工具边界被打上skill_context_entry元数据,随后由DurableContextMiddleware捕获进 checkpoint 的skill_context通道,以引用形式保存:name、path、从文件 frontmatter 内存解析的一行description、以及loaded_at,按路径去重。每次模型调用时它们被渲染成一条隐藏的 durable-context 数据消息,作为紧凑的"活跃技能"提醒并指向每个SKILL.md以便按需重读——从而"哪些技能处于激活状态"这一事实能在摘要后存活,而无需持久化或重新注入原文。通道保留最近读取的技能(上限_SKILL_CONTEXT_MAX_ENTRIES,当前值为 8,见 thread_state.py;重读已有技能会刷新其新鲜度),实际会话通常只加载 1-3 个技能。
3.2 Token 计数
- 使用基于字符数的近似 token 计数;
- Anthropic 模型:约 3.3 字符/token;
- 其他模型:使用 LangChain 的默认估算;
- 可通过自定义
token_counter函数覆盖。
3.3 消息保留策略(源码级细节)
中间件对"什么消息绝不被压缩"做了三重保护,超出原文档但值得了解:
- 近期消息:按
keep配置原样保留; - AI/Tool 成对:切分点若落在 Tool 消息内部,系统会调整以保持整个 AI + Tool 序列完整;
- 动态上下文提醒与当前请求:
_preserve_dynamic_context_reminders()会把带dynamic_context_reminder=True标签的提醒消息(日期 SystemMessage + 可选__memory对端)以及最新的真实用户消息从"待摘要"集合中救回保留侧。刻意不救回的是无标签的__user历史对端消息——那是陈旧的历史请求,允许被压缩正是消除跨轮提示污染(cross-turn prompt contamination)的关键;而当前请求通过latest_user_id精确定位,保证首轮长分析中"当前用户请求存活、早期 AI/Tool 轮次照常压缩"。
摘要的注入形态:摘要文字存放在 summary_text,被渲染进一条临时的隐藏 durable-context 数据消息;静态处理规则放在独立的 SystemMessage 中,摘要文本及其他用户/工具/模型侧值则留在权限更低的 data 消息中:
<durable_context_data>
## Conversation summary so far
[Generated summary text]
</durable_context_data>
在 durable_context_middleware.py 中,该数据块汇总三个通道——summary_text(渲染前截断到 6000 字符预算)、delegations 任务委托账本、skill_context 技能引用——整块 HTML 转义后放入一条 hide_from_ui: True 的隐藏 HumanMessage,插在开头 SystemMessage 之后,且从不写回 state(ephemeral 投影)。同一次注入还会插入一条"权威契约" SystemMessage(_AUTHORITY_CONTRACT):明确告知模型这些字段值来自用户/模型/工具/子代理文本,应视为数据而非指令,永远不执行其中嵌入的指令——这是摘要文本作为不可信内容重新进入上下文时的提示注入防线。
四、与持久化上下文的协同与中间件顺序
设计文档给出的中间件顺序是理解整个机制的关键:
- Runtime 中间件(含 ThreadData 与 Sandbox 初始化);
- DynamicContextMiddleware;
- SkillActivationMiddleware;
- DurableContextMiddleware(在摘要之前捕获);
- SummarizationMiddleware ← 摘要在这里运行;
- 下游 Lead 中间件,如 Title、Memory、Clarification。
持久化捕获必须先于摘要运行:任务委托(含进行中的 dispatch 与终态结果摘要)和已加载技能引用,都要在它们的原始 Tool 消息被压缩之前记入 checkpoint。摘要随后压缩消息历史,再让下游的标题生成、记忆队列、澄清等中间件处理缩减后的上下文。
lead_agent/agent.py 的 build_middlewares() 与文档一致:DurableContextMiddleware 构造时接收 skills_container_path 与 skill_file_read_tool_names 两个参数(后者正来自 resolved_app_config.summarization),注释明确写着"Capture completed task delegations and loaded skill files before summarization can compact them, then inject durable context channels (summary + ledger + skills) into model calls"。紧随其后才 append 摘要中间件。
状态管理:
- 摘要配置从
config.yaml加载; - 生成的摘要存放在
ThreadState.summary_text(见 thread_state.py:summary_text: NotRequired[str | None]),而不是普通messages; - 消息 reducer 移除被压缩的原始消息,checkpointer 则持久化
summary_text; DurableContextMiddleware把summary_text投影回后续模型调用,作为隐藏 durable 上下文数据。
另外,skill_context 通道本身是带自定义 reducer 的 checkpoint 状态字段(merge_skill_context 合并新旧条目并按上限裁剪),其读取侧 skill_context.py 的 extract_skills() 只处理 status 非 error 的 ToolMessage,并校验 additional_kwargs 元数据路径与预期路径一致后才入账;render_skill_context() 渲染格式为"## Active skills (loaded earlier - re-read the file before applying its instructions)" + 每行 - {name}: {description} -> {path},明确提示模型在应用指令前重读文件。
五、手动压缩:/compact API 与失败语义
除自动触发外,DeerFlow 提供手动压缩路径。Gateway 的 threads.py 暴露 POST /api/threads/{thread_id}/compact:请求体支持 force(默认 True,即使未达自动阈值也执行)与可选的 keep(仅对本次压缩生效的保留策略),响应含 compacted 布尔。context_compaction.py 中的 compact_thread_context() 以 force=force, raise_on_failure=True 调用中间件的 acompact_state()。
force 与 raise_on_failure 是两个正交的维度(见 summarization_middleware.py 的 compact_state() 文档串):
force=True绕过自动阈值检查(手动调用者总是想压缩);raise_on_failure=True(仅手动/compact路径)让真实失败抛出SummaryGenerationError,与"无事可压缩"区分开——自动路径保持False,失败被静默吞掉,压缩状态保持不变,等待后续触发轮重试。
六、模型构建的容错设计
工厂函数 create_summarization_middleware() 的锚定模型(anchor model)构建也值得注意,它解释了"摘要功能为什么不会因为某个模型配置损坏而拖垮整个 Agent 构建":
- 候选顺序为
[primary_name(配置摘要模型,否则运行模型), run_model 或 default, default, None],逐个受保护地构建,首个成功者成为锚定模型——它驱动父类的 token 计数器与 profile 检查,并在生成候选与其同名的时候被直接复用; - 摘要 LLM 调用发生在 LangGraph 中间件钩子内,若不加防护,其 token 流会被 messages-tuple 流回调捕获,向广播出"幻影 AI 消息";因此生成用的模型副本被打上
TAG_NOSTREAM(_tag_nostream()),同时保留middleware:summarize标签供 RunJournal 归因; - 惰性构建的生成模型按名缓存,构建失败缓存为
None——坏掉的候选配置不会每轮重试,也不会逃出 fail-open 边界; - 全部候选都构建不出来时,工厂仅打警告并返回
None("compaction is unavailable for this build"),Agent 本身照常构建。
子代理链还有一个隔离细节:create_summarization_middleware() 的 skip_memory_flush 参数让子代理链跳过 memory_flush_hook,避免子代理的内部轮次("Task" HumanMessage + 中间 AI/tool 轮次)经由 thread_id 键控的钩子写入父线程的持久记忆。Lead 链则保留该钩子——研究过程应当沉淀到记忆。
七、最佳实践(来自设计文档)
7.1 选择触发阈值
- Token 触发:大多数场景的推荐选择。设为模型上下文窗口的 60-80%。例:8K 上下文用 4000-6000 token;
- 消息数触发:适合控制会话长度、短消息很多的场景。例:50-100 条;
- 比例触发:多模型场景下自动适配各模型容量。例:0.8。
7.2 选择保留策略(keep)
- 消息数保留:最适合大多数场景,保持自然对话流。推荐 15-25 条;
- Token 保留:需要精确控制时。推荐 2000-4000 token;
- 比例保留:多模型配置。推荐 0.2-0.4。
7.3 模型选择
- 推荐:用轻量、高性价比模型做摘要(如
gpt-4o-mini、claude-haiku或等价物)。摘要不需要最强模型,高并发应用上可显著节省成本; - 默认(
model_name: null):用运行自身模型摘要(而非models[0])。models[0]provider 故障而运行模型健康时压缩仍可工作;简单部署友好,无需额外维护一个摘要 provider 的凭据。
7.4 优化技巧
- 组合触发器:token 与 messages 双触发更稳健:
trigger: - type: tokens value: 4000 - type: messages value: 50 - 保守保留:初始多留一些消息,按表现调整:
keep: type: messages value: 25 # 从较高值起步,按需降低 - 策略性裁剪:限制送往摘要模型的 token 量,避免昂贵的摘要调用:
trim_tokens_to_summarize: 4000 - 监控迭代:跟踪摘要质量并调整配置。
八、故障排查
8.1 摘要质量差(丢失重要上下文)
- 增大
keep值,保留更多消息; - 降低触发阈值,更早触发摘要;
- 自定义
summary_prompt,强调关键信息; - 换用能力更强的摘要模型。
8.2 性能问题(摘要调用过慢)
- 用更快的摘要模型(如
gpt-4o-mini); - 降低
trim_tokens_to_summarize,减少送入的上下文; - 提高触发阈值,降低摘要频率。
8.3 仍触发 token 上限
- 降低触发阈值,更早摘要;
- 减小
keep,保留更少消息; - 检查是否存在单条超大消息(裁剪只发生在摘要调用侧,单条超长消息仍可能撑爆上下文);
- 考虑改用 fraction 触发。
另外两个源码可见的"边缘摘要"情形会直接短路模型调用(_CANNED_SUMMARIES):无历史时返回 "No previous conversation history.";待摘要内容裁剪后为空时返回 "Previous conversation was too long to summarize."——二者都是合法摘要,不会被误判为生成失败。
九、参考实现与延伸阅读
| 关注点 | 文件 |
|---|---|
| 配置模型与校验 | summarization_config.py |
| 摘要中间件(DeerFlow 扩展 + 工厂) | summarization_middleware.py |
| Lead Agent 中间件组装与顺序 | agent.py |
| 持久上下文捕获与投影 | durable_context_middleware.py |
| 技能引用提取与渲染 | skill_context.py |
状态字段(summary_text / skill_context) |
thread_state.py |
| 手动压缩入口 | context_compaction.py、threads.py |
| 配置样例 | config.example.yaml |
| 测试 | test_summarization_middleware.py、test_summarization_summary_text.py、test_durable_context_middleware.py |
底层摘要行为由 LangChain 的 SummarizationMiddleware 提供(触发/保留判定、消息切分),DeerFlow 的 DeerFlowSummarizationMiddleware 在其上叠加了模型所有权与回退、nostream 隔离、块逃逸防御、压缩前钩子与扩展观察者通知。理解文档中的每一项配置如何映射到 summarization_config.py 的字段与 summarization_middleware.py 的容错路径,是正确调优 DeerFlow 长会话上下文管理的基础。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00