Skip to main content

DeerFlow 生产防线拆解

前置01 MAST 失效模式。用过或听说过 LangGraph 会更顺。

本篇回答:一个不自研、直接用现成框架的团队,在框架之上还得补多少东西才敢上线。

前面几家都是自研,DeerFlow 是唯一老老实实用 LangGraph 的,架构还是最保守的一家:单层星型,并发只有 3。

它的价值恰恰在保守

框架已经把「怎么编排」解决了,那么 middlewares/ 里那四十多个中间件,就全是框架不管、但线上一定会出事的东西。别家的生产经验要靠泄露物反推,它直接写在文件名里 —— 那份文件名列表本身就是一张能照抄的事故清单。

一、把代码拉下来

字节开源的深度研究 Agent 平台,80,280 star,是本专题体量最大的一个开源样本。

git clone --depth 1 https://github.com/bytedance/deer-flow.git
仓库Star版本后端快照
bytedance/deer-flow80,2802.1.0Python 3.122026-08-19

那份事故清单就在 middlewares/ 目录下。下一节把它拆成七类。

二、上线后的七类典型故障

Agent 系统上线后最常见的翻车,共同点是都不抛异常——代码没报错、API 没失败、模型没「坏掉」,每一步单独看都在正常工作:

#故障表现try/catch超时步数上限
1死循环任务跑 6 小时,模型在两步之间横跳事后砍❌ 认不出重复
2token 烧穿单次请求 300 万 token
3盲写覆盖未读原文即写,凭想象重建内容
4并发打满一次派 20 个子 Agent
5数据串租工具返回值未净化
6上下文冲垮单次 grep 返回 8 万行
7结果倒灌子 Agent 回传 5 万字,父 Agent 失忆

七类里没有一类能被 try/catch、超时、步数上限挡住。 这三者是点状防护,而故障发生在七个不同位置。

DeerFlow 的解法是把刹车抽成一层,每次模型调用与工具调用都必须穿过:

基类为 langchain.agents.middleware.AgentMiddleware,刹车与业务逻辑解耦,新增防护不改 Agent 代码、不漏路径。

每次模型调用与工具调用都要穿过这一层Lead Agent发起请求中间件层 · 40+子 Agent 治理 ×2成本刹车 ×4死循环防护 ×4上下文管理 ×5安全净化 ×5行为约束 ×5交互 ×6错误处理 ×2基类 langchain.agents.middleware.AgentMiddleware刹车与业务逻辑解耦:新增防护不改 Agent 代码,也不会漏掉某条调用路径七类线上故障,每一类都能在这里找到对应的拦截点LLM / 工具返回时再穿一次拦成本与失控拦上下文与安全交互与错误翻译
七类故障的共同点是都不抛异常,因此 try/catch、超时、步数上限这三种点状防护全部失效。中间件把刹车做成必经之路,是唯一能覆盖全部调用路径的位置。

三、先回答那个问题:它到底用不用 LangGraph

在讲中间件之前,先解决一个很多人关心的问题。

前面 00 索引 里提过一个说法:「Manus、Kimi 这些产品大多没用现成框架」。DeerFlow 是个反例,而且证据很硬。

backend/langgraph.json

backend/langgraph.json
{
"$schema": "https://langgra.ph/schema.json",
"python_version": "3.12",
"dependencies": ["."],
"env": "../.env",
"graphs": {
"lead_agent": "deerflow.agents:make_lead_agent"
},
"auth": {
"path": "./app/gateway/langgraph_auth.py:auth"
},
"http": {
"app": "./app/gateway/langgraph_studio.py:langgraph_app"
},
"checkpointer": {
"path": "./packages/harness/deerflow/runtime/checkpointer/async_provider.py:make_checkpointer"
}
}

四条信息:

配置项说明
graphs.lead_agent唯一入口图就叫 lead_agent,跟 Anthropic 的 lead agent 是同一个词
checkpointer没用 LangGraph 自带的,自己写了一个异步 provider
auth挂了鉴权,说明是多租户在线服务,不是本地 CLI
http接了 LangGraph Studio,官方调试 UI 直接能用

中间件基类来自 langchain.agents.middleware.AgentMiddleware。所以结论是:

它吃的是 LangChain 1.x 的中间件体系 + LangGraph 的运行时。

所以「大厂都自研不用框架」这个说法不成立。 真正的问题不是「用不用框架」,而是——站上去之后还要自己补什么?

答案就是下面这四十多个中间件。

四、四十多个中间件:一份生产事故清单

packages/harness/deerflow/agents/middlewares/ 下的文件名,按功能分组:

中间件挡的是第一节里的哪件事
子 Agent 治理subagent_limit_middleware.py周四(一次派 20 个)
delegation_ledger.py周日(结果太长)
成本刹车token_budget_middleware.py周二(账单 40 倍)
token_usage_middleware.py同上,负责统计
tool_output_budget_middleware.py周六(grep 8 万行)
tool_output_synopsis.py同上,超了就摘要
死循环防护loop_detection_middleware.py周一(跑 6 小时)
dangling_tool_call_middleware.py悬空的工具调用
model_length_finish_reason_middleware.py因长度被截断的响应
model_length_termination_detectors.py同上的判定器
上下文管理summarization_middleware.py上下文压缩
durable_context_middleware.py上下文持久化
dynamic_context_middleware.py动态注入
system_message_coalescing_middleware.py合并多条 system 消息
memory_middleware.py长期记忆
安全净化input_sanitization_middleware.py输入净化
tool_result_sanitization_middleware.py周五(串数据)
safety_finish_reason_middleware.py安全策略触发的终止
safety_termination_detectors.py同上
sandbox_audit_middleware.py沙箱审计
行为约束read_before_write_middleware.py周三(覆盖配置)
skill_tool_policy_middleware.py技能的工具权限
skill_activation_middleware.py技能激活
deferred_tool_filter_middleware.py延迟加载的工具过滤
mcp_routing_middleware.pyMCP 路由
交互clarification_middleware.py澄清提问
todo_middleware.pytodo 列表
tool_progress_middleware.py工具进度
terminal_response_middleware.py终端响应
view_image_middleware.py / uploads_middleware.py多模态与上传
错误处理llm_error_handling_middleware.pyLLM 层错误
tool_error_handling_middleware.py工具层错误

第一节那七件事,这里一件不落全都有对应的中间件。 这不是巧合——这份清单就是从那类事故里长出来的。

下面挑五个最有代表性的展开。

五、五道刹车

5.1 并发与总量:两个独立维度

先看子 Agent 的限流。config/subagents_config.py

backend/packages/harness/deerflow/config/subagents_config.py(节选)
DEFAULT_MAX_TOTAL_SUBAGENTS_PER_RUN = 6
MIN_TOTAL_SUBAGENTS_PER_RUN = 1
MAX_TOTAL_SUBAGENTS_PER_RUN = 50
MIN_CONCURRENT_SUBAGENT_CALLS = 1
MAX_CONCURRENT_SUBAGENT_CALLS = 4
backend/packages/harness/deerflow/subagents/executor.py:1261
MAX_CONCURRENT_SUBAGENTS = 3
一次运行(用户一句话)内的三道闸第 1 轮响应S1S2S3S4闸① 单次响应上限 4MAX_CONCURRENT_SUBAGENT_CALLS实际同时在跑S1S2S3排队闸② 并发上限 3MAX_CONCURRENT_SUBAGENTS · 护瞬时资源第 2 轮响应,累计123456闸③ 整轮累计上限 6(可配到 50)DEFAULT_MAX_TOTAL_SUBAGENTS_PER_RUN · 护成本触顶后不是简单拒绝,而是回一段带三个替代方案的指引:用已有结果 / 简单活自己干 / 总结剩余正交的第四道:token 预算70% 告警warn_threshold0200 万未开压缩时上限100 万开压缩后收紧阈值与压缩联动:150 轮无压缩可超 100 万,一刀切会误杀放行触顶 / 排队闸①②③ 管数量,token 预算管总消耗——两者正交,缺一不可
并发限的是瞬时资源(CPU、内存、连接数),总量限的是整体成本:只限并发时模型可串行派 50 个,资源没打满但账单已爆。源码注释写明「backstops must engage, not just exist」——兜底的值必须定在会真正触发的位置。

这里有三个不同的数字,很容易混淆:

限的什么挡的是什么
一次响应里最多派几个4模型一口气派 20 个
同时最多几个在跑3资源打满(周四那件事)
整轮累计最多几个6(可配到 50)派了又派,一轮下来几十个

为什么并发和总量要分开限:并发限的是瞬时资源(CPU、内存、连接数),总量限的是整体成本。只限并发的话,模型可以串行派 50 个,资源没打满但账单爆了。

对照 Manus Wide Research 的「上百个」,DeerFlow 的 3 差了两个数量级。原因在 03 里讲过:Manus 一个子 Agent 一台虚拟机,横向扩展只受基础设施限制;DeerFlow 的子 Agent 跑在同一个 LangGraph 运行时里,受进程资源约束。

架构选择决定了并发天花板,这是「隔离粒度」这个维度最实际的后果。

5.2 触顶消息:错误消息即提示词

限流触发了,怎么告诉模型?

backend/packages/harness/deerflow/agents/middlewares/subagent_limit_middleware.py(节选)
_TOTAL_LIMIT_STOP_MSG = (
"[SUBAGENT LIMIT REACHED] The subagent delegation limit for this run has been reached. "
"Continue using the subagent results already collected, execute remaining simple work "
"directly, or summarize the remaining work instead of launching more subagents."
)

翻译:「派活额度用完了。请基于已有的子 Agent 结果继续,剩下的简单活自己干,或者把没做完的部分总结一下。」

作用:防止模型撞到限制之后不知所措——反复重试同一个动作,或者干脆放弃。

注意它不只说「不行」,还给了三个具体的替代方案。这跟 Kimi CLI 那句「请试着把任务拆得更小」 是同一种手艺:

在 Agent 系统里,错误消息的读者是模型。除了「哪儿错了」,必须写「你接下来该怎么办」。

这条经验前面出现过两次了,值得当成通用规则记住。

5.3 token 预算:兜底必须会触发

这是我认为整个仓库里最值得读的一段。config/subagents_config.py

backend/packages/harness/deerflow/config/subagents_config.py(节选)
def default_subagent_token_budget(*, summarization_enabled: bool = False) -> TokenBudgetConfig:
"""Default per-run token budget for subagents (#3875 Phase 2 → Phase 3 coupling).

Enabled by default so the pathological-token-burn backstop actually
engages (per umbrella #3857 point 4 — backstops must engage, not just
exist). ``max_tokens`` is **coupled to whether subagent summarization is
on**:

- ``summarization_enabled=True``: **1M** — tighter ceiling still
covers legitimate deep research while catching degenerate runs earlier.
- ``summarization_enabled=False``: **2M** — ... legitimate deep-research runs
(``max_turns=150``, no summarization) "can genuinely accumulate >1M
cumulative input," so a 1M ceiling without compaction would prematurely
cap them.
"""
max_tokens = 1_000_000 if summarization_enabled else 2_000_000
return TokenBudgetConfig(enabled=True, max_tokens=max_tokens, warn_threshold=0.7)

先看结论:

场景单次运行 token 上限
开了上下文压缩100 万
没开压缩200 万
告警阈值用到 70% 时告警

再看这段注释里的三个信息,每个都很有分量:

① 「pathological-token-burn backstop」——病态 token 燃烧的兜底。

这个词组加上引用的 issue 编号(#3875、#3857),说明线上真的烧穿过。这不是防御性编程的假想敌。

② 「backstops must engage, not just exist」——兜底必须真的会触发,不能只是摆着。

这句话值得裱起来。很多团队会加一个上限,但值定得极高(比如 1 亿 token),等于永远不触发。那不叫兜底,叫心理安慰。

DeerFlow 的做法是把上限压到「刚好不误伤正常任务」的位置——所以才要区分开不开压缩两种情况。

③ 阈值是推导出来的,不是拍脑袋的。

注释里写了推导过程:正常的深度研究任务(150 轮、不压缩)确实能累积超过 100 万输入 token,所以不压缩时不能定 100 万,会误杀;开了压缩之后上下文不会病态膨胀,才能收紧到 100 万。

这种「为什么是这个数」的注释,比数字本身值钱。 半年后有人想调这个值,看到这段就知道该考虑什么。

对照 Anthropic 说的「多 Agent 是普通聊天的 15 倍 token」,就明白为什么这层刹车非有不可了。

5.4 死循环检测 vs 步数上限

第一节周一那件事:跑了 6 小时,每一步都正常。

loop_detection_middleware.py 解决的就是这个。它跟步数上限的区别:

两个都要。 步数上限是最后一道兜底(万一模式检测漏了),模式检测是早期发现。

顺带一提,Suna 有一起 2026-08-18 的线上事故 就是这一类——每一轮都干净地成功,只是在做同一件事,所有监听「错误」的机制全部失灵。那篇里有完整的事故记录。

5.5 委派台账:确定性截断

第一节周日那件事:子 Agent 把 5 万字全倒回来,父 Agent 上下文直接炸。

delegation_ledger.py 的处理:

backend/packages/harness/deerflow/agents/middlewares/delegation_ledger.py(节选)
_RESULT_BRIEF_CAP = 2000
_DESCRIPTION_CAP = 200
_LEDGER_RENDER_CHAR_BUDGET = 6000
_LEDGER_ENTRY_RESULT_RENDER_CAP = 120
_STATUS_ONLY_RESULT_BRIEFS = {
"failed": "Task failed.",
"cancelled": "Task cancelled by user.",
"timed_out": "Task timed out.",
"polling_timed_out": "Task polling timed out.",
}


def _bound_text(text: str, cap: int = _RESULT_BRIEF_CAP) -> str:
"""Deterministic head/tail truncation. This is not an LLM summary."""
if len(text) <= cap:
return text
if cap <= 0:
return ""
head = cap * 2 // 3
omitted_marker = "\n...\n"

注意那句注释:"This is not an LLM summary."(这不是 LLM 总结)——这是特意写的,防止后来人「优化」成调模型总结。

子 Agent 结果 → 父 Agent 上下文的两道裁剪子 Agent 原始输出(示例 9000 字符)超过 _RESULT_BRIEF_CAP = 2000确定性截断 · 不调 LLM头 2/3尾 1/3= 2000 字符,两次运行结果完全一致进台账时再裁一次120_LEDGER_ENTRY_RESULT_RENDER_CAP —— 台账里每条只留 120 字符台账整体 _LEDGER_RENDER_CHAR_BUDGET = 6000结论:台账是索引不是全文。父 Agent 要细节需另行取回,而非默认全量灌入上下文
源码注释写着 This is not an LLM summary.——特意标注是为了防止后来人「优化」成调模型总结。确定性截断的三个收益:零成本、零延迟、可复现;排查时能确认上次父 Agent 究竟看到了什么。

截断策略是头 2/3 + 尾 1/3,中间挖掉插一个省略标记。

为什么不用 LLM 总结? 三个理由:

LLM 总结确定性截断
成本多一次模型调用
延迟多几秒微秒级
可复现同样输入两次结果可能不同永远一样

第三条在排查问题时是决定性的。如果截断结果不确定,你根本无法复现「上次为什么父 Agent 拿到的信息不全」。

这跟 Kimi CLI 的做法正好相反,而且两家治的是相反的病:

Kimi CLIDeerFlow
治的病太短太长
手段再跑一轮 LLM纯字符串操作
确定性不确定完全确定
成本多一次调用

理想做法是两头都要:下限用 LLM 保证信息量,上限用确定性截断保证不炸。这是整个专题里我认为最直接可用的一条。

还有个细节:台账整体渲染预算 6000 字符,单条结果在台账里只渲染 120 字符。也就是说台账是个索引,不是全文——父 Agent 想看细节要另外取。

六、两个内建子 Agent:分工比别家粗

subagents/builtins/ 下只有两个文件:

backend/packages/harness/deerflow/subagents/builtins/general_purpose.py(节选)
GENERAL_PURPOSE_CONFIG = SubagentConfig(
name="general-purpose",
tools=None, # Inherit all tools from parent
disallowed_tools=["task", "ask_clarification", "present_files"], # Prevent nesting and clarification
max_turns=150,
)
backend/packages/harness/deerflow/subagents/builtins/bash_agent.py(节选)
BASH_AGENT_CONFIG = SubagentConfig(
name="bash",
tools=["bash", "ls", "read_file", "write_file", "str_replace"], # Sandbox tools only
disallowed_tools=["task", "ask_clarification", "present_files"],
max_turns=60,
)

对照 Kimi CLI 的三个工种(coder / explore / plan),DeerFlow 只有两个:一个万能的、一个只能操作沙箱的。分工更粗,但轮数上限给得很足(150 和 60)。

三个被禁的工具值得逐个说:

backend/packages/harness/deerflow/subagents/config.py:38
disallowed_tools: list[str] | None = field(default_factory=lambda: ["task"])
被禁的工具为什么禁
task禁止子 Agent 再派子 Agent,注释直接写了 # Prevent nesting
ask_clarification子 Agent 不该直接找用户说话,有问题要在总结里告诉父 Agent
present_files同理,展示产物是父 Agent 的职责

第二个跟 Kimi CLI 提示词里那句 "Do not directly ask the end user questions" 是同一件事,但手段不同:Kimi 靠提示词说,DeerFlow 直接把工具剥了。后者更硬——提示词能被绕过,工具没了就是没了。

至于递归,本专题拆到这里的分布是:

递归策略
Kimi CLI硬禁止(三重保险)
DeerFlow默认禁止disallowed_toolstask
Manus未公开,推测单层
Claude Code默认允许 3 层(带刹车)

三比一,禁止是多数派。

七、会在哪儿翻车:症状 → 原因 → 对策

症状原因对策
任务跑几小时不停,无报错模型在原地打转,每步都「成功」模式检测 + 步数上限,两个都要(Step 4)
单次请求烧掉几百万 token没有整轮的 token 预算token 预算兜底,且值要定得会真触发(Step 3)
服务器资源被打满只限了总量没限并发并发和总量分开限(Step 1)
账单没爆但任务巨慢只限了并发没限总量同上,两个是不同的东西
上下文被一次工具调用冲垮单个工具输出没有长度上限tool_output_budget,超了就摘要
父 Agent 拿到子 Agent 结果后失忆结果全文回传,撑爆上下文确定性截断,台账只当索引(Step 5)
排查时无法复现上次的截断结果用 LLM 做的总结,不确定换成确定性截断(Step 5)
模型凭想象覆盖了文件没强制先读后写read_before_write
用户看到别人的数据工具返回值没净化tool_result_sanitization
子 Agent 直接找用户提问,流程乱了只用提示词约束直接剥掉 ask_clarification 工具(第六节)
触顶后模型反复重试错误消息只说了「不行」消息里给出替代方案(Step 2)

八、全局定位

01 的五个维度 打分:

维度DeerFlow 的答案
D1 隔离单位LangGraph 运行时里的独立分支 + 独立工具集,同进程
D2 通信拓扑星型,无兄弟通信
D3 结果回收委派台账,确定性头尾截断(2000 字上限),明确不用 LLM
D4 递归深度单层disallowed_tools 默认含 task
D5 生命周期max_turns 150/60,超时 30 分钟,有 checkpointer 支持中断续跑

一句话总结:架构上最保守(单层星型、并发只有 3),但运行时治理是本专题里最厚的。它把力气全花在了「别炸」上——而这恰恰是从 demo 到生产最难的那部分。

九、小结与自查清单

DeerFlow 的中间件清单可以直接当上线前的 checklist 用:

成本刹车

  • 整轮运行有没有 token 总预算?
  • 这个预算的值会真的触发吗?还是定得高到形同虚设?(「backstops must engage, not just exist」)
  • 有没有告警阈值(比如 70%),能在触顶前发现?
  • 单个工具的输出有没有长度上限?

死循环

  • 除了步数上限,有没有识别「重复动作模式」的检测?
  • 想过「每一轮都成功、只是在做同一件事」这种情况吗?

子 Agent 治理

  • 并发数和总量分别限了吗?(这是两件事)
  • 子 Agent 能不能再派子 Agent?
  • 子 Agent 能不能直接找用户说话?(应该不能,而且最好直接剥工具)
  • 子 Agent 的结果回传是确定性的还是要过一次 LLM?
  • 结果太长太短两头都管了吗?

行为约束

  • 模型改文件之前强制读过没有?
  • 工具返回值做净化了吗?(多租户场景尤其重要)

给模型的信息

  • 每个限流/错误消息里,有没有告诉模型「接下来该怎么办」?

可维护性

  • 每个阈值旁边有没有写「为什么是这个数」?(半年后调它的人会感谢你)

十、参考