Knowledge記事の画像をAgentforceのRAG検索対象にする

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

Agentforceで社内ナレッジの問い合わせ対応を構築する際、Knowledge記事をData Cloudに取り込み、ベクトル検索で関連記事を引き当てるという構成はよくあるパターンだと思います。

Knowledge記事ではマニュアルやFAQの本文にリッチテキスト項目を作成して利用するケースが多く、画面キャプチャやフロー図のような画像が多用されます。しかし、Data Cloudの検索インデックスはテキストをチャンク化・ベクトル化して検索対象にする仕組みであるため、リッチテキスト内の画像はチャンク化の対象にならず、検索にヒットしません。つまり、画像に記載された情報がどれだけ有用でも、テキスト部分にその内容が書かれていなければ意味がないわけです。

今回は、この「Knowledge記事内の画像をRAGの検索対象にする」という課題に対して、Prompt Templateを活用した画像テキスト化の手法と、ガバナ制限を回避するためのジョブチェーン設計について書きたいと思います。

なお、本ブログの内容は2026年2月時点のSalesforce環境で検証したものです。

方針の検討

「検索」と「回答生成」で画像の最適な扱いは異なる

本題に入る前に、RAGにおける画像の扱いを整理しておきたいと思います。

RAGでは、まずデータソースから関連情報を「検索」し、次にその検索結果をもとにLLMが「回答生成」を行うという2段階の処理になります。画像については、この「検索」と「回答生成」で最適な扱い方が異なります。各フェーズでの画像の扱いを整理すると以下の通りです。

フェーズ 画像の扱い メリット デメリット
検索 ・テキスト化してベクトル検索の対象にする ・画像の内容が検索にヒットするようになる ・テキスト化の過程で情報が欠落する
回答生成 ・画像のままPrompt Templateに渡しLLMに解釈させる ・テキスト化して読ませる場合よりも情報量が多く、特にスクリーンショットやUI手順の認識精度が高い ・画像入力にはFlex Templateが必要で、その場合の入力上限は5枚となる
・検索には使えない

検索フェーズで「テキスト化」と書きましたが、これはLLMに画像を入力して内容を文章として出力させることを指しています。例えば画面キャプチャであれば、画面上のボタン名やラベル、操作手順などがテキストとして抽出されます。画像を画像のままベクトル化する方法もやり方としてはありますが、現状のData Cloudではテキストベースのベクトル検索が前提となるため、テキスト化が現実的な手段となります。

一方、回答生成フェーズでは、マニュアルやスクリーンショット付き手順書のように視覚情報そのものに意味があるコンテンツであれば、画像をそのままLLMに渡す方が回答精度は高くなると思われます。これは、テキスト化では失われてしまう図中の位置関係やUIのレイアウトといった情報も、LLMが直接考慮できるためです。とはいえ、画像のままでは検索に引っかからないという根本的な課題は解決できません。

そこで本ブログでは、検索フェーズに焦点を当て、Salesforce上で画像をテキスト化してベクトル検索の対象にする手法を紹介します。

なお、表では回答生成フェーズに「画像のまま渡す」方法のみを挙げましたが、検索フェーズで画像をテキスト化していれば、その結果を回答生成にも流用できます。つまり、検索のためのテキスト化が回答生成の品質向上にも寄与するわけです。

画像テキスト化の2つのアプローチ

画像をテキスト化する方法として、以下の2つのアプローチを検討しました。

① Prompt Template方式

画像をPrompt Templateで処理してテキスト化し、Knowledgeのカスタム項目に保存します。そのカスタム項目をData Cloudのベクトル検索の対象にします。

② LLM-Based Preprocessing方式

画像をUDLO(Unstructured Data Lake Object)/UDMO(Unstructured Data Model Object)に格納し、Data Cloudのチャンク化時にLLMベースのPreprocessing機能でテキスト化します。テキスト部分と画像部分で別々の検索インデックスを作成し、それぞれData Cloudのベクトル検索の対象にします。なお、この方式については弊社の過去ブログでも触れています。

今回は①のPrompt Template方式を採用しました。

Prompt Template方式を採用した理由

理由は2つあります。

1つ目は、テキストと画像情報を1つの項目に統合しやすいことです。①の方式では、元の記事テキストの中に画像のテキスト化の結果を埋め込み、1つの項目にまとめることができます。②の方式では、テキスト部分と画像部分が別々の検索インデックスになるため、例えば画像チャンクだけが検索にヒットした場合、対応する元記事のテキストを紐づけて取得するには別途DMOを辿る必要があるなど、運用上の手間がかかります。

2つ目は、LLMモデルの選択肢が広いことです。②のLLM-Based PreprocessingではGPT-4oが固定で使用されますが、①のPrompt Templateでは多様なLLMモデルから選択できるため、テキスト化精度の向上が見込めます。

一方で、①にはガバナ制限への対処が必要になるという弱みもあります。この点については後述します。

全体像

ここからは、①のPrompt Template方式の具体的な実装について説明していきます。

まず、処理フロー全体をシーケンス図で示します。

処理の流れは以下の通りです。

  1. Knowledge記事のリッチテキストからHTMLを解析し、画像を特定する
  2. 画像データをダウンロードし、ContentVersionとして保存する
  3. Prompt Templateで画像をテキスト化する
  4. テキスト化した結果を元のテキストにマージする

完成イメージは以下の通りです。左がリッチテキスト項目(画像を含む)で、右がリッチテキスト内の画像をテキスト化して埋め込んだロングテキスト項目です。

次のセクションでは、このパイプラインの中で特に読者がつまずきそうなポイントに絞って解説していきます。

実装の要所

HTML解析

リッチテキスト項目のデータはHTML形式で保存されています。画像が含まれる場合は<img>タグとして表現され、Salesforce内部で管理される画像を一意に特定するためのrefidパラメータが含まれています。

<p>プロンプトテンプレートのリストビューです。</p>
<p><img src="…&refid=XXXX"></img></p>

Apex側では、正規表現を用いてこのHTMLを解析し、処理対象となる全てのrefidを抽出してリスト化し、後続処理に渡します。

画像データのダウンロード

リッチテキスト内の画像の実体はSalesforceのシステムオブジェクトに保存されています。このオブジェクトは公開されておらず、SOQLでは取得できません。そのため、REST APIを使ったHTTPコールアウトでバイナリデータを取得する必要があります。

ContentVersion化とPrompt Templateでのテキスト化

前段で取得した画像のバイナリデータを、そのままPrompt Templateに渡すことはできません。Prompt Templateに画像を入力として渡すためには、Salesforce上のファイル(ContentDocument)レコードとして保存する必要があります。

そこで、取得したバイナリデータからContentVersionレコードを作成し、保存します。ContentVersionを作成すると、対応するContentDocumentが自動的に生成されます。Prompt Templateの入力変数には、このContentDocumentを指定することになります。

ここで、Prompt Templateに画像を渡す方法について補足します。Salesforceの公式ブログによると、Prompt Templateに画像を渡す方法は2つあります。1つはFile Object(ContentDocument)を入力変数として渡す方法、もう1つはレコードのNotes & Attachments関連リストに添付する方法です。

今回のケースでは、リッチテキストから抽出した画像をプログラム的に処理する必要があるため、Notes & Attachments方式は適しません。File Objectを入力変数として渡す方式を使うことになりますが、この方式はFlex Templateでのみ利用可能です。そのため、Flex Templateの入力変数にFile型を追加し、画像を入力として受け取るPrompt Templateを作成しておきます。

ここで押さえておきたいのは、画像をPrompt Templateに渡すには「ContentVersionとして保存する」というステップが必須であるということです。「バイナリデータを直接渡せないのか」と考えがちですが、Salesforce上のレコードとして保存しておかないとPrompt Templateからは参照できません。

元テキストへのマージ

全ての画像のテキスト化が完了した後、元のテキストと画像テキストのマージを行います。元のリッチテキスト項目は更新せず、検索用として別途作成したロングテキスト項目に結果を格納します。具体的には、Apexの文字列操作を使い、リッチテキストのHTMLタグを除去しつつ、<img>タグ部分を対応する画像テキストに置換することで、テキストと画像テキストを統合します。この検索用項目をData Cloudのベクトル検索の対象にすることで、画像の内容も検索にヒットするようになります。

非同期処理とジョブチェーン設計

ジョブチェーン方式の採用

ここまでの処理を1つのトランザクション内で全ての画像に対して実行しようとすると、ガバナ制限(実行時間やヒープサイズ)に抵触する可能性があります。画像のサイズや枚数はKnowledge記事ごとに異なるため、非同期処理(Queueable)を使い、画像ごとにトランザクションを分割するジョブチェーン方式を採用しています。

なぜ1画像の処理をさらに2段階に分けるのか

さらに、1画像あたりの処理も2つのジョブに分離しています。

  • 第1ジョブ:画像データのダウンロード+ContentVersionレコードの保存
  • 第2ジョブ:保存したContentVersionを入力としてPrompt Templateを呼び出し、テキスト化

なぜ1つのジョブにまとめられないかというと、ContentVersionレコードの保存がDBにコミットされるのはジョブの完了時だからです。同一ジョブ内でContentVersionを保存した直後にPrompt Templateの入力変数として参照しようとしても、まだコミットされていないため参照できません。ジョブを分けることで、第1ジョブの完了時にContentVersionが確実にコミットされた状態で、第2ジョブからPrompt Templateに渡せるようになります。

この「レコード保存と、そのレコードを参照するLLM呼び出しはジョブを分ける」という設計パターンは、Prompt Templateに限らず、Apex非同期処理でLLMを活用する場面全般で参考になるかと思います。

1ジョブあたりの処理枚数

今回の検証ではシンプルに1画像1ジョブとしましたが、実運用では画像枚数が多い場合にジョブ数が膨らみ、処理時間が問題になり得ます。例えば10枚の画像を含む記事では、2段階×10枚=20ジョブが直列に実行されることになります。

ここで、非同期トランザクションのガバナ制限を確認してみます。

制約 上限(非同期)
ヒープサイズ 12MB
CPU時間 60秒

画像1枚あたり最大1MB程度と想定すると、12MBのヒープ内に複数枚の画像を収めることが可能です。つまり、1ジョブあたり1枚に限定する必然性はなく、ガバナ制限の範囲内でバッチサイズを持たせることで、ジョブ数を大幅に削減できます。

例えば1ジョブあたり5枚の画像を処理する設計にすれば、10枚の画像でも4ジョブ(2バッチ×2段階)で完了します。2段階分離の構造を維持しつつ、バッチサイズを画像サイズの上限とヒープ上限から逆算して設定するのが、実運用に向けた現実的な設計と言えそうです。

なお、「1回のPrompt Template呼び出しに複数画像を渡して一括でテキスト化する」方法も一応考えられますが、以下の理由から採用しませんでした。

  • 出力から画像とテキスト化結果を正しくマッピングする処理が複雑になる。モデルの出力フォーマットが崩れた場合にパースが壊れるリスクがある
  • 1枚あたりのテキスト化精度が低下する恐れがある。一般的にLLMは1回のリクエストで複数のタスクを同時に処理させると精度が落ちる傾向があり、個別に処理する場合と比べて記述が粗くなる可能性がある

ただし、1画像1リクエストにすればその分Prompt Templateの実行回数が増え、処理時間やコストも増加します。精度とコストのトレードオフを考慮した設計判断が必要です。

なお、画像の量が非常に多い場合は、Salesforceの外で事前にまとめて画像をテキスト化しておき、その結果をKnowledge記事に取り込むという方法もあります。多くの場合、外部で処理する方が費用を抑えやすく、ガバナ制限の回避にもなるため、大量処理が見込まれるケースでは検討の余地がありそうです。

まとめ

今回は、Knowledge記事のリッチテキスト内の画像をAgentforceのRAG検索の対象にするための手法を紹介しました。

要点を振り返ると、以下の通りです。

  • RAGにおける画像の活用は「検索」と「回答生成」の2フェーズに分けて考える必要がある。今回は検索フェーズにフォーカスし、Prompt Templateによる画像テキスト化の手法を示した
  • Prompt Templateに画像を渡すにはContentVersionとして保存するステップが必要である
  • ガバナ制限を回避するためにジョブチェーン方式を採用し、さらにContentVersionのコミットタイミングの制約から、1画像の処理も2段階に分離した。実運用ではバッチサイズを持たせることでジョブ数を削減できる

画像はテキストでは表現しきれない多くの情報を含んでいますが、現状のAgentforceではテキスト化を介さないと検索対象にできません。本ブログで紹介した手法が、同様の課題に直面している方の参考になれば幸いです。

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