SpeeeにおけるDify構築・運用から得たTIPS共有


## はじめに
皆さん初めまして、株式会社Speee ビジネストランスフォーメーション事業本部でテックリードをしている和田と申します。[Dify Advent Calendar](https://qiita.com/advent-calendar/2024/dify)シリーズ1の20日目を担当させていただきます。

## 本記事の背景と目的

弊社では2024年より、「生成AI × 自動化 × データ＆分析」の3要素を中核的なケイパビリティとする基盤を「プラットフォーム民主化」の手法で導入し、組織全体の自律的な進化と抜本的な効率化を促すプロジェクトを推進しています（以下のイメージをご参照ください）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/44cef4c3-e33b-f3ae-fd5d-9cee75d8a47d.png)

その重要コンポーネントとして、Difyコミュニティ版をAWS上で分散システムとしてデプロイし、ビジョンの実現に向けた安定的かつ柔軟な運用体制を構築中です。

本記事では、当社がDifyコミュニティ版を構築・運用する中で実際に遭遇した問題点とその解決策や、Difyを活用する多くの業務ユーザーにとって典型的な課題に対する実践的なソリューションを共有することで、Difyコミュニティの発展に少しでも貢献できればと考えています。

## 本記事の概要とアジェンダ
本記事では、Difyコミュニティ版ユーザーがしばしばつまずく課題や、時間を著しく浪費したり挫折してしまいそうな難所の解決方法やTIPSをご紹介します。


**アジェンダ**：
前半（1-3）はDifyユーザ向け、後半（4-7）はテクニカルな内容となっています。

**1. LLMに大量出力をさせるには？**  
   - 大量出力ユースケース: 書籍並みの物量のトレーニングコンテンツを高品質かつ一発で出力するには？

**2. 超基本ユースケース:"ファイル添付&会話履歴保持可能チャットボット"はどう作る？**
   - ファイル+画像添付が可能な履歴保持型チャットボットの作り方

**3. 大量入力をチャットフローでうまく処理するには？**  
   - 大量入力ユースケース例: ソースコードリポジトリを読み込み質問に的確に答える

**4. 最低限押さえておくべきDifyの基本的なアーキテクチャ**
  - インフラアーキテクチャ
  - アプリケーションアーキテクチャ(超概要)
  - Difyのデプロイ方法とコンフィグファイル
  - Dify APIリソースにおけるコンフィグ読み込み実装方式
  - コンフィグ読み込み方式とRestエンドポイント登録の図解
  - Difyアプリケーションの実行制御

**5. コンフィグパラメータ最適化**
  - Frontendリソースの上限値最適化
  - APIリソースの上限値最適化
  - Sandboxリソースの上限値最適化

**6. ワークフロー実行時のネットワーク切断→バックエンド処理継続・結果未保存問題**
   - 発生事象とユーザからの見え方
   - 根本原因: ワークフロー実行時におけるマルチスレッド実行アーキテクチャ
   - 本バグのOSS側での対応状況

**7. Amazon Bedrock利用時の内部エラーについて**

**8. AWS上でのDifyワークロードの所感**  


## 1. LLMに大量テキスト出力をさせるには？
LLMに大量のテキストを出力させる際、モデルのアウトプットトークン数の制限が課題となります。また、制限内で出力しようとすると、LLMは情報を省略した箇条書きなどで出力する傾向があります。

本セクションでは、以下のケーススタディを通して、「情報を欠落させずに大規模で質の高い体系的なコンテンツを一括出力する」アプローチをご紹介します。

今回は入力としてドキュメントは読み込んでいませんが、ドキュメントも読み込むことで、色々な長編コンテンツを作成することができます。

### 想定ユースケース
本ケーススタディでは、Difyのソースコード読解に必要なPythonおよびフレームワークの知識を体系的に学習できるトレーニングコンテンツの作成を目指します。

#### 対象フレームワーク
- Python Flask
- Pydantic
- pydantic_settings
- SQLAlchemy

#### 使用モデル
Amazon Bedrock Claude Sonnet 3.5 v2を使用します。この種の大量テキスト出力には、Claude Sonnetクラスのモデルが適しています。GPT4-miniなどの軽量モデルでは、テキスト量を抑制する傾向が強く、詳細な解説の作成が困難です。

今回の出力コンテンツは以下に配置してあります。ソースコード事例付きで、恐ろしい量のコンテンツが出力されています。ごく一部変な部分もありますが、質・物量ともに想定を超えていると感じます。この物量で概ね合計100Kトークンの消費でした。

https://qiita.com/Kazuhisa-Wada/private/adeb6f2ba46de3c2af2e

### 全体のプロセス設計
#### プロセスの基本的な流れ
1. 全体構成定義(JSON形式で出力)
2. チャプター別詳細目次定義(JSON形式で出力)
3. チャプター内のセクション別ドラフトコンテンツ作成→マージ(テキスト出力)
4. 全体品質チェック(テキスト出力)
5. コンテンツ修正方針・方法作成と修正適用(テキスト出力)
4. 全体品質チェック(2回目)(テキスト出力)
5. コンテンツ修正方針・方法作成と修正適用(2回目)(テキスト出力)
6. コンテンツ全体の最終出力(テキスト出力)

#### プロセスの特徴
大量コンテンツの出力では、全体→部分→全体→部分と行き来しながらコンテンツを仕上げていきます。この手法は大量入力の場合でも有効です。

#### 実装のポイント
- LLMの出力をオブジェクトの配列で出力
- イテレーションノードにて要素別に処理を行い、最大トークンの制約を克服する
- プロンプト設計による品質管理
- 出力コンテンツを改善するプロセスも組み込み済み

### プロンプトはLLMに作らせるのが基本
プロンプトの作成は、LLMに原則として実施させます。網羅性があり深さのある期待通りのコンテンツを作成させるには、うまいプロンプトが必須になります。
理想的には業務領域・ユースケースごとのメタプロンプトパターンを用意して、それを使ってLLMにプロンプトを作らせるのが良いと考えています。

### 実際のワークフロー実装
上記のアプローチで作成したワークフローが以下です。
今回のワークフローは本記事用に作成した大雑把なプロンプトですので、内容については参考程度でお願いいたします。

プロセスの基本的な流れに沿って実装されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/126a97b9-93a9-73ff-b9f1-68d52323671f.png)

以下に、いくつかピックアップして代表的な実装をご紹介しますが、既述のとおりメタプロンプトを作っておけば自分でも簡単にできます。むしろ重要なのはJSON形式にしてOBJECTへ変換しイテレーションでループする部分になります。

**フェーズ1: 全体構成の定義フェーズ**
```hcl
### フェーズ1: 全体構成の定義フェーズ

Python Flask+ Flask-Restful + Pydantic + pydantic_settings + SQLAlchemy(ORM)を用いたWebアプリ開発の教育用コンテンツ（書籍相当）の章レベルまでの全体構成をJSON形式で定義してください。

【条件】
- 対象読者: Python基礎を把握し、Web開発の基礎的知識を持つ初～中級エンジニア
- 学習ゴール: 網羅的で豊富な解説と事例で読者のレベルを引き上げる
   エンタープライズ水準の実践力を独語に身につくようにする。
- コンテンツ範囲: 豊富な事例付きフレームワーク基礎と思想の解説、スキーマ定義、ORM、設定管理、ユニットテスト、セキュリティ、APIドキュメンテーション、CI/CD、ロギング
- コンテンツ深度: 基礎概念 → 中規模アプリ構築 → エンタープライズ的拡張
- コンテンツの質: 特に豊富な解説と事例で読者のレベルを引き上げる作りであること。実務に近い事例のコードを例としてふんだんに用いること、解説付きのトレーニング用の練習問題があること。
- 特にベースとなるPython FlaskとFlask-Restfulは少なくとも3Chapterは必要。ここが理解できないと全体に影響するためリッチなコンテンツにすること。

【チャプター構成の指針】
1. 基礎から応用への段階的な学習パスを形成すること
2. 各章は独立して学習可能でありながら、全体として一貫したアプリケーション開発の流れを形成すること
3. 全体で12章程度の構成とすること
4. Python Flask、Python Flask-Restfulは基礎のため最低3章以上は必須、豊富な解説を行い、箇条書きのみのコメントな可能な限り控える

【禁止事項】
- 「わかりました」「了解」などの了承応答は禁止
- 必ず指定のJSON形式で回答すること。Markdownのコードブロック内のJSONではなく回答全体が指定のJSON形式での回答であること。
- 自明かつ必要な場合を除き、箇条書きの使用を避けること


【JSON出力要件】
```json
{
  "meta": {
    "version": "バージョン番号",
    "date": "作成日",
    "target_audience": "対象読者の定義",
    "prerequisites": ["前提知識1", "前提知識2"]
  },
  "overview": {
    "goals": {
      "short_term": "短期目標の詳細説明",
      "mid_term": "中期目標の詳細説明",
      "long_term": "長期目標の詳細説明"
    },
    "content_scope": {
      "horizontal": ["技術範囲1", "技術範囲2"],
      "vertical": ["深度レベル1", "深度レベル2"]
    }
  },
  "chapters": [
    {
      "chapter_number": 整数,
      "title": "章タイトル",
      "introduction": "章の詳細な導入文",
      "learning_objectives": {
        "primary": "主要学習目標",
        "secondary": ["副次的な目標1", "副次的な目標2"]
      },
      "technical_focus": {
        "main_topics": ["主要トピック1", "主要トピック2"],
        "technologies": ["使用技術1", "使用技術2"]
      }
    }
  ],
  "final_outcomes": {
    "skills_acquired": ["獲得スキル1", "獲得スキル2"],
    "practical_capabilities": ["実践的能力1", "実践的能力2"]
  }
}
```
**Object変換・作成1**
```python
# Object変換・作成1

def main(arg1: str) -> dict:
    respJson1 = json.loads(arg1)
    return {
        "agenda": respJson1,
        "chapters": respJson1["chapters"],
        }
```

**全体構成の定義フェーズ**

```hcl
### フェーズ2: サブチャプター詳細定義フェーズ

**指示テンプレート**:
指定された章のサブチャプター構造を定義してください。各サブチャプターには具体的な学習内容、実装例、演習問題を含めます。

【入力パラメータ】
{{#1734639460687.item#}}

【JSON出力要件】

{
  "chapter_detail":       {
        "subchapter_number": 整数,
        "title": "サブチャプタータイトル",
        "introduction": "導入文",
        "key_concepts": ["重要概念1", "重要概念2"],
        "sections": [
          {
            "section_number": 整数,
            "title": "セクションタイトル",
            "content_outline": "セクションの内容概要",
            "implementation_examples": [
              {
                "title": "実装例タイトル",
                "description": "実装の説明",
                "key_points": ["ポイント1", "ポイント2"]
              }
            ],
            "exercises": [
              {
                "level": "難易度（初級/中級/上級）",
                "title": "演習タイトル",
                "description": "演習の説明",
                "requirements": ["要件1", "要件2"],
                "success_criteria": ["基準1", "基準2"]
              }
            ],
            "common_pitfalls": ["よくある問題1", "問題2"]
          }
        ],
        "knowledge_check": {
          "review_points": ["復習ポイント1", "ポイント2"],
          "practical_tips": ["実践的なヒント1", "ヒント2"]
        }
      }
}
【禁止事項】
- 「わかりました」「了解」などの了承応答は禁止
- 必ず指定のJSON形式で回答すること。Markdownのコードブロック内のJSONではなく回答全体が指定のJSON形式での回答であること。
- 自明かつ必要な場合を除き、箇条書きの使用を避けること

【注意事項】
1. 各サブチャプターは3-5個のセクションで構成すること
2. 実装例は実務を想定した具体的なものにするとし、解説を豊富に行うこと
3. 演習は段階的な難易度設定とし、必ず解説があること
4. 各セクションの学習時間は120分程度を想定すること
5. 実装例とコードは本質的な部分のみ示し、自明な部分は省略可とすること

【実装例の方針】
1. 基本実装から始め、段階的に機能を追加する形式とすること
2. エラーハンドリング、バリデーション等の実践的な要素を含めること
3. ベストプラクティスとアンチパターンの対比を示すこと
4. コードの説明には具体的なユースケースを含めること
5. 解説が豊富であること

【演習問題の方針】
1. 各難易度で1-2問の演習を用意し解説も付与すること
2. 実装例の応用または発展となる課題とすること
3. 現実のプロジェクトで遭遇する課題を基にすること
4. 明確な評価基準を設定すること
```

**セクション別コンテンツ初回ドラフト作成**
```hcl
Python Flask + Pydantic + pydantic_settings + SQLAlchemy(ORM)を用いたWebアプリ開発の教育用コンテンツ（書籍相当）のうち、以下Create_Targetタグのsectionのコンテンツを実際に作成してください。

なお、Content_Agendaタグに全体のアジェンダおよび作成に関する要件を記載しており、今回作成対象のセクションの内容と全体の整合を取ること。

<Content_Agenda>
{{#1734625290652.text#}}
</Content_Agenda>

<Create_Target>
{{#1734646740367.item#}}
</Create_Target>

【禁止事項】
- 「わかりました」等の了承表現禁止
- MARKDOWN形式で出力すること。JSON形式ではない。
- 必要な場合を除き、箇条書きの使用を避けること
- 書籍レベルの高い品質を確保すること

```
**Object変換・作成2**
```python

def main(arg1,chapters,index) -> dict:
    respJson1 = json.loads(arg1)
    added_chapter = chapters[index]
    added_chapter["chapter_detail"]=respJson1["chapter_detail"]
    return {
        "added_chapter": added_chapter,
        }
```

## 2.超基本ユースケース:"ファイル添付&会話履歴保持可能チャットボット"はどう作る？

Difyを社内に公開する際、チャットボットでファイル添付不可について質問されることがあると思います。
これは、チャットフローを用いて、メモリ機能をONにして、かつ、ファイルアップロードの機能を利用することで実現できます。

以下のようなチャットフローで実現できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/fc7e0a19-ef6f-3d28-9f1d-e51a77d7b0c9.png)

ポイントは以下の通り:
1. 右上の機能ボタンからファイルアップロード機能をONにしドキュメントと画像にチェックを入れます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/c52c25c7-29b8-d7b6-32bc-bd86615e92be.png)

2. STARTノードからドキュメント系と画像系のフローに分岐し、それぞれのドキュメント種類のみが通過するようにリスト処理ノードでフィルタする。画像のフローはLLMにそのまま接続します。

3. ファイル添付有無による分岐
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/ffdf4c0c-9c0c-bb37-05fb-e8b839e504b3.png)

4. テキスト抽出ツールでドキュメントファイルの内容を文字列に変換します

5. 本チャットフローではドキュメントの内容をメモリに保存することを想定するため、複数ファイルある場合はテンプレートノードを用いて配列から文字列に展開し、会話変数へ代入します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/22c868fd-9543-7300-2f5b-028d66d20a35.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/ee0b6e1f-a3ef-a376-0d8a-b7bdfc3e042e.png)

6. LLMにてメモリON、ビジョンON(+画像ファイル入力変数を指定)、テキストファイル変数の指定、を行う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/4ec60ec9-6ce1-c729-467a-76d1f0174202.png)

ユーザからのアップロードファイルについてシステムプロンプト側でファイル内容の結合済みテキストを参照します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/5c3e90be-1619-485c-63a1-face65daa6d7.png)

以上で、会話履歴ON、画像・テキスト系ファイルアップロード可能なチャットフローが完成します。この例ですとメモリ履歴50なので、ファイルを大量に読み込んでいるケースではもっと小さくする必要はあると思いますのでご注意ください。

## 3.大量入力をチャットフローでうまく処理するには？
こちらですが、解説が膨大になりすぎたため、今回に記事からは割愛します。また別の記事で投稿しますので、もしよろしければお待ちください。

## 4. 最低限押さえておくべきDifyの基本的なアーキテクチャ
本節では、Difyの実運用に必要なアーキテクチャの基本概念を最低限、解説します。システム運用や障害解析などにおいて、以下の要素の理解は必須となります。

### **インフラアーキテクチャ**
インフラ構成の基本については、以下の記事で詳しく解説されています：

https://mazyu36.hatenablog.com/entry/2024/12/05/214145

本記事では、この構成を前提に、主にFrontend、API、Sandboxの各コンポーネントのパラメータ調整について説明します。

### **アプリケーションアーキテクチャ(概要)**
Difyのアプリケーションの実行上の重要なコンポーネントについて簡単に箇条書きベースで解説します。ただし、本記事の理解に必要な部分に絞ります。Difyの構築時やシステム運用時のトラブルを考えると、特に以下の概要はまずは押さえておく必要があります。

### アプリケーションアーキテクチャ
#### UIの構成と通信フロー
- シングルページアプリケーション（SPA）として設計
- 画面遷移時の特徴：
  - 初回表示時のみFrontendへアクセス
  - 以降は必要に応じてREST APIをコールしAPIリソースへ
  - 特定操作（言語設定変更など）以外でページ再読み込みなし

#### コンポーネント構成
1. Frontendリソース
   - Next.jsによる実装
   - 主に初期ページロードを担当

2. APIリソース
   - Python（Flask + Flask-Restful）による実装
   - UIイベントに対応するRESTリソースがエントリーポイント
   - 障害解析時は特にこの処理フローの理解が重要

3. URIパスパターンとRESTリソース対応
   | 用途 | URIパス | 対応パッケージ |
   |-----|---------|---------------|
   | 通常画面 | /console/api/* | /api/controllers/console/* |
   | アプリ実行 | /api/* | /api/controllers/web/* |
   | APIからコール時 | /v1/* | /api/controllers/service_api/* |

### デプロイ方法
Difyのデプロイには主に2つの方法があります：

1. Docker Composeによるデプロイ
   - 設定: /docker/.env.exampleで一括定義
   - [詳細ドキュメントへのリンク]

2. ソースコードからのビルド・デプロイ
   - Frontend/API設定: /web/.env.example, /api/.env.example
   - Sandbox設定: 
     - ソースビルド時: dify-sandbox/conf
     - Docker利用時: /docker/.env.example

### Dify APIリソースにおけるコンフィグ読み込み実装方式
重要なポイントであるため、APIリソースにおけるコンフィグ読み込みの実装方式を簡単に解説します。

コンフィグの読み込みの仕組みは、pydantic_settingsのBaseSettingsを用いて、コンフィグモデルをクラスとして定義することで、クラス変数名に対応するパラメータを.envファイルから読み込み可能としています。これにより型安全にコンフィグを管理できる仕組みになっています(OSの環境変数読み込みやAWS ECSの環境変数も読み込み可、その場合は環境変数が優先されます)。実際のクラス定義はすべて/api/configs/配下のモジュールの__init__.pyおよび/api/configs/app_config.pyに定義されています。

理解促進のため、実際のコード例を以下に示します。configs配下のfeatureモジュールで定義されているコンフィグになりますが、理解しておくべきは、ここで定義されているクラス変数(例:OAUTH_REDIRECT_PATH)は全て.envで指定可能だということです。

```python
# /api/configs/feature/__init__.py
class AuthConfig(BaseSettings):
    """
    Configuration for authentication and OAuth
    """

    OAUTH_REDIRECT_PATH: str = Field(
        description="Redirect path for OAuth authentication callbacks",
        default="/console/api/oauth/authorize",
    )

    GITHUB_CLIENT_ID: Optional[str] = Field(
        description="GitHub OAuth client ID",
        default=None,
    )

    GITHUB_CLIENT_SECRET: Optional[str] = Field(
        description="GitHub OAuth client secret",
        default=None,
    )
```

pydantic_settingsについては、非常にわかりやすくまとまっている以下の記事が参考になると思います。非常にシンプルで誰でも理解できるライブラリなので、一読をお薦めします。

https://qiita.com/inetcpl/items/b4146b9e8e1adad239d8

ここで詳細を解説した背景は、コンフィグモデルクラスのクラス変数が定義されているのに、コンフィグファイルのテンプレートにそのパラメータが.env.exampleにて記載漏れしていることがあり、重要な設定項目を認識できないケースがあるからです。

Difyは非常に進化の速いOSSですが、その反面、.env.exampleが常に正しい状態にあるとは言い切れません。もしも大規模エンタープライズ環境でDifyを展開する際には、.env.exampleではなくコンフィグモデルクラスに設定されているクラス変数を網羅的にチェックし最適化するのは必須となります。

一方で、簡単な運用に乗せる程度でしたら、本記事に記載している項目を検討すれば、まずは十分と考えています。

### コンフィグ読み込み方式とRestエンドポイント登録の図解
ここまでの解説を詳細に図解したものが以下になります。初めてDifyのソースを読んだ時に非常に分かりにくいと思ったため理解促進のために自分用に整理したものです。
構築時のシステムテストや運用時にどうしてもコードリーディングを含めた解析が必要な場面に遭遇しますので、障害箇所の特定のはじめの一歩の知識として、把握しておくと良いかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/35fe3b7f-d064-7e53-a264-3bcca6686db1.png)

構築時やシステムテスト時に、仮に障害に遭遇せずとも、例えばワークフローで深刻な障害が出た想定で、Restエンドポイントの特定からワークフロー実行完了までのコードをトレースしてみると良いかと思います。

例えば、アプリ実行画面からのワークフロー実行は、/api/workflows/runがキックされ、対応する箇所は以下の通りです：

`api/controllers/web/__init__.py`にてwebモジュールをblueprintに登録し/apiプリフィックスを割り当て
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/cd5114da-59b2-c781-0ec0-8a7a016cedc1.png)

実際のURIパスとRestリソースクラスのマッピングは`api/controllers/web/workflow.py`の`api.add_resource`による(一番下の2行)。ただしapiプリフィックスは省略されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/12cfd7ff-5336-d8b9-0da1-edfb055e60c7.png)

### Difyアプリケーションの同時実行数制御

同一アプリケーションの同時実行数はRedisキャッシュ上で`dify:rate_limit:<application_id>:max_active_requests`をキーに実行数を管理し制御します。

後述するように、コンフィグAPP_MAX_ACTIVE_REQUESTSにて制御可能で、巨大ワークフローを無制限に多重実行されるた時のリスクが大きいため、10-20程度で制限すべきです。


## 5. コンフィグパラメータ最適化
Difyコミュニティ版の構築・運用において、最も頻繁に問題となるのがアプリケーションコンフィグの設定で、デフォルト設定のままでの複雑なワークフロー実行は、上限値超過によるエラーが発生しやすいです。

ここでは、コミュニティ版Difyをクラウド環境にデプロイし大規模に使い始める際に、まずは調整・最適化した方が良いとコンフィグパラメータをピックアップして解説します。

### Frontendリソースの上限値最適化
Frontendで気をつけるべきポイントは2つだけ: APIのURLおよびアプリ実行画面タイムアウト、になります。
対象となるファイルはgithubのdifyソースコードの/web/.env.exampleです。

**APIリソースのURL設定**
以下2点の設定が必要ですが、FrontendリソースとAPIリソースが同一サーバに配置されているならこのままで問題ありません。もしFrontend、API、sandboxなどそれぞれ別サーバに分散してデプロイしている場合は、localhostを適切な宛先で置き換えます。例えばAWSの場合、内部ロードバランサのDNSやECSのサービスコネクタで定義されたホスト名を設定します。

```hcl
# The base URL of console application, refers to the Console base URL of WEB service if console domain is
# different from api or web app domain.
# example: http://cloud.dify.ai/console/api
NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api
# The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
# console or api domain.
# example: http://udify.app/api
NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api
```
**アプリケーション個別実行画面の表示タイムアウト**
```hcl
# テキスト生成のタイムアウト（ミリ秒）
NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=60000
```
- デフォルト値（10分）では大規模ワークフロー実行時に不十分
- 弊社設定: 60分
- 注意: SaaS版では調整不可

なお、Chatflowや、ストリーム的にレスポンスが返却される作りのワークフローの場合、このパラメータが調整不可能でも困ることはありませんので、デフォルト値でも問題ありません。


### APIリソースの上限値最適化
APIで気をつけるべきポイントは非常に多いので、カテゴライズして解説します。
対象となるファイルはgithubのdifyソースコードの/api/.env.exampleです。

---

**ワークフロー実行制御設定**
ワークフロー全体の実行・制御に関する設定値について解説します。


| 設定項目                    | 説明                                                | デフォルト値          |
|-----------------------------|---------------------------------------------------|----------------------|
| WORKFLOW_MAX_EXECUTION_STEPS | ワークフローで許可される最大実行ステップ数。ツールとしてのワークフローを再帰呼び出しすることで無限ループが発生するリスクがあるため、ここを極端に大きくすることは避けるべきです。**大きな値の場合に大きなリスクを伴うパラメータです。**| 500                  |
| WORKFLOW_MAX_EXECUTION_TIME | ワークフロー実行の最大時間（秒）。巨大なワークフローを実行するケースでは20分は短すぎます。弊社では、そのような用途向けのデプロイのみ60分にしています。        | 1200                 |
| WORKFLOW_CALL_MAX_DEPTH     | ワークフロー内で許可されるネストの最大深度。特にワークフローをツールとして再帰呼び出しするような、汎用性や負荷の高いワークフローでほぼ調整必須となります。ただし、無限ループすると非常に危険なため、これを大きくする際にはWORKFLOW_MAX_EXECUTION_STEPSを小さくして事故を防止した方が良いと考えます。弊社では10まで拡張しています。| 5                    |

**ファイルアップロード制限**
ワークフローの入力のうち、ファイルアップロードの設定値について解説します。

ワークフローおよびナレッジ登録におけるファイルアップロードのファイル数と各ファイルのサイズ制限を設定します。

名前だけだと分かりにくいですが、ワークフローのファイルアップロード入力項目を作成する際の、ドキュメントファイルの１ファイルの最大サイズがUPLOAD_FILE_SIZE_LIMIT、画像・音声・映像がUPLOAD_IMAGE_FILE_SIZE_LIMIT・UPLOAD_AUDIO_FILE_SIZE_LIMIT・UPLOAD_VIDEO_FILE_SIZE_LIMITになります。また、一番下のアップロードファイル数上限がWORKFLOW_FILE_UPLOAD_LIMITです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/8037e85c-2e25-e0fc-0b83-a8e24a47f61e.png)


| 設定項目                      | 説明                                                                 | デフォルト値 |
|-------------------------------|----------------------------------------------------------------------|-------------|
| `UPLOAD_FILE_SIZE_LIMIT`      | 通常のファイルアップロードで許可される最大1ファイルサイズ（MB単位）     | 15          |
| `UPLOAD_IMAGE_FILE_SIZE_LIMIT`| 画像ファイルアップロードで許可される最大1ファイルサイズ（MB単位）       | 10          |
| `UPLOAD_VIDEO_FILE_SIZE_LIMIT`| 動画ファイルアップロードで許可される最大1ファイルサイズ（MB単位）       | 100         |
| `UPLOAD_AUDIO_FILE_SIZE_LIMIT`| 音声ファイルアップロードで許可される最大1ファイルサイズ（MB単位）       | 50          |
| `WORKFLOW_FILE_UPLOAD_LIMIT`  | ワークフロー内で一度にアップロード可能な最大ファイル数| 10          |

上記以外にBATCHという文言が入っているLIMITがありこれはナレッジ関連のようですが、現在ナレッジ活用と検証を進めているため今後こちらの情報はアップデートしたいと思います。ただし、ほとんど説明もなくコードを読む以外に手段がないですし、検証結果から類推しようとしてもこの上限にヒットせず、現時点では仕様不明です。

**コード実行環境設定**
ワークフローのコードノードまたはテンプレートノードの設定値のうち、特に重要なものを取り上げます。

| 設定項目                     | 説明                                              | デフォルト値                     |
|------------------------------|-------------------------------------------------|---------------------------------|
| CODE_MAX_STRING_LENGTH       | 処理可能な最大文字列長。大幅に調整すべきパラメータです。ファイルを読み込みテキスト変換し処理している場合などで、ここにヒットするケースがあります。大きいファイルを扱うケースは10倍の800,000でも小さいためユースケースに応じてさらに大きくすることも検討します。弊社では800,000をデフォルト、一部ユースケース専用デプロイでさらに巨大な値に設定しています。  | 80,000                         |
| TEMPLATE_TRANSFORM_MAX_LENGTH| テンプレート変換時の最大文字列長。jinja2を使うテンプレードノードを用いて巨大な出力をする際にエラーになるケースがあります。弊社では800,000に設定しています。 | 80,000                         |
| CODE_MAX_OBJECT_ARRAY_LENGTH | 処理可能なオブジェクト配列の最大長。コードノードの出力がオブジェクト配列の場合に、その配列長の最大値のこと。また、ネストされている場合はネスト先のオブジェクト配列にも適用されます。30は小さすぎるため、長くすることをオススメしますが、長すぎるとメモリを大きく消費されるため、運用しつつ調整必要になります。弊社では30に設定していますが、巨大なワークフローを実行する少人数チーム専用のDifyでは100まで拡張しています。| 30                              |
| CODE_MAX_DEPTH       | CODEノードにおけるObjectのネストの最大の深さ。複雑なワークフローを書いていると時々"Depth limit $5 reached, object too deep."というエラーに遭遇します。5でもほぼ問題はないですが弊社では10で設定しています。  | 5                         |

**APIツール設定**
ワークフローのAPIツールノードの設定値のうち、特に重要なものを取り上げます。

| 設定項目                        | 説明                                           | デフォルト値  |
|---------------------------------|----------------------------------------------|-------------|
| API_TOOL_DEFAULT_READ_TIMEOUT   | APIリクエストの読み取りタイムアウト（秒）。ツール呼び出しでどのような拡張ツールを導入しているか、に依存しますが、60秒は短すぎます。弊社の場合、今後、データ操作系のツール連携が増えていく見込みですので、現在は600、チームによっては将来的に1800まで拡張する予定です。 | 60          |


**HTTPリクエスト設定**
ワークフローのHTTPノードの設定値のうち特に重要なものを取り上げます。

| 設定項目                        | 説明                                           | デフォルト値     |
|---------------------------------|----------------------------------------------|-----------------|
| HTTP_REQUEST_MAX_READ_TIMEOUT   | HTTPリクエストの最大読み取り待機時間（秒）。長くする必要性はないと思いますが、もしDifyからn8nやDifyからDifyのワークフローをコールするようなケースでは、600秒ですと不足するケースはあります。弊社では一部のユースケース向けのDifyで今後拡張する予定です。| 600             |
| HTTP_REQUEST_NODE_MAX_TEXT_SIZE | HTTPリクエストのテキストデータ最大サイズ（バイト）。スクレイピングで利用しているケースでは1MBは小さすぎますので数倍に拡張する必要があります。弊社の場合、SaaS版Difyで最もエラーを引き起こした要因の一つで、3MBまで拡張しています。| 1,048,576（1MB） |
| HTTP_REQUEST_NODE_MAX_BINARY_SIZE| HTTPリクエストのバイナリデータ最大サイズ（バイト）。上記のバイナリデータ版ですが、こちらもスクレイピングでファイルダウンロードがあるケースではやや小さいと思いますのでユースケースによっては調整必須となります。弊社では20MBまで拡張しています。| 10,485,760（10MB）|

**セキュリティ設定**
Dify全般のセキュリティ関連設定のうち、特に重要なものを取り上げます。

| 設定項目                        | 説明                                           | デフォルト値  |
|---------------------------------|----------------------------------------------|-------------|
| LOGIN_LOCKOUT_DURATION | ログイン失敗時のロック解除までの時間(秒)。デフォルト24時間、かつ、それが画面上に表示されないため、Difyコミュニティ版の試験運用を開始して1時間以内で苦情が複数出ました。調整必須となります。ただし、Difyの公開範囲と利用企業のセキュリティ方針に準拠に依存します。弊社では閉域網での利用に閉じている関係で、15minで設定しています。なお、v.0.14.1以降のバージョンに含まれます。| 86400(24hours)           |


**App Configuration**
Difyアプリケーショn実行全般の制御について解説します。

| 設定項目                        | 説明                                                                                 | デフォルト値 |
|---------------------------------|------------------------------------------------------------------------------------|-------------|
| `APP_MAX_EXECUTION_TIME`        | アプリケーション実行の最大時間（秒単位）。WORKFLOW_MAX_EXECUTION_TIMEと整合している必要があります。弊社ではWORKFLOW_MAX_EXECUTION_TIMEと同値に設定しています。                                            | 1200        |
| `APP_MAX_ACTIVE_REQUESTS`       | 単一のアプリケーションが処理できる最大同時リクエスト数（0の場合は無制限）。無制限にするとリスクがあるため、弊社では20に制限しています。  | 0           |



**その他設定**

| 設定項目                        | 説明                                                                                 | デフォルト値 |
|---------------------------------|------------------------------------------------------------------------------------|-------------|
| `MAX_SUBMIT_COUNT`              | イテレーションノードの並列実行に使用されるスレッドプール内の最大スレッド数。実質、イテレーションノードの入力配列の最大長です。100は不十分なため弊社では150に設定しています。  | 100         |


### Sandboxリソースの上限値最適化
対象となるファイルは、githubのdify-sandboxというgithubリポジトリの/conf/config.yamlです。

| 設定項目                        | 説明                                                                                 | デフォルト値 |
|---------------------------------|------------------------------------------------------------------------------------|-------------|
| `WORKER_TIMEOUT`        | sandbox上で実行されるコードの実行タイムアウト(秒)。デフォルト5秒で短すぎるため変更必須です。| 5        |
| `max_requests`       | sandboxへの同時リクエスト数(秒)。一般的には50で問題になることはほとんどありません。 | 50           |


## 6. ワークフロー実行時のネットワーク切断→バックエンド処理継続・結果未保存問題

現在Difyには、ワークフロー実行時のネットワーク切断に起因して実行結果が未保存となる既知のバグがあります。特に実行時間が長いワークフローで影響が大きく、ユーザーからの問い合わせも多い問題です。

### 問題の概要
ワークフローの実行中にクライアントとAPIリソース間のTCP接続が切断された場合、以下の問題が発生します：

1. メインスレッドが強制終了
2. バックエンド処理は継続実行
3. 処理結果が1の発生時点までしか保存されない

### クラウド環境での注意点
AWS環境では以下の場合にも同様の問題が発生します：
- ALBのタイムアウト
- ECSのサービスコネクト タイムアウト

これらのタイムアウト値は、システムテストを通じて最適化が必要です。

### ユーザーへの影響

#### UI上の表示
- 処理中の状態が継続して表示される
- 「ワークフローがいつまでも終わらない」という状況に見える

#### 検証用ワークフローでの確認
以下の簡単なワークフローで動作を確認できます：

**検証に利用したワークフロー:** 5秒スリープするだけのコードブロックを3つ並べた以下のようなワークフロー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/05ad3c0c-be63-f3a8-7829-e5a1ab665402.png)

テスト結果：
1. デバッグ実行時：
   - 一つ目のコードでブラウザを終了すると、以降の処理は継続するものの結果は未保存
   - 履歴上は"Running"状態のまま

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/75ec8d55-a132-79d8-9677-4cf810c81bef.png)

2. アプリからの実行時：
   - 結果が表示されず
   - ログにも記録されない

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/7ea00171-ba50-c9f1-48fa-ff6b6b3e5bc9.png)

**APIコールによる実行**: 以下のように1つ目のコードブロックの処理が開始したイベントを受領した瞬間にCtrl+cで接続を切断すると発生します。

```bash
curl -X POST 'https://xxxxxxxx.dify.davinci.speee.jp/v1/workflows/run' \
--header 'Authorization: Bearer app-xxxxxxxxxxxxxxxxxxxxxxxx' \
--header 'Content-Type: application/json' \
--data-raw '{
    "inputs": {},
    "response_mode": "streaming",
    "user": "kazuhisa.wada@speee.jp"
}'

data: {"event": "workflow_started", "workflow_run_id": "eed1e794-9f4b-4a1e-b3c2-dfcbe477ce83", "task_id": "2ce950d8-8bd9-41e1-94fb-d14161e07c59", "data": {"id": "eed1e794-9f4b-4a1e-b3c2-dfcbe477ce83", "workflow_id": "5f938634-66d7-4994-ad94-9424a5b4d6ba", "sequence_number": 6, "inputs": {"sys.files": [], "sys.user_id": "abc-123", "sys.app_id": "433e1e5e-4051-4c9f-8795-54a20bd2d1c4", "sys.workflow_id": "5f938634-66d7-4994-ad94-9424a5b4d6ba", "sys.workflow_run_id": "eed1e794-9f4b-4a1e-b3c2-dfcbe477ce83"}, "created_at": 1734586057}}

data: {"event": "node_started", "workflow_run_id": "eed1e794-9f4b-4a1e-b3c2-dfcbe477ce83", "task_id": "2ce950d8-8bd9-41e1-94fb-d14161e07c59", "data": {"id": "0a2d42cf-43f9-43e9-aba0-fd7eb388394d", "node_id": "1734584830659", "node_type": "start", "title": "\u958b\u59cb", "index": 1, "predecessor_node_id": null, "inputs": null, "created_at": 1734586057, "extras": {}, "parallel_id": null, "parallel_start_node_id": null, "parent_parallel_id": null, "parent_parallel_start_node_id": null, "iteration_id": null, "parallel_run_id": null}}

data: {"event": "node_finished", "workflow_run_id": "eed1e794-9f4b-4a1e-b3c2-dfcbe477ce83", "task_id": "2ce950d8-8bd9-41e1-94fb-d14161e07c59", "data": {"id": "0a2d42cf-43f9-43e9-aba0-fd7eb388394d", "node_id": "1734584830659", "node_type": "start", "title": "\u958b\u59cb", "index": 1, "predecessor_node_id": null, "inputs": {"sys.files": [], "sys.user_id": "abc-123", "sys.app_id": "433e1e5e-4051-4c9f-8795-54a20bd2d1c4", "sys.workflow_id": "5f938634-66d7-4994-ad94-9424a5b4d6ba", "sys.workflow_run_id": "eed1e794-9f4b-4a1e-b3c2-dfcbe477ce83"}, "process_data": null, "outputs": {"sys.files": [], "sys.user_id": "abc-123", "sys.app_id": "433e1e5e-4051-4c9f-8795-54a20bd2d1c4", "sys.workflow_id": "5f938634-66d7-4994-ad94-9424a5b4d6ba", "sys.workflow_run_id": "eed1e794-9f4b-4a1e-b3c2-dfcbe477ce83"}, "status": "succeeded", "error": null, "elapsed_time": 0.017287, "execution_metadata": null, "created_at": 1734586057, "finished_at": 1734586057, "files": [], "parallel_id": null, "parallel_start_node_id": null, "parent_parallel_id": null, "parent_parallel_start_node_id": null, "iteration_id": null}}

data: {"event": "node_started", "workflow_run_id": "eed1e794-9f4b-4a1e-b3c2-dfcbe477ce83", "task_id": "2ce950d8-8bd9-41e1-94fb-d14161e07c59", "data": {"id": "cca24767-09ce-41a6-b6b6-c205761e18aa", "node_id": "1734584832910", "node_type": "code", "title": "\u30b3\u30fc\u30c9", "index": 2, "predecessor_node_id": "1734584830659", "inputs": null, "created_at": 1734586057, "extras": {}, "parallel_id": null, "parallel_start_node_id": null, "parent_parallel_id": null, "parent_parallel_start_node_id": null, "iteration_id": null, "parallel_run_id": null}}

^C
```

実行ログですが、上記が処理終了すれば、アカウント列に当方のメールアドレスが記載されているログが表示されず、やはりログは表示されません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/4c5b43b7-8d0e-cda1-1810-1aefd08f79d7.png)

一方で、Ctrl+cでTCPを切断せずに、ワークフロー停止APIをキックする場合は、以下のようにSTOP by userで停止することはもちろん可能です。残念ながら現時点では、Ctrl＋cでTCP切断後にこのAPIをキックしても停止することはできません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/6caad744-8911-b66e-a4aa-007b5382255a.png)



**タイムアウトなどを待つ以外に正攻法で止める方法がないため要注意**

特に注意すべきは、再帰呼び出しを行うワークフローを起動してしまい、ロジックミスがあり無限ループした場合です。途中で停止不可能なため、アプリケーションコンフィグのWORKFLOW_MAX_EXECUTION_STEPSやAPP_MAX_EXECUTION_TIMEが重要になりますので、これらの上限値を極端に大きくすることがないようにしましょう。最悪のケースにおける対応策として、他のユーザに影響が出る形でもOKな場合は、sandboxやAPIリソースのサーバを落とす、利用しているLLMのAPIキー(Bedrockの場合は不可)をdeactivateする、、、、、などケースバイケースで対応方法はあります。

### 技術的な詳細

#### 根本原因
問題の核心は、ワークフロー実行時のマルチスレッド実行アーキテクチャにあります：

1. メッセージング方式
   - メインスレッド：リクエスト受付とDB更新を担当
   - ワーカースレッド：実際の処理実行を担当
   - Python標準キューによるpub/sub方式でイベント通知

2. 処理フロー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930093/76c312e3-1b23-229d-feb0-28fb5fd36211.png)

この設計により、メインスレッドが切断されても処理は継続しますが、結果の保存ができなくなります。

### 現在の対応状況
- Issue: 未登録。
- OSSコミュニティ貢献の一環で年末年始目処に当方で修正対応検討中

### 一時的な回避策
1. 長時間実行ワークフローの分割
2. タイムアウト値の適切な設定
3. 必要に応じてワークフローの再実行

## 7. Amazon Bedrock利用時の内部エラー対策

弊社では、LLMはAmazon Bedrockをメインに、特にClaudeをOregon Region US Cross Region Inferenceを利用しています。しかし、直近はBedrockの内部エラー起因の503エラーの発生頻度が思ったより高く、特に、チャットではなく1ワークフロー内でBedrockを100回以上コールする場合も弊社では多いため、ワークフロー単位で見ると遭遇確率が激増してしまいます。また、経験上は本事象が一度発生し始めると一定のタイムウインドウ内ではエラー発生頻度が3~20%程度まで跳ね上がる傾向があります。

AWS Supportによると、本件はRate Limitには無関係の内部的なエラー事象(詳細は非公開)らしく、クライアント側でのExponential Backoff+Jitterによるリトライ実装や、他リージョンへのFallbackの実装が必要との案内がなされました。しかし、現状はDifyのLLMアクセス周りはこのようなエラーハンドリングは未実装の状況です。

Issueは私が以下の通り発行しており、upcoming releaseにて検討するようですが、現状は未定となっております。

https://github.com/langgenius/dify/issues/11636

以上の状況のため、Amazon Bedrockをワークフローでヘビーユースする予定の方は、本件について頭の片隅に置いておくと良いかと思います。

ご参考までに、本事象発生時の見え方として、CloudWatchにてBedrockへのInvocation Errorの増加や、メッセージ"Bedrock is unable to process your request"とともに503が返却されます。

## 8. AWS上でのサイジングについての所感
当初、性能負荷試験の結果をサマリしたものを用いてワークロードの特徴とリソース使用状況をシェアする予定でしたが、本記事投稿までに性能負荷試験の検証結果をまとめ切れなかったため、ざっくりとしたワークロードの傾向についてのみ本節では言及します。

弊社でのDify利用における主なワークロードでは、LLMとAPIやHTTPブロックが多いため、ネットワークIOが多い傾向があります。このため、10分以上かかるワークフローを同時実行20-40程度で継続的に負荷をかけ続けても、APIリソースは1vCPUメモリ4GB程度で捌けることを確認しています(使用率80%以上となるため安定運用には多少の拡張は必要です)。webとsandboxはさらに小さいリソースで捌くことができます。

ただし、弊社における主な構成は以下の通りです。

**構成**
- サーバ(web/api/worker/sandbox)は全てECS Fargate: api,worker,sandboxは同じサービス上にデプロイし、特にapi-->sandboxの通信はlocalhost通信になるような構成としている
- redisはElastiCache for Redisで実装
- PostgresはRDS Aurora Postgresで実装
- ALBをnginxの代替として実装
- VDBはOpenSearchにより実装


## まとめ

以上、弊社でDifyの構築や運用を開始してつまづいたポイントから共通的なものをなるべくピックアップしてご紹介しました。

弊社ではDifyに非常に大きな可能性を感じており、引き続きAgent開発や機能拡張を進めていく予定です。本記事が、少しでもDifyコミュニティ版を自社運用するDifyユーザの皆様のお役に立てば幸いです。
そして、ご興味ある方は是非OSSコミュニティへ開発者として積極的に参加いただけると幸いです。一緒にDifyの発展に貢献していきましょう！