Read OSS

Dify アーキテクチャ概観:6,000 ファイルの LLM プラットフォームを読み解く

中級

前提知識

  • Flask と Python Web アプリケーションの基礎知識
  • Docker Compose の基本的な使い方
  • LLM アプリケーションの概念的な理解

Dify アーキテクチャ概観:6,000 ファイルの LLM プラットフォームを読み解く

Dify は、チャットボット・RAG パイプライン・多段階ワークフロー・エージェント型ツールを、ビジュアルインターフェースと充実した API を通じて構築できるオープンソースの LLM プラットフォームです。Python と TypeScript で書かれた 6,000 以上のファイルからなるコードベースは、一見すると圧倒的なボリュームに感じられます。本記事では、モノレポの構成、デプロイトポロジー、起動シーケンス、ルーティング構造、設定システムを順に解説し、コードベース全体を自信を持って読み進めるための地図を提供します。

リポジトリ構成とドメイン駆動レイアウト

Dify のモノレポは、3つのトップレベルドメインで構成されています。それぞれに AGENTS.md が置かれており、そのドメインの規約が明文化されています。

ディレクトリ 言語 役割 おおよそのファイル数
api/ Python (Flask) バックエンド API サーバーと Celery ワーカー 約 650 コアファイル
web/ TypeScript (Next.js) コンソールフロントエンドおよび埋め込みチャット UI 約 5,100 ファイル
docker/ YAML / Shell Docker Compose オーケストレーションと SSRF プロキシ設定 約 20 ファイル

プロジェクトルートの AGENTS.md には設計思想が明示されています。バックエンドはドメイン駆動設計(DDD)とクリーンアーキテクチャの原則に従い、非同期処理は Redis をブローカーとする Celery が担います。依存関係はコンストラクタインジェクションで解決し、エラーはドメイン固有の例外として適切なレイヤーで処理します。

api/ 内のディレクトリ構成は、DDD の境界を忠実に反映しています。

api/
├── controllers/     # HTTP層: blueprint、リクエストバリデーション
├── services/        # アプリケーションサービス: オーケストレーションロジック
├── core/            # ドメインコア: ワークフローエンジン、RAG、モデル管理
│   ├── app/         # アプリ実行パイプライン
│   ├── rag/         # Retrieval-Augmented Generation
│   ├── workflow/    # ワークフローエンジンとノードファクトリ
│   ├── tools/       # ツール抽象化とランタイム
│   └── plugin/      # プラグインデーモン連携
├── 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 環境変数によってのみ動作が切り替わります。HTTP サーバーとして起動する場合は api、Celery ワーカーとして起動する場合は worker を指定します。

SSRF プロキシ(Squid)は、意図的に設けられたセキュリティ境界です。ワークフローノードが URL の取得・API 呼び出し・Webhook の実行といった外向きの HTTP リクエストを行う際、これらはすべてこのプロキシ経由でルーティングされます。悪意あるワークフローが内部ネットワークのサービスを探索するような Server-Side Request Forgery(SSRF)攻撃を防ぐための仕組みです。サンドボックスのコード実行環境も同様に隔離されており、専用ネットワーク上の ssrf_proxy にのみ接続できます。

プラグインデーモンは、信頼されていないプラグインコードを分離されたプロセスで実行する独立した Go サービスです。INNER_API_KEY_FOR_PLUGIN で認証された内部 API を通じて Dify API に「バックワードインボケーション」チャネルで通信します。この仕組みの詳細は第 5 部で掘り下げます。

二プロセスアーキテクチャ:API サーバーと Celery ワーカー

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 ワーカーとして起動した場合、app は Flask の WSGI アプリケーションとして機能します。Celery ワーカーとして起動した場合も同じ app が生成され、Flask の extensions レジストリから celery オブジェクトが取り出されます。このコードベース共有パターンにより、両プロセスがモデル・サービス・設定に対して同一のアクセス手段を持つことが保証されます。

gevent モンキーパッチの適用戦略は、両方の起動パスで慎重に設計されています。Gunicorn の gunicorn.conf.pypost_patch イベントサブスクライバを使用しており、gevent が組み込みモジュールにパッチを当てた後に処理が実行されます。この順序は非常に重要です。gevent が標準ライブラリにパッチを当てる前に gRPC や psycopg2 にパッチを適用してしまうと、デッドロックが発生します。

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

一方、api/celery_entrypoint.py はよりシンプルなアプローチを取っており、インポート時に即座にパッチを適用します。Celery の gevent プールがこのモジュールの読み込み前にモンキーパッチ処理を済ませるためです。

ヒント: 開発中に原因不明のハングやデッドロックが起きた場合は、gevent パッチの適用順序を確認しましょう。gunicorn.conf.py にある issue #26689 へのコメントには、実際にこの問題が発生した事例が記録されています。

起動シーケンス:create_app() と 28 の拡張

起動シーケンスの中核は app_factory.py にあります。create_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() を確認し、デバッグモードでは各拡張の初期化にかかった時間もログに記録します。

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_database の後に ext_migrate — マイグレーションにはデータベース接続が必要
  • ext_storage の後に ext_logstore — ログストレージはストレージバックエンドに依存
  • ext_logstore の後に ext_celery — Celery タスクは初期化中にログアクセスが必要になる場合がある
  • ext_blueprints はほぼ最後 — ルート登録は他のほぼすべての拡張に依存する

また、flask db コマンド実行時に使用される create_migrations_app() という関数も別に用意されており、こちらは ext_databaseext_migrate のみを初期化します。

blueprint 登録とルートグループ

Dify の HTTP API は七つの blueprint グループに整理されており、ext_blueprints.py で登録されます。

Blueprint URL プレフィックス CORS ポリシー 役割
console_app_bp /console/api オリジン制限あり、認証情報あり 管理コンソール API
service_api_bp /v1 Authorization ヘッダーを許可するオープン設定 外部開発者向け API
web_bp /api オリジン制限あり、ルートごとに設定 Web アプリ(エンドユーザー向け)
files_bp /files CSRF トークン付きのオープン設定 ファイルのアップロード・ダウンロード
inner_api_bp /inner/api なし(内部専用) プラグインデーモンのコールバック
mcp_bp /mcp なし Model Context Protocol エンドポイント
trigger_bp /trigger Webhook ヘッダーを許可するオープン設定 Webhook トリガーエンドポイント

CORS 設定はヘルパー関数 _apply_cors_once() によって管理されており、テストインスタンスで blueprint が再利用された際の二重適用を防いでいます。最も複雑な CORS 設定を持つのは web_bp です。埋め込みボットのエンドポイント(/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 の値に応じて ApolloSettingsSource または NacosSettingsSource に処理を振り分けます。これにより、同一のコードベースを維持しながらエンタープライズの設定管理システムへ設定を集中管理できます。

ヒント: MiddlewareConfig グループだけで、30 種類以上のベクターデータベース・複数のキャッシュバックエンド・ストレージプロバイダーの設定が含まれています。設定値が見つからない場合は、親クラスを検索しましょう。設定は api/configs/middleware/api/configs/feature/ など複数のファイルに分散しています。

次のステップ

アーキテクチャの基盤を把握できたところで、次は実際のリクエストがシステムに到達した際の流れを追ってみましょう。第 2 部では、HTTP リクエストがコントローラーレイヤーに届いてから AppGenerateService のモードディスパッチャーを経由する過程を追います。Generator → Runner → QueueManager → TaskPipeline という四段階の実行パターンを詳しく解説します。