第 1 章
DeerFlow 是什么
先从一项做不完的任务,建立最小运行模型
假设你已经有一个能调用大模型的函数。现在用户上传 sales-notes.pdf,要求补充调研并生成 sales-review.md。顺着这项任务缺少的能力往下看,DeerFlow 的边界会比一串模块名清楚得多。
模型负责判断和生成;Harness 把它放进一套可以运行、观察、恢复和交付的结构里。
- 01Goal
分析销售下滑
- 02Agent loop
模型与工具反复协作
- 03Runtime
Run、状态与事件
- 04Delivery
Sandbox 文件成为 Artifact
先别谈架构,看看用户到底要什么
用户上传 sales-notes.pdf,里面是销售团队的访谈记录。他希望系统找出销售额下滑的主要原因,必要时上网核对行业数据,最后生成 sales-review.md。这个要求看起来像一句普通提示词,实际包含了读取附件、规划调查、调用搜索工具、整理证据、写入文件和交付下载地址。
如果只调用一次大模型,输入只能是当时塞进上下文的文字,输出也只是一段响应。模型可以写出一篇像报告的文本,却不知道文件位于哪里,不能自行决定什么时候检索,也没有一个可靠位置保存中间结论。请求中断以后,更没有 run_id 让系统继续查询这项工作。
因此,本章不从目录结构开始,而是顺着任务往前走:模型要反复使用工具,所以需要 Agent;任务不能依赖浏览器连接,所以需要 Run;执行过程要保存进度,所以需要 ThreadState;工具要安全读写文件,所以需要 Sandbox;最终文件要明确交给用户,所以需要 Artifact。后面每一节只解决其中一个问题。
从一次模型调用到 Agent 循环
一次模型调用只能得到一次输出,做不完“读附件、查资料、写报告”这串动作。最小的 Agent 因此是一段循环:把目标和当前状态交给模型,模型或者直接回答,或者选择一个工具;工具结果写回消息,模型再判断下一步。为了完成报告,它可能先读附件,再搜索市场数据,然后写草稿,最后检查输出文件。
DeerFlow 的 Agent 工厂没有只接收 model。它还要接收 tools、middleware、state_schema 和 checkpointer,因为一次可运行的 Agent 需要知道能做什么、每一步怎样被拦截、状态长什么样,以及状态怎样保存。先看函数签名,不必急着理解每个参数。
模型判断下一步;工具改变外部世界;结果回到状态,直到模型认为任务完成。
def create_deerflow_agent(
model: BaseChatModel,
tools: list[BaseTool] | None = None,
*,
system_prompt: str | None = None,
middleware: list[AgentMiddleware] | None = None,
features: RuntimeFeatures | None = None,
extra_middleware: list[AgentMiddleware] | None = None,
plan_mode: bool = False,
state_schema: type | None = None,
checkpoint_channel_mode: CheckpointChannelMode = "full",
checkpoint_snapshot_frequency: int | None = None,
checkpointer: BaseCheckpointSaver | None = None,
name: str = "default",
subagent_runtime: SubagentRuntime | None = None,
) -> CompiledStateGraph:create_deerflow_agent 返回的不是一段最终文字,而是 CompiledStateGraph。model 提供推理,tools 提供动作,middleware 包住模型与工具调用,state_schema 规定共享状态,checkpointer 负责图状态的持久化。它们共同组成循环,但职责没有揉成一个大类。
这也解释了 DeerFlow 为什么自称 Super Agent Harness。Agent 是会判断下一步的执行循环;Harness 是把循环装配起来并提供运行条件的外壳。DeerFlow 的价值主要在后者:让同一套 Agent 能被网页启动、被事件流观察、在 Sandbox 里工作,并把结果变成用户可见的 Artifact。
长任务为什么需要一条 Run
Agent 循环解决了多步工作,却带来一个新问题:它可能运行几分钟,浏览器连接却随时会刷新或断开。如果执行只属于那条 HTTP 连接,页面一关,系统就很难回答“刚才的报告还在跑吗”“能不能取消”“失败发生在哪一步”。因此 DeerFlow 在真正调用模型之前,先创建一条 Run。
Run 是一次执行,不是一段对话。相同 thread 可以先运行一次生成初稿,再运行一次按新数据修订;它们共享对话背景,却应该有不同的 run_id、状态、开始时间和事件。Gateway 用 RunCreateRequest 把启动一次执行所需的数据收在一起。
class RunCreateRequest(BaseModel):
"""Validated run request used by both HTTP and internal launch paths."""
model_config = ConfigDict(extra="forbid")
assistant_id: str | None = Field(default=None, description="Agent / assistant to use")
input: dict[str, Any] | None = Field(default=None, description="Graph input (e.g. {messages: [...]})")
command: dict[str, Any] | None = Field(default=None, description="LangGraph Command")
metadata: dict[str, Any] | None = Field(default=None, description="Run metadata")RunCreateRequest 里的 input 是图输入,command 用于继续或控制 LangGraph,metadata 保存随 Run 查询的附加信息。请求通过校验后,系统会建立 RunRecord,再把执行交给 worker。客户端拿到记录只代表任务已经受理,并不代表 sales-review.md 已经生成。
把 Run 和连接分开以后,查询、取消、重连才有稳定目标。SSE 可以观察一条 Run,普通接口也可以只创建 Run;两种入口最终管理的是同一种执行,而不是两套 Agent。第 02 章会沿这条路径看到 start_run 和 worker。
ThreadState 把任务进度放在一个地方
Run 给一次执行分配了身份,却没有说明执行中的消息、附件、待办和文件应该放在哪里。若每个 Middleware 和工具都各存一份,很快会出现彼此不一致的状态。DeerFlow 因而让 Agent 图围绕 ThreadState 工作。
ThreadState 可以把它理解成“这个 thread 中,Agent 当前能看到并继续修改的业务状态”。它继承消息状态,同时增加 Sandbox、artifacts、todos、goal、uploaded_files、delegations 等字段。字段旁边的 reducer 决定新结果怎样与旧值合并。
class ThreadState(AgentState):
sandbox: SandboxStateField
thread_data: NotRequired[ThreadDataState | None]
title: NotRequired[str | None]
artifacts: Annotated[list[str], merge_artifacts]
todos: Annotated[list | None, merge_todos]
goal: Annotated[GoalState | None, merge_goal]
uploaded_files: NotRequired[list[dict] | None]
viewed_images: Annotated[dict[str, ViewedImageData], merge_viewed_images] # image_path -> metadata (no base64)
promoted: Annotated[PromotedTools | None, merge_promoted]
delegations: Annotated[list[DelegationEntry], merge_delegations]
skill_context: Annotated[list[SkillEntry], merge_skill_context]
summary_text: NotRequired[str | None]
background_tasks: NotRequired[list[BackgroundTaskState]]ThreadState 中的 artifacts 只是已登记的文件路径列表,uploaded_files 是用户带入的附件,delegations 记录 Sub-agent 委派,summary_text 保存压缩后的上下文。它们共同描述 sales-review 线程的工作进展,但不负责创建数据库连接或事件后端。
这里要区分 thread 与 Run。thread 是连续工作空间,允许多次执行共享消息和工作状态;Run 是其中一次有起止时间的尝试。用户要求修改报告时,通常是在同一个 thread 里再建一条 Run,而不是把上一条 Run 神奇地重新打开。
Gateway 管理 Run,Agent 读写 ThreadState,工具在 Sandbox 中产生文件,present_files 把路径登记为 Artifact。
插叙:业务状态之外,还有本次运行的基础设施
刚才的主线停在 ThreadState:它保存 Agent 正在处理的事实。但 worker 执行一条 Run 时还需要 checkpointer、event_store、thread_store 和应用配置。把这些对象也塞进 ThreadState,会让可持久化业务数据和进程内依赖混在一起。
DeerFlow 用 RunContext 收拢这类基础设施。它是 frozen dataclass,表示一条执行开始时已经确定的运行依赖;它不等于模型会反复读写的图状态。
@dataclass(frozen=True)
class RunContext:
"""Infrastructure dependencies for a single agent run.
Groups checkpointer, store, and persistence-related singletons so that
``run_agent`` (and any future callers) receive one object instead of a
growing list of keyword arguments.
"""
checkpointer: Any
store: Any | None = field(default=None)
event_store: Any | None = field(default=None)
run_events_config: Any | None = field(default=None)
thread_store: Any | None = field(default=None)
mcp_task_repo: Any | None = field(default=None)
app_config: AppConfig | None = field(default=None)
extensions: Any | None = field(default=None)
checkpoint_channel_mode: CheckpointChannelMode = "full"
# Delta snapshot cadence frozen at startup; ``None`` means "not frozen in
# this process" (embedded/tests) and resolves to the config default.
checkpoint_snapshot_frequency: int | None = None
on_run_completed: Any | None = field(default=None)RunContext 中的 checkpointer 保存 LangGraph 检查点,event_store 保存可查询的运行事件,thread_store 读写 thread 资料,app_config 提供本次装配需要的配置。ThreadState 回答“任务做到哪了”,RunContext 回答“这次运行靠哪些后端完成”。
把这两个概念分开,后面会容易很多:Memory 写回用户经验时需要运行身份,StreamBridge 发布事件时需要 run_id,但这些都不应该变成报告正文的一部分。插叙到这里结束,主线回到 Agent 怎样接触文件和外部世界。
Middleware 负责横切规则,不替模型做决定
随着任务变长,很多规则会同时影响模型与工具:调用前补充动态上下文,长对话时摘要,工具失败时转换错误,写文件前要求先读取,执行后捕获 Memory。若把这些规则逐个写进每个工具,主循环会被重复逻辑淹没。
Middleware 是包在 Agent 生命周期和调用边界上的规则层。它可以在模型调用前修改上下文,在工具调用外记录进度和处理异常,也可以在 Agent 完成后安排记忆写入。它改变“这一步怎样执行”,但不负责决定“下一步该搜索还是写报告”;后一个判断仍由模型和状态共同完成。
顺序也有意义。一个外层 Middleware 可以观察内层工具调用的整个过程,错误处理层则需要先把失败变成统一结果。第 02 章走到 agent.astream 内部时,会专门停下来查看实际装配顺序;现在只需记住,它是 Harness 插入工程规则的接缝。
Sandbox 把文件操作关进明确边界
ThreadState 可以记住附件和文件路径,却不能替工具真正读取文件或运行脚本。报告任务仍要读取 sales-notes.pdf、清洗数据,并把最终内容写到 /mnt/user-data/outputs/sales-review.md。直接让工具使用 Gateway 主机的任意文件系统既危险,也无法适配本地、容器或远程执行环境。
Sandbox 提供统一边界。上层工具不必知道底下是本地目录、容器还是远程服务,只通过约定接口执行命令和访问文件。
附件进入 uploads,工具在 workspace 中处理,最终文件写入 outputs;只有登记后才成为页面里的 Artifact。
class Sandbox(ABC):
"""Abstract base class for sandbox environments"""
_id: str
def __init__(self, id: str):
self._id = id
@property
def id(self) -> str:
return self._id
@abstractmethod
def execute_command(
self,
command: str,
env: dict[str, str] | None = None,
timeout: float | None = None,
) -> str:抽象类 Sandbox 暴露 id 和 execute_command 等能力。execute_command 接收 command、env 与 timeout,而具体 provider 决定命令在哪里运行。thread 可以绑定稳定的 sandbox_id,使多条 Run 继续看到同一工作区;安全策略和资源限制也可以留在 provider 层实现。
需要注意,Sandbox 解决的是“代码在哪里运行、文件写到哪里”,不是“用户看到了什么”。sales-review.md 即使已经存在于 outputs,前端也不会因为扫描磁盘就自动把它当成结果。这里还差最后一道显式交付边界。
Artifact 只交付 Agent 明确选中的文件
Sandbox 让 Agent 安全地生成了文件,但“文件已经存在”不等于“这个文件应该交给用户”。工具完成报告后,还要调用 present_files。这个工具校验路径位于允许的输出目录,再把规范化路径写进 ThreadState.artifacts,并返回一条 ToolMessage。页面根据状态里的 Artifact 渲染下载入口,而不是遍历 Sandbox 寻找所有文件。
这种设计看起来多一步,却把“工作文件”和“交付结果”分开了。脚本缓存、临时 CSV、抓取的网页都可以留在工作区;只有 Agent 明确选择的 sales-review.md 才进入交付列表。权限检查、路径规范化和 UI 展示也因此有了共同边界。
# The merge_artifacts reducer will handle merging and deduplication
return Command(
update={
"artifacts": normalized_paths,
"messages": [ToolMessage("Successfully presented files", tool_call_id=tool_call_id)],
},
)Command.update 把 normalized_paths 写入 artifacts,并附上一条 ToolMessage。看到磁盘上有文件但页面没有时,不该先怀疑 React:先确认文件位于 outputs,再确认模型是否调用 present_files,最后查看这次状态更新是否进入图。第 02 章会把它放回完整控制流。
现在可以给 DeerFlow 下一个不绕的定义
DeerFlow 不是另一个大模型,也不只是提示词集合。它是一套 Super Agent Harness:把模型、工具和 Middleware 装配成 Agent,把每次执行登记成 Run,让 Agent 围绕 ThreadState 工作,为工具提供 Sandbox,并用 Artifact 与事件把结果交回产品界面。
它也没有替你抹平所有选择。模型供应商可以替换,工具可以增减,Memory 后端可以换成 OpenViking,Sandbox provider 和 StreamBridge 也有不同实现。Harness 提供稳定接缝,具体部署仍要根据可靠性、隔离和成本决定。
这张地图的重点不在背类名,而在故障时知道问题属于哪一层:模型判断错了,看提示与上下文;工具执行失败,看 Middleware 和 Sandbox;任务查不到,看 RunRecord;文件不能下载,看 present_files 与 Artifact。
三个容易混淆的“为什么不”
为什么不直接调用一次模型?因为报告任务包含多轮工具使用、状态变化和文件交付,一次响应没有这些运行语义。为什么不让 Run 属于 SSE 连接?因为页面会断开,而后台执行、查询和取消必须继续拥有稳定身份。
为什么不把 outputs 里的文件全部当 Artifact?因为工作区里存在中间产物和潜在敏感文件,交付必须是一次显式、可审计的选择。为什么还需要 ThreadState 与 RunContext 两套对象?因为任务事实需要随图演进,基础设施依赖只服务本次执行。
下一章开始不再增加抽象定义。我们会回到同一条教学命令,从前端 thread.submit 开始,依次经过 RunRecord、run_agent、agent.astream、工具循环和 present_files,直到 sales-review.md 出现在页面上。