DeerFlow 架构演进:Gateway 与 LangGraph Server 的合并实践

摘要

DeerFlow 是字节跳动开源的 LangGraph-based AI Agent 平台。在其 v2.0 架构演进中,项目团队做出了一项重要的架构决策------将原本独立运行的 LangGraph Server 进程合并至自研的 FastAPI Gateway 内部,实现了进程内直接调用 LangGraph 图引擎。本文将从架构动机、实现方案、核心组件设计等维度,深入分析这一合并的技术细节。

1. 背景与动机

1.1 原始架构的痛点

在 DeerFlow v1.x 时代,系统采用的是 双进程架构

scss 复制代码
前端 (Next.js) → nginx → LangGraph Server (langgraph-cli 启动, port 2024)
                       → Backend API (FastAPI, port 8001)

该架构存在以下问题:

  1. 运维复杂度高:需要同时管理两个后端进程(LangGraph Server + Gateway),增加部署和监控负担。
  2. 功能受限:langgraph-cli 提供的 HTTP API 是标准化的 LangGraph Platform 协议,无法在其上扩展 DeerFlow 特有的业务逻辑(如 MCP 管理、Skills 系统、IM 频道集成等)。
  3. 数据一致性难题:Run 的生命周期管理、Token 用量统计、用户反馈等应用数据需要在两个进程间同步,增加了系统复杂性。
  4. 配置热加载受限 :LangGraph Server 的配置变更通常需要重启进程,而 Gateway 侧希望支持 config.yaml 的 mtime 级热加载。

1.2 合并目标

DeerFlow 团队的目标是:在保持 LangGraph Platform API 协议兼容的前提下,将 LangGraph 图引擎的执行嵌入 Gateway 进程内部,从而获得:

  • 单进程部署,简化运维
  • 对 Run 生命周期的完全控制
  • 应用数据(持久层)与图执行引擎的同进程访问
  • 自定义中间件链的灵活注入
  • 统一的认证/授权边界

2. 整体架构设计

合并后的架构如下:

scss 复制代码
前端 (Next.js)
    → nginx (反向代理,统一为同源)
        → Gateway (FastAPI, port 8001)
            ├─ LangGraph Platform 兼容 API (/api/threads/*/runs/*)
            ├─ DeerFlow 扩展 API (/api/models, /api/mcp, /api/skills, ...)
            └─ LangGraph 图引擎 (进程内直接调用)

Gateway 成为唯一的后端入口。LangGraph 图不再作为独立服务运行,而是作为 Gateway 内的运行时组件按需实例化。

3. 核心实现

3.1 应用生命周期管理

Gateway 使用 FastAPI 的 lifespan 机制管理所有运行时组件的初始化与销毁:

python 复制代码
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
    startup_config = get_app_config()  # 加载 config.yaml 快照
    
    async with langgraph_runtime(app, startup_config):
        # 所有 LangGraph 运行时单例就绪
        await _ensure_admin_user(app)
        channel_service = await start_channel_service(startup_config)
        yield
        await stop_channel_service()

langgraph_runtime() 是一个异步上下文管理器,负责引导和销毁所有 LangGraph 相关的单例组件:

python 复制代码
@asynccontextmanager
async def langgraph_runtime(app: FastAPI, startup_config: AppConfig):
    async with AsyncExitStack() as stack:
        # StreamBridge:Agent 任务与 SSE 端点之间的事件管道
        app.state.stream_bridge = await stack.enter_async_context(make_stream_bridge(config))
        
        # 持久化引擎(SQLite/Postgres)
        await init_engine_from_config(config.database)
        
        # Checkpointer:LangGraph 图状态的跨 turn 持久化
        app.state.checkpointer = await stack.enter_async_context(make_checkpointer(config))
        
        # Store:LangGraph KV 存储
        app.state.store = await stack.enter_async_context(make_store(config))
        
        # 应用数据仓库
        app.state.run_store = RunRepository(session_factory)
        app.state.feedback_repo = FeedbackRepository(session_factory)
        app.state.thread_store = make_thread_store(session_factory, app.state.store)
        
        # 事件流存储
        app.state.run_event_store = make_run_event_store(run_events_config)
        
        # RunManager:运行注册表 + 并发控制
        app.state.run_manager = RunManager(store=app.state.run_store)
        
        yield

所有单例通过 app.state 暴露,路由层通过依赖注入函数 get_xxx(request) 获取,若依赖未就绪则返回 HTTP 503。

3.2 配置热加载边界

DeerFlow 在配置管理上做了一个精妙的分层设计:

配置类型 加载方式 适用场景
引擎级配置(数据库连接、Checkpointer) 启动时一次性加载 需要重启才能变更
应用级配置(模型列表、工具参数) 每请求 mtime 检测热加载 编辑 config.yaml 立即生效
python 复制代码
def get_config() -> AppConfig:
    """每个请求都通过此函数获取最新配置。
    内部通过 mtime 检测 config.yaml 是否变更,变更时重新加载。"""
    return get_app_config()

AppConfig 刻意不缓存app.state 上,避免出现"分裂脑"问题------即 worker 线程看到过时的启动快照。

3.3 Run 的生命周期管理

RunManager:内存注册表 + 持久化后备

python 复制代码
class RunManager:
    _runs: dict[str, RunRecord]  # 内存中的活跃 Run
    _store: RunStore | None      # SQL 持久化后备
    _lock: asyncio.Lock          # 所有操作加锁

并发控制策略

RunManager.create_or_reject() 方法在单个锁内原子地完成冲突检测与 Run 创建,消除了 TOCTOU 竞态:

python 复制代码
async def create_or_reject(self, thread_id, ...):
    async with self._lock:
        inflight = [r for r in self._runs.values() 
                   if r.thread_id == thread_id 
                   and r.status in (pending, running)]
        
        if multitask_strategy == "reject" and inflight:
            raise ConflictError(...)
        
        if multitask_strategy in ("interrupt", "rollback") and inflight:
            for r in inflight:
                r.abort_event.set()
                r.task.cancel()
        
        # 创建新 Run 并持久化
        self._runs[run_id] = record
        await self._persist_new_run_to_store(record)

该设计支持三种并发策略:

  • reject:同一 thread 已有活跃 Run 时拒绝新请求(返回 409)
  • interrupt:取消旧 Run 后创建新 Run
  • rollback:取消旧 Run 并回滚 checkpoint 后创建新 Run

启动恢复机制

Gateway 重启时,通过 reconcile_orphaned_inflight_runs() 将持久化存储中残留的 pending/running 状态的 Run 标记为 error,避免前端永远显示"加载中":

python 复制代码
recovered_runs = await run_manager.reconcile_orphaned_inflight_runs(
    error="Gateway restarted before this run reached a durable final state.",
    before=now_iso(),
)

3.4 StreamBridge:生产者-消费者桥接

StreamBridge 是实现 SSE 流式响应的关键组件,它解耦了后台 Agent 任务(生产者)与 HTTP 端点(消费者):

python 复制代码
class StreamBridge(ABC):
    async def publish(self, run_id: str, event: str, data: Any) -> None:
        """Agent 任务写入事件"""
    
    async def publish_end(self, run_id: str) -> None:
        """Agent 完成,发送终止信号"""
    
    def subscribe(self, run_id, *, last_event_id=None, heartbeat_interval=15.0):
        """SSE 端点消费事件(支持 Last-Event-ID 断线重连)"""

每个 StreamEvent 携带单调递增的 id 字段,客户端通过 Last-Event-ID 请求头即可实现断线重连,无需重新执行图。

3.5 Agent 图的按需构建

与传统 LangGraph Server 在启动时编译图不同,DeerFlow 的 Gateway 在每次请求时按需创建 CompiledGraph:

python 复制代码
async def run_agent(bridge, run_manager, record, *, ctx, agent_factory, ...):
    # 每次 run 创建一个全新的图实例
    agent = agent_factory(config=runnable_config)
    agent.checkpointer = ctx.checkpointer
    agent.store = ctx.store
    
    async for chunk in agent.astream(graph_input, config=runnable_config, stream_mode=lg_modes):
        await bridge.publish(run_id, sse_event, serialize(chunk))

agent_factory 指向 make_lead_agent(),其内部完成:

  1. 模型解析 :根据请求中的 model_name 从配置的模型列表中选择,回退到默认模型
  2. 工具组装:加载内置工具 + MCP 工具 + Skill 限制的白名单过滤
  3. 中间件链构建:15+ 个中间件按固定顺序注入
  4. 系统提示词生成:基于当前 agent 配置动态渲染
  5. 图编译create_agent(model, tools, middleware, system_prompt, state_schema) 返回 CompiledGraph

按需创建的设计使得每次请求都能反映最新的配置(模型、工具、Skill 状态),无需重启服务。

3.6 Runtime Context 注入

LangGraph 官方 Server 通过内部机制自动为 Tool 注入 ToolRuntime.context。DeerFlow 的 Gateway 需要手动完成这一步:

python 复制代码
runtime_ctx = {
    "thread_id": thread_id,
    "run_id": run_id,
    "app_config": app_config,
    "__run_journal": journal,  # 内部通道:中间件写入审计事件
}

runtime = Runtime(context=runtime_ctx, store=store)
config["configurable"]["__pregel_runtime"] = runtime

Tool 在执行时通过 runtime.context 访问 thread_idapp_config 等上下文信息,从而实现虚拟路径解析、sandbox 初始化等逻辑。

3.7 LangGraph Platform API 协议兼容

为保证前端 @langchain/langgraph-sdkuseStream hook 无需修改即可工作,Gateway 严格遵循 LangGraph Platform 的 SSE 协议:

python 复制代码
def format_sse(event: str, data: Any, *, event_id: str | None = None) -> str:
    """格式化 SSE 帧,字段顺序:event → data → id → 空行"""
    payload = json.dumps(data, default=str, ensure_ascii=False)
    parts = [f"event: {event}", f"data: {payload}"]
    if event_id:
        parts.append(f"id: {event_id}")
    parts.append("")
    parts.append("")
    return "\n".join(parts)

响应头中包含 Content-Location 字段,指向 Run 资源的规范 URL:

python 复制代码
headers={
    "Content-Location": f"/api/threads/{thread_id}/runs/{record.run_id}",
}

useStream hook 通过正则从该 header 中提取 run_id,这与 LangGraph Platform 官方行为完全一致。

4. 取消与回滚机制

DeerFlow 实现了两种取消语义:

4.1 Interrupt(保留当前状态)

python 复制代码
record.abort_event.set()
record.task.cancel()
# Checkpointer 中已保存的中间状态得以保留
# 后续可通过新 Run 恢复执行

4.2 Rollback(回退到运行前状态)

python 复制代码
# 运行前快照 checkpoint
pre_run_checkpoint_id = ...

# 取消后,将 checkpointer 恢复到快照
await _rollback_to_pre_run_checkpoint(
    checkpointer=checkpointer,
    thread_id=thread_id,
    pre_run_checkpoint_id=pre_run_checkpoint_id,
    pre_run_snapshot=pre_run_snapshot,
)

Rollback 机制在 Run 启动前对 checkpoint 做深拷贝快照,取消时通过 checkpointer.aput() 写入一个新的 checkpoint 记录,其内容为运行前的状态。这确保了即使 LangGraph 在执行过程中已经写入了多个中间 checkpoint,rollback 也能正确恢复。

5. 统一持久层的协同

Gateway 进程内同时管理两套数据:

存储 管理者 用途
LangGraph Checkpoint checkpointer 图执行状态(messages、channel_values)
LangGraph Store store 线程 KV 数据
DeerFlow ORM 表 persistence (SQLAlchemy) Run 元数据、Token 统计、用户反馈、事件流

两套数据在同一进程内直接访问,无需跨进程 RPC。例如,run_agentfinally 块中同时:

  • 通过 journal.flush() 将事件写入 RunEventStore
  • 通过 run_manager.update_run_completion() 将 Token 统计写入 RunRow
  • 通过 checkpointer.aget_tuple() 读取 title 并写入 ThreadMetaRow

6. 中间件架构

合并后的一大优势是可以深度控制 Agent 的行为。DeerFlow 在 LangGraph 图内注入了 15+ 个中间件,覆盖以下关注点:

scss 复制代码
请求方向 (before_model):
  ToolErrorHandling → DanglingToolCall → Uploads → ThreadData →
  DynamicContext → Summarization → Todo → TokenUsage → Title →
  Memory → ViewImage → DeferredToolFilter → SubagentLimit →
  LoopDetection → SafetyFinishReason → Clarification

响应方向 (after_model):反序执行

中间件的执行发生在 LLM 调用的前后,无需经过任何网络边界。这是进程内合并带来的天然优势------中间件可以直接访问 app_configRunJournalthread_store 等运行时对象。

7. 总结

DeerFlow 的 Gateway-LangGraph 合并方案展示了一种在保持协议兼容的前提下,将通用 AI Agent 运行时深度集成到业务 Gateway 中的实践路径。其核心思路可概括为:

  1. 进程内嵌入:LangGraph 图引擎不再作为独立服务,而是作为 Gateway 的运行时组件按需实例化。
  2. StreamBridge 解耦:通过内存事件管道桥接后台 Agent 任务与 HTTP SSE 端点,保持协议兼容。
  3. 分层配置管理:引擎级配置绑定启动快照,应用级配置每请求热加载。
  4. 原子并发控制:单锁保护的 RunManager 消除 TOCTOU 竞态。
  5. 按需图构建:每请求创建图实例,反映最新配置,无需重启。

该方案在降低运维复杂度的同时,为 DeerFlow 提供了对 Agent 行为的完全控制能力,为后续的 IM 频道集成、自定义 Agent、Skill 系统等高级功能奠定了基础。


本文基于 DeerFlow v2.0-m1 版本源码分析,项目地址:github.com/bytedance/d...

相关推荐
Artech10 小时前
[MAF预定义的AIContextProvider-01]TextSearchProvider——RAG在MAF中的实现
ai·agent·rag·maf
协享科技10 小时前
AI 视频理解:让 Agent 看视频并总结内容
人工智能·go·音视频·agent·ai编程
奋飛10 小时前
从 Prompt 到 Agent:LangChain 究竟解决了什么问题
ai·langchain·prompt·agent
searchforAI12 小时前
啥是LLM?大语言模型从原理到选型的完整科普
人工智能·科技·深度学习·ai·语言模型·知识图谱·agent
染指111019 小时前
26.RAG进阶(Advanced RAG)-假设性问题索引
人工智能·windows·agent·rag·advanced rag
后端小肥肠21 小时前
小红书笔记爆了 17 万后,我用 Obsidian + Skill 实现了“一句话选品”
人工智能·aigc·agent
SelectDB技术团队1 天前
2026 SelectDB AI 产品发布会:Agent Native 数据基础设施能力全景发布
数据库·人工智能·agent·apache doris·selectdb
米小虾1 天前
Loop Engineering 深度实践指南:9 种 2026 年最新做法与完整代码
人工智能·agent
智海观潮1 天前
OpenClaw生态全景解析 - 9大核心工具赋能 AI 自动化落地
ai·agent·skills·ai 自动化·openclaw
chenjim1 天前
你的 Agent 是个黑箱:eBPF 如何看见它真正在做什么
llm·agent