Salesforce 生成AI + RAG:2案件を Flow と Apex に分けた判断とは

こんにちは。エンジニアの大橋です。

今回は、Salesforce の生成AI機能(Data 360 + Prompt Template)を使った RAG システムの設計について、実案件での設計判断をベースに共有したいと思います。

前提として、RAG システムの処理は以下の流れになることが多いです。

  1. 入力を受け取る(ユーザーの質問・レコードの項目値 など)
  2. 検索クエリを作る(入力をそのまま使う、LLM でキーワードを抽出する など)
  3. 検索を実行する(ベクトル検索・ハイブリッド検索 など)
  4. 検索結果をプロンプトに差し込む
  5. LLM を呼び出して回答を生成する
  6. 出力を受け取る(パースする)
  7. 出力先に書き戻す・表示する

このうち、2.〜7. の実装で、難易度の異なる2案件(以下、「素直なユースケース」「複雑なユースケース」と呼びます)を担当しました。

興味深いことに、どちらも入力の受け取り方から、 Prompt Template の種類、検索の実装方法、出力の受け取り方にいたるまで、ほぼ同じ構成だったのですが、「全体の流れをどの機能がオーケストレーションするか」という1点のみが違いました。

結論を先にお伝えすると、素直なユースケースでは Flow を採用し、複雑なユースケースでは Apex を採用しました。

この記事では、その違いがなぜ生まれたのか、を共有したいと思います。

案件その1:素直なユースケース

1つ目の案件は、比較的素直なユースケースでした。

要件はざっくり以下のようなものです。 (業務内容などの具体的な中身は割愛していますが、ご了承ください。)

担当者が業務レコードを参照中に手動ボタンを押すと、そのレコードの情報をもとに2種類のデータソースから過去の類似事例を検索する。検索結果を LLM で整形し、画面の複数項目に書き戻してから表示する。

この要件に対して、個別の設計を順に考えていきたいと思います。

これから挙げる各設計は、後半の複雑なユースケースともほぼ共通する土台になります。共通部分を押さえておくと、唯一の違いである「全体のオーケストレーション」の選択が、後半で掴みやすくなると思います。

(手動ボタンはクイックアクション+画面フローで実装していますが、生成AIの話から外れるので割愛します。)

なお、冒頭の結論で「素直なユースケースでは Flow を採用した」と書きましたが、それはあくまでも全体のオーケストレーションに Flow を使うということです。

後述するように、素直なユースケースでも個別の処理には Apex を使っています。

Prompt Template の種類

まず、Prompt Template の種類です。

今回は、レコードの複数項目を1回でまとめて生成したかったので、項目ごとに1つの Prompt Template が必要になる Field Generation Prompt Template ではなく、自由度の高い Flex Prompt Template を採用しました。

この「複数項目を1回で生成する」という判断が、後ほどの「出力の受け取り方」に関連してきます。

検索結果の差し込み方

次に、検索結果の差し込み方です。

標準レトリーバーを使って画面上だけで設定する方法も検討しましたが、過去案件で実装パターンが蓄積されていた Apex による検索を選びました。

出力の受け取り方

続いて、出力の受け取り方です。

先ほど説明したように、今回は Flex Prompt Template を選択し、複数項目の結果を1回で生成することにしました。 しかし、Prompt Template の出力構造は単一テキストかフラットな JSON しか指定できません。 そのため、Prompt Template からは JSON 形式で出力し、Apex でパースして各項目に振り分けることにしました。

このように、「Prompt Template の出力を複数の場所に振り分けたい」場合には、Apex による JSON パースが必要になります。

全体のオーケストレーション

最後に、全体のオーケストレーションについて考えます。

今回の要件では、Apex でレコード項目から検索条件を組み立てる前処理と検索実行を行い、その結果を Prompt Template にマージします。 そして、 LLM で回答生成し、出力を Apex で JSON パースする、という流れです。 分岐もなく、シンプルな一本道となっています。

したがって、全体のオーケストレーションを Flow に任せました。

整理すると、処理フローは以下のようになります。

冒頭でお伝えした通り、この先に紹介する複雑なユースケースも、ここまでの土台はほぼ同じでした。 2件で共通していたのは、次の5点です。

  • ユーザー操作を起点にした画面フローで処理開始
  • Prompt Template の種類は Flex Prompt Template
  • 検索は Apex で実装
  • LLM の出力は JSON で受け取り、Apex でパース
  • 複数の項目に対して、Flow で書き戻し

差分は、全体のオーケストレーションのみです。

ここからは、複雑なユースケースで、なぜ Apex に寄せる判断をしたのか、を整理していきたいと思います。

案件その2:複雑なユースケース

複雑なケースの要件はざっくりと以下のようなものです。 (こちらも業務内容などの具体的な中身は割愛していますが、ご了承ください。)

担当者が質問を入力して手動ボタンを押すと、問い合わせ内容に対して回答を自動生成する。FAQ・ナレッジレコード・ドキュメントを多段階に検索する。ドキュメント検索の前には LLM によるキーワード抽出を挟む。生成した回答は画面の複数項目に書き戻したうえで、LLM による回答だけでなく、ハイブリッド検索の結果も全件表示する。

要件を補足します。

ドキュメント検索の前に LLM によるキーワード抽出を挟むのは、検証段階で、ユーザーの自然言語での質問をそのまま使用して検索すると、検索精度が落ちることが把握できていたためです。

また、画面にハイブリッド検索の結果を表示するのは、最終的に LLM によって生成された回答だけでなく、検索結果も全件表示することで、回答根拠を追えるようにするためです。

素直なケースとの違いを考えると、それは処理が一本道のシンプルな流れではなく、何段にも重なり複雑になっている点です。 複雑な点というのは、具体的には以下の3点です。

  1. 検索の前処理で LLM を活用する:最初に LLM で検索用のキーワードを抽出し、そのキーワードで検索した結果を使って LLM が回答する
  2. 多段階の検索・回答を回答可否の判定で進める:FAQ→ナレッジレコード→ドキュメントの順に検索と回答生成を行い、各段階で「十分な回答を作れたか」を LLM 自身に判定させ、その結果で「処理を終了/次のデータソースの検索に進む」を切り替える
  3. 回答だけでなく検索結果も表示する:LLM の回答だけでなく、検索結果や検索周りのメタデータ(検索クエリなど)も表示する

なぜ全体のオーケストレーションを Flow ではなく Apex にしたのか

処理がこのように複雑になると、素直なケースのように Flow へ任せて良いのか、が論点になります。

そこで、まずは Flow で良いかを検討しました。

結論からお伝えすると、上の3つの複雑さのうち、Apex を選ぶ決め手になったのは3つ目のみでした。 残りの2つは、Apex の方がやりやすい場面はあるものの、Flow でも実装できる範囲です。

順に見ていきます。

1つ目の「検索の前処理で LLM を活用する」は、Flow か Apex かの選択にはあまり影響しませんでした。

前処理の Prompt Template で検索用のキーワード(検索クエリ)を抽出し、そのキーワードで検索します。 前処理から次工程へ受け渡すのはこの検索クエリ(単純なテキスト)だけです。Apex でしか扱えない複雑なデータ型を経由する必要がないため、Flow でも受け渡せます。

そのため、この点は Flow でも Apex でも大きく変わりません。

2つ目の「多段階の検索・回答を、回答可否の判定で進める」は、Apex だと整理しやすいものの、それほどメリットがあるわけではありません。

FAQ→ナレッジ→ドキュメントと段階的に検索と回答生成を行い、各段階で回答できたかを LLM 自身に判定させて分岐させるわけですが、分岐自体は Flow でも組めます。

ただ、LLM の判定結果は JSON で返ってくるため、Flow で扱うには各段階でパース用の Apex を挟み、「Prompt Template → Apex → 判定」を段数分繰り返すことになります。 この部分を Apex でまとめれば、JSON パースから判定までを続けて書けるため、整理しやすくなります。

とはいえ、これは「Apex だと書きやすい」という副次的な利点にとどまります。

3つ目の「回答だけでなく検索結果も表示する」が、Apex を選んだ最大の理由となりました。

素直なユースケースで採用したFlow オーケストレーションでは、Apex が取得した検索結果をそのまま Prompt Template にマージしていました。この方式では、Apex の検索結果は Prompt Template 内で使われるだけで、保持できません。

しかし、検索結果を画面に表示するには、検索結果を Prompt Template に渡すだけでなく、表示用に画面フローへも受け渡す必要が出てきます。

画面フローへ受け渡すには、Prompt Template から検索結果を出力させるしかありません。 これをやるには、例えば、ヒットした全レコードの ID を JSON に含めて返すようプロンプトに指示することになります。

ですが、これは回答の出力にノイズが混ざるうえ、LLM が ID を正確に転記する保証もなく、良い方法とは言えません。

そこで Apex オーケストレーションでは、検索結果を Apex 側で変数として保持し、それを Prompt Template の入力変数として渡す形にしました。

こうすることで、Prompt Template の出力は「回答+参照元」だけで済み、検索結果は Apex 内に保持した変数から画面フローへ受け渡せます。

整理すると、このケースの流れは以下のようになります。

最後に、Apex オーケストレーションにすることのデメリットをお伝えしたいと思います。

この方法だと、Prompt Builder のプレビュー機能を使用して、検索結果の差し込みから LLM の回答までを通してチェックする、というチューニングが難しくなります。

なぜなら、検索結果は Prompt Template の入力変数へ渡す形になるので、プレビュー機能を使用する場合は、別途、検索結果を用意しておく必要があるからです。

まとめ

今回は、Salesforce Data 360 + RAG システムで、ほとんどの設計が共通する2つのケースを取り上げ、ただ1点「全体のオーケストレーションを Flow に任せるか、Apex に寄せるか」という違いだけがなぜ生まれたのかを紹介しました。

そしてその理由は、「検索結果を LLM の回答だけでなく、画面や後続処理に対しても受け渡す必要があるか」であったことを共有しました。

補足すると、素直なユースケースでは「処理が一本道だから」という理由で Flow を選んでいました。 その時は、処理の流れが複雑かどうか、が判断軸だと考えていたのです。

しかし、複雑なユースケースと比較して初めて、複雑さ自体(分岐や多段検索)は Flow でも実装でき、決め手ではないと分かりました。重要な分かれ目は「検索結果を回答以外にも渡す必要があるか」だった、というのが2案件を振り返って得られた考察です。

今回はこれで以上となります。最後までお読みいただき、ありがとうございました。