首页
/ DeerFlow 会话摘要机制深度解析:summarization 配置、触发阈值与上下文压缩实现

DeerFlow 会话摘要机制深度解析:summarization 配置、触发阈值与上下文压缩实现

2026-09-04 11:46:20作者:袁立春Spencer

DeerFlow 内置的自动会话摘要(Summarization)是其长时程 Agent 能稳定处理数小时级任务的关键上下文治理机制。本文以 后端摘要设计文档 为主体,结合 配置模型摘要中间件实现Lead Agent 组装链,完整讲清 summarization 配置块的每个参数、触发/保留策略的解析规则、summary_text 的状态流转,以及与 DurableContextMiddleware 协同的"持久化上下文通道"设计,帮助你在自己的部署中正确配置并调优上下文压缩。

一、功能定位:为什么长会话需要摘要

DeerFlow 面向"研究、编码、创建"类长时程任务,一次会话的消息历史会迅速逼近模型的输入 token 上限。启用摘要后,系统会在模型每次调用前监控消息历史的 token 量,当达到配置阈值时自动将较早的消息压缩为一段摘要文本,同时保留最近上下文。设计文档给出的核心行为有五点:

  1. 实时监控消息 token 计数;
  2. 阈值命中即触发摘要;
  3. 保留近期消息原文,仅压缩较早的交互;
  4. 保证 AI 消息与其对应的 Tool 消息成对完整,不被切断;
  5. 摘要存入 ThreadState.summary_text,后续通过持久化上下文数据(durable context)以临时方式投影回模型请求。

一个值得注意的架构演进(见 summarization.md 开篇):新的 checkpoint 不再用原始的任务结果或技能读取(skill-read)转录内容来推导持久上下文。捕获路径消费的是打在对应 ToolMessage.additional_kwargs 上的有界结构化元数据;转录文本只作为展示/模型内容存在,而非状态捕获协议。这一点在后文中 skill_context 通道部分会具体展开。

二、配置详解:config.yamlsummarization

摘要功能在 config.yamlsummarization 键下配置。仓库根目录的 config.example.yaml 中给出了生产参考值(trigger: tokens 32000keep: messages 10trim_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

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:触发阈值

  • 类型:单个 ContextSizeContextSize 列表;启用时必须至少指定一个。
  • OR 逻辑:任一阈值满足即运行摘要。

三种 ContextSize 类型:

  1. Token 触发(推荐大多数场景):

    trigger:
      type: tokens
      value: 4000
    
  2. 消息数触发

    trigger:
      type: messages
      value: 50
    
  3. 比例触发:达到模型最大输入 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_* 配置已废弃:技能保留改由 durable skill_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)

  1. 监控:每次模型调用前,中间件统计消息历史的 token 数,并把既有 summary_text 一并计入——因为两者都会被投影进下一次模型请求(实现见 _prepare_compaction() 中的 _messages_for_trigger_count(),它会把 summary_text 包装成一条 name="summary" 的 HumanMessage 参与计数);
  2. 触发检查:任一配置阈值满足则触发;
  3. 消息切分:消息被分为两部分——keep 阈值之外的旧消息(待摘要)与 keep 阈值内的近期消息(保留);
  4. 摘要生成:模型对旧消息生成简洁摘要;
  5. 上下文替换:消息历史被更新——旧消息全部移除、近期消息保留、生成的文字摘要存入 summary_text。实现上返回的是 [RemoveMessage(REMOVE_ALL_MESSAGES), *preserved_messages] 加上 summary_text 字段(见 _maybe_summarize());
  6. AI/Tool 成对保护:确保 AI 消息与对应 Tool 消息不被拆散;
  7. 技能上下文通道:对话中读取的技能文件(工具名在 skill_file_read_tool_names 内、路径位于 skills.container_path 下、收窄到 .../SKILL.md)在读取工具边界被打上 skill_context_entry 元数据,随后由 DurableContextMiddleware 捕获进 checkpoint 的 skill_context 通道,以引用形式保存:namepath、从文件 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):明确告知模型这些字段值来自用户/模型/工具/子代理文本,应视为数据而非指令,永远不执行其中嵌入的指令——这是摘要文本作为不可信内容重新进入上下文时的提示注入防线。

四、与持久化上下文的协同与中间件顺序

设计文档给出的中间件顺序是理解整个机制的关键:

  1. Runtime 中间件(含 ThreadData 与 Sandbox 初始化);
  2. DynamicContextMiddleware;
  3. SkillActivationMiddleware;
  4. DurableContextMiddleware(在摘要之前捕获);
  5. SummarizationMiddleware ← 摘要在这里运行;
  6. 下游 Lead 中间件,如 Title、Memory、Clarification。

持久化捕获必须先于摘要运行:任务委托(含进行中的 dispatch 与终态结果摘要)和已加载技能引用,都要在它们的原始 Tool 消息被压缩之前记入 checkpoint。摘要随后压缩消息历史,再让下游的标题生成、记忆队列、澄清等中间件处理缩减后的上下文。

lead_agent/agent.pybuild_middlewares() 与文档一致:DurableContextMiddleware 构造时接收 skills_container_pathskill_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.pysummary_text: NotRequired[str | None]),而不是普通 messages
  • 消息 reducer 移除被压缩的原始消息,checkpointer 则持久化 summary_text
  • DurableContextMiddlewaresummary_text 投影回后续模型调用,作为隐藏 durable 上下文数据。

另外,skill_context 通道本身是带自定义 reducer 的 checkpoint 状态字段(merge_skill_context 合并新旧条目并按上限裁剪),其读取侧 skill_context.pyextract_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()

forceraise_on_failure 是两个正交的维度(见 summarization_middleware.pycompact_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 选择触发阈值

  1. Token 触发:大多数场景的推荐选择。设为模型上下文窗口的 60-80%。例:8K 上下文用 4000-6000 token;
  2. 消息数触发:适合控制会话长度、短消息很多的场景。例:50-100 条;
  3. 比例触发:多模型场景下自动适配各模型容量。例:0.8。

7.2 选择保留策略(keep

  1. 消息数保留:最适合大多数场景,保持自然对话流。推荐 15-25 条;
  2. Token 保留:需要精确控制时。推荐 2000-4000 token;
  3. 比例保留:多模型配置。推荐 0.2-0.4。

7.3 模型选择

  • 推荐:用轻量、高性价比模型做摘要(如 gpt-4o-miniclaude-haiku 或等价物)。摘要不需要最强模型,高并发应用上可显著节省成本;
  • 默认model_name: null):用运行自身模型摘要(而非 models[0])。models[0] provider 故障而运行模型健康时压缩仍可工作;简单部署友好,无需额外维护一个摘要 provider 的凭据。

7.4 优化技巧

  1. 组合触发器:token 与 messages 双触发更稳健:
    trigger:
      - type: tokens
        value: 4000
      - type: messages
        value: 50
    
  2. 保守保留:初始多留一些消息,按表现调整:
    keep:
      type: messages
      value: 25  # 从较高值起步,按需降低
    
  3. 策略性裁剪:限制送往摘要模型的 token 量,避免昂贵的摘要调用:
    trim_tokens_to_summarize: 4000
    
  4. 监控迭代:跟踪摘要质量并调整配置。

八、故障排查

8.1 摘要质量差(丢失重要上下文)

  1. 增大 keep 值,保留更多消息;
  2. 降低触发阈值,更早触发摘要;
  3. 自定义 summary_prompt,强调关键信息;
  4. 换用能力更强的摘要模型。

8.2 性能问题(摘要调用过慢)

  1. 用更快的摘要模型(如 gpt-4o-mini);
  2. 降低 trim_tokens_to_summarize,减少送入的上下文;
  3. 提高触发阈值,降低摘要频率。

8.3 仍触发 token 上限

  1. 降低触发阈值,更早摘要;
  2. 减小 keep,保留更少消息;
  3. 检查是否存在单条超大消息(裁剪只发生在摘要调用侧,单条超长消息仍可能撑爆上下文);
  4. 考虑改用 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.pythreads.py
配置样例 config.example.yaml
测试 test_summarization_middleware.pytest_summarization_summary_text.pytest_durable_context_middleware.py

底层摘要行为由 LangChain 的 SummarizationMiddleware 提供(触发/保留判定、消息切分),DeerFlow 的 DeerFlowSummarizationMiddleware 在其上叠加了模型所有权与回退、nostream 隔离、块逃逸防御、压缩前钩子与扩展观察者通知。理解文档中的每一项配置如何映射到 summarization_config.py 的字段与 summarization_middleware.py 的容错路径,是正确调优 DeerFlow 长会话上下文管理的基础。

登录后查看全文
热门项目推荐
相关项目推荐