UPSIDER Tech Blog

DifyでチャットボットをリリースしてわかったRAGの精度向上のためのチャンク設計

はじめに

こんにちは!PRESIDENT CARDでソフトウェアエンジニアをしているアダチ@dikxs118です。

前回の記事では、Difyを使ったチャットボット構築を題材に「AIネイティブな開発フロー」についてご紹介しました。

tech.up-sider.com

今回はその続編として、チャットボットの 中身、つまり、DifyのワークフローやRAGのナレッジ設計について書いていきます。

具体的には以下のような内容です。

  • Difyのワークフローをどう構成したか
  • ナレッジベース(KB)をどう設計し、どう運用しているか
  • QA × 汎用チャンクで組んだが精度が出なかった話
  • RAGの限界に向き合い、親子チャンクでどうアプローチを変えたか

特にRAGのナレッジ設計は「一度組んで終わり」ではなく、どんなデータをどう整形するか、チャンクをどう切るか、Difyの設定でどう実現するか。 この設計の試行錯誤がチャットボットの品質をほぼ決めると言っても過言ではありません。

開発の背景と要件

私たちが今回チャットボットを導入することになった目的は「CS(カスタマーサポート)の運用コスト削減」と「ユーザーの悩みの自己解決を促すことでプロダクト体験そのものを向上させること」でした。

数ある選択肢の中でDifyを採用した理由は、単なるFAQ回答に留まらず、将来的にはユーザーのステータス(DB参照)に応じた動的な対応など拡張したいことはたくさんあったので、開発の自由度と汎用性が高いDifyを選定しました。

当初の要件は以下の通りです。

  • Bot内で解決可能なFAQは自動回答
  • 回答できない問題はオペレーターへ引き継ぎ
  • 選択肢などを駆使してユーザーを誘導していく
  • ナレッジをCSチームで追加・編集できること

ちなみに、Betaとして試験的に運用しているため、現時点ではSaaS版を利用しています。

Difyワークフローの構成

全体のワークフローを示します。

ワークフローの実装自体は本質ではないので結構端折りますが、レイヤーごとの大枠をざっくり説明します。

なお、前回の記事でご紹介した通り、今回はセキュリティと柔軟性を担保するため、Difyの公開APIを直接フロントエンドから叩くのではなく、前段にBFFを構築しています。たとえば、ユーザーが機密情報や個人情報を入力した場合にLLMへ渡さないようマスキングする処理や、将来的なプラットフォーム移行への備えとしての役割を持っています。

L0: Entry Point

すべての会話の開始点です。ユーザーからの入力を受け取り、後続のフロー制御レイヤーへ渡します。

L1: Question Classifier(質問分類器)

すべての入力をLLMに通すとコストもレイテンシも嵩みます。そのため、初回の質問に対しては分類器を挟んでいます。

挨拶やいたずらのような「FAQ検索が不要な入力」をここで早期リターンさせることで、トークン消費を抑えつつ、本来のサポート機能へリソースを集中させています。 2回目以降は既に文脈があるため直接処理に回します。たとえば、1回目の質問に対して「それってどういうこと?」と聞かれた場合に、これを不適切な質問として弾いてしまうのを防ぐためです。

L2: Query Processing

Dify標準の「メモリ機能」は手軽ですが、実際に運用してみると「それってどういうこと?」といった代名詞を含む追加質問に対し、文脈を正しく認識できないケースが散見されました。メモリ機能が私の思ったように機能しないことが多くて、前の文脈を読み取ってくれないことが多々あったんですよね。

なので、標準メモリに頼り切るのではなく、会話変数を独自に定義し、LLMを使って「過去の文脈を含んだ完全な質問文」にリライト(Query Generation)させる処理を挟みました。 これにより、「利用限度額について」→「いつ復活しますか?」という流れでも、「利用限度額はいつ復活しますか?」という検索クエリを生成でき、検索精度を担保しています。

また、ユーザーの入力が不明瞭だったり言葉足らずだったりする場合に、文章を補足してより検索に適した入力に変換する役割もあります。

L3: Knowledge Retrieval & Scoring Layer

RAGの肝となる検索部分です。RerankモデルにはCohereの rerank-v3.5 を使用しています。 検索結果(TopK=10)を取得した後、最も高いスコアに基づいて明確に処理を分岐させています。(閾値は少しずつ調整中です)

  • High (Score > 0.3): 信頼できる情報あり → 直接回答
  • Medium (0.15 ≤ Score ≤ 0.3): 関連しそうだが確証がない → 候補を選択肢として提示
  • Low (Score < 0.15): 関連情報なし → オペレーターへ誘導

なお、当初はHighの閾値を0.7に設定していましたが、後述する親子チャンクへの転換でコンテキスト量が増えた結果、rerankスコアが全体的に低く出る傾向になりました。加えて、Mediumに分類されるたびに「お探しの内容はこれですか?」と選択肢を挟むのはユーザーにとってストレスです。検証の結果、0.3まで下げても回答精度は十分に担保できたため、UXを優先して現在の閾値に落ち着いています。

プロンプトで「わからなければ『わからない』と答えて」と指示するだけでは、ハルシネーションを完全には防げません。スコアという数値で閾値を設け、コードベースで分岐させることで、信頼性の高いボットを作ることができます。

また、チャットボットの精度は、ここで参照されるナレッジベースがどこまで適切に設計・最適化されているかで決まると言っても過言ではありません。ナレッジベースやチャンクの設計、RAGの最適化については後述します。

L4: Response Layer

スコア分岐に基づき、ユーザー体験を最適化します。

High Confidence: 検索結果を基に直接回答を生成します。

Medium Confidence: いきなり回答せずに「お探しの内容はこれですか?」とボタン形式で選択肢を提示します。ユーザーがそれを選択した時点で「明確な入力」となり、Highスコアのフローへ合流するため、確実な回答が可能になります。

Low Confidence: オペレーターへ引き継ぐためのメールアドレスを提示します。このスコアはKBにヒットしていないということなので、ないものはどう頑張っても回答できません。将来的には、ユーザーが希望する場合にFreshdeskのAPIを呼び出して直接チケットを作成する仕組みも検討しています。

RAGの限界に向き合う

ナレッジを集める

ナレッジベースの設計に入る前に、そもそも「どこからデータを集め、どう管理するか」が大きな課題でした。

大体のプロダクトでは、コードだったり、Notionなどのドキュメント管理ツールだったり、FigmaやGoogle Docsだったり、あらゆる場所にナレッジが散らばっていると思います。

そこで、MCPなどを駆使して、チャットボットに必要そうな情報をすべて特定のリポジトリに集約することにしました。サポートチームのナレッジから、個人情報が含まれない内容を集約してもらってQAを生成したりBDDのテストコードから仕様を生成したり結構色々しましたね。(最終的にはあまり使ってないものも多いが)

今思えば、「AIとの親和性を高めるにはAll in Gitしか勝たん、全部ぶち込め!」という結構短絡的な思考です(笑)

Gitに集約することで、AIとの親和性以外にも、バージョン管理・PRでのレビュー・CI/CDとの統合など、エンジニアにとって馴染みのある運用フローに乗せられるのは大きなメリットですが、デメリットも結構あります。

たとえば、Notionのヘルプページをデータ化してGit管理しているのですが、Notionを更新したらGitも更新しなければならないという二度手間が発生します。しかも、Git側の作業はエンジニアにしか対応できません。全職種にCursorを配布してGit操作を強制するようなパワープレイをしない限り、この運用コストは結構きついです。

多様なデータソースを受け付けて、特定のフォーマットで出力してくれるFacadeのような仕組みを作ることも考えましたが、コストが見合わず断念しました。ちなみに、現在はDify側に「ナレッジパイプライン」なるものがあるようです。まさにこれじゃん!と思いましたが、当時はまだ存在しておらず、リプレイスするには至っていません。(取得元のデータ更新に自動追従してくれるのかとかも気になる)

チャンクを設計する

さて、ここが一番時間をかけたところで頭を悩ませたところでもあります。集めたナレッジを、どのような形式で、どのような方式でDifyに渡すか、つまり、チャンクの設計です。この設計がチャットボットの品質を決定的に左右します。

QA × 汎用チャンクの限界

最初は、すべてのナレッジをQA形式のCSVに変換し、Difyの汎用チャンクモードでQA1組 = 1チャンクとして格納しました。これはQA単位で細かく分割し、ベクトル検索にかける「王道のRAG」手法です。

キーワードがドンピシャでヒットする質問には問題なく回答できます。しかし、ユーザーの自由入力や複合的な質問には耐えられませんでした。

最大の原因は 「チャンク間のコンテキスト欠損」です。本来のドキュメントであれば「第1章の前提」があって「第6章の結論」があるといった文脈が存在しますが、QA形式で細切れにされたチャンクはそれぞれが孤立しています。TopKで複数チャンクを取得してLLMに渡すことはできますが、そもそも関連するチャンクが検索で揃わなかったり、チャンク間の前後関係や依存関係が失われているため、LLMが文脈を正しく組み立てられないケースが多発しました。

結局「チャンクにヒットするかしないか」の世界でした。

「NotebookLM」からの着想

当時、私はNotebookLMを多用していまして、同じナレッジを食わせてQAをやるだけなら、NotebookLMの方が自分の作ったチャットボットより明らかに精度が良いという体験がありました。

NotebookLMは、Geminiの広大なコンテキストウィンドウを活かし、検索で取得した情報に加えて極めて広い文脈を保持したまま回答を生成していると推測されます。 これはトークン消費量が増える「力技」ですが、コストよりもUX(ユーザー体験)を優先すべきだと考え、「Dify上でNotebookLMのような挙動や体験を再現できないか?」という仮説に至りました。

Markdown × 親子チャンクの共存

そこでナレッジをMarkdown形式に再構成し、Difyの 「親子チャンクモード」を採用しました。

親子チャンクの仕組みは、検索と生成の役割を分離できる点にあります。

  • 検索: 小さな「子チャンク」で行い、検索精度を高める。
  • 生成: LLMには、その子が属する巨大な「親チャンク」を渡す。

「ヒットした断片だけを渡す」汎用チャンクに対し、親子チャンクは「ヒットした箇所の周辺にある巨大な文脈」を渡せます。

具体的には、親チャンクを1つのヘルプページ全体(または章ごと)とし、子チャンクは検索に引っかかりやすい細かい段落単位で設定しました。 そして、検索時のTopKを最大の10に設定しました。

これにより、「検索には細かい子チャンクがヒットするが、LLMに渡されるのはその親(巨大な文脈)」という状態を作り出し、検索の粒度と生成時のコンテキスト粒度を分離することで、通常のRAGよりも広い文脈をLLMに渡せる構成になっています。

つまり、RAGのパイプライン自体は王道のまま(Embedding → ベクトル検索 → Rerank → LLMに渡す)で、LLMに渡すコンテキスト量を最大化することでNotebookLMの「全体を理解した上で引き出す」方向に寄せています。

NotebookLMの要領でGeminiを使おうかとも思いましたが、検証したところGPT-4oで十分対応できたのでそちらを採用しています。 トレードオフとしてはtoken消費が増えますが、そこは「コスパよりUX」の判断です。NotebookLMほどではありませんが、「細切れの情報をつなぎ合わせてその場で頑張る」設計から、「十分なコンテキストを理解した上で引き出す」設計への転換です。 なお、QA × 汎用チャンクのナレッジベースも併用して残しています。キーワードが明確にヒットするFAQ系の質問にはQA形式の方が強いので、親子チャンクで拾えない領域をカバーする役割です。

このハイブリッドな構成により、チャットボットの精度は向上しました。 ロングコンテキストを活かした「文脈理解」と、キーワード検索による「的確さ」を組み合わせることで、ユーザーの多様な質問に対応できるようになりました。トークンの消費と、ハルシネーションリスクは上がりますが、それを上回るUXの向上だと感じています。

最近、Claude Codeの開発者であるBoris Cherny氏が「王道のRAGからAgentic Searchへ転換した」という話がありましたが、今回はAgentic Searchそのものは実装していませんが、「細切れの検索(RAG)よりも、コンテキストの維持(Long Context)を重視する」という方向性は完全に一致しており、ケースバイケースだとは思いますが、非常に共感しました。

「ベンチマークよりもVibe」もまさにその通りだなと思いました。

実際、今回の親子チャンク導入の判断も、Rerankスコアなどの定量指標だけでは見えない、実際に触ったときの回答の自然さを重視した結果です。数値上は大差なくても、ユーザー体験として明らかに良くなったと感じるケースが多く、定量評価だけに頼らない判断の重要性を実感しました。

CI/CD

最後にこれらをどのように運用(CI/CD)しているかについても触れておきます。

前提として、Difyのワークスペースは一つで、KBとワークフローをそれぞれ、stgとprodの2つの環境を運用しています。

  1. KBデプロイ
    knowledge/ 配下に、ナレッジとなるCSVやMarkdownファイルを配置しています。これらが更新されると、Dify APIを通じてナレッジベースを自動更新するワークフローです。 運用フローとしては、サポートチームがナレッジを追加・修正した場合にPRを出してもらい、レビュー後にマージすることでstgのナレッジベースが自動更新されます。stg環境で動作確認を行い、問題なければprod用のワークフローを実行してデプロイします。
    ナレッジに無いものはどう頑張っても回答できないですからね。ユーザーから受け取った入力をフィードバックとして高速でナレッジとして更新できる仕組みは重要です。

  2. STGテスト
    workflow/ 配下に、Difyの実際のワークフローをエクスポートしたYAMLファイルをstg・prodそれぞれ配置しています。stgのワークフローが変更されPRが作成されると、STG環境のチャットボットAPIに対してテスト用の質問を自動で投げ、回答をCSVとしてArtifactに出力します。 質問リストは事前に数百個のデータを用意し、その回答セットをQAチーム監修のもと作成しています。出力された回答とAIを使って突き合わせ、合否を判断する仕組みです。 ただし、定量的な自動判定はまだ導入できておらず、現時点では人の目で最終確認しています。ここは今後の改善ポイントです。

  3. STG → PROD同期
    2のテストで問題がなくPRがマージされると、STGからPRODへ変更を同期するワークフローが自動で発火します。STGの変更をPRODにも適用したPRが自動作成され、nameやdataset_idsなど環境固有の値は維持したまま、ロジック部分だけを同期する仕組みです。 この3つによって、「ナレッジの更新 → テスト → ワークフローの本番反映」という一連の流れを効率化しています。

Difyを使ってみた感想

Difyを使ってみた感想として、多機能で自由度が高く、ワークフローの設計が非常に手軽にできる点は良かったです。RAGまわりの実装はDify側が吸収してくれるので、開発者はナレッジの設計に集中できます。 一方で、凝ったことをやろうとすると運用の複雑さが一気に増す印象です。「簡単に始められるが、スケールさせると辛くなる」という典型的なローコードの壁を感じました。 活発に開発が進んでいるプロダクトならではの課題もいくつかありました。

  • アップデートの影響を受けやすい:
    SaaS版を利用していると、アップデートに伴い既存のワークフローや設定が意図せず影響を受けるケースがありました。こちらから事前に検知する手段がなく、原因調査も難航し、issueを立てて確認することもありました。これが実際に起きた例です。

  • 安定性:
    挙動が安定しない箇所や細かなバグがあり、原因調査に時間を取られることがよくありました。

  • UIの複雑さ:
    多機能ゆえにUIの設計が難しいのは理解しつつも、「これどこにあるんだ?」と迷うことがありました。例えば、KBのAPI Keyの場所がわからず、公式Discordで教えていただいてようやく見つけたということもありました。機能が急速に増えている分、ドキュメントやUIの整備が追いついていない部分があるようでした。

これらはDifyが急速に進化しているがゆえの課題でもあり、今後の改善に期待しています。いずれにしても、KBやBFFなどは独立した構成にしているため、将来的に別のプラットフォームへ移行する選択肢も残せています。この「疎結合にしておく」判断は正解だったなと思います。

まとめ

今回は、Difyチャットボットのワークフロー構成からナレッジベースの設計、チャンク方式の転換、そしてCI/CDによる運用まで、一連の開発と試行錯誤について書きました。

振り返ると、一番の学びは「DifyのRAGの精度はチャンクの設計でほぼ決まる」ということです。ワークフローのルーティングやプロンプトをどれだけ作り込んでも、ナレッジの渡し方が悪ければ精度は出ないこともあります。

もう一つ実感したのは、「ベンチマークよりVibe」の重要性です。スコアや数値ではなく、実際にユーザーが触ったときに「ちゃんと答えてくれる」と感じるかどうか。その体験を追求した結果が、NotebookLM的な発想であり、コンテキストを最大化する設計への転換でした。コスパだけを見ていたら、この判断にはたどり着けなかったと思います。

課題もまだまだ残っています。テストの定量的な自動判定、ナレッジの二重管理の解消、Difyへの依存度をどこまで下げるかの判断など、運用を続ける中で向き合うべきことは多いです。

特にテストは、実際には入力や出力が固定されない中で、特定のデータセットでテストしても効果は薄いと感じています。

今後は LLM-as-a-judge を取り入れ、Ragasなどのフレームワークを用いて評価する方法も模索したいと思います。「Vibeの良さ」を単なる主観で終わらせず、CI/CDの中で定量的にモニタリングできる状態を目指しています。

チャットボットは「作って終わり」ではなく、ナレッジを育て、設計を見直し、ユーザーのフィードバックを反映し続けるプロダクトです。今後もその過程で得た知見を発信していければと思います。

We Are Hiring !!

UPSIDERでは現在積極採用をしています。 ぜひお気軽にご応募ください。

herp.careers

herp.careers

UPSIDER Engineering Deckはこちら📣

speakerdeck.com