1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Dify のワークフローを GUI で組むのをやめた — DSL を Claude Code に書かせる

1
Posted at

Dify を進んで選んだことはない。業務自動化は n8n のほうが素直だし、実際に一度は Dify を捨てて n8n に寄せた。それでも Dify を書かざるを得ない場面がある。社内の AI 基盤が Dify で立っている、RAG とチャット UI が最初から付いてくる、動かす人が非エンジニアで画面が必要——理由は大抵、技術選定の外側にある。

問題は、そういう「選んだわけではない Dify」を GUI でポチポチ組むのが、思っている以上に遅いということだ。

結論から書くと、Dify のワークフローは DSL(YAML)で書いて、それを Claude Code に吐かせるのが速い。GUI には一度も戻らなくていい、とまでは言わない。むしろ後述するとおり、最初の1回だけは GUI を触る必要がある。ただそれ以降の設計・修正・複製は、全部テキストで済む。

この記事は、その手順と、実際にインポートが通らなくて潰した罠を順に書く。Dify 1.x / DSL version: 0.6.0 で確認したものだ。

GUI が遅い理由

Dify のワークフロー画面はよく出来ている。それでも遅い。

  • ノードを1つ足すたびに、変数参照をプルダウンで選び直す。 ノードが7つある頃には、どのノードのどの出力がどこに流れているかを画面から読み取れなくなる
  • プロンプトを直すのが苦しい。 システムプロンプトが50行あると、あの小さいテキストエリアの中でスクロールしながら直すことになる。差分も取れない
  • 同じ形のワークフローを作り直せない。 「入力を1つ増やしただけの別バージョン」を作るのに、複製してから全ノードを見て回る
  • レビューできない。 何を変えたのかを人に説明する手段がスクリーンショットしかない

一方 Dify は、ワークフロー全体を1枚の YAML(DSL)としてエクスポート/インポートできる。つまりワークフローはテキストである。テキストなら Claude Code が書けるし、git に乗るし、diff が出る。

最短の手順

4ステップ。順番が大事で、特に1番を飛ばすと後半で必ず詰まる。

1. まず GUI で適当なワークフローを1つ作って、エクスポートする

Start → LLM → End の3ノードでいい。中身は何でもいい。作ったら「DSLファイルをエクスポート」で YAML を落とす。

これが唯一の正解サンプルになる。 Dify の DSL はバージョンとプラグイン構成で細部が変わるので、ドキュメントやネットで拾った YAML は自分の環境では通らないことがある。自分の Dify が吐いた1枚が、最も信頼できる仕様書だ。

ここで1つ落とし穴がある。インポートした DSL をエクスポートし直しても検証にならない。 自分が書いた値がそのまま返ってくるだけで、Dify が正規化してくれたわけではない。「エクスポートし直したら同じだったから正しい」は成り立たない。正解サンプルは、必ず画面で組んだものから取る。

2. その YAML を Claude Code に読ませて、雛形として登録する

このYAMLは自分のDifyからエクスポートした実物。
これ以降Difyのワークフローを作るときは、このファイルの構造(キーの並び、
ノードの持ち方、エッジの書き方)に厳密に合わせて生成して。

構造を推測させないのがポイント。Dify の DSL は、後述するとおり直感に反する箇所がいくつかあって、そこを「たぶんこうだろう」で埋められるとインポートで落ちる。

3. やりたいことを日本語で渡す

入力: 検索語(テキスト1つ)
処理: LLMに関連語を8個までJSONで出させる
出力: そのJSON文字列
モデルは <自分の環境で使えるモデル名>

ノードの選定・座標の配置・エッジの張り方・変数参照の解決は全部任せる。ここが GUI との差が一番出るところで、ノード5〜10個くらいの構成なら数十秒で出てくる。

4. インポートして、画面で2箇所だけ確認する

スタジオ → アプリを作成する → DSLファイルをインポート

インポートが通ったら、次の2つは必ず画面で見る。DSL に書いた値が環境と合っていないことが多い。

  • LLM ノードのモデルが選択されているか。 未選択(空欄)になっていたら、プルダウンから選び直す
  • ファイルや画像を LLM に渡している場合、その入力が繋がっているか

この2つ以外は、だいたい書いたとおりに入る。

DSL の骨格

正解の構造はこうなっている。トップレベルは5キー。

app:
  description: 検索語から関連語を作る
  icon: 🔎
  icon_background: '#E8F6F5'
  icon_type: emoji
  mode: workflow            # or advanced-chat
  name: 検索ワード拡張
  use_icon_as_answer_icon: false
dependencies:               # 使うモデルプロバイダのプラグイン
- current_identifier: null
  type: marketplace
  value:
    marketplace_plugin_unique_identifier: langgenius/openai:1.0.1@b513bf84...
    version: null
kind: app
version: 0.6.0
workflow:
  conversation_variables: []
  environment_variables: []
  features: { ... }
  graph:
    edges: [ ... ]
    nodes: [ ... ]
    viewport: {x: 0, y: 0, zoom: 1}
  rag_pipeline_variables: []

workflow.graph の下に nodesedges を置く。ここが階層を間違えやすいところで、古い資料だと app.graph 直下にノードを置く書き方が出てくる。その構造は現行の Dify ではインポートできない(「アプリの作成に失敗しました」で終わる)。

潰した罠

ここが本題。全部インポートか実行で実際に踏んだ。

ノードの typecustom 固定

一番わかりにくい。ノードの種類は type ではなく data.type に書く。

- data:
    title: 関連語を作る
    type: llm              # ← 機能の種類はこっち
    model: { ... }
  id: expand
  position: {x: 340, y: 260}
  type: custom             # ← ここは常に custom
  width: 241
  height: 98

type: llm とトップレベルに書くと、Dify は未知のノードとして扱う。インポート自体は通ってしまうことがあり、画面を開くとブロックが真っ白になる。エラーが出ないので原因に辿り着きにくい。

width / height は必須

省略できそうに見えるが、省くと 150 × 21 px が当てられて中身が描画されない。実測値は幅 241〜242、高さ 94〜213 くらい。ノードの種類ごとに違うので、正解サンプルから拾う。

positionpositionAbsolute は同じ値を両方書く。

HTTP ノードの headers / params は文字列

これは実行時まで気づけない。{key: ..., value: ...} のリストで書くと、インポートは通り画面にも出るのに、実行すると弾かれる。

# ✗ 実行時エラー
headers:
  - key: Authorization
    value: Bearer xxx

# ○ 改行区切りの文字列
headers: 'Authorization: Bearer {{#start.token#}}'
params: ''

HTTP ノードの body は空でも書く

body キー自体が無いと、画面のレンダリングで JS が落ちてノードが開けなくなる。使わなくても必ず置く。

body:
  data: []
  type: none

ついでに retry_config について。書けるが、実測ではリトライは効いていない。ここに頼った設計にしないほうがいい。

dependencies の識別子を間違えるとインポートで落ちる

marketplace_plugin_unique_identifier はハッシュ付きの長い文字列で、当然覚えられない。手で書かず、正解サンプルからコピーする。 空配列にしてもインポート自体は通るが、モデルが解決されず結局画面で選び直すことになる。

provider の書式はバージョンで変わる

model:
  provider: openai                       # 古い書式
  provider: langgenius/openai/openai     # マーケットプレイス版プラグイン

Dify 1.x のマーケットプレイス経由でプラグインを入れていると、後者の形になる。前者を書くと、インポートは通ってモデルだけ未選択になる。ここも正解サンプルから取る。

そしてモデル名は、環境で実際に接続済みのものを書く。ドキュメントに載っている最新モデルが自分の Dify で使えるとは限らない(社内のセルフホストだと、管理者が繋いだものだけが生きている)。存在しないモデル名を書くと 404 になる。

version の値で弾かれることがある

インポートが理由もなく失敗するときは、まず1行目の version: を疑う。正解サンプルの値をそのままコピーするのが確実。ここを合わせるだけで通ることが何度かあった。

動く1枚(全文)

Start → LLM → End だけの最小構成。ここまでに書いた作法を全部含んでいる。モデル名とプラグイン識別子は自分の環境の値に差し替える。

app:
  description: 検索語から関連語を作って、検索に当たりやすくする
  icon: 🔎
  icon_background: '#E8F6F5'
  icon_type: emoji
  mode: workflow
  name: 検索ワード拡張
  use_icon_as_answer_icon: false
dependencies:
- current_identifier: null
  type: marketplace
  value:
    marketplace_plugin_unique_identifier: langgenius/openai:1.0.1@<自分の環境の値>
    version: null
kind: app
version: 0.6.0
workflow:
  conversation_variables: []
  environment_variables: []
  features:
    file_upload:
      enabled: false
      allowed_file_extensions: []
      allowed_file_types: []
      allowed_file_upload_methods: []
      number_limits: 3
    opening_statement: ''
    retriever_resource:
      enabled: true
    sensitive_word_avoidance:
      enabled: false
    speech_to_text:
      enabled: false
    suggested_questions: []
    suggested_questions_after_answer:
      enabled: false
    text_to_speech:
      enabled: false
      language: ''
      voice: ''
  graph:
    edges:
    - data:
        isInIteration: false
        sourceType: start
        targetType: llm
      id: start-expand
      source: start
      sourceHandle: source
      target: expand
      targetHandle: target
      type: custom
    - data:
        isInIteration: false
        sourceType: llm
        targetType: end
      id: expand-end
      source: expand
      sourceHandle: source
      target: end
      targetHandle: target
      type: custom
    nodes:
    - data:
        desc: 検索ページに入力された語を受け取る
        title: 開始
        type: start
        variables:
        - label: 検索語
          max_length: 200
          options: []
          required: true
          type: text-input
          variable: query
      height: 96
      id: start
      position: {x: 40, y: 260}
      positionAbsolute: {x: 40, y: 260}
      type: custom
      width: 242
    - data:
        context:
          enabled: false
          variable_selector: []
        desc: 検索に効く関連語だけを出す
        model:
          completion_params:
            temperature: 0.3
          mode: chat
          name: gpt-4o-mini
          provider: langgenius/openai/openai
        prompt_template:
        - id: sys-expand-001
          role: system
          text: |
            利用者が入力した語から、同じものを指しうる関連語を挙げてください。

            - 言い換え・上位語・下位語・別表記(漢字/かな/カタカナ/英語)を含めます
              例: 川 → 河川, 河原, 水辺, 渓流, リバー
            - 抽象語や感情語は入れません(例: 美しい, 印象的)
            - 元の語そのものは入れません
            - 多くて8語。関連が薄いものを無理に足さないでください
            - 出力は JSON のみ。コードフェンスを付けないでください

            {"terms": ["関連語1", "関連語2"]}

            思いつかない場合は {"terms": []} を返してください。
        - id: user-expand-001
          role: user
          text: '{{#start.query#}}'
        title: 関連語を作る
        type: llm
        variables: []
        vision:
          enabled: false
      height: 98
      id: expand
      position: {x: 340, y: 260}
      positionAbsolute: {x: 340, y: 260}
      type: custom
      width: 241
    - data:
        desc: 呼び出し元がこのJSONを読む
        outputs:
        - value_selector: [expand, text]
          variable: result_json
        title: 出力
        type: end
      height: 94
      id: end
      position: {x: 640, y: 260}
      positionAbsolute: {x: 640, y: 260}
      type: custom
      width: 241
    viewport: {x: 0, y: 0, zoom: 1}
  rag_pipeline_variables: []

変数参照は {{#<ノードID>.<変数名>#}} の形。上の例だと {{#start.query#}} で Start ノードの query を引いている。ノード ID を自分で読める名前(start / expand / end)にしておくと、後から読んだときにフローが目で追える。Dify が自動生成する ID は 1712... のような数字列なので、手書きするならここは自分で付けたほうがいい。

画像やファイルを LLM に渡す

vision を有効にして、どのノードのファイル出力を使うかを指定する。

vision:
  enabled: true
  configs:
    detail: high
    variable_selector: [<ファイルを出すノードのid>, files]

HTTP ノードでバイナリを取ってきて LLM に渡す構成は、Dify のバージョンによって成立しない。HTTP ノードの応答をファイル変数として扱えないバージョンだと、LLM ノードのファイル入力欄に候補として出てこない。インポート後に画面で確認して、出てこなければ設計を変える(外部のファイル API を HTTP ノードで直接叩く、など)。ここは DSL の書き方の問題ではないので、先に画面で1回試したほうが早い。

大きいファイルを通すとき(セルフホスト)

DSL とは別に、サーバ側の設定で詰まる。既定値のままだと動画のような大きいファイルは通らない。

HTTP_REQUEST_NODE_MAX_BINARY_SIZE=1073741824
UPLOAD_VIDEO_FILE_SIZE_LIMIT=1024        # 単位はMB
NGINX_CLIENT_MAX_BODY_SIZE=1024M

.env に書いて再起動する。NGINX_CLIENT_MAX_BODY_SIZE を上げないと nginx が先に 413 を返すので、他の値を上げても効かない。

それと HTTP ノードのタイムアウトは既定 30 秒。数百 MB のダウンロードは途中で切れる。

timeout:
  connect: 10
  read: 600
  write: 600

なお、.env を触れない環境(会社が管理しているセルフホスト Dify など)では、この上限は動かせない。その場合は「Dify に大きいファイルを持ち込まない」設計にするしかない。DSL を工夫しても越えられない壁なので、着手前に確認しておくと無駄が減る。

API から叩く

ワークフローを外から呼べるようにしておくと、GAS や n8n から使える。

curl -sS -X POST "$DIFY_BASE_URL/v1/workflows/run" \
  -H "Authorization: Bearer $DIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"inputs":{"query":"川"},"response_mode":"blocking","user":"test"}'

結果は data.outputs.<End ノードで定義した変数名> に入る。上の例なら data.outputs.result_json

この方法が向かないとき

正直に線を引いておく。

GUI のほうが速いケース

  • ノードが3つ以下。3ノードなら画面で組んだほうが早い
  • プロンプトを何十回も試行錯誤する段階。ここは画面のプレビューが強い
  • Knowledge(RAG)の設定。ナレッジベースの ID を DSL に書くことになるので、結局画面を触る

Dify 自体が向かないケース

外部サービス連携・cron・webhook が主役なら n8n のほうが素直だ。自分も一度そう判断して、Dify のワークフロー生成の仕組みを畳んで n8n に寄せた。

それでも数ヶ月後に Dify を書いている。理由は単純で、動かす環境が Dify だったからだ。ツールの優劣とは別のところで決まる。だから「Dify を使うべきか」ではなく「Dify を書かされたときにどう最短で終わらせるか」を用意しておくほうが、実務では効く。

Dify n8n
得意 RAG・チャットボット・LLM 前提のフロー 外部サービス連携・cron・webhook
非 AI のデータ処理 一応できるが設計が LLM 前提 得意
非エンジニアに渡す画面 最初から付いてくる 別途作る

両方立てて、n8n から Dify を API で呼ぶのが結局いちばん楽だった。

まとめ

  • Dify のワークフローはテキスト(DSL)である。だから Claude Code に書かせられる
  • 最初に1回だけ GUI で組んでエクスポートする。 これが唯一の正解サンプルになる。ネットで拾った YAML は自分の環境では通らない
  • インポートした DSL をエクスポートし直しても検証にならない
  • 踏みやすい罠: ノードの typecustom 固定(種類は data.type)/widthheight 必須/HTTP ノードの headers は文字列/body は空でも書く/versionproviderdependencies はサンプルからコピー
  • インポート後に画面で見るのは「モデルが選択されているか」と「ファイル入力が繋がっているか」の2箇所

GUI で30分かけて組んでいたものが、数分で出てくるようになる。加えて git に乗るので、diff が読めるようになるのが地味に大きい。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?