Difyとはノーコードで簡単に生成AIアプリを作成できるOSSの開発ツールであり、チャットボットやAIエージェントといったアプリケーションをGUIで作成することができます。
私は問い合わせ管理システムを運用するチームに所属しており、その中でシステムに登録されているデータと生成AIを連携させて精度の高い回答を作成する、いわゆるRAGの仕組みを導入することを検討しています。今回は精度の高いRAGアプリをDifyで作成するにはどのような構成が考えられるかを検討していきます。
1. RAGの課題
1. 1 RAGとは
最初に簡単にRAGについて説明します。

図 1 RAGの概要
一番ベーシックなRAGは上図のようになるかと思います。RAGとは、ユーザーの問い合わせについてLLMが回答を作成する際に、問い合わせ内容に関連する情報を追加することで、LLMに渡すコンテキストを拡張する仕組みです。これによって、LLMが知識を持たない内容についても回答が可能になります。
検索では、ユーザーの問い合わせをクエリとして(正確には問い合わせ内容を一度埋め込みモデルでベクトル化して)、ベクトルストアに格納されている情報を検索します。このとき、ユーザーの問い合わせ内容に「似ている」情報を検索します。
データ登録(埋め込み)では、まず、登録するデータをチャンクと呼ばれる細かい単位に分割します。細かくすることで検索の精度や効率を高めることができます。また、データが大きなサイズだと、埋め込みモデルの入力上限に引っかかる可能性がある、検索処理で検索結果をLLMに渡すときに余計なトークンが増える、といった理由もあります。そして、チャンク化したデータをそのまま登録するのではなく、埋め込みモデルを用いてベクトル化し、チャンクとそれに対応するベクトルデータを登録します。ベクトル化によって、ある2つの文章が与えられたとき、2つの文章のベクトルの類似度を計算することで、関連性の高い情報の検索が可能になるのです。
1. 2問題点
ベーシックなRAGの仕組みは1.1で説明した通りですが、続いて問題点を列挙していきます。なお、ここで挙げる以外にも様々な課題が考えられると思います。
検索
l 複数の要素が絡む問い合わせの検索
Ø ユーザーの問い合わせは複合的であり、問い合わせと情報が1対1で対応しないこともあります。問い合わせに複数の要素が含まれる場合、それらすべての要素が含まれる情報があればよいですが、複数の情報を参照する場合がほとんどだと思います。そのようなケースでは、ある要素に関する情報しか拾えない、ある要素が別の要素の検索においてノイズになる、といった可能性があり、検索精度が下がることが考えられます。
l 「似ている」情報の検索における構造上のミスマッチ
Ø ユーザーは自身の知らない情報を問い合わせるため、専門用語や正式名称を知らない可能性があります。また、語彙や表現が異なる場合があります。(例:「パソコンが重い」と「パフォーマンス」「メモリスワップ」等)
Ø 問い合わせと登録されている情報では本質的な文章構造が異なります。問い合わせは疑問形や命令形であることがほとんどであるのに対し、登録されている情報は平叙文であることがほとんどです。(例:「○○について教えて」と「○○は××です」)
Ø ユーザーの知りたいことが漠然としているとき、抽象度の高い問い合わせをすることがありますが、登録されている情報の記述は具体的な場合が多いです。(例:「最近のAIトレンドについて教えて」と「2024年に発表された○○について」)
l 絶対に含めたいキーワードがある場合の検索
Ø ベクトル検索では必ずしもキーワードが一致していなくても検索できる可能性があります。この点はベクトル検索のメリットなのですが、「あるキーワードが含まれる情報を検索したい」といったキーワードが重要である検索も考えられます。
l 状態遷移や因果関係が絡む検索
Ø ベクトル検索では問い合わせに含まれる状態遷移や因果関係を考慮して検索することはできません。例えば、「動画配信サービスのプレミアムプランを一度解約して、キャンペーン期間中に同じアカウントで再登録した場合、初回限定の30日間無料トライアルは適用されますか?」といった質問があった場合、ユーザーの履歴から「新規契約者」ではなく、「再契約者」であることを推論し、規約等を検索する必要がありますが、そのような検索はできません。
l データ全体に関する集計や分析
Ø ベクトル検索の動作は、「関連していそうなデータを見つけてくる」であり、自分に合った本をいくつか見つけてくれる図書館の司書のようなものです。データ全体に関する集計や分析、例えば、「~について上位○〇件を教えてください」、といった検索はできません。データ全体を扱うことができないため、データの網羅性が重要な場合は留意する必要があります。
l 数値の大小や範囲を条件とする検索
Ø ベクトル検索では数値の大小や範囲といった条件に基づいた検索が苦手です。文章の意味をとらえる上で、数値の大小は無関係ということです。これは具体例で考えるとわかりやすいかもしれません。例えば、ある商品に関する記述において、その商品の値段が「9,000円」の場合と「10,000円」の場合で、文脈が大きく変わることはなさそうです。
埋め込み
l チャンクの適切な分割サイズ
Ø チャンクが小さすぎると、必要な文脈が失われ、そのチャンク単体では意味をなさなくなる可能性があります。例えば、「Aの原因はBです。なぜならCだからです。」という文が、「Aの原因はBです。」と「なぜならCだからです。」という2つのチャンクに分かれてしまった場合、後半のチャンクだけでは何を指しているのか読み取れません。また、必要な情報が複数のチャンクにまたがっている場合、検索時にすべてのチャンクを取得することが難しくなります。
Ø チャンクが大きすぎると、質問に関係のない情報が多く含まれる可能性が高くなります。検索結果をLLMに渡して回答を作成する際、ノイズとなる情報が多いと、回答精度が下がる可能性があります。LLMの長文処理において、文章の中間にある重要な情報を見落とす現象は「Lost in the Middle」として知られています。
Ø LLMに渡すテキスト(トークン量)が増えると、APIの利用料金が高くなり、回答生成までの時間も長くなります。
チャンク間の関連
Ø チャンクはドキュメントを特定のルール(固定長やデリミタ等)で機械的に分割したものであり、それぞれのチャンクは独立しています。単にチャンクを検索するだけでは、1冊の本をページごとにバラバラにして、そのうちの1ページだけを読んで理解しようとするようなものです。しかし、実際は調べたい内容について、複数のセクションを参照する必要があるケースは珍しくありません。例えば、あるソフトウェアでエラーコード「E-XXXX」のエラーが発生したとします。エラーコード「E-XXXX」で調べると、どうやらデータベースの接続でエラーになっているようなので、次はデータベースへの接続について、設定ファイルの構成に関するセクションを調べる必要がある、といった具合です。
表や画像といった非テキストデータ
Ø 通常、ドキュメントには表や画像、レイアウトといった非テキストデータも含まれますが、これらの情報はチャンク化する過程で失われてしまいます。
2. 構成案
1ではベーシックなRAGに関する課題を挙げてきました。最近ではこのような課題を解決して精度を向上させる、Advanced RAGと呼ばれる手法も多く出てきています。
今回はDifyで比較的簡単に実装できそうなMulti-Queryを試してみたいと思います。この手法を簡単に説明すると、ユーザーの問い合わせをそのまま検索に利用するのではなく、LLMを用いて、ユーザーの問い合わせから複数の異なる問い合わせを生成し、それらを用いて情報を検索するというものです。

図 2 Multi-Query概要
引用;RAG from scratch: Query Translation (Multi-Query) - Google スライド
3. Difyでの実装
3.1 構成概要
思った以上に前置きが長くなってしまいましたが、Difyで実装していきます。最終的には以下のようなチャットフローになります。

図 3 チャットフロー完成イメージ
3.2 サブクエリ作成
まずは、ユーザーの問い合わせから複数のクエリを作成します。使用したプロンプトは以下の通りです。詳細は割愛しますが、HyDEという手法も試そうとしていたため、プロンプトにHyDE用の指示も混ざっています。HyDEは簡単に説明すると、検索時においてユーザーの質問ではなく、質問に対する仮の回答をクエリとする手法です。先に結果を書いてしまうと、実はこのHyDE向けのプロンプトにしたせいで、思ったような結果が得られなかったので、参考にする場合はご留意ください。ちなみに、プロンプトは自分で一から考えた訳ではなくLLMに作成してもらいました。

図 4 サブクエリ作成プロンプト

図 5 出力形式の指定
LLMに出力してもらうクエリは後続の処理で利用するため、利用しやすいJSON形式で出力してもらいます。構造化出力については、JSONスキーマの解析能力の関係で推奨されるモデルがあるようです。今回はAzure OpenAIのGPT-4oを利用しています。スキーマは以下のように設定しました。answerはHyDE用です。

図 6 JSONデータスキーマ
3.3 情報抽出
3.2のLLMの出力から処理で利用する情報を抽出します。これにはPythonでの処理が必要であるため、コード実行ブロックで行います。

図 7 JSON抽出ノード
入力変数は前に設定したノードの出力が選択できますが、JSONデータを抽出したいので、structured_outputを設定します。
PythonコードについてはDifyのページにもサンプルの記載があるので、参考にできると思います。
出力変数は、今回はObjectの配列になるため、Array[Object]を指定します。これで、structured_outputの要素subqueriesが抽出できます。
続いて、subqueriesからanswerのみが含まれる配列を作成します。

図 8 answer抽出ノード
以上でanswer配列が作成できました。続いてそれぞれのanswerに対してRAGによって回答を生成します。
3.4 イテレーション
イテレーションの内部の処理は通常のRAGと同様です。

図 9 イテレーションブロック
イテレーションブロックの入力は、先ほど抽出したanswer配列、出力は各answerに対するRAGの回答の配列になります。

図 10 データ検索(サブクエリ)ノード
検索変数がイテレーションの配列の変数であるくらいで、通常のRAGにおける検索と変わるところはありません。ナレッジベースについては、ベクトル検索とキーワード検索を併用するハイブリッド検索の設定にしています。

図 11 回答生成(サブクエリ)ノード
サブクエリの回答生成についても通常のRAGにおける処理と変わるところはありません。
3.5 回答生成
最後にイテレーション処理で作成した各回答を基に、元のユーザーの問い合わせに対する回答を作成します。

図 12 回答生成ノード
以上でチャットフローが完成しました。
4. おわりに
以上、今回はRAGの精度向上の手法について、Difyで作成してみました。
今回の検証は業務データを利用しているため、実際にLLMが作成した回答については省略させていただきますが、作成したチャットフローを試してみたところ、ハルシネーションがひどく、残念ながら期待していたような結果にはなりませんでした。原因はHyDE用のプロンプトによって、回答が一般的な内容を多く含んでしまっていたせいだと考えられます。
Multi-Queryの実装に関してはうまくいったと思いますが、フローを長く複雑にするほど、回答までに要する時間は増加し、LLMに渡すトークン量(≒API利用料金)も増えることになるという点は考慮が必要だと感じました。ちなみに、今回のチャットフローでは、回答までに15~20秒ほどかかっていました。
今回は以上になります。
Dify以外にも様々なAI製品の紹介や検証を予定していますので、今後のブログにもご期待ください。
最後までお読みいただきありがとうございました。












