Dify プラグインシステム：設計と実装


Dify v1.0.0 のリリースにより、プラグイン機構が正式にシステムの中核機能として導入されました。本機構の主要設計者である Dify バックエンドエンジニアの Yeuoly が、その設計思想と技術的な意義をご紹介します。

プラグイン機構の中核は、Dify において水平方向に拡張可能なモジュールを切り離し、独立したランタイムとして動作させることにあります。これにより、以下の重要な変化がもたらされました。

- **モジュールの分離 (Module Decoupling)**：以前 Difyは、本体と密結合していたモデルやツールが、個別にインストール・アンインストールが可能なプラグインパッケージとして独立して動作するようになり、柔軟性が大幅に向上しました。
- **プラグインマーケットプレイスと共有メカニズム**：ユーザーやコミュニティが自由にプラグインを作成・共有できるプラグインマーケットプレイスが導入されました。
- **エンドポイントプラグイン**：新しい API エンドポイントを拡張するプラグインであり、これにより、より多くの実際のユースケースを Dify の内部エコシステム内で完結できるようになりました。

これらの変更はユーザーエクスペリエンスに大きく貢献しますが、プラグイン機構の技術的な実装は、多くのユーザーにとってまだブラックボックスと言えるでしょう。この記事では、プラグイン機構の設計思想、ユーザー価値、技術的な実装の詳細について段階的に深く掘り下げていきます。

## ユーザーニーズの分析

プラグインシステムを設計するにあたり、いくつかの重要な課題に直面しました。

1. **コードの密結合**：Difyに新しいモデルやツールを追加する作業が煩雑で、過剰な依存関係を生み、ツールやモデルのバージョン管理上の問題を引き起こしていました。
2. **ユーザーからの要望の欠如**：IMサービスとの連携など、Difyの外部で追加のサービスを構築する必要がある要求が存在しました。
3. **カスタムモジュールの柔軟性欠如**：例えば、Dify の PDF パーサーのパフォーマンスが悪く、RAG（検索拡張生成）などのカスタムモジュールも容易に調整できないといった問題がありました。

これらの問題を解決するため、Dify のツールとモデルを分離し、必要に応じて個別にインストールおよび選択できる統一フレームワークを実装することにしました。ドキュメントパーサーや OCR など、RAG に関連する機能もプラグイン化され、さまざまなユースケースのニーズを対応できるようにしました。また、Dify 内部だけでは完結できないシナリオのために、プラグインシステムが外部システムとの統合のためのオープンインターフェースを提供します。例えば、IMプラットフォームへのOutgoing Webhookをサポートするなど。

これらの要件は一見シンプルに見えますが、エンジニアリング実装はかなり困難でした。初期設計段階のわずか1週間以内に、私たちは以下のような一連の課題に直面しました：

- **マルチワークスペース設計**：Dify はマルチワークスペース設計を採用しており、Pythonソースコードを単にツールやモデルディレクトリにマウントするだけでは機能を実装できません。
- **プラグイン環境の一貫性**: プラグインが異なる環境で一貫して動作するようにしたいと考えていました。Dockerを使用すれば、各プラグインを隔離することでこの問題を解決できます。しかし、各プラグインに個別のDockerコンテナを割り当てることは、デプロイの複雑さを大幅に増大させ、リソース消費も大きくなります。
- **高負荷なクラウドサービス**: Dify の SaaS サービスには数十万人のユーザーがいます。もし10人のユーザーごとにカスタムプラグインが存在する場合、Difyは何万ものプラグインの同時実行という高い負荷に耐えなければならず、クラウドサービスのコストに大きな圧力がかかります。
- **プラグインの開発とデバッグ**: 開発者として、コードを修正するたびにプラグインを再パッケージ化してインストールする必要があります。さらに、ログもDifyのバックエンドに送信する必要があり、開発体験が著しく損なわれます。
- **プラグインの長期実行**: 例えば、ユーザーがIMプラットフォームのWebhookイベントをリアルタイムで監視するために、HTTPサーバーを長期間実行することを許可すべきでしょうか？ これはセキュリティやリソース管理の面で慎重な検討が必要です。

## ソリューション

### デバッグエクスペリエンス

プラグインシステムの実装方法を検討する前に、まず開発者にとって最適なデバッグエクスペリエンスについて考察しました。理想的なデバッグエクスペリエンスは、以下の2つの要件を満たす必要があると考えます：

1. **所見即所得**: つまり、コードを修正した後、インストール作業を必要とせず、Dify 上で直接変更が反映されることです。
2. **ローカルデバッグ**: プラグインのコードをローカル環境で実行し、ブレークポイント等を利用して容易にデバッグできることです。

GDB のような実績のあるデバッガを参考に、デバッガとランタイムを分離する方式を採用しました。デバッガ側でランタイムからの接続要求を待機し、接続が確立されると、ローカルのプラグインは Dify との間に永続的な接続 (長接続) を確立します。Dify はこのプラグインをインストール済みのプラグインとして認識し、デバッグモードとしてマークします。ユーザーからのリクエストは、この長接続を通じてローカルプラグインに転送され、プラグインからの応答も同様に Dify に返送されます。これにより、スムーズなデバッグ体験が実現されます。

しかし、この設計には課題がありました。長接続はステートフルである一方、Dify のサービスはステートレスなのです。Kubernetes クラスタ環境では、ロードバランシングによってリクエストが異なる Dify Pod にルーティングされます。例えば、開発者が Plugin 1 を Dify 1 の Pod に接続し、別の開発者が Plugin 2 を Dify 2 の Pod に接続したとします。この状態で、ユーザーが Plugin 1 をリクエストした場合、リクエストが Dify 2 にルーティングされてしまい、Plugin 1 にアクセスできなくなる可能性があります。この課題を克服するために、トラフィック転送メカニズムの実装が不可欠となりました。

![2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924419/d5cdc02c-2e2a-4ab7-be45-ec25ef71c422.png)

### エンドポイントプラグイン

多くの既存の IM やオフィスコラボレーションツールを調査し、現在のニーズを踏まえて解決すべき重要な課題を明確にしました。その課題とは、Dify がこれらのプラットフォームからの Webhook リクエストを受け付け、プラグインが HTTP リクエストを処理できる仕組みを構築することでした。例えば、Dify のアプリケーションを使用してユーザーメッセージを処理するといったケースが考えられます。

この課題解決のため、Discord などのプラットフォームと連携するランダム生成 URL メカニズムを設計しました。これにより各プラグインはサーバーを常時稼働させる必要がなくなります。Dify が HTTP リクエストの転送を担うことで、生成された URL 経由でプラットフォームからの Webhook リクエストを受信し、プラグインは転送されたリクエストを処理する仕組みです。このアプローチにより、リソース効率が向上します。

メッセージの受信方法を解決した後、次の課題はメッセージをどのように処理するかです。例えば、Discord ボットを開発する場合、Dify のチャットフロー機能を用いてユーザーメッセージへの返信を実装することが考えられます。その場合、コードは次のようになるかもしれません。

```python
class Webhook:
    def _invoke(self, r: Request) -> Response:
        message = r.json()['message']
        response = invoke_app(app_id, message)
        return Response(response)
```

このプラグインでは、Dify の app を呼び出してリクエストを処理し、ボット機能を有効にする必要があります。これにより、Dify v1.0.0 の重要な概念である **逆呼び出し** が導入されます。

### 逆呼び出し (リバースコール)
逆呼び出し (リバースコール) は、Dify のプラグインシステムの重要な概念であり、プラグインが Dify 内部の認証されたモデル、ツール、またはアプリケーションを呼び出すことができます。例えば、プラグインがユーザーからのリクエストを処理する際に、内部のチャットフローやモデルにアクセスして応答を生成することが可能です。逆呼び出しは、いくつかのシナリオで重要な役割を果たします。

- **LlamaIndex の実装**: LlamaIndex は、LLM を使用して取得したリストを要約するために、さまざまな agentic RAG 戦略を実装します。Dify では、モデルパラメータと入力するだけで、ツールをインストールまたはアンインストールできるツールとして使用されます。
- **ツールとしてのモデル**: 以前は、OCR、ASR、および TTS モデルはスタンドアロンモデルとしてのみ使用できました。現在、それらをツールとして使用できます。たとえば、Gemini を OCR ツールとして使用して、操作プロセスを簡素化します。
- **OpenAI 互換 API**: エンドポイントプラグインを介して、Dify は OpenAI 互換のフォーマットを提供します。これにより、プラグインは Dify のアプリケーションを呼び出して統一された形式で応答を返すことができ、Claude や Gemini など、様々なモデルに対応できます。
- **ツールとしてのエージェント**: エージェントは本質的にツールのように機能し、パラメータを受信し、アクションを実行し、結果を返します。プラグイン化後、ストリーミング出力をサポートするエージェントは、ニーズに基づいてカスタマイズされた戦略を持つことができます。

### 実装について
プラグインランタイムの設計は、解決する必要がある最初の課題でした。最終的に、プラグインランタイムは Docker (ドッカー) コンテナ、プロセス、仮想マシン、またはサーバーレスランタイムである必要がありますか？Dify のユーザーベースを評価した後、4 つの完全に異なるランタイムを実装することにしました。

- **ローカルデプロイメント**: 小規模なチームと個々の開発者を対象としており、デプロイメントの要求が比較的低く、大規模な使用を必要とせずに高可用性に焦点を当てています。
- **SaaS サービス**: 数十万人のユーザー向けに設計されており、Dify は高いユーザー負荷を考慮する必要があります。
- **エンタープライズバージョン**: SaaS と同様ですが、エンタープライズバージョンでは、より高い制御性、プライバシー保護、およびプライベートデプロイメントが必要です。
- **リモートデバッグ**: デバッグモードをサポートし、そのランタイムサポートの提供を検討する必要があります。

### ローカルデプロイメント
ローカルデプロイメントは、「ワンクリックデプロイ」とすぐに使用できる使いやすさを重視しています。ユーザーは、単純に `docker compose up -d` コマンドを入力しで Dify 全体を実行し、追加の構成なしでプラグインをインストールできます。この環境では、プラグインランタイムは親プロセスによって管理されるサブプロセスとして設計されており、親プロセスは依存関係のインストールなど、プラグインのライフサイクルを制御します。2 つの間の通信は、標準入出力パイプを介して行われます。

### SaaS サービス
大規模なユーザーのニーズを考慮して、SaaS バージョンは、使用量に基づいて伸縮自在に拡張できるサーバーレスアーキテクチャで設計されており、高い並行性、リソース使用率、および可用性を保証します。最終的に、AWS Lambda をソリューションとして選択しました。AWS は Dify のパートナーであり、Dify の既存の SaaS ビジネスをすでにサポートしており、Dify はネットワーク経由で Lambdaと通信し、適切なアーキテクチャを提供します。

### エンタープライズバージョン
エンタープライズクライアントに Lambda をソリューションとして強制することはできないため、制御可能で信頼できるランタイムを設計しました。このバージョンは、高い制御性とプライバシー保護を提供し、企業内でのプライベートデプロイメントをサポートします。

### リモートデバッグ
デバッグモードの場合、Dify は TCP ネットワークの長期的な接続を介したプラグインのデバッグをサポートし、ステートフルの問題を解決します。etcd と同様の設計を使用し、プラグインリクエストが正しいポッドに転送されるように、Redis HashMap を介してプラグインの接続状態を管理しました。IP を管理するために 2 つのメカニズムを設定しました:
- クラスターは IP プールを維持する必要があり、ポッドは IP をプールに追加します。本番環境では、マシンは異なるサブネットにまたがる複数の IP を持つ可能性があるため、ポッドは投票メカニズムを使用して到達可能性をテストし、使用可能な IP をマークします。
- クラスターには、ポッドのヘルスを定期的にチェックし、終了したノードのステータスをクリーンアップしてクラスターの安定性を確保するマスターノードが必要です。

## セキュリティ
### システムセキュリティ
プラグインシステムのセキュリティは、制限的なサンドボックス方式ではなく、暗号署名に基づいています。サンドボックスは制限が厳しく、許可されていない可能性のあるシステム操作を行うおそれがあるため、多くの依存関係パッケージ（プラグインが正常に動作するために必要なソフトウェア部品）の使用が妨げられ、プラグインの利用体験に深刻な悪影響を及ぼします。一方、プラグインは事前にコード化されたパッケージであり、インストール前に手動レビューで「安全」とラベル付けすることで、リスクを大幅に軽減できます。

公開鍵暗号基盤の署名方式を採用しており、審査を通過したプラグインには秘密鍵による電子署名を付与し「認証済み」と表示します。未審査または審査不合格の場合は「安全性未確認」の警告メッセージがユーザーに提示される仕組みです。署名されていないプラグインは、ユーザーが手動で設定を変更しないと、インストールできません。

### プライバシーポリシー
プラグインは、特に機密データを扱う場合は、必要なアクセス権限やデータ保存方法など、プライバシー関連の規定を明示しなければなりません。基本的なアクセス権限に関しては、開発者がプラグインの動作に必要な権限を厳密に定義することが義務付けられており、Dify は明示されていない権限を直接拒否します。より複雑なプライバシーポリシーについては、開発者はマニフェスト（プラグインの設定ファイル）で参照される詳細なプライバシー戦略を提供する必要があります。また、公式マーケットで配布する全プラグインは、厳格なプライバシー審査を通過することが必須要件となっています。

## 結論
この記事では、Dify のプラグインシステムの設計と技術的な実装について簡単に紹介しました。プラグイン化により、Dify はより高い柔軟性とカスタマイズ性を提供し、さまざまなユースケースをサポートします。また、開発者に対しては、逆呼び出し (リバースコール) やプラグインマーケットプレイスを通じて、より効率的なデバッグおよび開発エクスペリエンスを提供します。これにより、ユーザーは自分のニーズに合わせたプラグインを簡単に作成・共有・利用することが可能となります。関連するコードはオープンソース化されています。皆様のご参加と新しいプラグインのアイデアのご提供を心よりお待ちしております。
-  [dify-plugin-daemon](https://github.com/langgenius/dify-plugin-daemon)
-  [dify-plugin-sdks](https://github.com/langgenius/dify-plugin-sdks)
-  [dify](https://github.com/langgenius/dify)

ぜひ、Difyのプラグイン開発に参加して、一緒に未来を切り開きましょう！