🧐

現場で使える!Dify x Pythonハイブリッド開発実践!

に公開

はじめに

こんにちは!株式会社Sapeetのソリューション事業部でアルゴリズムエンジニアとして働いている堀ノ内です。ソリューション事業部では受託開発を行なっており、近年は生成AIを活用したプロジェクトが非常に増えています。
弊社では開発を迅速に進めるため、PoCの段階ではノーコードツールであるDifyを活用しており、多くのプロジェクトに導入してきました。これまではDifyを単体で使うことが多かったのですが、ノーコードツールであるが故、少し凝った処理をしようとしたり、UIをリッチにしようとすると実現が難しいという課題があります。
こちらの解決策の1つとして、AI周りのワークフローはDifyで構築、それをPythonからAPI呼び出しするという方法が考えられます。これにより頻繁に発生する生成AIのプロンプト、モデル種類やアルゴリズムなどはDify上でスピーディに修正でき、Python側でデータのDB保存、複雑なロジックの実装やWebサーバの構築を行い、任意のUIにAIが処理した結果を表示することができます。
PoCを開発するプロジェクトで実際にこの構成にトライし、多くの学びを得ることができましたので、本ブログではそれらをご紹介したいと思います。これからDifyを活用していきたいと考えている方々の参考になれば幸いです!
なお、Difyの基本的な操作についてはWebに多くの記事があるため割愛させて頂きます。また今回はDify version 1.7.1を対象とします。

Dify導入のキッカケ

私の関わったプロジェクトでは元々AIワークフローを含むバックエンド全般をPython + LangChainで実装していました。しかし、お客様のニーズに合わせてプロンプトやAI関連の処理フローを頻繁に変更する必要があり、その都度Pythonのコードを修正するのが非常に手間という課題がありました。また、Sapeetではプロジェクトマネージャ(以降PMと記載)とエンジニアがタッグを組んでプロジェクトを進めるのですが、PMもプロンプトを修正できた方がより良いサービスが出来るのでは?との発想からDifyの導入を検討し始めました。

Difyを導入してみよう!

ここからはDify + Pythonでサービスを開発するにあたり、検討したことや学びを実際の開発の流れに沿ってご紹介します。是非、開発を追体験するようなイメージでご覧ください!

環境構築

クラウド版 vs セルフホスト版

開発するにあたりまず必要になるのがDifyを動かす環境です。Difyを使う場合、実行環境を丸ごと提供している「クラウド版」と、実行環境となるサーバを用意しそこに自前でホストする「セルフホスト版」の2つの手段があります。一般的なメリットデメリットは他のWebサイトでも言及されているため割愛し、ここではプロジェクトで使う場合に実際どちらが良いのか?について紹介します。

プロジェクトでは当初クラウド版を使っていました。しかし、稀にワークフローの実行が不安定になり失敗したり(サーバ側の負荷の影響と予想)、後述するPythonからのAPI呼び出しにおいてDifyのサーバの入り口に設置されているCloudflareにブロックされたりと安定したワークフローの実行ができないことがありました。後者に関してはサポートへの問い合わせを行うも明確な解決策を得ることができず、最終的にセルフホスト版に移行することでこれらの問題は解消しました。

Cloudflareにブロックされた時にレスポンスに含まれるメッセージ
Please enable cookies.
Sorry, you have been blocked
You are unable to access dify.ai
Why have I been blocked?
This website is using a security service to protect itself from online attacks. The action you just performed triggered the security solution. There are several actions that could trigger this block including submitting a certain word or phrase, a SQL command or malformed data.

What can I do to resolve this?
You can email the site owner to let them know you were blocked. Please include what you were doing when this page came up and the Cloudflare Ray ID found at the bottom of this page.

まずは環境が用意されているクラウド版で感触を掴み、その後セルフホストに移行することをオススメします。

実際の構築と費用感

それでは実際にセルフホストで構築してみましょう。弊社ではインフラとしてAWSを使用しており、AWS上に手っ取り早く環境を構築するにはAWSの公式ブログが参考になります。こちらのページにCloudFormationのテンプレートが紹介されており、必要なリソースの作成からDifyの立ち上げまで一気に行い、最終的にEC2上で動作する環境が完成します。ただし、ブログ中にも言及されている通りセキュリティや可用性について考慮されていない構成となっていますので、この辺りは適宜カスタマイズして利用しましょう。

テンプレートにはDifyのバージョンが記載されており、変更することで任意のバージョンで立ち上げることができます。今回はここを当時の最新版であった1.7.1としました。

Difyのバージョンを指定する箇所
sudo git checkout 1.1.3
sudo git pull origin 1.1.3

また気になる費用感ですが、これは基本的にEC2で立ち上げるインスタンスタイプに依存しており、テンプレートではt3.medium(vCPU2/メモリ4.0GiB)となっています。
実際にこのタイプで立ち上げて数人で開発を行いましたが特に問題無く利用できていますので、小規模の開発であればこのタイプで問題無さそうです。価格は執筆時点(2025年9月)で一時間あたり0.0418USDですので、月間30USD(=4500円)程度となります。クラウド版における小規模チーム向けのProfessionalプランは59USDですので、セルフホスト版の方が割安かつ安定した環境を手にいれることができます。

ワークフローの開発

環境が立ち上がったら、いよいよDifyでワークフローを構築します!今回はサンプルとして、Geminiを使った飲食店レシート文字起こし処理を作成しました。

作成したワークフロー
作成したワークフロー

このワークフローをエクスポートしたファイルも添付しておきます。

ワークフローをエクスポートしたもの
app:
  description: ''
  icon: 🤖
  icon_background: '#FFEAD5'
  mode: workflow
  name: レシート文字起こし
  use_icon_as_answer_icon: false
dependencies:
- current_identifier: null
  type: marketplace
  value:
    marketplace_plugin_unique_identifier: langgenius/gemini:0.2.4@1dc8cd4a3198584fd7639023951f388fff023caa234a4bdb8bba69d6091c2e64
kind: app
version: 0.3.1
workflow:
  conversation_variables: []
  environment_variables: []
  features:
    file_upload:
      allowed_file_extensions:
      - .JPG
      - .JPEG
      - .PNG
      - .GIF
      - .WEBP
      - .SVG
      allowed_file_types:
      - image
      allowed_file_upload_methods:
      - local_file
      - remote_url
      enabled: false
      fileUploadConfig:
        audio_file_size_limit: 50
        batch_count_limit: 5
        file_size_limit: 15
        image_file_size_limit: 10
        video_file_size_limit: 100
        workflow_file_upload_limit: 10
      image:
        enabled: false
        number_limits: 3
        transfer_methods:
        - local_file
        - remote_url
      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
        isInLoop: false
        sourceType: start
        targetType: if-else
      id: 1757664485803-source-1757664829041-target
      selected: false
      source: '1757664485803'
      sourceHandle: source
      target: '1757664829041'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInLoop: false
        sourceType: if-else
        targetType: llm
      id: 1757664829041-true-1757664518411-target
      selected: false
      source: '1757664829041'
      sourceHandle: 'true'
      target: '1757664518411'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        isInLoop: false
        sourceType: if-else
        targetType: llm
      id: 1757664829041-false-1757664863920-target
      selected: false
      source: '1757664829041'
      sourceHandle: 'false'
      target: '1757664863920'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInLoop: false
        sourceType: llm
        targetType: end
      id: 1757664518411-source-1757664637426-target
      source: '1757664518411'
      sourceHandle: source
      target: '1757664637426'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInLoop: false
        sourceType: llm
        targetType: end
      id: 1757664863920-source-1757664908241-target
      source: '1757664863920'
      sourceHandle: source
      target: '1757664908241'
      targetHandle: target
      type: custom
      zIndex: 0
    nodes:
    - data:
        desc: ''
        selected: false
        title: 開始
        type: start
        variables:
        - allowed_file_extensions: []
          allowed_file_types:
          - image
          allowed_file_upload_methods:
          - local_file
          label: file
          max_length: 48
          options: []
          required: true
          type: file
          variable: file
        - label: function
          max_length: 48
          options:
          - オーダー抽出
          - 店名抽出
          required: true
          type: select
          variable: function
      height: 116
      id: '1757664485803'
      position:
        x: 30
        y: 373
      positionAbsolute:
        x: 30
        y: 373
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        context:
          enabled: false
          variable_selector: []
        desc: ''
        model:
          completion_params:
            temperature: 0.7
          mode: chat
          name: gemini-2.5-flash-preview-05-20
          provider: langgenius/gemini/google
        prompt_template:
        - id: 7f8dd5bf-5f36-415e-8ec3-a3a9042d3efc
          role: system
          text: レシート画像に含まれるメニューの名前と金額と注文個数を抽出してください。
        selected: false
        structured_output:
          schema:
            properties:
              items:
                items:
                  properties:
                    amount:
                      minimum: 0
                      type: number
                    menu_name:
                      type: string
                    quantity:
                      minimum: 1
                      type: integer
                  required:
                  - menu_name
                  - amount
                  - quantity
                  type: object
                type: array
            required:
            - items
            type: object
        structured_output_enabled: true
        title: メニュー抽出LLM
        type: llm
        variables: []
        vision:
          configs:
            detail: high
            variable_selector:
            - '1757664485803'
            - file
          enabled: true
      height: 90
      id: '1757664518411'
      position:
        x: 789
        y: 258
      positionAbsolute:
        x: 789
        y: 258
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        desc: ''
        outputs:
        - value_selector:
          - '1757664518411'
          - structured_output
          value_type: object
          variable: structured_output
        - value_selector:
          - '1757664518411'
          - usage
          value_type: object
          variable: usage
        selected: false
        title: 終了
        type: end
      height: 116
      id: '1757664637426'
      position:
        x: 1171.463804654296
        y: 258
      positionAbsolute:
        x: 1171.463804654296
        y: 258
      selected: true
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        cases:
        - case_id: 'true'
          conditions:
          - comparison_operator: is
            id: 81ace52c-8e28-4352-815f-f89aa6979a18
            value: オーダー抽出
            varType: string
            variable_selector:
            - '1757664485803'
            - function
          - comparison_operator: is not
            id: e855abbf-744b-4376-95ad-504bf88d778a
            value: 店名抽出
            varType: string
            variable_selector:
            - '1757664485803'
            - function
          id: 'true'
          logical_operator: and
        desc: ''
        selected: false
        title: IF/ELSE
        type: if-else
      height: 152
      id: '1757664829041'
      position:
        x: 364
        y: 373
      positionAbsolute:
        x: 364
        y: 373
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        context:
          enabled: false
          variable_selector: []
        desc: ''
        model:
          completion_params:
            temperature: 0.7
          mode: chat
          name: gemini-2.5-flash-preview-05-20
          provider: langgenius/gemini/google
        prompt_template:
        - id: 1675d249-af78-420d-bffb-67bc715a25a5
          role: system
          text: レシート画像に含まれる店名を抽出してください。
        selected: false
        structured_output:
          schema:
            properties:
              store_name:
                type: string
            required:
            - store_name
            type: object
        structured_output_enabled: true
        title: 店名抽出LLM
        type: llm
        variables: []
        vision:
          configs:
            detail: high
            variable_selector:
            - '1757664485803'
            - file
          enabled: true
      height: 90
      id: '1757664863920'
      position:
        x: 789
        y: 574.5311517897978
      positionAbsolute:
        x: 789
        y: 574.5311517897978
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        desc: ''
        outputs:
        - value_selector:
          - '1757664863920'
          - structured_output
          value_type: object
          variable: structured_output
        - value_selector:
          - '1757664863920'
          - usage
          value_type: object
          variable: usage
        selected: false
        title: 終了 2
        type: end
      height: 116
      id: '1757664908241'
      position:
        x: 1171.463804654296
        y: 574.5311517897978
      positionAbsolute:
        x: 1171.463804654296
        y: 574.5311517897978
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    viewport:
      x: 151.4389728877689
      y: 76.96942079324572
      zoom: 0.5918689215403616

このワークフローでは、ユーザがレシート画像をアップロード、functionのドロップダウンメニューで「オーダー抽出」を選択した場合、レシートから注文したメニュー名、単価、注文個数を抽出、「店名抽出」を選択した場合、店名のみを抽出します。オーダー抽出と店名抽出は同時に行うこともできますが、今回は1つのワークフローで複数機能を実装する例としてあえて処理を分けています。

実行画面
実行画面

また入力データとして以下の画像を用います。

娘二人と行った思い出のサイゼリア
娘二人と行った思い出のサイゼリア

ここからはワークフローのポイントを解説します。

ポイント1: 似た処理は1つのワークフローにまとめる

最終的にこのワークフローをPythonからAPI呼び出しすることが目標なのですが、1ワークフローにつき、APIキーを1つ発行する必要があります。ワークフローが増えてくるとその分APIキーも増えてしまい、管理が煩雑となるため、似た処理は1つのワークフローにまとめ、実行時のパラメータでどの処理を実行するか指定できるようにしました。
開始ノードの必須入力フィールドは画像ファイルと実行する処理を指定するfunctionで構成されており、functionの入力フィールドのオプションに「オーダー抽出」と「店名抽出」を設定しています。このfunctionの値を受け取り、IF/ELSEノードで処理の分岐を行います。

開始ノードの入力に画像ファイルと処理分岐用の選択肢を設定
開始ノードの入力に画像ファイルと処理分岐用の選択肢を設定

処理分岐用の選択肢に「オーダー抽出」と「店名抽出」を設定
処理分岐用の選択肢に「オーダー抽出」と「店名抽出」を設定

処理分岐の設定
処理分岐の設定

ポイント2: LLMの構造化出力を使おう

実際に画像を解析するLLMは以下のような設定にしました。モデルはGemini 2.5 Flash Preview 05-20です。

LLMノードの設定
LLMノードの設定

単純にシステムプロンプトだけを書いてもメニューの名前などを抽出してくれるのですが、実行する度に微妙に出力の形式が変わったり、余計な文字列が付いてきたりします。

出力例
レシート画像から抽出したメニュー、金額、注文個数は以下の通りです。

メニュー名: 小エビのサラダ, 金額: ¥350, 注文個数: 1
メニュー名: 辛味チキン, 金額: ¥300, 注文個数: 1

最終的にこの出力をPythonで扱うことを考えるとプログラムで扱いやすいjson形式が望ましいため、構造化出力を使います。出力変数の右にある構造化出力のトグルをONにし、structured_outputの右の設定ボタンを押します。すると構造化データスキーマという出力のデータ構造を指定する画面が表示されます。

構造化出力の設定をONに
構造化出力の設定をONに

出力する構造を設定するための画面
出力する構造を設定するための画面

この画面ではGUIでフィールドを追加したり、JSONで構造を作成して読み込ませたりすることができるのですが、この作業は煩雑でかつ失敗しやすいのが難点です。しかし、なんとDifyにはAIサポート機能が付随しており、以下のように自然言語で指示するとスキーマを生成してくれます!

スキーマの生成をAIに依頼
スキーマの生成をAIに依頼

生成されたスキーマは以下のようになりました。

生成されたスキーマ
生成されたスキーマ

ワークフローを実行するとstructured_outputのキーの箇所に構造化されたデータが出力されるようになります。メニュー名、単価、注文個数が正しく抽出されていますね!
ちなみにOpenAIのgpt-4oを使うと精度が悪く一部期待したデータが抽出されませんでした。簡単にモデルを切り替えて評価ができるのもDifyの強みですね。

構造化された出力データ
{
  "text": "{\"items\": [{\"menu_name\": \"小エビのサラダ\", \"amount\": 350, \"quantity\": 1}, {\"menu_name\": \"辛味チキン\", \"amount\": 300, \"quantity\": 1}, {\"menu_name\": \"ポップコーンシュリンプ\", \"amount\": 300, \"quantity\": 1}, {\"menu_name\": \"小エビのカクテル\", \"amount\": 280, \"quantity\": 1}, {\"menu_name\": \"ソーセージピザ\", \"amount\": 400, \"quantity\": 1}, {\"menu_name\": \"若鶏のディアボラ風\", \"amount\": 500, \"quantity\": 1}, {\"menu_name\": \"チョコレートケーキ\", \"amount\": 300, \"quantity\": 2}]}",
  "usage": {
    "prompt_tokens": 1460,
    "prompt_unit_price": "0.15",
    "prompt_price_unit": "0.000001",
    "prompt_price": "0.000219",
    "completion_tokens": 621,
    "completion_unit_price": "0.6",
    "completion_price_unit": "0.000001",
    "completion_price": "0.0003726",
    "total_tokens": 2081,
    "total_price": "0.0005916",
    "currency": "USD",
    "latency": 4.830918299034238
  },
  "finish_reason": "STOP",
  "structured_output": {
    "items": [
      {
        "menu_name": "小エビのサラダ",
        "amount": 350,
        "quantity": 1
      },
      {
        "menu_name": "辛味チキン",
        "amount": 300,
        "quantity": 1
      },
      {
        "menu_name": "ポップコーンシュリンプ",
        "amount": 300,
        "quantity": 1
      },
      {
        "menu_name": "小エビのカクテル",
        "amount": 280,
        "quantity": 1
      },
      {
        "menu_name": "ソーセージピザ",
        "amount": 400,
        "quantity": 1
      },
      {
        "menu_name": "若鶏のディアボラ風",
        "amount": 500,
        "quantity": 1
      },
      {
        "menu_name": "チョコレートケーキ",
        "amount": 300,
        "quantity": 2
      }
    ]
  },
  "files": []
}

ポイント3: 実行コストを取得しよう

生成AIを使うサービスを構築するにあたりコストの把握はとても重要です。PythonからこのワークフローをAPI呼び出しすることを考えた場合、Python側でコストも取得しDBに入れて集計したいというケースがあります。LLMノードの出力にはusageという項目があり、ここにtotal_priceという名前でコストが格納されています。抽出結果であるstructured_outputに加えて、usageも終了ノードに追加してあげることで、Python側でコストを得ることができます。

抽出したデータに加えてusageも一緒に返す
抽出したデータに加えてusageも一緒に返す

Pythonからの呼び出し

Dify上でワークフローの作成が完了しましたので、次はこのワークフローをPythonからAPI呼び出しします。このためにエンドポイントの把握とAPIキーの発行を行う必要があるのですが、こちらに関してはDify上のサイドバーにある「APIアクセス」のページを開くことでドキュメントを見ることができ、またWeb上に情報も多いため割愛します。

API呼び出しのために「APIアクセス」ページを開く
API呼び出しのために「APIアクセス」ページを開く

レシート画像をアップロードしてLLMによる解析を行うには、まずファイルアップロードAPIでファイルをアップロードしファイルIDを取得、ファイルIDに対してワークフローを実行するという流れとなります。今回作成したコードは以下です。

サンプルコード
import os
import uuid
from enum import Enum
from typing import Optional, Tuple

import requests
from dotenv import load_dotenv

load_dotenv()

# DIFY_API_KEYとDIFY_BASE_URLを環境変数にセットした状態で実行してください
DIFY_API_KEY = os.getenv("DIFY_API_KEY", "")
DIFY_BASE_URL = os.getenv("DIFY_BASE_URL", "")

WORKFLOW_RUN_URL = f"{DIFY_BASE_URL}/workflows/run"
FILE_UPLOAD_URL = f"{DIFY_BASE_URL}/files/upload"

class WorkflowFunction(Enum):
    ORDER_EXTRACTION = "オーダー抽出"
    STORE_NAME_EXTRACTION = "店名抽出"

def upload_file(user_id: str, file_path: str) -> Tuple[bool, str]:
    headers = {
        "Authorization": f"Bearer {DIFY_API_KEY}",
    }
    try:
        with open(file_path, "rb") as f:
            files_payload = {
                "user": (None, user_id),
                "file": (os.path.basename(file_path), f, "image/jpeg"),
            }
            response = requests.post(
                FILE_UPLOAD_URL, headers=headers, files=files_payload
            )

            response.raise_for_status()
            file_info = response.json()
            file_id = file_info.get("id")
            return True, file_id
    except Exception as e:
        print(f"Failed to upload file: {e}")
        return False, ""

def run_workflow(
    user_id: str,
    file_id: str,
    function: WorkflowFunction,
) -> Tuple[bool, Optional[str], float]:
    headers = {
        "Authorization": f"Bearer {DIFY_API_KEY}",
        "Content-Type": "application/json",
    }

    json_payload = {
        "inputs": {
            "file": {
                "type": "image",
                "transfer_method": "local_file",
                "upload_file_id": file_id,
            },
            "function": function.value,
        },
        "response_mode": "blocking",
        "user": user_id,
    }

    try:
        response = requests.post(WORKFLOW_RUN_URL, headers=headers, json=json_payload)
        response.raise_for_status()
        response_json = response.json()

        status = response_json.get("data", {}).get("status")
        error = response_json.get("data", {}).get("error", "")
        outputs = response_json.get("data", {}).get("outputs", {})

        if status != "succeeded" or error:
            print(f"Failed to run workflow: {response_json}")
            return False, None, 0.0

        structured_output = outputs.get("structured_output", {})
        usage = outputs.get("usage", {})
        cost = usage.get("total_price", 0.0)
        return True, structured_output, cost

    except requests.exceptions.RequestException as e:
        print(f"Failed to run workflow: {e}")
        return False, None, 0.0

def main():
    # 画像のファイルパス
    file_path = "20250929_205528.jpg"
    user_id = str(uuid.uuid4())

    # 画像のアップロード
    success, file_id = upload_file(user_id, file_path)
    if not success:
        print(f"Failed to upload file: {file_id}")
        return

    # ワークフローの実行
    success, result, cost = run_workflow(
        user_id, file_id, WorkflowFunction.ORDER_EXTRACTION,
        # user_id, file_id, WorkflowFunction.STORE_NAME_EXTRACTION
    )
    if not success:
        print("Failed to run workflow")
        return

    print(f"{cost=}")
    print(result)

if __name__ == "__main__":
    main()

ポイントは以下の箇所であり、ワークフロー実行時に処理分岐用の文字列を一緒に渡すことでDify上で任意の処理を行うことができます。

class WorkflowFunction(Enum):
  ORDER_EXTRACTION = "オーダー抽出"
  STORE_NAME_EXTRACTION = "店名抽出"

...

success, result, cost = run_workflow(
    user_id, file_id, WorkflowFunction.ORDER_EXTRACTION
    # user_id, file_id, WorkflowFunction.STORE_NAME_EXTRACTION
)

以下のような結果が得られました。構造化されたデータやコストが期待通り取得できています!

# メニュー抽出
cost='0.0004176'
{'items': [{'menu_name': '小エビのサラダ', 'amount': 350, 'quantity': 1}, {'menu_name': '辛味 チキン', 'amount': 300, 'quantity': 1}, {'menu_name': 'ポップコーンシュリンプ', 'amount': 300, 'quantity': 1}, {'menu_name': '小エビのカクテル', 'amount': 280, 'quantity': 1}, {'menu_name': 'ソーセージピザ', 'amount': 400, 'quantity': 1}, {'menu_name': '若鶏のディアボラ風', 'amount': 500, 'quantity': 1}, {'menu_name': 'チョコレートケーキ', 'amount': 300, 'quantity': 2}]}

# 店名抽出
cost='0.0000486'
{'store_name': 'サイゼリヤ'}

バージョン管理

PythonからAPI呼び出しすることで、AI周りの処理はDify、それ以外の処理はPythonで実行できるようになりました。
ここで気になるのはDifyのワークフローがGUI上に置かれており、誰かが間違って削除したり意図しない変更をしてしまうことです。サービス開発を行うにあたり、このような不測の事態やデグレが発生した場合にバージョンを戻せるように管理できることが望ましいです。一応Dify上でもバージョン管理の仕組みがあるのですが、今回はPythonのコードとセットで管理したいため、gitを用います。
DifyはワークフローをDSLという形でエクスポートしてローカルに保存したり、ローカルのDSLをインポートする機能があり、ある程度機能の区切りがついたタイミングなどでエクスポートしてDSLをgitで管理しておくと、何かあった際にインポートしてそのバージョンに戻すことができます。

インポートとエクスポート
インポートとエクスポート

DLSの実態はymlファイルであり、意外とgit diffで差分が見やすく表示されるため、バージョンごとの差分も把握できます。以下はプロンプト修正前後のdiffです。

@@ -198,8 +198,8 @@ workflow:
         prompt_template:
         - id: 7f8dd5bf-5f36-415e-8ec3-a3a9042d3efc
           role: system
-          text: レシート画像に含まれるメニューの名前と金額と注文個数を抽出してください。
+          text: レシート画像に含まれるメニューの名前だけを抽出してください。
         structured_output:
           schema:
             properties:

最後に

Difyの導入検討から環境構築、Dify×Pythonによるハイブリッド開発、バージョン管理まで、開発の一連の流れとそこから得られた知見をご紹介しました。本ブログを通じて、導入のイメージが少しでも膨らんだのであれば幸いです。
この分野はまだ発展途上でベストプラクティスと呼べるものが無く試行錯誤の連続です。そうした状況の中でも、SapeetのメンバーはAIの可能性を信じ、最新の技術をキャッチアップしながら、現場の創意工夫を重ねてお客様に価値のあるサービスを開発しています。

最後までお読みいただきありがとうございました。もしSapeetにご興味をお持ち頂けましたら、ぜひ以下の採用サイトからご連絡ください。今が一番面白いフェーズです!

https://open.talentio.com/r/1/c/sapeet/homes/4557

株式会社Sapeet テックブログ

Discussion