DeerFlow Plan Mode 实战指南:通过 is_plan_mode 启用 write_todos 工具实现多步任务跟踪
本文基于 DeerFlow 官方文档 plan_mode_usage.md 并结合当前仓库源码展开,讲解 Plan Mode 的启用方式、is_plan_mode 运行时参数的传递链路、write_todos 工具的默认使用策略,以及 TodoMiddleware 在消息历史截断、提前退出等边界场景下的保护机制。读完本文,你可以在 DeerFlow 的 lead agent 上按请求粒度动态开启计划模式,并理解其中间件在代理组装流程中的实际位置与行为。
Plan Mode 是什么
Plan Mode 是 DeerFlow 2.0 提供的一个按请求粒度开关的任务跟踪能力:开启后,代理会挂载一个 TodoList 中间件,该中间件向模型注入一个 write_todos 工具,帮助代理:
- 把复杂任务拆解为更小、可管理的步骤;
- 在工作推进过程中实时跟踪进度;
- 让用户对"当前正在做什么"有可见性。
按照文档说明,该中间件构建在 LangChain 的 TodoListMiddleware 之上。从源码结构看,DeerFlow 并没有直接使用基类,而是定义了一个子类 TodoMiddleware(见 todo_middleware.py),在继承 LangChain 基础行为之外,额外实现了上下文丢失检测与"未完成待办时禁止提前退出"的守护逻辑,这部分是文档未展开的实现细节,下文会专门解析。
通过运行时配置启用 Plan Mode
Plan Mode 由 RunnableConfig.configurable 中的 is_plan_mode 参数控制,属于运行时配置:可以为每次请求动态开启或关闭,无需任何全局配置项。
from langchain_core.runnables import RunnableConfig
from deerflow.agents.lead_agent.agent import make_lead_agent
# Enable plan mode via runtime configuration
config = RunnableConfig(
configurable={
"thread_id": "example-thread",
"thinking_enabled": True,
"is_plan_mode": True, # Enable plan mode
}
)
# Create agent with plan mode enabled
agent = make_lead_agent(config)
配置项语义(继承自原文档,并与源码核对一致):
- is_plan_mode(bool):是否启用带 TodoList 中间件的 Plan Mode,默认
False。- 通过
config.get("configurable", {}).get("is_plan_mode", False)读取; - 可以为每次代理调用动态设置;
- 不需要全局配置。
- 通过
参数读取的源码位置
在 lead_agent/agent.py 中,is_plan_mode 的读取发生在 _assemble_lead_agent()(L890 附近):
cfg = _get_runtime_config(config)
is_plan_mode = cfg.get("is_plan_mode", False)
这里有一个值得注意的实现细节:参数并不是只从 configurable 读取,而是经由 _get_runtime_config()(agent.py#L194-L200)先把 config["configurable"] 与 LangGraph 的 config["context"] 合并后再取值。也就是说,is_plan_mode 同时可以通过 configurable 或 context 两个通道传入,这为 Gateway 等上层运行时提供了统一的注入口径。
解析出的 is_plan_mode 还会被写入 config["metadata"](agent.py#L961-L973),用于 LangSmith/Langfuse 等追踪系统的 run 标签,便于在 trace 中区分哪些 run 启用了计划模式。
Gateway 与嵌入式客户端如何传入该参数
从源码可以确认两条实际的注入路径:
- Gateway HTTP 路径:gateway/services.py 中定义了白名单
_CONTEXT_CONFIGURABLE_KEYS,is_plan_mode是其中之一。langgraph-compat 层会把请求body.context中该白名单内的键同时写入configurable与context,因此通过 Gateway 发起的每次 run 都可以独立指定是否启用 Plan Mode。 - 嵌入式
DeerFlowClient路径:client.py 的_get_runnable_config()把客户端构造参数plan_mode映射为 configurable 中的is_plan_mode,且每次调用可用overrides.get("plan_mode", self._plan_mode")覆盖。更重要的是,is_plan_mode是代理配置缓存键的组成部分(client.py#L298-L310)——当该值发生变化时,客户端会判定配置键不匹配并重建代理,从而保证开关切换立即生效。
默认行为:何时用、何时不用 TodoList
当 Plan Mode 以默认设置启用后,代理会获得 write_todos 工具,其行为策略由自定义提示词约束(提示词全文见下文"自定义提示词"一节)。原文档归纳的使用策略与源码提示词一致:
应当使用 TodoList 的场景
- 复杂的多步骤任务(3 个以上明确步骤);
- 需要仔细规划的非平凡任务;
- 用户明确要求使用 todo list;
- 用户一次给出多个任务(编号或逗号分隔列表)。
不应使用 TodoList 的场景
- 单一的、直接的任务;
- 琐碎任务(少于 3 步);
- 纯对话式或信息性请求。
此外,agent.py 中的系统提示词还补充了一条策略要点:当计划可能需要根据中间结果修订时,也应使用 todo list。
任务状态
- pending:任务尚未开始;
- in_progress:正在处理(并行执行时可有多个);
- completed:任务已成功完成。
提示词对状态管理有明确约束:每完成一步立即标记 completed(禁止批量补标)、同一时间保持恰有一个 in_progress(除非任务可并行)、随工作实时更新列表;并强调只有在任务被完全完成时才允许标记 completed——存在未解决问题、部分完成、遇到阻塞器、缺少依赖或质量不达标时,应保持 in_progress 并新建任务描述待解决项。
使用示例
基本用法
from langchain_core.runnables import RunnableConfig
from deerflow.agents.lead_agent.agent import make_lead_agent
# Create agent with plan mode ENABLED
config_with_plan_mode = RunnableConfig(
configurable={
"thread_id": "example-thread",
"thinking_enabled": True,
"is_plan_mode": True, # TodoList middleware will be added
}
)
agent_with_todos = make_lead_agent(config_with_plan_mode)
# Create agent with plan mode DISABLED (default)
config_without_plan_mode = RunnableConfig(
configurable={
"thread_id": "another-thread",
"thinking_enabled": True,
"is_plan_mode": False, # No TodoList middleware
}
)
agent_without_todos = make_lead_agent(config_without_plan_mode)
按任务复杂度动态切换
可以为不同的会话或任务动态启停 Plan Mode,例如按任务复杂度决定:
from langchain_core.runnables import RunnableConfig
from deerflow.agents.lead_agent.agent import make_lead_agent
def create_agent_for_task(task_complexity: str):
"""Create agent with plan mode based on task complexity."""
is_complex = task_complexity in ["high", "very_high"]
config = RunnableConfig(
configurable={
"thread_id": f"task-{task_complexity}",
"thinking_enabled": True,
"is_plan_mode": is_complex, # Enable only for complex tasks
}
)
return make_lead_agent(config)
# Simple task - no TodoList needed
simple_agent = create_agent_for_task("low")
# Complex task - TodoList enabled for better tracking
complex_agent = create_agent_for_task("high")
IM 渠道同样受益于此机制:从 test_channels.py 可见,渠道会话支持以 default_session={"context": {"is_plan_mode": True}} 的方式为某个会话固定携带该运行参数,即不同 IM 会话可以有不同的计划模式策略。
工作原理:从 is_plan_mode 到 write_todos 的调用链
原文档给出的工作机制步骤如下,每一步都可以在源码中找到对应实现:
- 调用
make_lead_agent(config)时,从config.configurable中提取is_plan_mode; - 该配置被传入
build_middlewares(config); build_middlewares()读取is_plan_mode并调用_create_todo_list_middleware(is_plan_mode);- 若
is_plan_mode=True,则创建一个TodoListMiddleware实例并加入中间件链; - 中间件自动向代理工具集注入
write_todos工具; - 代理在执行过程中使用该工具管理任务;
- 中间件维护 todo list 状态并向代理提供它。
源码级的调用链(lead_agent/agent.py):
make_lead_agent(config) # L749
└─> assemble_lead_agent / _assemble_lead_agent
│ is_plan_mode = cfg.get("is_plan_mode", False) # L890
└─> build_middlewares(config, ...) # L457
│ cfg = _get_runtime_config(config) # L579
│ is_plan_mode = cfg.get("is_plan_mode", False)
│ todo_list_middleware = _create_todo_list_middleware(is_plan_mode) # L581
│ if todo_list_middleware: middlewares.append(...) # L582-L583
└─> _create_todo_list_middleware(is_plan_mode) # L332
└─> return TodoMiddleware(system_prompt=..., tool_description=...) # L444
要点:
_create_todo_list_middleware()(agent.py#L332-L444)在is_plan_mode为假时直接返回None,即"关闭时零成本";build_middlewares()是公共入口(agent.py#L457),被make_lead_agent与嵌入式DeerFlowClient复用,因此两条路径下 Plan Mode 的中间件组装逻辑完全一致;- 测试用例 test_create_deerflow_agent.py 直接验证了契约:
plan_mode=True时中间件链中出现 TodoMiddleware,默认plan_mode=False时不出现;test_client_e2e.py 则验证了is_plan_mode参与客户端配置缓存键。
架构与中间件位置
原文档给出的架构示意如下:
make_lead_agent(config)
│
├─> Extracts: is_plan_mode = config.configurable.get("is_plan_mode", False)
│
└─> build_middlewares(config)
│
├─> ThreadDataMiddleware
├─> SandboxMiddleware
├─> SummarizationMiddleware (if enabled via global config)
├─> TodoListMiddleware (if is_plan_mode=True)
├─> TitleMiddleware
└─> ClarificationMiddleware
(示意中的 TodoListMiddleware 在当前实现中即 TodoMiddleware。)
从源码结构看,当前 build_middlewares() 的完整中间件链比该示意更长(还包括技能激活、持久上下文、Token 使用、记忆、图像查看、循环检测、Token 预算、终止响应等中间件),但 Plan Mode 相关的位置关系保持不变:
- TodoMiddleware 在 SummarizationMiddleware 之后追加(agent.py#L569-L583);
- 其位置注释明确要求"TodoListMiddleware should be before ClarificationMiddleware to allow todo management during clarification flows"(agent.py#L447-L456),即中间件顺序保证在澄清(clarification)流程中仍可管理 todo;
ClarificationMiddleware恒定位于链尾(agent.py#L692-L693)。
自定义提示词:DeerFlow 风格的 system_prompt 与 tool_description
DeerFlow 为 TodoListMiddleware 定制了 system_prompt 与 tool_description,与主系统提示词风格保持一致(定义于 _create_todo_list_middleware(),agent.py#L345-L444)。原文档概括的特征及对应源码实现如下。
System Prompt 特征
- 使用 XML 标签
<todo_list_system>组织内容,与 DeerFlow 主提示词的标签风格一致; - 以 CRITICAL RULES 开头,强调关键规则:
- 每完成一步立即标记
completed,不得批量补标; - 同一时间保持恰好一个任务为
in_progress(可并行的任务除外); - 随工作实时更新列表,向用户提供进度可见性;
- 少于 3 步的简单任务不要用该工具,直接完成即可;
- 每完成一步立即标记
- 明确的 "When to Use" 与 "When NOT to Use" 对照清单;
- 聚焦实时更新与即时完成任务,并提示"写 todo 本身消耗时间与 token,只在管理复杂问题时使用"。
Tool Description 特征
- 详细的使用场景与示例(复杂多步任务、非平凡任务、用户显式要求、多任务列表、计划需随中间结果更新);
- 强烈强调不要在简单任务上使用(开头即声明 "Only use this tool for complex tasks (3+ steps)");
- 清晰的任务状态定义(pending / in_progress / completed);
- 独立的 "Task Completion Requirements" 章节,防止过早标记完成:存在未解决错误、部分完成、遇到阻塞、缺少依赖、未达质量标准时不得标记
completed;若被阻塞,应保持in_progress并新建描述待解决问题的任务; - 最佳实践章节,包括:写下 todo 列表时立即把首个任务标记为
in_progress;除非全部完成,始终至少保持一个任务处于in_progress以体现进度。
源码纵深:TodoMiddleware 的两项增强机制
DeerFlow 的 TodoMiddleware(todo_middleware.py)在 LangChain TodoListMiddleware 之上解决了两个长任务场景的真实问题。
1. write_todos 上下文丢失检测(before_model)
当消息历史被截断(例如被 SummarizationMiddleware 压缩)后,最初的 write_todos 工具调用及其 ToolMessage 可能已滚出活动上下文窗口,模型会"忘记"当前还有哪些待办。before_model 钩子(todo_middleware.py#L124-L160)的检测逻辑是:
- 若 state 中没有 todos,直接返回;
- 若消息历史中仍能看到
write_todos工具调用(_todos_in_messages()),无需处理; - 若已注入过 reminder(
_reminder_in_messages()),避免重复注入; - 否则注入一条名为
todo_reminder的HumanMessage(带hide_from_ui: True,对用户不可见),列出当前 todo 状态并要求模型继续通过write_todos跟踪。
2. 防止带未完成任务提前退出(after_model)
after_model 钩子(todo_middleware.py#L269-L313)在模型产出"干净的最终回复"(无工具调用意图,由 _has_tool_call_intent_or_error() 判定)而 todo 尚未全部完成时:
- 先执行基类的并行
write_todos检查,保持原有语义; - 所有 todo 已完成或无 todo 时放行退出;
- 每个
(thread_id, run_id)的完成提醒最多_MAX_COMPLETION_REMINDERS = 2次(todo_middleware.py#L173),防止代理无法推进时无限循环; - 否则将提醒文本入队(而非持久化为用户可见消息),返回
{"jump_to": "model"}让流程跳回模型节点继续工作。
提醒文本在下一轮模型请求中经 wrap_model_call / awrap_model_call(todo_middleware.py#L343-L365)注入——这里特意保留了对基类 wrap_model_call 的调用,因为基类正是在该钩子里追加 write_todos 系统提示词;TodoMiddleware 在其上叠加 todo_completion_reminder 的 HumanMessage(同样 hide_from_ui)。提醒账本按 (thread_id, run_id) 分键、带锁与 LRU 式裁剪(上限 4096 键),在 before_agent/after_agent 中清理,避免跨 run 泄漏。
与其他中间件的联动
- Token 统计:token_usage_middleware.py 将
write_todos作为精确 token 统计的特例之一处理; - 进度展示:tool_progress_middleware.py 将
write_todos列入免展示工具(与ask_clarification、present_files、task同列),避免纯记账型调用刷屏; - 中间件清单:agents/middlewares/AGENTS.md 将 TodoListMiddleware 标注为 "optional, if
is_plan_mode",与本文的开关语义一致。
关键收益与注意事项
原文档总结的 Plan Mode 优势,结合源码可进一步落实为:
- 动态控制:按请求启停,无全局状态——实现上就是
configurable/context中的一个布尔键,DeerFlowClient通过配置缓存键保证切换即重建; - 灵活性:不同会话可以有不同的 Plan Mode 策略(HTTP run、嵌入式客户端、IM 渠道会话均可独立指定);
- 简单性:无需管理全局配置,关闭时中间件函数返回
None,零开销; - 上下文感知:开关决策可以基于任务复杂度、用户偏好等。
注意事项(继承原文档 Notes 并对照源码确认):
- Todo 中间件使用 LangChain 内置
TodoListMiddleware作为基类,但注入的是 DeerFlow 风格自定义提示词; - Plan Mode 默认关闭(
is_plan_mode=False),以维持向后兼容; - 中间件位于
ClarificationMiddleware之前,使澄清流程期间仍可管理 todo; - 自定义提示词与 DeerFlow 主系统提示词强调相同原则:清晰、行动导向、关键规则(CRITICAL rules)。
综上,Plan Mode 是 DeerFlow lead agent 中一个典型的"运行时配置 → 中间件组装 → 模型行为约束 → 状态守护"完整链路:一个布尔参数即可为长时程任务挂上任务账本,并在上下文压缩与提前退出两类常见失效模式下自动兜底,适合作为研究 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 StartedRust0622
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