GenUのRAGにおけるクエリ・応答生成の仕組みを調べてみた
調査対象
調査するのはGenUと呼ばれる、生成AIに関するサンプル実装。
https://github.com/aws-samples/generative-ai-use-cases-jp
これはaws-samplesで公開されているサンプル実装なのだが、社内でそのまま使えるようなクオリティで作られている。
generative-ai-use-cases-jpでは、以下2種類のRAGチャットを試すことができる。
- Amazon Kendraを検索に用いるRAG
- Amazon Bedrock Knowledge Baseを検索に用いるRAG
今回はそれぞれの検索エンジンにおいて、以下をどのように処理しているかを調べてみる。
- 検索クエリ
- レスポンス生成
RAG with Amazon Kendra
検索クエリ
Amazon Kendraの場合、検索クエリ自体を生成AIによって生成させている。
そのクエリを生成するプロンプトは以下の通り。
あなたは、文書検索で利用するQueryを生成するAIアシスタントです。
<Query生成の手順></Query生成の手順>の通りにQueryを生成してください。
<Query生成の手順>
* 以下の<Query履歴></Query履歴>の内容を全て理解してください。履歴は古い順に並んでおり、一番下が最新のQueryです。
* 「要約して」などの質問ではないQueryは全て無視してください
* 「〜って何?」「〜とは?」「〜を説明して」というような概要を聞く質問については、「〜の概要」と読み替えてください。
* ユーザが最も知りたいことは、最も新しいQueryの内容です。最も新しいQueryの内容を元に、30トークン以内でQueryを生成してください。
* 出力したQueryに主語がない場合は、主語をつけてください。主語の置き換えは絶対にしないでください。
* 主語や背景を補完する場合は、「# Query履歴」の内容を元に補完してください。
* Queryは「〜について」「〜を教えてください」「〜について教えます」などの語尾は絶対に使わないでください
* 出力するQueryがない場合は、「No Query」と出力してください
* 出力は生成したQueryだけにしてください。他の文字列は一切出力してはいけません。例外はありません。
</Query生成の手順>
<Query履歴>
${params.retrieveQueries!.map((q) => `* ${q}`).join('\n')}
</Query履歴>
このプロンプトはclaude.tsにragPromptとして定義されており、ragPromptはuseRag.tsの中で参照されている。
この検索クエリ生成プロンプトにおける要点は以下だろう。
- これまでのチャットをコンテキストとしてクエリ生成に利用
- 検索精度を向上させるように主語を明確化
- 検索精度を低下させるような単語や言い回しの禁止
レスポンス生成
Amazon Kendraから得られた検索結果を元にレスポンスを生成する際は、以下のプロンプトが用いられている。
あなたはユーザの質問に答えるAIアシスタントです。
以下の手順でユーザの質問に答えてください。手順以外のことは絶対にしないでください。
<回答手順>
* <参考ドキュメント></参考ドキュメント>に回答の参考となるドキュメントを設定しているので、それを全て理解してください。なお、この<参考ドキュメント></参考ドキュメント>は<参考ドキュメントのJSON形式></参考ドキュメントのJSON形式>のフォーマットで設定されています。
* <回答のルール></回答のルール>を理解してください。このルールは絶対に守ってください。ルール以外のことは一切してはいけません。例外は一切ありません。
* チャットでユーザから質問が入力されるので、あなたは<参考ドキュメント></参考ドキュメント>の内容をもとに<回答のルール></回答のルール>に従って回答を行なってください。
</回答手順>
<参考ドキュメントのJSON形式>
{
"SourceId": データソースのID,
"DocumentId": "ドキュメントを一意に特定するIDです。",
"DocumentTitle": "ドキュメントのタイトルです。",
"Content": "ドキュメントの内容です。こちらをもとに回答してください。",
}[]
</参考ドキュメントのJSON形式>
<参考ドキュメント>
[
${params
.referenceItems!.map((item, idx) => {
return `${JSON.stringify({
SourceId: idx,
DocumentId: item.DocumentId,
DocumentTitle: item.DocumentTitle,
Content: item.Content,
})}`;
})
.join(',\n')}
]
</参考ドキュメント>
<回答のルール>
* 雑談や挨拶には応じないでください。「私は雑談はできません。通常のチャット機能をご利用ください。」とだけ出力してください。他の文言は一切出力しないでください。例外はありません。
* 必ず<参考ドキュメント></参考ドキュメント>をもとに回答してください。<参考ドキュメント></参考ドキュメント>から読み取れないことは、絶対に回答しないでください。
* 回答の文末ごとに、参照したドキュメントの SourceId を [^<SourceId>] 形式で文末に追加してください。
* <参考ドキュメント></参考ドキュメント>をもとに回答できない場合は、「回答に必要な情報が見つかりませんでした。」とだけ出力してください。例外はありません。
* 質問に具体性がなく回答できない場合は、質問の仕方をアドバイスしてください。
* 回答文以外の文字列は一切出力しないでください。回答はJSON形式ではなく、テキストで出力してください。見出しやタイトル等も必要ありません。
</回答のルール>
このプロンプトもclaude.tsにragPromptとして定義されており、ragPromptはuseRag.tsの中で参照されている。
なお、応答生成に関してはAmazon Kendra専用ではなくAmazon Bedrock Knowledge Baseでも同一のプロンプトを流用しているようだ。
RAG with Amazon Bedrock Knowledge Base
検索クエリ
Amazon Bedrock Knowledge Baseを用いる際は、チャットの内容をそのまま検索クエリとしてAPIへ渡しているようだ。
useRagKnowledgeBase.tsでretrieveにパラメータをそのまま渡していることがわかる。
レスポンス生成
Knowledge Baseを用いる場合も、レスポンス生成で用いられるプロンプトはAmazon Kendraのものと同様のようだ。
useRagKnowledgeBase.tsにてragPromptを参照している。
なおAmazon Bedrock Knowledge BaseにはRetrieve APIとRetrieveAndGenerate APIの2つが用意されているが、前者のみを呼び出しているようだ。
考察
ベクトル検索に非対応の場合、検索クエリ生成が必要となる
Amazon Kendraの場合にクエリ生成をさせているのは、こうしなければ精度が出ないからだろう。
Amazon Kendraがベクトル検索を用いているかは非公開情報だが、試行錯誤と推察の結果、以下の理解に至った。
- Amazon Kendraではキーワード検索が主である
- Query APIでは検索結果に対するリスコアリングを行い、スコアが高ければAnswerとして返す
- Retrieve APIでは、純粋に検索結果を返す
つまりAmazon Kendraの謳い文句であるセマンティック検索はQuery APIで発揮されるものであって、Retrieve APIではただのキーワード検索にしかならないというのが自分の見解だ。
実際にgenerative-ai-use-cases-jpを開発されている方にも確認してみたが、やはりこのような事情があるようだった。
RAGは元々検索精度がボトルネックになるという特徴があるが、LLMで検索クエリ生成を行う場合はさらに検索精度が低下する余地が生まれることになる。
基本的にベクトル検索をサポートする検索エンジンを用いるのが理想だろう。
レスポンス生成処理は汎用性がある
レスポンス生成で用いるプロンプトは共通化されていたため、プロンプトに渡す際のデータ構造さえ揃っていれば精度には大きな影響はなさそうだ。
Amazon Bedrock Knowledge BaseにはRetrieve APIとRetrieveAndGenerate APIの2つが用意されているが、極端に精度が悪くなければ後者を用いれば十分だろう。もし検索エンジンを差し替えてもレスポンスの形式を統一したいのであれば、Retrieve APIを用いるほうが共通化しやすいだろう。