主题
上下文工程:摘要与循环检测
本章目标:
- 讲清长任务为何必须做上下文压缩(摘要)与循环检测,理解二者要解决的根本问题。
- 拆解
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(工具结果常常很大,例如读一个大文件)。两个失控风险随之出现:
- 上下文窗口爆掉:消息累积到超过模型 max input tokens,请求直接失败或被截断,任务中断。
- 工具调用死循环:模型反复用相同参数调同一个工具(或对几十个不同文件反复
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)的不同钩子点上,互不重叠:
- 回合最前:
DynamicContextMiddleware.before_agent注入日期/记忆 system-reminder dynamic_context_middleware.py:198-204。 - 调用模型前:
DeerFlowSummarizationMiddleware.before_model判断 token 预算、必要时压缩历史 summarization_middleware.py:120-150。 - 模型回复后:
LoopDetectionMiddleware.after_model哈希本回合工具调用、判断是否循环 loop_detection_middleware.py:380-425。
中间件链的完整装配顺序属第 12 章;本章只关心这三者在「上下文」上的协作:摘要会主动把 DynamicContextMiddleware 注入的提醒从待压缩集合里捞出来保留,否则压缩后 DynamicContextMiddleware 会误把摘要 HumanMessage 当成首条用户消息、在错误位置重注入 summarization_middleware.py:185-196。
Source 列表:
backend/packages/harness/deerflow/agents/middlewares/summarization_middleware.py—DeerFlowSummarizationMiddleware、SummarizationEvent、BeforeSummarizationHookbackend/packages/harness/deerflow/agents/middlewares/loop_detection_middleware.py—LoopDetectionMiddleware、哈希与硬停止backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.py—DynamicContextMiddleware、is_dynamic_context_reminderbackend/packages/harness/deerflow/agents/memory/summarization_hook.py—memory_flush_hookbackend/packages/harness/deerflow/config/summarization_config.py—SummarizationConfig、ContextSize.to_tuplebackend/packages/harness/deerflow/config/loop_detection_config.py—LoopDetectionConfig、ToolFreqOverridebackend/packages/harness/deerflow/agents/lead_agent/agent.py— 工厂装配_create_summarization_middleware
Components / Subsystems
DeerFlowSummarizationMiddleware
职责:在调用模型前,若历史 token 超过触发阈值,就把旧消息交给一个轻量模型生成摘要,用一条 name="summary" 的 HumanMessage 替代旧历史,只保留近期消息。它继承 LangChain 内置 SummarizationMiddleware 并增强:摘要前钩子分发、技能抢救、动态提醒保护。
关键类/方法:
DeerFlowSummarizationMiddlewaresummarization_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_messages、token_counter、_create_summary来自上游 LangChainSummarizationMiddleware基类。_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-339SummarizationEvent/BeforeSummarizationHook:压缩前事件与钩子协议 summarization_middleware.py:23-38_fire_hooks:构造SummarizationEvent(含 thread_id、agent_name、runtime)逐个调钩子,单钩子异常被捕获不影响其他 summarization_middleware.py:352-374
LoopDetectionMiddleware
职责:模型每次回复后,对其 tool_calls 做指纹哈希并按工具类型计数,两层检测重复;到 warn 阈值注入一次警告文本,到 hard 阈值剥光工具调用强制模型产出最终文本。
关键类/方法:
LoopDetectionMiddlewareloop_detection_middleware.py:144-197,from_config由 Pydantic 校验过的配置构造 loop_detection_middleware.py:199-210_hash_tool_calls:对工具调用集合做顺序无关哈希(排序后 md5 取前 12 位),同一多重集任意排列得同一哈希 loop_detection_middleware.py:112-130_stable_tool_key:从显著参数提取稳定 key,read_file按 200 行分桶降噪,write_file/str_replace哈希全参防误报 loop_detection_middleware.py:69-109_track_and_check:两层检测核心,返回(警告或None, 是否硬停止)loop_detection_middleware.py:231-341_build_hard_stop_update:清tool_calls、清additional_kwargs里的tool_calls/function_call、把finish_reason从tool_calls改stoploop_detection_middleware.py:360-378- 每线程独立追踪,超
max_tracked_threads按 LRU 淘汰 loop_detection_middleware.py:219-229
DynamicContextMiddleware
职责:把当前日期(始终注入)与按用户记忆(memory.injection_enabled 为真时)作为一条隐藏 <system-reminder> HumanMessage,在首条用户消息前注入一次,跨午夜时补注日期更新提醒。系统提示保持完全静态以最大化 prefix-cache 命中 dynamic_context_middleware.py:1-27。
关键类/方法:
is_dynamic_context_reminder:用additional_kwargs["dynamic_context_reminder"]标志判定,而非内容子串匹配,避免用户消息里含<system-reminder>被误判 dynamic_context_middleware.py:57-73- 摘要中间件用此函数把提醒从待压缩集合捞出保留 summarization_middleware.py:196-201
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 type | tokens | token 数达到 value 时触发 | summarization_config.py:7 |
| trigger type | messages | 消息数达到 value 时触发 | summarization_config.py:33-38 |
| trigger type | fraction | 达到模型 max input tokens 的 value 比例时触发 | summarization_config.py:33-38 |
| trigger 形态 | 单个或 list | list 时任一阈值满足即触发(OR 逻辑) | summarization_config.py:32-38 |
| keep | messages/tokens/fraction | 压缩后保留多少近期历史,默认 20 条消息 | summarization_config.py:39-45 |
to_tuple() | (type, value) | 转成基类 SummarizationMiddleware 期望的元组 | summarization_config.py:16-18 |
loop 检测参数矩阵:
| 参数 | 默认 | 作用 | Source |
|---|---|---|---|
| warn_threshold | 3 | 相同工具调用集出现该次数注入警告 | loop_detection_middleware.py:35 |
| hard_limit | 5 | 相同集出现该次数剥光 tool_calls | loop_detection_middleware.py:36 |
| window_size | 20 | 每线程滑窗追踪最近 N 个哈希 | loop_detection_middleware.py:37 |
| max_tracked_threads | 100 | 超出按 LRU 淘汰线程追踪状态 | loop_detection_middleware.py:38 |
| tool_freq_warn | 30 | 同一工具类型(不论参数)调用该次数注入频率警告 | loop_detection_middleware.py:39 |
| tool_freq_hard_limit | 50 | 同一工具类型调用该次数强制停止 | 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 |
|---|---|---|---|
enabled | false | 是否启用自动摘要;false 则工厂返回 None 不挂中间件 | summarization_config.py:24-27 / agent.py:58-59 |
model_name | null | 摘要用模型,null 用默认;创建时打 middleware:summarize tag | agent.py:76-80 |
trigger | null | 单个或 list 的 ContextSize,工厂逐个 to_tuple() | agent.py:62-67 |
keep | messages/20 | 压缩后保留策略 | summarization_config.py:39-45 |
trim_tokens_to_summarize | 4000 | 准备摘要输入时最多保留 token,null 跳过裁剪 | summarization_config.py:46-49 |
preserve_recent_skill_count | 5 | 排除最近 N 个技能文件不被压缩,0 关闭抢救 | summarization_config.py:54-58 |
preserve_recent_skill_tokens | 25000 | 抢救技能总 token 预算 | summarization_config.py:59-63 |
preserve_recent_skill_tokens_per_skill | 5000 | 单技能 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 |
|---|---|---|---|
enabled | true | 是否启用循环检测;true 时工厂经 from_config 挂入 | loop_detection_config.py:27-30 / agent.py:308-310 |
warn_threshold / hard_limit | 3 / 5 | 校验器强制 hard_limit >= warn_threshold | loop_detection_config.py:66-73 |
window_size / max_tracked_threads | 20 / 100 | 滑窗大小与线程追踪上限 | loop_detection_config.py:41-50 |
tool_freq_warn / tool_freq_hard_limit | 30 / 50 | 同工具类型频率阈值,校验器强制 hard ≥ warn | loop_detection_config.py:51-73 |
tool_freq_overrides | {} | ToolFreqOverride 映射,自身也校验 hard_limit >= warn | loop_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
- backend/packages/harness/deerflow/agents/middlewares/summarization_middleware.py
- backend/packages/harness/deerflow/agents/middlewares/loop_detection_middleware.py
- backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.py
- backend/packages/harness/deerflow/agents/memory/summarization_hook.py
- backend/packages/harness/deerflow/config/summarization_config.py
- backend/packages/harness/deerflow/config/loop_detection_config.py
- backend/packages/harness/deerflow/agents/lead_agent/agent.py
Related Pages
| 页面 | 关系 |
|---|---|
| ./12-中间件链机制.md | 本章三机制的中间件装配顺序与钩子点(before_model/after_model)的完整链路定义在第 12 章 |
| ./23-长期记忆系统.md | memory_flush_hook 把待压缩对话冲进的记忆队列、抽取与存储细节见第 23 章 |
| ./11-ThreadState与状态管理.md | 摘要用 RemoveMessage(REMOVE_ALL_MESSAGES) 重建 messages、循环硬停止 model_copy 替换最后消息,均作用于 ThreadState/AgentState |