• トップ
  • ブログ一覧
  • Dify×Google AI Studioで簡単LLMアプリ開発 ― GeminiとRAGでごみ分別AIを作ってみた
  • Dify×Google AI Studioで簡単LLMアプリ開発 ― GeminiとRAGでごみ分別AIを作ってみた

    はじめに

    LLMアプリをコードなしで視覚的に開発できるプラットフォーム「Dify」を触ってみました。

    Difyとは何か

    Difyは、プロンプト管理、RAG(検索拡張生成)、APIによる外部連携までを一つのGUI上で完結できるオープンソースのツールです。

    環境は「クラウド版」と「セルフホスト版」の2種類があり、用途に応じて使い分けられます。

    比較項目クラウド版(Sandbox)セルフホスト版(Docker)
    環境構築アカウント登録のみ、即日利用可Docker環境の構築が必要
    料金無料無料(サーバー費用は自己負担)
    データの保管場所Difyのクラウド上自前のローカル/サーバー上
    無料で使えるLLM特定モデルが回数制限付きで利用可 ※1自分でAPIキーを用意して接続する必要あり
    メッセージ数の上限200回/月 ※2制限なし
    カスタマイズ性Difyの提供範囲内設定・拡張を自由に変更可

    ※1 クラウド版に含まれるLLMの無料枠はDifyプラットフォームが提供するものです。別途、自身で用意したAPIキーを使うことも可能です。
    ※2 メッセージ数はDifyプラットフォーム側のカウントで、Difyアプリ上でチャットをやり取りした回数です。LLMプロバイダー側のレート制限とは別の制限です。

    クラウド版のSandboxプランは環境構築が不要で試せるため、とりあえず触ってみたい場合に向いています。
    セルフホスト版はデータを外部に送りたくない場合や、制限なしで検証したい場合に向いているかと思います。
    今回はセルフホスト版を中心に、動作検証を行いました。

    なお、セルフホスト版のLLMには、開発者向けに無料枠が提供されているGoogle AI Studio(Gemini API)を活用しています。
    アカウント登録後すぐにAPIキーを発行でき、無料枠の範囲内であれば費用なしで使い始められます。
    今回のような検証を行うにあたって、少し試してみたいんだよな…という場合に便利です。

    この記事で作るもの

    検証の題材として「ごみ分別AI」を構築しました。
    チャット形式のWebアプリで「◯◯(ごみ名)は△△市では何ごみですか?」と入力すると、DifyのRAG(ナレッジ機能)が特定の自治体のルールを参照し、分別方法を返す仕組みです。

    全体の作業の流れはざっくり以下のとおりです。

    1. ローカルにセルフホスト版Difyを立てる/クラウド版のアカウント版を取得する
    2. まずはシンプルなチャットフローを作り、LLMとつなぐ
    3. 自治体ごとの分別情報をナレッジに登録し、RAGを構築する
    4. 最後にWebアプリからDify APIを叩いて動かす

    それでは順を追って説明していきます。


    Difyの環境準備

    クラウド版のセットアップ

    公式サイト(Dify.ai)からGitHubやGoogleアカウントでサインアップするだけです。アカウント登録後、すぐにダッシュボードから利用できます。

    セルフホスト版(Docker)の構築

    公式リポジトリをクローンし、docker compose upを実行するだけで、Difyを動かすためのバックエンド、フロントエンド、ベクトルDB(Weaviate)など、必要なコンテナ群が一発で立ち上がります。

    1git clone https://github.com/langgenius/dify.git
    2cd dify/docker
    3cp .env.example .env
    4docker compose up -d

    起動後、http://localhostにアクセスして管理者アカウントを作成すれば準備完了です。


    基本的なチャットフローの構築

    ダッシュボードの「アプリを作成する」から「チャットフロー」を選択します。
    Difyでは、各処理のまとまりである「ノード」を視覚的に繋いでアプリを作っていきます。

    まずはRAGを挟まない、シンプルな「開始ノード → LLMノード →回答ノード」の構成を作成しました。

    LLMにGoogle AI Studioを接続する

    ここではDocker版のDifyでLLMと接続する手順について説明します。

    Difyで「設定」→「モデルプロバイダー」を開き、Googleを選択してAPIキーを入力します。

    設定が完了したら、LLMノードでGeminiのモデルを選択します。

    フローを実際に動かしてみる

    フロー画面右上の「プレビュー」ボタンから動作確認ができます。 試しに
    「ペットボトルは沖縄市では何ごみですか?」
    と入力してみます。

    無事フローが動作し、LLMが一般的な知識で回答してくれました。
    これだけでもちょっと感動。
    といっても、これでは普段LLMに直接質問しているのと変わりませんし、この段階では自治体ごとのルールも一切考慮されていません。

    ということで、ここからRAGを組み込んでいきます。


    RAGを使ってごみ分別に対応する

    ナレッジ機能について

    LLMは学習データに含まれない情報(例えば社内の情報など。今回のケースでは「各自治体独自のごみ分別ルール」)を答えることができません。
    これを補うのが、外部ドキュメントを検索してLLMに文脈として与えるRAG(検索拡張生成) という仕組みです。

    DifyではこのRAGが「ナレッジ」機能として統合されており、データをアップロードするだけで、以下の複雑な処理を自動で行ってくれます。

    1【処理の流れ】
    2ユーザー入力 ➔ ナレッジ検索(ベクトル検索) ➔ 関連データの抽出 ➔ LLMによる回答生成

    あらかじめ各自治体のデータをナレッジに登録しておくことで、ユーザーの質問に合わせた情報を自動でLLMへ引き渡せるようになります。

    ごみ分別データを用意してナレッジを作成する

    ナレッジ機能に登録するドキュメントとして、実際の自治体サイトで公開されている、ごみ分別ルールが掲載されたPDFを用います。
    ナレッジ機能でファイルをアップロードすることで、インデックス化が実施され、ドキュメントがチャンク単位でベクトルDBに格納されます。

    なお、ナレッジ作成時の設定は以下のとおりです。今回はどちらもデフォルトのまま使用しました。

    インデックスモード:高品質
    テキストをEmbeddingモデルでベクトル化する方式です。「経済的」モードよりも検索精度が高い反面、Embeddingモデルへのリクエストが発生します。

    検索設定:ベクトル検索
    クエリとドキュメントの意味的な類似度でチャンクを取得する方式です。
    フルテキスト検索(キーワード一致)やハイブリッド検索(両方の組み合わせ)も選択できますが、今回は構造化されたExcelデータとベクトル検索の組み合わせで十分な精度が得られたため、リランキング(Reranking)の設定も行っていません。

    チャットフローに「知識検索」ノードを追加する

    フロービルダーでノード構成を以下のように変更します。

    1[開始][知識検索][LLM][回答]

    LLMノードのプロンプトも更新します。

    1あなたは自治体のごみ分別を案内するアシスタントです。 
    2以下のナレッジベースの情報をもとに回答してください。
    3{{#context#}} 
    4
    5ナレッジベースに情報がない場合は「お調べの自治体の情報は登録されていません」と答えてください。

    自治体ごとのルール差異をRAGで吸収できるか検証する

    ここからが、今回の検証で最も試行錯誤が必要だったRAGの精度検証フェーズです。

    非構造化データの壁

    最初は、実際の自治体サイトで配布されている、ごみの分別方法が掲載されているPDFをそのままナレッジにアップロードして検証しました。
    複数自治体のPDFをそのままRAGに学習させて「〇〇市では何ごみですか?」と質問すれば、市を指定することで出し分けてくれると期待したのです。

    しかし実際にテストしてみると、別の自治体のルールが混ざって返ってきたり、登録済みのどのファイルからの引用かを教えてくれるのですが、実際にはそのファイルに関連情報が載っていなかったりと、想定と異なる結果になってしまいました。
    非構造化データ(PDF)をそのまま突っ込むだけでは、細かいルールの出し分けでうまくいかない結果となりました。

    アプローチの変更

    そこで、PDFの切り出し精度に頼るのをやめ、市区町村ごとの分別情報をExcelでマトリクス化(構造化)してナレッジに登録し直すことにしました。
    ポイントは、AIが迷わないよう「1行=1品目・1自治体」になるようデータを整理したことです。カラム構成も極めてシンプルにまとめました。

    【Excelデータのサンプル】

    市区町村品目分別カテゴリ
    沖縄市ペットボトル資源ごみ
    名護市ペットボトルペットボトル
    那覇市ペットボトル資源化物

    改めて検証:自治体ごとのルール差異を見事に吸収

    構造化データを再登録したRAGに対し、それぞれの市で「ペットボトル」について実際に質問してみました。

    質問1:「ペットボトルは那覇市では何ごみですか?」

    回答:那覇市では、ペットボトルは「資源化物」として分別してください。

    質問2:「ペットボトルは沖縄市では何ごみですか?」

    回答:沖縄市では、ペットボトルは「資源ごみ」に分類されます。

    ナレッジ内に複数自治体のデータが混在していても、質問文の「〇〇市」というキーワードをベクトル検索が正確に拾い、正しいルールで出し分けることができました。
    RAGにおいては、「最初からAIが迷わない形にデータを構造化して渡す」ことの重要性を痛感しました。


    開発中に遭遇したその他のエラーと対策

    RAGの利用以外に、Difyを使っている中で詰まったポイントを記載しておきます。同じエラーが出る方の対応方法の参考になれば幸いです。

    429エラー:チャット中のクォータ制限にぶつかる

    フローを試していると、急にこんなエラーが返ってきました。

    1Error: 429 Too Many Requests
    2RESOURCE_EXHAUSTED: Quota exceeded for quota metric
    3'generate_content_request_count' and limit 'GenerateContent
    4request limit per minute for a region'

    Google AI Studioの無料枠には1分あたりのリクエスト数(RPM)に上限があります。
    フローをテストしながら短時間に何度もリクエストを送ったために制限に引っかかってしまいました。

    しばらく待つことで解消されましたが、検証が止まってしまうことは地味にストレスでした。

    PDFの画像枚数制限エラー:1リクエストあたりの画像数を超えてしまう

    画像や図が多く含まれるPDFをアップロードすると、429とは別のエラーが発生しました。

    1[models] Error: Too many images in batch: 78. Gemini Embedding 2 supports at most 6 images per request.

    原因はGemini Embedding 2の仕様です。PDF内の各ページがマルチモーダルデータ(画像)としてパースされ、ベクトル化(Embedding)の1リクエストあたりの制限(最大6枚)を超えてしまったのが原因です。
    テキスト中心のデータであれば、PDFをあらかじめテキストファイル(.txt)やMarkdown(.md)に変換してからアップロードするか、PDFを5ページ単位で分割して投入することで回避できます。

    今回は①のPDFを分割してアップロードする方法で対処しました。
    RAGに登録する時、こういった情報の加工のひと手間が必要になるのが大変に感じました。


    WebアプリとDifyを連携させる

    Difyのプレビュー機能で問題なく問い合わせに回答できているのが確認できたら、あとはWebアプリケーションと連携させるだけです。

    DifyのAPIキーを取得する

    Difyのアプリ画面左サイドバーから「APIアクセス」を開くと、APIキーとエンドポイントが確認できます。
    「新しいシークレットキーを作成」ボタンでキーを発行しておきます。
    セルフホスト版の場合、エンドポイントは http://localhost/v1 になります。

    チャット形式のWebアプリを実装する

    今回フロントエンドはNext.jsで実装しています。
    Dify APIの /chat-messagesエンドポイントにPOSTするだけでチャット機能が動きます。

    1. Dify APIクライアント(src/lib/dify.ts)
      Difyへのリクエストを担当する関数。サーバーサイド専用で、APIキーはここで使用。
    1const DIFY_BASE_URL = process.env.DIFY_BASE_URL ?? "https://api.dify.ai/v1";
    2const DIFY_API_KEY = process.env.DIFY_API_KEY;
    3
    4export async function callDifyChatApi(
    5  message: string,
    6  conversationId: string | null
    7): Promise<{ answer: string; conversationId: string }> {
    8  const response = await fetch(`${DIFY_BASE_URL}/chat-messages`, {
    9    method: "POST",
    10    headers: {
    11      "Content-Type": "application/json",
    12      Authorization: `Bearer ${DIFY_API_KEY}`,
    13    },
    14    body: JSON.stringify({
    15      inputs: {},
    16      query: message,
    17      response_mode: "blocking",
    18      conversation_id: conversationId ?? "",
    19      user: "anonymous",
    20    }),
    21  });
    22
    23  // エラーハンドリング(省略)
    24
    25  const data = await response.json();
    26  return {
    27    answer: data.answer,
    28    conversationId: data.conversation_id,
    29  };
    30}
    1. Next.js APIルート(src/app/api/chat/route.ts)
      APIキーをクライアントに露出させないための中継レイヤー。
    1import { callDifyChatApi } from "@/lib/dify";
    2
    3export async function POST(request: Request): Promise<Response> {
    4  const { message, conversationId } = await request.json();
    5
    6  // バリデーション(省略)
    7
    8  const result = await callDifyChatApi(message, conversationId ?? null);
    9
    10  return Response.json(
    11    { answer: result.answer, conversationId: result.conversationId },
    12    { status: 200 }
    13  );
    14}
    1. フロントエンド(src/components/chat/ChatContainer.tsx)
      APIルートを呼び出し、回答をチャット画面に表示する部分。
    1async function handleSubmit(text: string) {
    2  addMessage({ role: "user", content: text });
    3  setIsLoading(true);
    4
    5  const res = await fetch("/api/chat", {
    6    method: "POST",
    7    headers: { "Content-Type": "application/json" },
    8    body: JSON.stringify({ message: text, conversationId }),
    9  });
    10
    11  const data = await res.json();
    12  addMessage({ role: "assistant", content: data.answer }); // Difyの回答を表示
    13  setConversationId(data.conversationId); // 文脈維持のために会話IDを記憶
    14
    15  // エラーハンドリング(省略)
    16
    17  setIsLoading(false);
    18}

    レスポンスの answerフィールドにLLMの回答が入っています。
    conversation_id を保持して次のリクエストに渡すことで、会話の文脈が維持されます。


    まとめ

    作ったものの全体像を振り返る

    今回作ったものを改めて整理すると以下のようになります。

    1[Webアプリ(Next.js)]
    2HTTP POST /v1/chat-messages
    3[Dify セルフホスト版(Docker)]
    4    ├── チャットフロー
    5    │   ├── 知識検索ノード(RAG6    │   └── Gemini(Google AI Studio)
    7    └── ナレッジ
    8        └── 分別ガイド

    自治体ごとに異なるごみ分別ルールを、RAGが自然に吸収してくれました。
    「A市では」「B市では」と質問するだけで該当する自治体のデータを取得してくれて、想定以上に実用的な動きでした。

    Difyを使ってみた所感

    これまでRAGを使っているサービスを触っている中で、これはどのような仕組みで構築しているのだろうと気になっていましたが、今回Difyを触ってみることでイメージを掴むことができました。

    また、Difyを使うことで、LLMアプリのベースを作るまでの工数が短縮できそうだという感触も得られました。
    ベクトルDB・プロンプト管理・APIエンドポイント・プレビュー機能があらかじめ用意されていることで、これらを自前で組もうとすると数日かかる作業が、半日で動くものができました。

    Difyの入門としては十分な手応えが得られたと思います。
    LLMを使ったアプリのプロトタイプを素早く試したいという方の参考になれば幸いです。

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!カジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background