Skip to content

上下文工程:摘要与循环检测

本章目标:

  • 讲清长任务为何必须做上下文压缩(摘要)与循环检测,理解二者要解决的根本问题。
  • 拆解 DeerFlowSummarizationMiddleware 的触发判定(tokens/messages/fraction)、保留(keep)策略、技能抢救与 BeforeSummarizationHook 钩子机制。
  • 拆解 LoopDetectionMiddleware 的重复工具调用检测算法,以及硬停止时如何同时清空结构化 tool_calls 与 raw provider 工具调用元数据强制最终文本。

TL;DR

DeerFlow 用三个机制做「上下文工程」:DeerFlowSummarizationMiddleware 在 token 接近上限时把旧对话压成一段摘要、只保留近期消息(并抢救最近加载的技能文件);LoopDetectionMiddleware 用「哈希去重」+「按工具类型计数」两层检测,在重复达到硬上限时剥光工具调用强制模型给出最终文本;DynamicContextMiddleware 注入日期/记忆提醒,且其提醒消息会被摘要中间件特意保留不压缩。摘要前会触发 memory_flush_hook,把即将被丢弃的对话先冲进长期记忆队列,避免压缩造成记忆丢失。

Overview

一个 super agent 跑长任务时,会持续往对话里追加 AIMessage、ToolMessage(工具结果常常很大,例如读一个大文件)。两个失控风险随之出现:

  1. 上下文窗口爆掉:消息累积到超过模型 max input tokens,请求直接失败或被截断,任务中断。
  2. 工具调用死循环:模型反复用相同参数调同一个工具(或对几十个不同文件反复 read_file),直到 LangGraph 的 recursion limit 把整个 run 杀掉,既烧钱又无产出。注释明确把后者列为 P0 安全问题 loop_detection_middleware.py:3-4

「上下文工程」就是用工程手段在每个回合主动管理上下文:回合开始前(before_model)判断是否需要压缩历史 summarization_middleware.py:120-121;模型回复后(after_model)检查工具调用是否陷入循环 loop_detection_middleware.py:419-425。压缩不是无脑丢弃:摘要前要先把待丢弃内容冲进长期记忆 summarization_hook.py:12-34,并保留最近加载的技能文件 summarization_middleware.py:203-249 与日期提醒 summarization_middleware.py:185-201

Architecture

三机制挂在一个回合(turn)的不同钩子点上,互不重叠:

中间件链的完整装配顺序属第 12 章;本章只关心这三者在「上下文」上的协作:摘要会主动把 DynamicContextMiddleware 注入的提醒从待压缩集合里捞出来保留,否则压缩后 DynamicContextMiddleware 会误把摘要 HumanMessage 当成首条用户消息、在错误位置重注入 summarization_middleware.py:185-196

Source 列表:

  • backend/packages/harness/deerflow/agents/middlewares/summarization_middleware.pyDeerFlowSummarizationMiddlewareSummarizationEventBeforeSummarizationHook
  • backend/packages/harness/deerflow/agents/middlewares/loop_detection_middleware.pyLoopDetectionMiddleware、哈希与硬停止
  • backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.pyDynamicContextMiddlewareis_dynamic_context_reminder
  • backend/packages/harness/deerflow/agents/memory/summarization_hook.pymemory_flush_hook
  • backend/packages/harness/deerflow/config/summarization_config.pySummarizationConfigContextSize.to_tuple
  • backend/packages/harness/deerflow/config/loop_detection_config.pyLoopDetectionConfigToolFreqOverride
  • backend/packages/harness/deerflow/agents/lead_agent/agent.py — 工厂装配 _create_summarization_middleware

Components / Subsystems

DeerFlowSummarizationMiddleware

职责:在调用模型前,若历史 token 超过触发阈值,就把旧消息交给一个轻量模型生成摘要,用一条 name="summary" 的 HumanMessage 替代旧历史,只保留近期消息。它继承 LangChain 内置 SummarizationMiddleware 并增强:摘要前钩子分发、技能抢救、动态提醒保护。

关键类/方法:

  • DeerFlowSummarizationMiddleware summarization_middleware.py:98-118
  • _maybe_summarize / _amaybe_summarize:核心流程,token_counter 计 token → _should_summarize 判断 → _determine_cutoff_index 取切点 → 分区 → fire 钩子 → 建摘要 → RemoveMessage(REMOVE_ALL_MESSAGES) 后重建消息 summarization_middleware.py:126-176。其中 _should_summarize_determine_cutoff_index_partition_messagestoken_counter_create_summary 来自上游 LangChain SummarizationMiddleware 基类。
  • _build_new_messages:覆写基类,摘要消息带特殊 name "summary",前端隐藏但仍作为模型上下文 summarization_middleware.py:178-183
  • _partition_with_skill_rescue / _find_skill_bundles / _select_bundles_to_rescue:技能抢救,把最近加载的技能文件读取从待压缩集合捞回保留集合 summarization_middleware.py:203-339
  • SummarizationEvent / BeforeSummarizationHook:压缩前事件与钩子协议 summarization_middleware.py:23-38
  • _fire_hooks:构造 SummarizationEvent(含 thread_id、agent_name、runtime)逐个调钩子,单钩子异常被捕获不影响其他 summarization_middleware.py:352-374

LoopDetectionMiddleware

职责:模型每次回复后,对其 tool_calls 做指纹哈希并按工具类型计数,两层检测重复;到 warn 阈值注入一次警告文本,到 hard 阈值剥光工具调用强制模型产出最终文本。

关键类/方法:

DynamicContextMiddleware

职责:把当前日期(始终注入)与按用户记忆(memory.injection_enabled 为真时)作为一条隐藏 <system-reminder> HumanMessage,在首条用户消息前注入一次,跨午夜时补注日期更新提醒。系统提示保持完全静态以最大化 prefix-cache 命中 dynamic_context_middleware.py:1-27

关键类/方法:

memory_flush_hook

职责:作为 BeforeSummarizationHook,在对话被压缩丢弃前先把这些消息冲进记忆队列,避免压缩造成长期记忆丢失。仅当 memory 启用且有 thread_id 时生效;过滤出含用户与助手消息后,检测纠错/强化信号并 add_nowait 入队 summarization_hook.py:12-34。它由工厂在 memory.enabled 时挂入 hooks 列表 agent.py:95-97

Data Flow

场景一:接近 token 上限 → 触发摘要 → 保留近期 + 摘要旧的

场景二:循环检测 → 硬停止剥光工具调用

Implementation Details

摘要触发判定(token 计数后判断,切点 ≤0 直接放弃):

python
total_tokens = self.token_counter(messages)
if not self._should_summarize(messages, total_tokens):
    return None
cutoff_index = self._determine_cutoff_index(messages)
if cutoff_index <= 0:
    return None
messages_to_summarize, preserved_messages = self._partition_with_skill_rescue(messages, cutoff_index)
messages_to_summarize, preserved_messages = self._preserve_dynamic_context_reminders(messages_to_summarize, preserved_messages)

解读:token_counter/_should_summarize/_determine_cutoff_index/_partition_messages 均来自上游 LangChain 基类,DeerFlow 仅在分区后插入两步增强——技能抢救与动态提醒保护——再 fire 钩子并生成摘要 summarization_middleware.py:130-141

循环硬停止清空 tool_calls(含 raw provider):

python
update = {"tool_calls": [], "content": content}
additional_kwargs = dict(getattr(last_msg, "additional_kwargs", {}) or {})
for key in ("tool_calls", "function_call"):
    additional_kwargs.pop(key, None)
update["additional_kwargs"] = additional_kwargs
response_metadata = deepcopy(getattr(last_msg, "response_metadata", {}) or {})
if response_metadata.get("finish_reason") == "tool_calls":
    response_metadata["finish_reason"] = "stop"

解读:只清结构化 tool_calls 不够——provider 原始工具调用还藏在 additional_kwargs["tool_calls"](以及 OpenAI 风格的 function_call)里,不一并清除会导致序列化出残留的工具调用、破坏 tool_call/tool_message 配对;finish_reason 也要从 tool_calls 改回 stop,这样这条消息才能作为纯助手文本被正确序列化 loop_detection_middleware.py:360-378

循环检测状态机(滑窗内同一哈希出现次数驱动):

速查表

摘要 trigger / keep 类型:

取值含义Source
trigger typetokenstoken 数达到 value 时触发summarization_config.py:7
trigger typemessages消息数达到 value 时触发summarization_config.py:33-38
trigger typefraction达到模型 max input tokens 的 value 比例时触发summarization_config.py:33-38
trigger 形态单个或 listlist 时任一阈值满足即触发(OR 逻辑)summarization_config.py:32-38
keepmessages/tokens/fraction压缩后保留多少近期历史,默认 20 条消息summarization_config.py:39-45
to_tuple()(type, value)转成基类 SummarizationMiddleware 期望的元组summarization_config.py:16-18

loop 检测参数矩阵:

参数默认作用Source
warn_threshold3相同工具调用集出现该次数注入警告loop_detection_middleware.py:35
hard_limit5相同集出现该次数剥光 tool_callsloop_detection_middleware.py:36
window_size20每线程滑窗追踪最近 N 个哈希loop_detection_middleware.py:37
max_tracked_threads100超出按 LRU 淘汰线程追踪状态loop_detection_middleware.py:38
tool_freq_warn30同一工具类型(不论参数)调用该次数注入频率警告loop_detection_middleware.py:39
tool_freq_hard_limit50同一工具类型调用该次数强制停止loop_detection_middleware.py:40
tool_freq_overrides{}按工具名覆写 warn/hard,可调高 bash 等高频工具loop_detection_middleware.py:165-172

Configuration

summarization config.example.yaml:750-794:

默认说明Source
enabledfalse是否启用自动摘要;false 则工厂返回 None 不挂中间件summarization_config.py:24-27 / agent.py:58-59
model_namenull摘要用模型,null 用默认;创建时打 middleware:summarize tagagent.py:76-80
triggernull单个或 list 的 ContextSize,工厂逐个 to_tuple()agent.py:62-67
keepmessages/20压缩后保留策略summarization_config.py:39-45
trim_tokens_to_summarize4000准备摘要输入时最多保留 token,null 跳过裁剪summarization_config.py:46-49
preserve_recent_skill_count5排除最近 N 个技能文件不被压缩,0 关闭抢救summarization_config.py:54-58
preserve_recent_skill_tokens25000抢救技能总 token 预算summarization_config.py:59-63
preserve_recent_skill_tokens_per_skill5000单技能 token 上限,超过不抢救summarization_config.py:64-68
skill_file_read_tool_names[read_file,read,view,cat]视为技能文件读取的工具名summarization_config.py:69-72

loop_detection config.example.yaml:515-529:

默认说明Source
enabledtrue是否启用循环检测;true 时工厂经 from_config 挂入loop_detection_config.py:27-30 / agent.py:308-310
warn_threshold / hard_limit3 / 5校验器强制 hard_limit >= warn_thresholdloop_detection_config.py:66-73
window_size / max_tracked_threads20 / 100滑窗大小与线程追踪上限loop_detection_config.py:41-50
tool_freq_warn / tool_freq_hard_limit30 / 50同工具类型频率阈值,校验器强制 hard ≥ warnloop_detection_config.py:51-73
tool_freq_overrides{}ToolFreqOverride 映射,自身也校验 hard_limit >= warnloop_detection_config.py:6-21

Common Pitfalls/Tips

  • 硬停止会清掉 raw provider tool_calls:仅清结构化 tool_calls 不够,provider 原始负载藏在 additional_kwargs["tool_calls"] / function_call,必须一并 pop 并把 finish_reason 改回 stop,否则序列化出残留工具调用、破坏 tool_call/tool_message 配对 loop_detection_middleware.py:360-378
  • warn 警告是追加进 AIMessage.content,不是单独 HumanMessage:这是 v2.0-m1 的临时绕过(见 #2724)。在 AIMessage(tool_calls=...) 与其 ToolMessage 之间插入任何非工具消息会破坏 OpenAI/Moonshot 严格配对校验,因此把警告文本拼进 content,保留 tool_calls 让工具节点仍执行——副作用是该框架文本会泄漏给下游消费者(记忆抽取/标题/遥测/模型重放)当成模型说的话 loop_detection_middleware.py:391-415
  • 摘要会特意保留动态提醒:若不保留,压缩后 DynamicContextMiddleware 会误把摘要 HumanMessage 当首条用户消息、在错误位置重注入日期/记忆 summarization_middleware.py:190-201
  • 摘要消息 name="summary" 前端隐藏:覆写 _build_new_messages 让摘要带特殊 name,前端不展示但仍作模型上下文;_is_user_injection_target 也据此排除 summary 消息 summarization_middleware.py:178-183 / dynamic_context_middleware.py:76-78
  • 技能抢救失败会安全降级:_find_skill_bundles 抛异常时记录日志并回退到默认分区,不阻断摘要 summarization_middleware.py:214-218
  • read_file 哈希按 200 行分桶:同一文件相邻行段读取会归到同一 key,易触发循环误判;反之 write_file/str_replace 哈希全参,同路径不同内容不会被误判为循环 loop_detection_middleware.py:71-99
  • 某些 provider 把 args 序列化成 JSON 字符串:_normalize_tool_call_args 防御性解析,解析失败保留稳定 fallback key,避免循环检测崩溃 loop_detection_middleware.py:43-66
  • memory_flush_hook 在摘要前冲记忆,但需用户+助手消息齐全:过滤后若缺用户或助手消息直接返回,不入队 summarization_hook.py:18-21

References

页面关系
./12-中间件链机制.md本章三机制的中间件装配顺序与钩子点(before_model/after_model)的完整链路定义在第 12 章
./23-长期记忆系统.mdmemory_flush_hook 把待压缩对话冲进的记忆队列、抽取与存储细节见第 23 章
./11-ThreadState与状态管理.md摘要用 RemoveMessage(REMOVE_ALL_MESSAGES) 重建 messages、循环硬停止 model_copy 替换最后消息,均作用于 ThreadState/AgentState

公司内部参考 · 由 claude-wiki-gen 基于源码自动生成的二次分析