Read OSS

Dify 架构全览:探索一个拥有 6,000 个文件的 LLM 平台

中级

前置知识

  • 具备 Flask 和 Python Web 应用的基础知识
  • 了解 Docker Compose 的基本用法
  • 对 LLM 应用的基本概念有所了解

Dify 架构全览:探索一个拥有 6,000 个文件的 LLM 平台

Dify 是一个开源的 LLM 应用构建平台,支持通过可视化界面和完整的 API 来搭建聊天机器人、RAG 流水线、多步骤工作流以及 Agent 工具。整个代码库横跨 Python 和 TypeScript,文件总数超过 6,000 个,初次接触时难免感到无从下手。本文将帮你建立全局视角,带你了解 monorepo 的目录结构、部署拓扑、启动流程、路由组织方式以及配置系统。

仓库结构与领域驱动的目录设计

Dify 的 monorepo 由三个顶层目录构成,每个目录都有独立的 AGENTS.md 文件,用于记录相应的开发规范:

目录 语言 用途 大致文件数
api/ Python (Flask) 后端 API 服务与 Celery Worker 约 650 个核心文件
web/ TypeScript (Next.js) 控制台前端与可嵌入的聊天 UI 约 5,100 个文件
docker/ YAML / Shell Docker Compose 编排配置与 SSRF 代理设置 约 20 个文件

项目根目录的 AGENTS.md 明确阐述了设计理念:后端遵循领域驱动设计(DDD)和整洁架构原则。异步任务通过 Celery 处理,以 Redis 作为消息队列。依赖通过构造函数注入,异常处理则在对应层级使用领域专属的异常类型。

api/ 目录的结构清晰地反映了 DDD 的边界划分:

api/
├── controllers/     # HTTP 层:Blueprint、请求校验
├── services/        # 应用服务层:业务编排逻辑
├── core/            # 领域核心:工作流引擎、RAG、模型管理
│   ├── app/         # 应用执行流水线
│   ├── rag/         # 检索增强生成
│   ├── workflow/    # 工作流引擎与节点工厂
│   ├── tools/       # 工具抽象与运行时
│   └── plugin/      # Plugin Daemon 集成
├── models/          # SQLAlchemy ORM 模型
├── configs/         # Pydantic 配置系统
├── extensions/      # Flask 扩展初始化器
└── tasks/           # Celery 任务定义

提示: 第一次探索 Dify 代码库时,建议按 api/app.pyapi/app_factory.pyapi/extensions/ 的顺序阅读。这条启动链路可以让你清楚地看到每个子系统是如何被串联在一起的。

Docker Compose 服务拓扑

生产环境中的 Dify 部署由七个主要服务组成,通过 Docker Compose 进行编排。整体架构以 API 服务为中心,呈轮辐状分布。

flowchart TD
    subgraph External
        Client[Browser / API Client]
    end

    subgraph Docker Network
        Nginx[nginx reverse proxy]
        API[api - Gunicorn/Flask]
        Worker[worker - Celery]
        Beat[worker_beat - Celery Beat]
        Web[web - Next.js]
        PluginDaemon[plugin_daemon]
        Sandbox[sandbox - Code Execution]
        SSRFProxy[ssrf_proxy - Squid]

        subgraph Data Stores
            Postgres[(PostgreSQL)]
            Redis[(Redis)]
        end
    end

    Client --> Nginx
    Nginx --> API
    Nginx --> Web
    API --> Postgres
    API --> Redis
    API --> PluginDaemon
    API --> SSRFProxy
    Worker --> Postgres
    Worker --> Redis
    Beat --> Redis
    Sandbox --> SSRFProxy
    PluginDaemon --> Postgres
    PluginDaemon -->|backwards invocation| API

docker/docker-compose.yaml 定义了上述服务拓扑。apiworker 两个服务使用的是同一个 Docker 镜像langgenius/dify-api),通过环境变量 MODE 来区分角色——api 表示 HTTP 服务,worker 表示 Celery Worker。

SSRF 代理(Squid)是一道刻意设置的安全边界。当工作流节点发起出站 HTTP 请求时——例如抓取 URL、调用外部 API、触发 Webhook——这些请求都会经由该代理路由。这样可以有效防止服务端请求伪造(SSRF)攻击,避免恶意工作流探测内部网络服务。沙箱代码执行环境同样做了隔离处理,仅允许连接到专属网络中的 ssrf_proxy

Plugin Daemon 是一个独立的 Go 服务,负责在隔离进程中运行不受信任的插件代码。它通过"反向调用"(backwards invocation)通道与 Dify API 通信——这是一个使用 INNER_API_KEY_FOR_PLUGIN 进行认证的内部 API。我们将在第 5 部分深入探讨这一架构。

双进程架构:API 服务与 Celery Worker

Dify 的后端基于同一套代码,以两种不同的进程形态运行。入口文件 api/app.py 同时承担两种启动路径的职责:

flowchart TD
    Start[app.py] --> IsDB{is_db_command?}
    IsDB -->|Yes| MigApp[create_migrations_app<br/>minimal Flask + DB + Migrate]
    IsDB -->|No| FullApp[create_app<br/>full Flask + 28 extensions]
    FullApp --> FlaskApp[app = DifyApp]
    FullApp --> CeleryApp[celery = app.extensions.celery]
    FlaskApp --> Gunicorn[Gunicorn serves HTTP]
    CeleryApp --> CeleryWorker[Celery processes tasks]

这个文件出奇地简洁。当进程作为 Gunicorn Worker 运行时,app 就是 Flask 的 WSGI 应用;当它作为 Celery Worker 运行时,同样的 app 会被创建,然后从 Flask 的扩展注册表中取出 celery 对象。这种共享代码库的模式意味着两个进程能以完全相同的方式访问模型、服务和配置。

gevent monkey-patching 的策略在两条启动路径中都经过了精心设计。Gunicorn 的 gunicorn.conf.py 使用了 post_patch 事件订阅器,该事件在 gevent 完成内建模块的 patch 之后才触发。这个时序至关重要——如果在 gevent patch 标准库之前就对 gRPC 或 psycopg2 进行 patch,将会导致死锁:

def post_patch(event):
    if not isinstance(event, gevent_events.GeventDidPatchBuiltinModulesEvent):
        return
    grpc_gevent.init_gevent()
    pscycogreen_gevent.patch_psycopg()

api/celery_entrypoint.py 则采用了更简单的方式——在模块导入时立即执行 patch,因为 Celery 的 gevent 池会在该模块加载之前就已完成 monkey-patching。

提示: 如果你在开发过程中遇到莫名的挂起或死锁问题,首先检查 gevent 的 patch 顺序是否正确。gunicorn.conf.py 中引用的 issue #26689 注释记录了一个真实发生过的案例。

启动流程:create_app() 与 28 个扩展

启动流程的核心是 app_factory.pycreate_app() 函数在创建并配置 Flask 应用之后,会按序初始化恰好 28 个扩展。

这个顺序并非随意排列,而是依赖关系链的体现:

flowchart LR
    subgraph Phase 1: Foundation
        TZ[ext_timezone]
        LOG[ext_logging]
        WARN[ext_warnings]
        IMP[ext_import_modules]
    end
    subgraph Phase 2: Core Infrastructure
        DB[ext_database]
        REDIS[ext_redis]
        STOR[ext_storage]
        CEL[ext_celery]
    end
    subgraph Phase 3: Application Layer
        BP[ext_blueprints]
        CMD[ext_commands]
        OTEL[ext_otel]
        SESS[ext_session_factory]
    end
    TZ --> LOG --> WARN --> IMP --> DB --> REDIS --> STOR --> CEL --> BP --> CMD --> OTEL --> SESS

每个扩展模块遵循统一的约定:暴露一个 init_app(app) 函数,并可选地提供一个 is_enabled() 守卫函数。app_factory.py#L171-L213 中的初始化循环会在调用 init_app() 前先检查 is_enabled(),并在 debug 模式下记录每个扩展的初始化耗时:

for ext in extensions:
    short_name = ext.__name__.split(".")[-1]
    is_enabled = ext.is_enabled() if hasattr(ext, "is_enabled") else True
    if not is_enabled:
        if dify_config.DEBUG:
            logger.info("Skipped %s", short_name)
        continue
    start_time = time.perf_counter()
    ext.init_app(app)

几个值得注意的顺序约束包括:

  • ext_databaseext_migrate 之前 — 数据库迁移依赖数据库连接
  • ext_storageext_logstore 之前 — 日志存储依赖底层存储后端
  • ext_logstoreext_celery 之前 — Celery 任务在初始化阶段可能需要访问日志
  • ext_blueprints 靠近末尾 — 路由注册几乎依赖于所有其他组件

此外,还有一个独立的 create_migrations_app(),仅初始化 ext_databaseext_migrate,专门用于执行 flask db 命令。

Blueprint 注册与路由分组

Dify 的 HTTP API 按功能划分为七个 Blueprint 组,在 ext_blueprints.py 中完成注册:

Blueprint URL 前缀 CORS 策略 用途
console_app_bp /console/api 限定来源,携带凭据 管理控制台 API
service_api_bp /v1 开放 Header,使用 Authorization 外部开发者 API
web_bp /api 限定来源,按路由配置 Web 应用(面向终端用户)
files_bp /files 开放,携带 CSRF Token 文件上传/下载
inner_api_bp /inner/api 无(仅限内部) Plugin Daemon 回调
mcp_bp /mcp Model Context Protocol 端点
trigger_bp /trigger 开放,携带 Webhook Header Webhook 触发端点

CORS 配置通过辅助函数 _apply_cors_once() 分层处理,以防止在测试实例中重复注册 Blueprint 时出现重复配置。web_bp 的 CORS 设置最为复杂——嵌入式聊天机器人端点(/chat-messages)采用宽松的跨域策略且不携带凭据,而需要认证的端点则要求更严格的来源校验。

flowchart TD
    Request[HTTP Request] --> PathMatch{Path prefix?}
    PathMatch -->|/console/api| Console[Console Blueprint<br/>Cookie auth + CSRF]
    PathMatch -->|/v1| ServiceAPI[Service API Blueprint<br/>Bearer token auth]
    PathMatch -->|/api| WebApp[Web Blueprint<br/>Passport token auth]
    PathMatch -->|/files| Files[Files Blueprint<br/>Signed URL auth]
    PathMatch -->|/inner/api| Inner[Inner API Blueprint<br/>API key auth]
    PathMatch -->|/mcp| MCP[MCP Blueprint]
    PathMatch -->|/trigger| Trigger[Trigger Blueprint<br/>Webhook auth]

配置系统:基于多重继承的 Pydantic 配置与远程配置源

Dify 的配置系统基于 Pydantic 的 BaseSettings,通过多重继承将九个配置分组组合成一个统一的 DifyConfig 类:

classDiagram
    class DifyConfig {
        +model_config: SettingsConfigDict
        +settings_customise_sources()
    }
    class PackagingInfo
    class DeploymentConfig
    class FeatureConfig
    class MiddlewareConfig
    class ExtraServiceConfig
    class ObservabilityConfig
    class RemoteSettingsSourceConfig
    class EnterpriseFeatureConfig
    class EnterpriseTelemetryConfig

    DifyConfig --|> PackagingInfo
    DifyConfig --|> DeploymentConfig
    DifyConfig --|> FeatureConfig
    DifyConfig --|> MiddlewareConfig
    DifyConfig --|> ExtraServiceConfig
    DifyConfig --|> ObservabilityConfig
    DifyConfig --|> RemoteSettingsSourceConfig
    DifyConfig --|> EnterpriseFeatureConfig
    DifyConfig --|> EnterpriseTelemetryConfig

配置的解析优先级由 settings_customise_sources() 类方法定义,共分六层,从高到低依次为:

  1. Init settings — 程序化覆盖
  2. 环境变量os.environ
  3. 远程配置 — Apollo 或 Nacos 配置中心(通过 RemoteSettingsSourceFactory
  4. Dotenv 文件.env
  5. 文件密钥 — Docker Secrets
  6. TOML 文件pyproject.toml 默认值

RemoteSettingsSourceFactory 的设计颇为巧妙——它通过 match 语句,根据 REMOTE_SETTINGS_SOURCE_NAME 的值分发到 ApolloSettingsSourceNacosSettingsSource。这使得企业团队可以将配置集中管理在配置中心,同时保持代码库的统一。

提示:MiddlewareConfig 这一个分组就包含了 30 多种向量数据库、多种缓存后端和存储提供商的配置项。排查配置缺失问题时,记得在各父类配置中逐一查找——它们分散在 api/configs/middleware/api/configs/feature/ 等多个目录下。

下一步

有了这份架构全览作为基础,我们已经准备好去追踪一个真实请求进入系统后的完整旅程。在第 2 部分,我们将从 controller 层出发,跟随一个 HTTP 请求穿越 AppGenerateService 的模式分发器,深入 Generator → Runner → QueueManager → TaskPipeline 这四阶段执行模式——Dify 中每一次 LLM 交互都由此驱动。