
如果只看 README,你很容易把 DeerFlow 理解成"一个基于 LangGraph 的开源 Agent UI"。
但真正把源码翻一遍后,会发现它的定位更接近一套完整的 Super Agent Harness:
- 前端不是单纯聊天壳子,而是一个带线程、任务、Artifacts、记忆和设置面板的工作区
- 后端不是单个服务,而是
Gateway + Harness + Sandbox + Skills + Memory + Subagents的组合 - Agent 不是一段固定 prompt,而是由模型、工具、状态、中间件链和配置动态拼装出来的运行时
更关键的是,DeerFlow 解决的不是"模型会不会调工具",而是更接近真实生产环境的问题:
- 上下文怎么压缩,才不会越聊越乱
- 旧消息被摘要掉之后,怎么继续沉淀成长期记忆
- 子代理怎么并发、怎么回传中间态、怎么取消
- 工具出错了,系统怎么继续跑,而不是直接崩掉
- 沙箱和文件系统怎么设计,才能既能动手又不失控
总结:
DeerFlow 最值得研究的,不是它功能多,而是它已经长出了一套可以持续演化的 Agent 工程骨架。
一、整体架构:一套 Agent 运行时的系统
从目录结构看,DeerFlow 设计了一个很清楚的分层:
text
deer-flow-main/
├─ frontend/ # Next.js 前端
├─ backend/
│ ├─ app/ # FastAPI Gateway 与 IM 渠道
│ └─ packages/harness/ # deerflow-harness,Agent 运行时核心
├─ skills/ # public/custom 技能
├─ docs/
├─ docker/
└─ scripts/
其中最关键的不是"有前后端",而是后端内部又分成了两层:
backend/app/:应用层,对外暴露 API、管理文件上传、线程清理、skills/memory 配置等能力backend/packages/harness/deerflow/:框架层,负责 Agent、Sandbox、Subagent、Skills、MCP、Memory、Tracing
这意味着 DeerFlow 的核心不只是某个业务应用,而是一套可复用的 Agent Harness。
如果用部署视角理解,可以把它看成下面这张图:
Browser / Workspace UI
Nginx :2026
Next.js :3000
FastAPI Gateway :8001
LangGraph Runtime :2024
config.yaml / extensions_config.json
deerflow-harness
Model Factory
Skills System
Memory Queue / Storage
Sandbox Provider
Subagent Executor
这个拓扑很像现代应用常见的分工:
- 前端负责工作区交互
- Gateway 负责管理类 API
- LangGraph Runtime 负责线程和运行
- Harness 负责真正的 Agent 执行逻辑
二、前端:Agent 不只是聊天框,而是一个可视化工作区
DeerFlow 前端基于:
- Next.js 16
- React 19
- App Router
- TanStack Query
@langchain/langgraph-sdk
这里值得注意的是,它没有试图在浏览器里自己做一套 Agent 状态机,而是围绕 LangGraph SDK 做线程流、状态恢复和事件消费。也就是说:
前端负责"工作区体验",运行时负责"Agent 执行语义"。
2.1 useThreadStream():前端与运行时的关键连接点
useThreadStream() 定义在:
frontend/src/core/threads/hooks.ts:137-530
这段代码几乎把 DeerFlow 前端的设计哲学写明了:
- 用
useStream对接 LangGraph - 处理线程创建
- 监听工具结束事件
- 监听自定义事件,比如
task_running - 做 optimistic messages
- 处理文件上传和提交流程
- 在线程切换时清理本地 pending UI 状态
源码节选:
frontend/src/core/threads/hooks.ts:204-297
ts
const thread = useStream<AgentThreadState>({
client: getAPIClient(isMock),
assistantId: "lead_agent",
threadId: onStreamThreadId,
reconnectOnMount: runMetadataStorageRef.current
? () => runMetadataStorageRef.current!
: false,
fetchStateHistory: { limit: 1 },
onCreated(meta) {
handleStreamStart(meta.thread_id);
setOnStreamThreadId(meta.thread_id);
if (context.agent_name && !isMock) {
void getAPIClient()
.threads.update(meta.thread_id, {
metadata: { agent_name: context.agent_name },
})
.catch(() => ({}));
}
},
onLangChainEvent(event) {
if (event.event === "on_tool_end") {
listeners.current.onToolEnd?.({
name: event.name,
data: event.data,
});
}
},
onCustomEvent(event: unknown) {
if (
typeof event === "object" &&
event !== null &&
"type" in event &&
event.type === "task_running"
) {
const e = event as {
type: "task_running";
task_id: string;
message: AIMessage;
};
updateSubtask({ id: e.task_id, latestMessage: e.message });
return;
}
},
});
这段代码的价值不在于"它能连上 SDK",而在于它清楚地表达了前端职责:
- 线程创建由后端决定,前端只接收真实 thread id
- 工具事件和子任务事件会被翻译成 UI 状态
- 前端不自己发明执行逻辑,而是订阅和呈现运行时状态
2.2 发送消息
源码位置:
frontend/src/core/threads/hooks.ts:326-530
节选如下:
ts
const sendMessage = useCallback(
async (
threadId: string,
message: PromptInputMessage,
extraContext?: Record<string, unknown>,
) => {
if (sendInFlightRef.current) {
return;
}
sendInFlightRef.current = true;
prevMsgCountRef.current = thread.messages.length;
const newOptimistic: Message[] = [];
if (!hideFromUI) {
newOptimistic.push({
type: "human",
id: `opt-human-${Date.now()}`,
content: text ? [{ type: "text", text }] : "",
additional_kwargs: optimisticAdditionalKwargs,
});
}
setOptimisticMessages(newOptimistic);
await thread.submit(
{
messages: [
{
type: "human",
content: [{ type: "text", text }],
additional_kwargs: {
...(filesForSubmit.length > 0 ? { files: filesForSubmit } : {}),
},
},
],
},
{
threadId,
streamSubgraphs: true,
streamResumable: true,
config: { recursion_limit: 1000 },
context: {
...extraContext,
...context,
thinking_enabled: context.mode !== "flash",
is_plan_mode: context.mode === "pro" || context.mode === "ultra",
subagent_enabled: context.mode === "ultra",
thread_id: threadId,
},
},
);
},
[thread, context],
);
这段实现有几个很重要的点:
- optimistic UI 和真实流式消息是分开的
- 文件先上传,再把虚拟路径作为 metadata 一起提交
- 不同模式通过
context映射成运行时配置,比如is_plan_mode、subagent_enabled
这让前端不是一个"样式层",而是真正参与了 Agent 运行上下文的编排。
2.3 Chat 页面本身也说明它是"工作区"而不是"聊天框"
源码位置:
frontend/src/app/workspace/chats/[thread_id]/page.tsx:32-149
节选如下:
tsx
export default function ChatPage() {
const { threadId, setThreadId, isNewThread, setIsNewThread, isMock } =
useThreadChat();
const [settings, setSettings] = useThreadSettings(threadId);
const [thread, sendMessage, isUploading] = useThreadStream({
threadId: isNewThread ? undefined : threadId,
context: settings.context,
isMock,
onStart: (createdThreadId) => {
setThreadId(createdThreadId);
setIsNewThread(false);
history.replaceState(null, "", `/workspace/chats/${createdThreadId}`);
},
});
return (
<ThreadContext.Provider value={{ thread, isMock }}>
<ChatBox threadId={threadId}>
<ThreadTitle threadId={threadId} thread={thread} />
<TokenUsageIndicator messages={thread.messages} />
<ArtifactTrigger />
<MessageList threadId={threadId} thread={thread} />
<TodoList todos={thread.values.todos ?? []} />
<InputBox
threadId={threadId}
context={settings.context}
onContextChange={(context) => setSettings("context", context)}
onSubmit={(message) => void sendMessage(threadId, message)}
/>
</ChatBox>
</ThreadContext.Provider>
);
}
从这里可以看出,DeerFlow 前端的核心对象已经不只是 message list,而是:
- 线程
- 设置上下文
- token usage
- artifacts
- todo
- 输入框状态
也就是说,用户面对的是一个"Agent 工作台",而不是一块聊天区域。
三、后端:Gateway 负责管理面,Harness 负责运行时
DeerFlow 后端最值得讲的,是它没有把所有逻辑堆在一个服务里,而是分成:
backend/app/:应用层backend/packages/harness/deerflow/:运行时框架层
3.1 Gateway:把非推理能力统一收口
入口文件:
backend/app/gateway/app.py
Gateway 暴露了这些能力:
- models
- mcp
- memory
- skills
- uploads
- threads
- artifacts
- agents
- suggestions
- channels
- runs
这种拆分的意义在于:
- 前端对管理类能力有统一 API
- IM 渠道可以复用同样的服务
- Agent Runtime 可以更聚焦于执行逻辑
- 后续更容易调整部署模式
3.2 Harness:DeerFlow 真正的核心价值
agents/sandbox/subagents/skills/mcp/models/runtime/config/guardrails/tracing/
换句话说,DeerFlow 不是在堆一个 Agent 产品,而是在构建一套 Agent Runtime。
四、Lead Agent:不是写死一个 Agent,而是在造一个 Agent 工厂
主 Agent 入口在:
backend/packages/harness/deerflow/agents/lead_agent/agent.py
其中最重要的函数是:
make_lead_agent(config)
源码位置:
backend/packages/harness/deerflow/agents/lead_agent/agent.py:280-356
python
def make_lead_agent(config: RunnableConfig):
from deerflow.tools import get_available_tools
from deerflow.tools.builtins import setup_agent
cfg = config.get("configurable", {})
thinking_enabled = cfg.get("thinking_enabled", True)
reasoning_effort = cfg.get("reasoning_effort", None)
requested_model_name = cfg.get("model_name") or cfg.get("model")
is_plan_mode = cfg.get("is_plan_mode", False)
subagent_enabled = cfg.get("subagent_enabled", False)
max_concurrent_subagents = cfg.get("max_concurrent_subagents", 3)
is_bootstrap = cfg.get("is_bootstrap", False)
agent_name = cfg.get("agent_name")
model_name = _resolve_model_name(requested_model_name or agent_model_name)
if is_bootstrap:
return create_agent(
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled),
tools=get_available_tools(model_name=model_name, subagent_enabled=subagent_enabled) + [setup_agent],
middleware=_build_middlewares(config, model_name=model_name),
system_prompt=apply_prompt_template(
subagent_enabled=subagent_enabled,
max_concurrent_subagents=max_concurrent_subagents,
available_skills=set(["bootstrap"]),
),
state_schema=ThreadState,
)
return create_agent(
model=create_chat_model(
name=model_name,
thinking_enabled=thinking_enabled,
reasoning_effort=reasoning_effort,
),
tools=get_available_tools(
model_name=model_name,
groups=agent_config.tool_groups if agent_config else None,
subagent_enabled=subagent_enabled,
),
middleware=_build_middlewares(config, model_name=model_name, agent_name=agent_name),
system_prompt=apply_prompt_template(
subagent_enabled=subagent_enabled,
max_concurrent_subagents=max_concurrent_subagents,
agent_name=agent_name,
available_skills=set(agent_config.skills) if agent_config and agent_config.skills is not None else None,
),
state_schema=ThreadState,
)
这段代码说明了三件事:
- Agent 不是一个静态对象,而是按运行时参数动态构造出来的
- 模型、工具、状态、中间件和系统 prompt 是并列的一组核心要素
bootstrapagent 和默认 lead agent 用的是同一条运行时主链,只是装配不同
这种写法,比"在一个 prompt 文件里写尽所有逻辑"的可扩展性高很多。
五、中间件链:DeerFlow 最有工程味的部分
如果只挑一个最值得学习的设计,我会选中间件链。
5.1 中间件是如何被组装起来的
源码位置:
backend/packages/harness/deerflow/agents/lead_agent/agent.py:215-277
python
def _build_middlewares(config, model_name, agent_name=None, custom_middlewares=None):
middlewares = build_lead_runtime_middlewares(lazy_init=True)
summarization_middleware = _create_summarization_middleware()
if summarization_middleware is not None:
middlewares.append(summarization_middleware)
is_plan_mode = config.get("configurable", {}).get("is_plan_mode", False)
todo_list_middleware = _create_todo_list_middleware(is_plan_mode)
if todo_list_middleware is not None:
middlewares.append(todo_list_middleware)
if get_app_config().token_usage.enabled:
middlewares.append(TokenUsageMiddleware())
middlewares.append(TitleMiddleware())
middlewares.append(MemoryMiddleware(agent_name=agent_name))
if model_config is not None and model_config.supports_vision:
middlewares.append(ViewImageMiddleware())
if subagent_enabled:
middlewares.append(SubagentLimitMiddleware(max_concurrent=max_concurrent_subagents))
middlewares.append(LoopDetectionMiddleware())
if custom_middlewares:
middlewares.extend(custom_middlewares)
middlewares.append(ClarificationMiddleware())
return middlewares
这段代码最厉害的地方,不是"中间件很多",而是顺序本身有语义:
- 先建线程与沙箱,再跑其他能力
- 先做上下文压缩,再做后续推理
- 标题在前,记忆在后
ClarificationMiddleware被明确要求放最后
这不是简单的功能堆叠,而是运行时执行顺序的设计。
5.2 共享 runtime middleware
源码位置:
backend/packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py:68-130
python
def _build_runtime_middlewares(
*,
include_uploads: bool,
include_dangling_tool_call_patch: bool,
lazy_init: bool = True,
) -> list[AgentMiddleware]:
middlewares: list[AgentMiddleware] = [
ThreadDataMiddleware(lazy_init=lazy_init),
SandboxMiddleware(lazy_init=lazy_init),
]
if include_uploads:
middlewares.insert(1, UploadsMiddleware())
if include_dangling_tool_call_patch:
middlewares.append(DanglingToolCallMiddleware())
middlewares.append(LLMErrorHandlingMiddleware())
if guardrails_config.enabled and guardrails_config.provider:
provider_cls = resolve_variable(guardrails_config.provider.use)
provider = provider_cls(**provider_kwargs)
middlewares.append(
GuardrailMiddleware(provider, fail_closed=guardrails_config.fail_closed, passport=guardrails_config.passport)
)
middlewares.append(SandboxAuditMiddleware())
middlewares.append(ToolErrorHandlingMiddleware())
return middlewares
这段代码的价值在于,它把 lead agent 和 subagent 的公共执行层抽了出来。
这说明 DeerFlow 不是在写"一个 Agent 的特例",而是在做"多个 Agent 可复用的运行时基底"。
5.3 它把失败当作系统主路径处理
源码位置:
backend/packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py:19-65
python
class ToolErrorHandlingMiddleware(AgentMiddleware[AgentState]):
def _build_error_message(self, request: ToolCallRequest, exc: Exception) -> ToolMessage:
tool_name = str(request.tool_call.get("name") or "unknown_tool")
tool_call_id = str(request.tool_call.get("id") or _MISSING_TOOL_CALL_ID)
detail = str(exc).strip() or exc.__class__.__name__
content = (
f"Error: Tool '{tool_name}' failed with {exc.__class__.__name__}: "
f"{detail}. Continue with available context, or choose an alternative tool."
)
return ToolMessage(
content=content,
tool_call_id=tool_call_id,
name=tool_name,
status="error",
)
async def awrap_tool_call(...):
try:
return await handler(request)
except GraphBubbleUp:
raise
except Exception as exc:
return self._build_error_message(request, exc)
这个设计非常重要。
它不是"工具一报错就让 run 整体失败",而是把错误转成 ToolMessage,让模型继续在错误上下文里推理。
在真实 Agent 系统里,这种"失败可恢复"能力,远比再加几个工具更有价值。
六、上下文工程:DeerFlow 真正有技术含量的地方
DeerFlow 对上下文问题不是单点处理,而是分成三层:
- 短期:线程内状态
- 中期:摘要压缩
- 长期:结构化用户记忆
6.1 短期状态:ThreadState
源码位置:
backend/packages/harness/deerflow/agents/thread_state.py:21-55
python
def merge_artifacts(existing: list[str] | None, new: list[str] | None) -> list[str]:
if existing is None:
return new or []
if new is None:
return existing
return list(dict.fromkeys(existing + new))
def merge_viewed_images(existing: dict[str, ViewedImageData] | None, new: dict[str, ViewedImageData] | None) -> dict[str, ViewedImageData]:
if existing is None:
return new or {}
if new is None:
return existing
if len(new) == 0:
return {}
return {**existing, **new}
class ThreadState(AgentState):
sandbox: NotRequired[SandboxState | None]
thread_data: NotRequired[ThreadDataState | None]
title: NotRequired[str | None]
artifacts: Annotated[list[str], merge_artifacts]
todos: NotRequired[list | None]
uploaded_files: NotRequired[list[dict] | None]
viewed_images: Annotated[dict[str, ViewedImageData], merge_viewed_images]
这里值得讲的不只是字段,而是 reducer 的设计:
merge_artifacts会去重并保持顺序merge_viewed_images支持合并,也支持通过空字典清空
这说明 ThreadState 不是随手扩展的字段堆,而是认真考虑过状态演化规则的运行时容器。
6.2 长期记忆入口:MemoryMiddleware
源码位置:
backend/packages/harness/deerflow/agents/middlewares/memory_middleware.py:24-98
python
class MemoryMiddleware(AgentMiddleware[MemoryMiddlewareState]):
@override
def after_agent(self, state: MemoryMiddlewareState, runtime: Runtime) -> dict | None:
config = get_memory_config()
if not config.enabled:
return None
thread_id = runtime.context.get("thread_id") if runtime.context else None
if thread_id is None:
config_data = get_config()
thread_id = config_data.get("configurable", {}).get("thread_id")
if not thread_id:
return None
messages = state.get("messages", [])
filtered_messages = filter_messages_for_memory(messages)
user_messages = [m for m in filtered_messages if getattr(m, "type", None) == "human"]
assistant_messages = [m for m in filtered_messages if getattr(m, "type", None) == "ai"]
if not user_messages or not assistant_messages:
return None
correction_detected = detect_correction(filtered_messages)
reinforcement_detected = not correction_detected and detect_reinforcement(filtered_messages)
queue = get_memory_queue()
queue.add(
thread_id=thread_id,
messages=filtered_messages,
agent_name=self._agent_name,
correction_detected=correction_detected,
reinforcement_detected=reinforcement_detected,
)
这段代码体现了 DeerFlow 的一个重要判断:
长期记忆不是原样存所有消息,而是只保留"用户输入 + 最终 AI 响应"的有效部分。
这能大幅减少工具输出、系统噪声、半成品步骤对长期记忆的污染。
6.3 最值得单独写的一段:memory_flush_hook
源码位置:
backend/packages/harness/deerflow/agents/memory/summarization_hook.py:11-31
python
def memory_flush_hook(event: SummarizationEvent) -> None:
if not get_memory_config().enabled or not event.thread_id:
return
filtered_messages = filter_messages_for_memory(list(event.messages_to_summarize))
user_messages = [message for message in filtered_messages if getattr(message, "type", None) == "human"]
assistant_messages = [message for message in filtered_messages if getattr(message, "type", None) == "ai"]
if not user_messages or not assistant_messages:
return
correction_detected = detect_correction(filtered_messages)
reinforcement_detected = not correction_detected and detect_reinforcement(filtered_messages)
queue = get_memory_queue()
queue.add_nowait(
thread_id=event.thread_id,
messages=filtered_messages,
agent_name=event.agent_name,
correction_detected=correction_detected,
reinforcement_detected=reinforcement_detected,
)
这段代码是全项目最值得点名的工程亮点之一。
因为它解决了一个很现实的问题:
- 对话太长,必须摘要
- 但摘要掉的旧消息,又可能包含未来还需要记住的信息
DeerFlow 的答案是:
在消息被摘要删掉之前,先把有价值的部分送进长期记忆队列。
这就把"中期压缩"和"长期记忆"连接起来了。
很多 Agent 项目只做到摘要,但没处理"摘要以后还剩下什么"。
6.4 记忆不是同步写入,而是异步去抖队列
源码位置:
backend/packages/harness/deerflow/agents/memory/queue.py:27-170
python
class MemoryUpdateQueue:
def __init__(self):
self._queue: list[ConversationContext] = []
self._lock = threading.Lock()
self._timer: threading.Timer | None = None
self._processing = False
def add(...):
with self._lock:
self._enqueue_locked(...)
self._reset_timer()
def _enqueue_locked(...):
existing_context = next(
(context for context in self._queue if context.thread_id == thread_id),
None,
)
context = ConversationContext(...)
self._queue = [c for c in self._queue if c.thread_id != thread_id]
self._queue.append(context)
def _process_queue(self) -> None:
with self._lock:
if self._processing or not self._queue:
return
self._processing = True
contexts_to_process = self._queue.copy()
self._queue.clear()
updater = MemoryUpdater()
for context in contexts_to_process:
updater.update_memory(...)
这段实现的好处非常明确:
- 同一线程的记忆更新可以合并
- 高频对话不会每轮都立刻写 memory
- 记忆更新不会阻塞主对话链路
这就是典型的"Agent 工程化细节":不是功能更炫,而是系统更稳。
七、Sandbox:Agent 真正"能动手"的前提
DeerFlow 的沙箱能力不只是"给模型一个 bash 工具",而是做了完整的路径语义和安全校验。
7.1 本地读路径会先做校验和映射
源码位置:
backend/packages/harness/deerflow/sandbox/tools.py:330-336
python
def _resolve_local_read_path(path: str, thread_data: ThreadDataState) -> str:
validate_local_tool_path(path, thread_data, read_only=True)
if _is_skills_path(path):
return _resolve_skills_path(path)
if _is_acp_workspace_path(path):
return _resolve_acp_workspace_path(path, _extract_thread_id_from_thread_data(thread_data))
return _resolve_and_validate_user_data_path(path, thread_data)
这段代码虽然短,但非常能体现 DeerFlow 的思路:
- 不是直接用宿主机路径
- 先校验路径是否合法
- 再按不同语义映射到 skills、ACP workspace 或 user-data
也就是说,Agent 面向的是一个"受控虚拟文件系统",不是宿主机文件系统裸奔。
7.2 bash 工具也不是直接放开
源码位置:
backend/packages/harness/deerflow/sandbox/tools.py:989-1035
python
@tool("bash", parse_docstring=True)
def bash_tool(runtime: ToolRuntime[ContextT, ThreadState], description: str, command: str) -> str:
try:
sandbox = ensure_sandbox_initialized(runtime)
if is_local_sandbox(runtime):
if not is_host_bash_allowed():
return f"Error: {LOCAL_HOST_BASH_DISABLED_MESSAGE}"
ensure_thread_directories_exist(runtime)
thread_data = get_thread_data(runtime)
validate_local_bash_command_paths(command, thread_data)
command = replace_virtual_paths_in_command(command, thread_data)
command = _apply_cwd_prefix(command, thread_data)
output = sandbox.execute_command(command)
return _truncate_bash_output(mask_local_paths_in_output(output, thread_data), max_chars)
return _truncate_bash_output(sandbox.execute_command(command), max_chars)
except SandboxError as e:
return f"Error: {e}"
这里能看到 DeerFlow 对 shell 执行的态度:
- host bash 默认不是无条件开放
- 命令里的路径会被校验和替换
- 输出还会做截断和路径掩码
这不是"能跑就行"的 Agent 工具层,而是明显往可控运行时靠拢。
7.3 read_file / write_file / str_replace 也是受控的
源码位置:
backend/packages/harness/deerflow/sandbox/tools.py:1202-1325
节选如下:
python
@tool("read_file", parse_docstring=True)
def read_file_tool(runtime, description, path, start_line=None, end_line=None) -> str:
sandbox = ensure_sandbox_initialized(runtime)
ensure_thread_directories_exist(runtime)
if is_local_sandbox(runtime):
thread_data = get_thread_data(runtime)
validate_local_tool_path(path, thread_data, read_only=True)
if _is_skills_path(path):
path = _resolve_skills_path(path)
elif _is_acp_workspace_path(path):
path = _resolve_acp_workspace_path(path, _extract_thread_id_from_thread_data(thread_data))
elif not _is_custom_mount_path(path):
path = _resolve_and_validate_user_data_path(path, thread_data)
content = sandbox.read_file(path)
@tool("write_file", parse_docstring=True)
def write_file_tool(runtime, description, path, content, append=False) -> str:
sandbox = ensure_sandbox_initialized(runtime)
ensure_thread_directories_exist(runtime)
if is_local_sandbox(runtime):
thread_data = get_thread_data(runtime)
validate_local_tool_path(path, thread_data)
if not _is_custom_mount_path(path):
path = _resolve_and_validate_user_data_path(path, thread_data)
with get_file_operation_lock(sandbox, path):
sandbox.write_file(path, content, append)
return "OK"
读写文件之所以能成为"平台能力",就在于这些边界都被提前处理过了。
八、Subagent:它不是概念,而是真能工作的执行系统
DeerFlow 的子代理不是"多套 prompt 套娃",而是完整的后台执行机制。
8.1 task_tool.py:子代理的入口
源码位置:
backend/packages/harness/deerflow/tools/builtins/task_tool.py:22-210
python
@tool("task", parse_docstring=True)
async def task_tool(
runtime: ToolRuntime[ContextT, ThreadState],
description: str,
prompt: str,
subagent_type: str,
tool_call_id: Annotated[str, InjectedToolCallId],
max_turns: int | None = None,
) -> str:
config = get_subagent_config(subagent_type)
if config is None:
return f"Error: Unknown subagent type '{subagent_type}'."
skills_section = get_skills_prompt_section()
if skills_section:
config = replace(config, system_prompt=config.system_prompt + "\n\n" + skills_section)
sandbox_state = runtime.state.get("sandbox")
thread_data = runtime.state.get("thread_data")
thread_id = runtime.context.get("thread_id") if runtime.context else None
tools = get_available_tools(model_name=parent_model, subagent_enabled=False)
executor = SubagentExecutor(
config=config,
tools=tools,
parent_model=parent_model,
sandbox_state=sandbox_state,
thread_data=thread_data,
thread_id=thread_id,
trace_id=trace_id,
)
task_id = executor.execute_async(prompt, task_id=tool_call_id)
writer = get_stream_writer()
writer({"type": "task_started", "task_id": task_id, "description": description})
while True:
result = get_background_task_result(task_id)
if result.status == SubagentStatus.COMPLETED:
writer({"type": "task_completed", "task_id": task_id, "result": result.result})
return f"Task Succeeded. Result: {result.result}"
这段代码说明,DeerFlow 的子代理具备几个关键特征:
- 子代理会继承父级必要上下文
- 子代理不会继续递归拿到
task工具,避免无限套娃 - 子代理的状态会通过事件流回到前端
- 子代理的运行不是同步阻塞,而是后台执行 + 状态轮询
8.2 SubagentExecutor:真正的运行器
源码位置:
backend/packages/harness/deerflow/subagents/executor.py:128-248
python
class SubagentExecutor:
def __init__(self, config, tools, parent_model=None, sandbox_state=None, thread_data=None, thread_id=None, trace_id=None):
self.config = config
self.parent_model = parent_model
self.sandbox_state = sandbox_state
self.thread_data = thread_data
self.thread_id = thread_id
self.trace_id = trace_id or str(uuid.uuid4())[:8]
self.tools = _filter_tools(tools, config.tools, config.disallowed_tools)
def _create_agent(self):
model_name = _get_model_name(self.config, self.parent_model)
model = create_chat_model(name=model_name, thinking_enabled=False)
middlewares = build_subagent_runtime_middlewares(lazy_init=True)
return create_agent(
model=model,
tools=self.tools,
middleware=middlewares,
system_prompt=self.config.system_prompt,
state_schema=ThreadState,
)
def _build_initial_state(self, task: str) -> dict[str, Any]:
state = {"messages": [HumanMessage(content=task)]}
if self.sandbox_state is not None:
state["sandbox"] = self.sandbox_state
if self.thread_data is not None:
state["thread_data"] = self.thread_data
return state
这个实现有两个特别好的地方:
- 子代理不是"另一个 prompt call",而是完整 agent
- 子代理直接复用了共享 runtime middleware 和
ThreadState
这使得 DeerFlow 的多智能体能力不是概念演示,而是框架级能力。
九、几个最值得放进博客的技术亮点
如果你打算把这篇文档进一步压成一篇更短的技术博客,我建议重点保留下面几组代码:
1. make_lead_agent() 的工厂式装配
位置:
backend/packages/harness/deerflow/agents/lead_agent/agent.py:280-356
亮点:
- 动态模型选择
- 动态工具组合
- 动态中间件链
ThreadState统一状态容器
2. 中间件链与共享 runtime builder
位置:
backend/packages/harness/deerflow/agents/lead_agent/agent.py:215-277backend/packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py:68-130
亮点:
- 顺序语义明确
- lead/subagent 共用一套底层 runtime 构造逻辑
3. ToolErrorHandlingMiddleware
位置:
backend/packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py:19-65
亮点:
- 把工具异常转成可恢复的
ToolMessage
4. ThreadState + memory_flush_hook + MemoryUpdateQueue
位置:
backend/packages/harness/deerflow/agents/thread_state.py:21-55backend/packages/harness/deerflow/agents/memory/summarization_hook.py:11-31backend/packages/harness/deerflow/agents/memory/queue.py:27-170
亮点:
- 短期状态、中期摘要、长期记忆被有机地连在了一起
5. task_tool.py + SubagentExecutor
位置:
backend/packages/harness/deerflow/tools/builtins/task_tool.py:22-210backend/packages/harness/deerflow/subagents/executor.py:128-248
亮点:
- 子代理异步执行
- 状态事件回流
- 父上下文继承
- 防止递归嵌套
6. useThreadStream()
位置:
frontend/src/core/threads/hooks.ts:204-530
亮点:
- SDK 对接、事件消费、乐观更新、上下文提交都在一条链里
十、横向比较:DeerFlow 和其他同类产品差在哪
如果把 DeerFlow 放在几类常被一起讨论的系统旁边看,它的定位会更清楚。
10.1 DeerFlow vs LangGraph Platform
LangGraph Platform 更强调:
- threads
- runs
- durable execution
- persistence
- streaming
- human-in-the-loop
这些能力更接近基础设施平台。
DeerFlow 则在其上继续补了:
- 工作区 UI
- skills
- sandbox
- 结构化长期记忆
- 子代理运行系统
- artifacts / uploads / settings 管理
可以简单理解成:
- LangGraph Platform 更像 Agent 基础设施
- DeerFlow 更像建立在基础设施之上的开源 Agent Runtime 系统
10.2 DeerFlow vs OpenHands
OpenHands 的重心更偏向开发代理体验:
- 代码编辑
- 终端
- 浏览器
- Docker runtime
- 面向仓库开发任务
DeerFlow 与它的差异在于:
- 更强调 runtime 抽象
- 更强调中间件和上下文工程
- 长期记忆是一等公民
- skills、memory、sandbox、subagent 的框架感更强
如果想要"直接拿来做开发代理",OpenHands 很自然。
如果想要"研究并改造一个 Agent 底座",DeerFlow 更有延展性。
10.3 DeerFlow vs Dify
Dify 更像 AI 应用平台,核心在:
- workflow / chatflow
- knowledge base
- 低代码编排
- 企业场景应用
DeerFlow 的重心则在:
- 运行时治理
- 子代理机制
- 沙箱与工具层
- 长对话上下文工程
- 结构化长期记忆
一句话概括:
- Dify 更适合业务应用交付
- DeerFlow 更适合构建长期协作型 Agent 系统
10.4 DeerFlow vs AutoGen Studio
AutoGen Studio 官方定位更偏原型工具:
- low-code
- multi-agent team 原型
- playground / gallery
- 强调快速试验
DeerFlow 则明显更偏工程系统:
- 状态
- 运行时
- 沙箱
- 管理 API
- 失败恢复
可以说:
- AutoGen Studio 擅长把想法快速搭出来
- DeerFlow 擅长把系统跑起来并长期维护下去
十一、最终评价:为什么 DeerFlow 值得认真看
如果只把 DeerFlow 当成"又一个开源 Agent 项目",其实低估它了。
它真正值得研究的,是它在这些关键问题上表现出了难得的边界感:
- 前端不自己接管 runtime,而是围绕 runtime 做工作区
- Gateway 不和 Harness 混成一团
- 上下文压缩和长期记忆不是互相打架,而是用 hook 连起来
- 工具不是裸奔,而是带路径语义和安全约束
- 子代理不是概念,而是有 executor、有事件流、有取消机制
- 失败不是异常边缘,而是系统主路径的一部分
这些设计共同说明了一件事:
DeerFlow 已经不只是一个 Demo,而是一套开始具备平台气质的 Agent 系统。
如果你正在寻找一个值得精读源码、适合二次开发、又足够有工程味的 Agent 项目,DeerFlow 很值得放进名单。
本文只是让你产生兴趣去看源码,我一直以为,光靠看是做不到学习的,正如老夫子言:"学而不思则罔,思而不学则殆"。只有源码才能带给你震撼,而且相较于以往,现在在AI的加持下,读懂源码也并不是什么难事,这篇文章就是使用AI总结优化的,目的是为了让大家学会设计的思想,而不是只是会写代码(虽然现在古法编程已经大部分被AI取代了),行文至此,祝大家都有所收获。