dce5e9598b
Add 7 ADRs and a phase-0 plan covering the multi-tenant redesign of DeerFlow, plus an architecture-overview snapshot of the current state. ADRs: - 001 data isolation: row-level tenant_id + Postgres RLS, including LangGraph-owned checkpoint tables (subquery RLS or column upgrade path). - 002 sandbox isolation: K8s namespace + gVisor + NetworkPolicy default- deny, threat model and pod spec defaults. - 003 LLM key & billing: hybrid platform/BYO with pessimistic reservation to handle the "ghost token" overflow on the last call, plus a usage category split for memory/title/summarization charges. - 004 RBAC: two-level (owner/admin/member), JWT-with-role + 30s LRU cache for reads, strict DB lookup for sensitive writes, token_version bump as the single revocation path. - 005 storage topology: Postgres (structured) + S3-compatible object store (large objects) + emptyDir (ephemeral); explicit treatment of the extensions_config.json migration's downstream effects. - 006 runtime & channel tenancy: per-tenant MCP cache, dual-track skills loader, sandbox provider routing by namespace, internal LLM call billing, IM channel-to-tenant binding model. - 007 routing & frontend: path-slug URL form, JWT-only API auth, TenantProvider, hard-reload tenant switch, Better Auth integration. These docs are decision records; no code changes are included.
189 lines
13 KiB
Markdown
189 lines
13 KiB
Markdown
# DeerFlow 整体架构鸟瞰
|
||
|
||
按"自外向内、自顶向下"分层讲,并指出每一层对应的代码位置,方便后续深入。
|
||
|
||
## 一、进程与部署拓扑
|
||
|
||
DeerFlow 表面是 4 个端口,本质是 **3 个进程 + 1 个反向代理**。
|
||
|
||
```
|
||
┌──────────────────────┐
|
||
浏览器 / IM ─────────▶│ nginx :2026 │ 统一入口
|
||
│ (含 CORS、SSE 透传) │
|
||
└──────────┬───────────┘
|
||
│
|
||
┌──────────────────┴──────────────────┐
|
||
│ │
|
||
▼ ▼
|
||
┌────────────────────┐ ┌──────────────────────────┐
|
||
│ Frontend (Next.js) │ │ Gateway (uvicorn) │
|
||
│ :3000 │ │ :8001 │
|
||
│ pnpm dev / preview │ │ ┌──────────────────────┐ │
|
||
└────────────────────┘ │ │ FastAPI 路由层 │ │
|
||
│ │ /api/models, /skills │ │
|
||
│ │ /threads, /runs ... │ │
|
||
│ ├──────────────────────┤ │
|
||
│ │ LangGraph Runtime │ │
|
||
│ │ (RunManager, │ │
|
||
│ │ StreamBridge, │ │
|
||
│ │ Checkpointer) │ │
|
||
│ ├──────────────────────┤ │
|
||
│ │ lead_agent 图 │ │
|
||
│ │ + 18 个中间件 │ │
|
||
│ │ + Sandbox / Tools │ │
|
||
│ └──────────────────────┘ │
|
||
└────────────┬─────────────┘
|
||
│
|
||
┌──────────────────────────┼─────────────────────────┐
|
||
▼ ▼ ▼
|
||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||
│ Sandbox │ │ MCP Servers │ │ LLM 提供商 │
|
||
│ Local / AIO Docker│ │ (stdio/sse/http) │ │ OpenAI/Anthropic │
|
||
│ 提供 bash/fs │ │ │ │ /vLLM/Codex CLI │
|
||
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||
```
|
||
|
||
关键事实(容易踩坑):
|
||
|
||
- **LangGraph 运行时不是独立进程**,而是嵌在 Gateway 这同一个 uvicorn 进程里。`scripts/serve.sh:225` 只起了一个后端进程:`uvicorn app.gateway.app:app`。
|
||
- nginx 把 `/api/langgraph/*` 重写成 `/api/*` 再代理到 Gateway(`docker/nginx/nginx.local.conf:49-73`),所以前端用的"标准 LangGraph SDK 协议"和 DeerFlow 自己的 REST 是同一个 8001 端口。
|
||
- 路由协议在 nginx 里特意为 SSE 关闭了缓冲(`proxy_buffering off; X-Accel-Buffering no`),否则流式响应会被吃掉。
|
||
|
||
## 二、后端代码二分:Harness vs App
|
||
|
||
后端最重要的边界,**决定新写代码该放哪里**。
|
||
|
||
```
|
||
backend/
|
||
├── packages/harness/deerflow/ ← 可发布的智能体框架(import: deerflow.*)
|
||
│ ├── agents/ lead_agent + memory + middlewares + ThreadState
|
||
│ ├── runtime/ checkpointer / runs / stream_bridge / events / store
|
||
│ ├── sandbox/ Sandbox 抽象 + local 实现 + 文件/bash 工具
|
||
│ ├── subagents/ 子代理注册表 + 后台执行池
|
||
│ ├── tools/ 内建工具(present_files / ask_clarification / view_image)
|
||
│ ├── mcp/ MultiServerMCPClient + 缓存 + OAuth
|
||
│ ├── skills/ SKILL.md 加载、工具白名单
|
||
│ ├── models/ 模型工厂、vLLM/Codex/Claude 自定义 provider
|
||
│ ├── community/ tavily / jina / firecrawl / aio_sandbox(可选实现)
|
||
│ ├── memory/ 长期记忆(事实抽取、debounce 队列)
|
||
│ ├── persistence/ SQLAlchemy 模型(用户、运行、事件、反馈)
|
||
│ ├── guardrails/ 工具调用前置鉴权(可插拔 provider)
|
||
│ ├── tracing/ LangSmith / Langfuse callback
|
||
│ ├── reflection/ "module:variable" 字符串 → 实例(配置驱动的关键)
|
||
│ ├── uploads/ 上传文件转换 markdown
|
||
│ └── client.py DeerFlowClient(嵌入式 Python 客户端)
|
||
│
|
||
└── app/ ← 应用代码(import: app.*)
|
||
├── gateway/
|
||
│ ├── app.py FastAPI 入口 + lifespan
|
||
│ ├── auth_middleware.py 会话/Token 鉴权
|
||
│ ├── csrf_middleware.py 双重 cookie CSRF
|
||
│ ├── langgraph_auth.py 注入到 langgraph.json 的鉴权钩子
|
||
│ └── routers/ ↓ 下表
|
||
└── channels/ IM(Slack/Telegram/Feishu/DingTalk/微信/企微)
|
||
```
|
||
|
||
**铁律:app 可以 import deerflow,deerflow 不能 import app**(CI 用 `tests/test_harness_boundary.py` 强制)。这意味着 harness 必须自给自足——任何"agent 运行时需要的能力"都要在 harness 里完成抽象,app 层只做 HTTP/IM 适配。
|
||
|
||
## 三、Gateway 路由总览
|
||
|
||
`backend/app/gateway/routers/` 14 个路由文件,分三类职责:
|
||
|
||
| 类别 | 路由 | 干什么 |
|
||
|---|---|---|
|
||
| **配置/资源管理** | `models` `skills` `mcp` `memory` `agents` | 列出/启停 LLM 模型、技能、MCP、自定义 agent |
|
||
| **会话/数据** | `threads` `uploads` `artifacts` `suggestions` | 管理线程、上传文件、产物下载、追问建议 |
|
||
| **运行(核心)** | `thread_runs` `runs` `feedback` `assistants_compat` | 创建运行、SSE 流、消息分页、反馈打分、LangGraph 兼容协议 |
|
||
| **横切** | `auth` `channels` | 用户登录注册、IM 渠道状态 |
|
||
|
||
`assistants_compat.py` 是关键:它把前端用的 LangGraph SDK 协议(`POST /threads/{id}/runs/stream`、`messages-tuple` 流模式等)翻译成 DeerFlow 内部的 `RunManager` 调用——这就是 nginx 那条 `/api/langgraph/*` 重写规则的接收端。
|
||
|
||
## 四、一次对话的完整生命周期
|
||
|
||
把上面所有零件串起来——用户在前端输入一句话,会发生这些事:
|
||
|
||
```
|
||
1. 前端 useThreadStream hook
|
||
└─▶ LangGraph SDK 调用 POST /api/langgraph/threads/{id}/runs/stream
|
||
(stream_mode=["values","messages-tuple","custom"])
|
||
|
||
2. nginx 重写 → /api/threads/{id}/runs/stream → Gateway
|
||
|
||
3. Gateway thread_runs 路由
|
||
├─▶ AuthMiddleware 解析 session → user_id 注入到 user_context(contextvar)
|
||
├─▶ CSRFMiddleware 校验
|
||
└─▶ runtime.RunManager 创建 Run → 落库(runs / run_events 表)
|
||
|
||
4. RunManager 调用 lead_agent 图(langgraph.json: deerflow.agents:make_lead_agent)
|
||
├─▶ 解析 configurable: model_name / thinking_enabled / is_plan_mode / subagent_enabled
|
||
├─▶ create_chat_model() 实例化 LLM(reflection 从 "module:Class" 字符串实例化)
|
||
└─▶ create_agent(model, tools, middlewares, state_schema=ThreadState)
|
||
|
||
5. 18 个中间件按顺序拦截每一轮 model→tool→model:
|
||
ThreadDataMiddleware 创建 .deer-flow/users/{uid}/threads/{tid}/...
|
||
UploadsMiddleware 注入新上传文件
|
||
SandboxMiddleware acquire 沙箱,state.sandbox_id 写入
|
||
DanglingToolCall 修复中断的 tool_call 序列
|
||
LLMErrorHandling LLM 报错降级
|
||
Guardrail 工具调用前鉴权(可选)
|
||
SandboxAudit 记录 bash/fs 操作
|
||
ToolErrorHandling tool 异常 → ToolMessage 不中断
|
||
Summarization token 接近上限时压缩历史
|
||
TodoList plan_mode 才挂
|
||
TokenUsage 累计 token
|
||
Title 首轮后自动起标题
|
||
Memory 队列异步抽取记忆
|
||
ViewImage 视觉模型注入 base64
|
||
DeferredToolFilter 需要时才暴露 tool schema
|
||
SubagentLimit 限制 task 并发到 3
|
||
LoopDetection 检测重复工具循环
|
||
Clarification ask_clarification 触发 interrupt(END)
|
||
|
||
6. Tools 由 get_available_tools() 拼装:
|
||
├─ Sandbox 工具:bash / ls / read_file / write_file / str_replace
|
||
├─ 内建工具:present_files / ask_clarification / view_image / setup_agent
|
||
├─ MCP 工具:从 extensions_config.json 启用的 server 拉取
|
||
├─ Community 工具:tavily / jina / firecrawl / image_search(按 config.yaml)
|
||
└─ task 工具(可选):派遣 subagent
|
||
|
||
7. StreamBridge 把图执行的事件流转换成 SSE:
|
||
- "values" 完整状态快照
|
||
- "messages-tuple" 增量 token / 工具调用 / 工具返回
|
||
- "custom" StreamWriter 自定义事件
|
||
- "end" 收尾,附 token usage
|
||
|
||
8. 前端 LangGraph SDK 接 SSE,按 message id 累加 delta,更新 UI
|
||
|
||
9. 运行结束后,MemoryMiddleware 后台 30s debounce 抽取记忆事实写入
|
||
.deer-flow/users/{uid}/memory.json
|
||
```
|
||
|
||
## 五、状态与持久化的几条线
|
||
|
||
DeerFlow 的状态被有意拆成"快/慢/历史"三层,因为它要同时支持长会话、跨进程恢复、文件级产物:
|
||
|
||
| 状态 | 位置 | 谁写 |
|
||
|---|---|---|
|
||
| **会话状态(messages, todos, artifacts)** | LangGraph checkpointer(内置 SQLite/PG,路径在 `runtime/checkpointer/async_provider.py`) | 每个 step 自动 |
|
||
| **运行元数据/事件流** | `persistence/` 下 SQLAlchemy 模型(`runs`、`run_events`、`feedback`、`threads_meta`) | RunManager + StreamBridge |
|
||
| **每用户每线程文件** | `.deer-flow/users/{uid}/threads/{tid}/user-data/{workspace,uploads,outputs}` | ThreadDataMiddleware + 沙箱工具 |
|
||
| **长期记忆** | `.deer-flow/users/{uid}/memory.json`(可叠加 per-agent) | MemoryMiddleware(异步) |
|
||
| **配置** | `config.yaml`(模型、工具、沙箱、记忆…) + `extensions_config.json`(MCP、技能开关) | `make setup` 或 Gateway PUT |
|
||
|
||
agent 看到的永远是 **虚拟路径** `/mnt/user-data/...` 和 `/mnt/skills/...`,由 `sandbox/tools.py` 的 `replace_virtual_path()` 翻译成上面物理路径。这层抽象让"本地沙箱"和"Docker 沙箱"对 agent 完全透明。
|
||
|
||
## 六、前端架构(一行总结)
|
||
|
||
`frontend/src/core/threads/hooks.ts` 里的 `useThreadStream` / `useSubmitThread` / `useThreads` 是整个前端的"主动脉"——它们包了 LangGraph SDK 单例(`core/api/`),所有 UI 组件订阅 thread 状态做渲染。Server Components 默认,需要交互的才 `"use client"`。`core/` 下其它子目录(artifacts/skills/mcp/memory/settings)都是为这条主动脉提供周边能力。
|
||
|
||
## 七、一图记住"它在做什么"
|
||
|
||
DeerFlow 本质上是一个 **"LangGraph 智能体 + 18 段切面 + 沙箱 + 记忆"** 的组合:
|
||
|
||
- **LangGraph** 提供图执行、checkpoint、stream 协议
|
||
- **18 个中间件** 是 DeerFlow 自己加的"切面层",每个解决一个具体的健壮性/能力问题(错误恢复、上下文压缩、记忆、子代理限流……)
|
||
- **沙箱+技能+MCP+工具** 是 agent 的"手脚"
|
||
- **Gateway + IM Channels + 嵌入式 Client** 是同一个 agent 的三种暴露方式(HTTP/聊天/Python 直调)
|
||
|
||
> 想继续往里钻的话,建议下一步选三个之一:(a)走读 lead_agent + 中间件链,理解 agent 一轮 think/act 的完整代码路径;(b)走读 sandbox + tools,理解虚拟路径和工具拼装;(c)走读 runtime + StreamBridge,理解 SSE 协议怎么映射回 LangGraph SDK。
|