コンテキスト検索(Contextual Retrieval)技術
(anthropic.com)- 業務知識ベースAIで使われる RAG は、文書を小さなチャンクに分割する過程で会社名や時点といったコンテキストを失い、関連情報を取りこぼすことがある
- Anthropicの Contextual Retrieval は、各チャンクの前に文書全体基準の短い説明を付けたうえで埋め込みとBM25インデックスを作成し、検索失敗率を下げる方式
- 実験では Contextual Embeddings により top-20チャンク検索失敗率が 5.7%から3.7%へ、35% 低下し、Contextual Embeddings と Contextual BM25 の組み合わせでは 2.9%へ、49% 低下
- 再ランキング(reranking) を追加すると、top-150候補を再スコアリングしてtop-20だけをモデルに渡し、検索失敗率が 5.7%から1.9%へ、67% 減少
- 知識ベースが 200,000トークン以下 なら全体をプロンプトに入れる方法も可能だが、より大きな知識ベースでは Contextual Retrieval と再ランキングの組み合わせをユースケースごとに評価する必要がある
RAGが失う文書コンテキスト
- 特定業務でAIモデルが有用であるには、背景知識 へのアクセスが必要
- カスタマーサポート用チャットボットには、そのビジネス知識が必要
- 法務分析ボットには過去判例の知識が必要
- 開発者は通常、Retrieval-Augmented Generation(RAG) でモデル知識を補強する
- 知識ベースから関連情報を検索する
- 検索した情報をユーザープロンプトに追加し、モデル応答を改善する
- 従来のRAGは、文書をチャンクに分割する過程で コンテキストを除去 してしまい、関連情報を見つけられないことがある
- Contextual Retrieval はRAGの検索段階を改善する方法であり、2つの下位技法を併用できる
- Contextual Embeddings: コンテキストを付けたチャンクで埋め込みを生成する
- Contextual BM25: コンテキストを付けたチャンクでBM25インデックスを作る
- この組み合わせは検索失敗数を 49% 減らし、再ランキングと組み合わせると 67% まで減らせる
- Claudeで Contextual Retrieval を展開するための cookbook が提供されている
小さな知識ベースでは長いプロンプトも可能
- 知識ベースが 200,000トークン以下、約500ページ未満であれば、RAGなしで知識ベース全体をプロンプトに入れられる
- Claudeの prompt caching はこのアプローチをより高速かつコスト効率よくする
- よく使うプロンプトをAPI呼び出し間でキャッシュできる
- レイテンシを 2倍以上 短縮できる
- コストを最大 90% 削減できる
- 動作方法は prompt caching cookbook で確認できる
- 知識ベースが大きくなると、よりスケーラブルな検索方式が必要になり、その際に Contextual Retrieval が使われる
一般的なRAGとBM25の役割
- 大きな知識ベースがコンテキストウィンドウに収まらない場合、一般的な解法は RAG
- RAGの前処理フローは次の通り
- 文書コーパスを通常は数百トークン以下の小さなテキストチャンクに分割する
- 埋め込みモデルでチャンクを意味を持つベクトル埋め込みに変換する
- 意味類似検索が可能なベクターデータベースに埋め込みを保存する
- 実行時には、ユーザーのクエリと意味的に最も近いチャンクをベクターデータベースから探し、関連チャンクを生成モデルのプロンプトに追加する
- 埋め込みモデルは意味的関係をよく捉えるが、正確な文字列一致 を見逃すことがある
- BM25(Best Matching 25) は語彙一致を利用するランキング関数で、固有識別子や技術用語を含むクエリに特に有効
- BM25はTF-IDFの概念に基づく
- TF-IDFは、コレクション内で特定の単語が文書にとってどれほど重要かを測る
- BM25は文書長を考慮し、term frequencyに飽和関数を適用して、よくある単語が結果を支配しないようにする
- たとえば技術サポートのデータベースで
"Error code TS-999"を検索すると、埋め込みモデルは一般的なエラーコード文書を見つけられても、正確な"TS-999"一致は見逃す可能性がある - BM25はその文字列を直接探して関連文書を特定する
埋め込みとBM25を併用するRAG
- RAGは埋め込みとBM25を組み合わせて、より正確なチャンクを検索できる
- 知識ベースを小さなチャンクに分割する
- 各チャンクについて TF-IDFエンコーディング と意味埋め込みを生成する
- BM25で厳密一致ベースの上位チャンクを見つける
- 埋め込みで意味類似度ベースの上位チャンクを見つける
- rank fusion 手法で結果を結合し、重複を除去する
- 上位K件のチャンクをプロンプトに入れて応答を生成する
- この方法は、正確な用語一致と広い意味理解のバランスを取る
- 単一プロンプトに収められない巨大な知識ベースまで、コスト効率よく拡張できる
- ただし従来のRAGシステムには、文書をチャンクに分割する際に コンテキストを破壊 してしまう限界がある
- SEC開示ベースの知識ベースで
"What was the revenue growth for ACME Corp in Q2 2023?"と尋ねる状況がその代表例- 関連チャンクが
"The company's revenue grew by 3% over the previous quarter."だけを含んでいることがある - このチャンクだけでは、どの会社のどの期間か分からない
- 検索段階でも利用段階でも、正しい情報を活用しにくい
- 関連チャンクが
Contextual Retrieval の仕組み
- Contextual Retrieval は、各チャンクを埋め込み、BM25インデックスを作成する前に、チャンクごとの説明コンテキストを先頭に付ける
- 埋め込みに適用すると Contextual Embeddings
- BM25インデックスに適用すると Contextual BM25
- SEC開示の例では、元のチャンクは次の通り
The company's revenue grew by 3% over the previous quarter.
- コンテキスト化されたチャンクは次のように変わる
This chunk is from an SEC filing on ACME corp's performance in Q2 2023; the previous quarter's revenue was $314 million. The company's revenue grew by 3% over the previous quarter.
- 過去にも検索改善のためにコンテキストを活用する方法が提案されてきた
- チャンクに 一般的な文書要約を追加 する方式は、実験ではごく限定的な改善しか示さなかった
- hypothetical document embedding がある
- summary-based indexing は評価で低い性能を示した
- これらの方法は、Contextual Retrieval のチャンク単位でコンテキストを追加する方式とは異なる
Claudeでチャンクコンテキストを生成する
- 数千から数百万のチャンクに人手で注釈を付けるのは現実的ではない
- Anthropicは、Claudeが文書全体のコンテキストをもとに各チャンクを説明する 短くチャンク単位のコンテキスト を作るようプロンプトを設計した
- Claude 3 Haiku に使用したプロンプトは次の通り
<document>
{{WHOLE_DOCUMENT}}
</document>
Here is the chunk we want to situate within the whole document
<chunk>
{{CHUNK_CONTENT}}
</chunk>
Please give a short succinct context to situate this chunk within the overall document for the purposes of improving search retrieval of the chunk. Answer only with the succinct context and nothing else.
- 生成されるコンテキストテキストは通常 50〜100トークン で、チャンクの前に付けたうえで埋め込みとBM25インデックス生成に使う
- prompt caching を使えば、各チャンクごとに参照文書を再送する必要がない
- 文書を一度キャッシュに載せ、以前にキャッシュされたコンテンツを参照する
- 800トークンのチャンク、8kトークンの文書、50トークンのコンテキスト指示文、チャンク当たり100トークンのコンテキストを仮定すると、コンテキスト化チャンク生成の一回限りのコストは 文書100万トークン当たり1.02ドル
実験方法と検索性能
- 実験は複数の知識領域、埋め込みモデル、検索戦略、評価指標にわたって行われた
- コードベース
- 小説
- ArXiv論文
- 科学論文
- 各領域の質問と回答の例は Appendix II に含まれる
- グラフはすべての知識領域における平均性能を示し、上位の埋め込み構成である Gemini Text 004 と top-20チャンク検索を使用している
- 評価指標は 1 - recall@20
- 上位20件のチャンク内に関連文書を検索できなかった割合を測る
- 全体結果では、評価したすべての embedding-source 組み合わせで、コンテキスト化を適用したとき性能が改善した
- 性能改善結果は次の通り
- Contextual Embeddings: top-20チャンク検索失敗率を 5.7% → 3.7%、つまり 35% 低下
- Contextual Embeddings + Contextual BM25: 検索失敗率を 5.7% → 2.9%、つまり 49% 低下
実装時の考慮点
- チャンク境界 は検索性能に影響することがある
- チャンクサイズ
- チャンク境界
- チャンクの重なり
- Contextual Retrieval はテストしたすべての埋め込みモデルで性能を改善したが、モデルごとに改善幅は異なる可能性がある
- 提供されている一般プロンプトはよく機能するが、ドメインやユースケースに合わせた カスタム contextualizer プロンプト でさらに良い結果が得られる可能性がある
- 例: 知識ベース内の他文書でのみ定義される重要用語の glossary を含める
- より多くのチャンクをコンテキストウィンドウに入れるほど、関連情報が含まれる可能性は高まる
- ただし情報が多いほどモデルが注意散漫になる可能性があり、限界がある
- 5件、10件、20件のチャンクを渡してみた結果、20件が最も性能が良かった
- 実際のユースケースでは実験が必要
- 応答生成にはコンテキスト化されたチャンクを渡し、何がコンテキストで何が元のチャンクかを区別すると改善する可能性がある
- 必ず評価を実行 すべき
再ランキングでさらに改善する
- 再ランキング は、大きな知識ベースで最初に検索された候補チャンクのうち、最も関連性の高いチャンクだけをモデルに渡すフィルタリング技法
- この段階は、モデルが処理する情報量を減らすことで、応答品質、コスト、レイテンシに影響し得る
- 手順は次の通り
- 初期検索で潜在的に関連する上位チャンクを取得する
- 実験では top 150 を使用
- 上位N件のチャンクとユーザークエリを再ランキングモデルに渡す
- 再ランキングモデルが各チャンクに関連性と重要度のスコアを与える
- 上位K件のチャンクを選択する
- 実験では top 20 を使用
- 選択されたチャンクをモデルコンテキストに入れて最終結果を生成する
- 実験は Cohere reranker で行われた
- Voyage reranker もあるが、テストする時間はなかった
- さまざまなドメインで再ランキング段階を追加すると、検索はさらに最適化される
- Reranked Contextual Embedding + Contextual BM25 は、top-20チャンク検索失敗率を 5.7% → 1.9%、つまり 67% 低下させた
コストとレイテンシのバランス
- 再ランキングはランタイムに追加段階を入れるため、小さなレイテンシを必然的に追加する
- 再ランキングモデルがすべてのチャンクを並列にスコアリングしても、レイテンシとコストへの影響は考慮が必要
- より多くのチャンクを再ランキングすれば性能は向上し得るが、コストとレイテンシは増える
- より少ないチャンクを再ランキングすればコストとレイテンシは下がるが、性能改善は小さくなる可能性がある
- 適切なバランスは、特定のユースケースで複数の設定を実験して見つける必要がある
組み合わせた場合の結果
- 複数のデータセット種類で、埋め込みモデル、BM25使用有無、Contextual Retrieval 使用有無、再ランキング使用有無、top-K検索件数の組み合わせを比較した
- 結果の要約は次の通り
- Embeddings + BM25 は埋め込み単独より優れている
- テストした埋め込みの中では Voyage と Gemini が最も良かった
- top-20チャンクをモデルに渡す方が top-10 や top-5 より効果的
- チャンクにコンテキストを追加すると検索精度が大幅に改善する
- 再ランキングは、再ランキングなしより優れている
- 性能改善を最大化するには、Voyage または Gemini ベースの Contextual Embeddings、Contextual BM25、再ランキング段階、プロンプトへの20件のチャンク追加を組み合わせられる
- 開発者は Contextual Retrieval cookbook でこのアプローチを試せる
1件のコメント
Hacker News の意見
政府機関向けのエンタープライズ RAGを作っている。RAGAS 指標で実験的な A/B テストをしてみたところ、合成評価質問を基準にすると、ハイブリッド検索(意味検索 + ベクトル)の後ろに LLM ベースの再ランキングを付けても大きな変化はなく、HyDE は回答品質と検索品質を大きく低下させた。
まだ専門家の質問と実際のユーザー質問で、RAGAS 評価をさらに行う必要がある。
なので、本番運用/エンタープライズ RAG でハイブリッド検索が常に良いというのは目新しい話ではないが、ある手法がいつも勝つわけではない。私たちの場合は、ベクトル類似度の横に置く第2の手法として、Azure AI Search の意味検索だけで十分だった。別の環境では BM25 や、微調整されたクエリ後処理用の小型言語モデルが良いかもしれないので、結局はユースケースごとに継続的にテストする必要がある。
次は RAPTOR、SelfRAG、エージェント型 RAG、クエリ改善(拡張とサブクエリ)、GraphRAG を試す予定。
これまでの教訓は、ベースラインと実験群を置き、RAGAS のような指標で帰無仮説を反証しようとすべきであり、評価用の質問/回答には、専門家が作成した Q&A、ログから得た実ユーザー質問、原文ドキュメントから生成した合成 Q&A の3種類を使うべきだということ。
この方式で一番気に入っているのは、プロンプトキャッシュをうまく活用している点。
キャッシュされたプロンプトは通常コストの約 1/10 程度なので、すべてのチャンクを元ドキュメント全体と一緒に回すような小技は、以前はコスト的に現実的ではなかったが、今は可能になった。
キャッシュによるコスト削減のおかげで、こうした実用的な手法がもっと広がりそうだ。
コンテキスト検索のメモ: https://simonwillison.net/2024/Sep/20/introducing-contextual... そしてプロンプトキャッシュのメモ: https://simonwillison.net/2024/Aug/14/prompt-caching-with-cl...
最新モデルや機能についていくにはどの程度使う必要があるのか、感覚をつかむ上で役立つ文脈になりそう。
伝統的な検索のバックグラウンドから見ると、RAG を既成の埋め込みモデルとベクトル検索だけに限定するのは、かなり素朴な戦略に見える。ベクトル検索はそれ自体そこまで優れているわけではなく、LLM に渡す文脈を改善するには追加の情報検索戦略が必要だ。ここでやっていることも実質的にはそれだ。
Microsoft は以前 Graph RAG の論文を出しており、インデックスデータからエンティティ抽出で作った概念グラフを基盤に、RAG とベクトル検索を組み合わせている。こうすると、マッチしたチャンクに文脈上関連する情報を引き上げられる。
ベクトル検索なしでもかなり遠くまで行けそうな感触がある。コストもずっと安くなる。従来型の検索エンジンと、よく調整されたクエリだけを使えばよい。もちろん肝はクエリチューニングで、汎用的なケースにはあまり合わないかもしれないが、より特化したケースでは通用する可能性がある。
文脈を補うと、このアプローチはそれほど新しくはない。RAG の結果を改善する一般的な方法の一つは、LLM で基盤となるチャンクを「拡張」し、マッチする意味的な表面積を増やすことだ。
HyDE[1] でクエリ拡張を回すとさらに改善できることもあるが、常に良くなるわけではないので、私は代替ルートとして使っている。
Anthropic がここで何を新たに出したのかはよく分からない。cookbook のコードを見ても、その文脈を作る過程を示しているだけで、「コンテキスト検索」について API が実際に変わったわけではない。
変わったのは1か月前に導入されたプロンプトキャッシュで、長いドキュメント全体を文脈として提供し、個々のチャンクにより良い文脈を非常に安価に付けられるようにしたことだ。キャッシュは開発者に公開するのに適した優れた機能で、その価値は認める。
ただしそれ以外は、特定の RAG ワークフローを示す cookbook がすべてに見える。
付け加えると、Cohere は使った API の中でかなり気に入っている方だ。所属関係はなく、Cohere RAG APIは他のプロバイダーと違って非常に良かった。強くおすすめする。
1: https://arxiv.org/abs/2212.10496
データに数万個のチャンクがあるなら、これを全部やる必要があるのでコストがかさむ。ドキュメントをキャッシュすれば、そのコストを減らせる。
自分の RAG 要件にはあまり合わず、おそらく大半も同じだと思う。クエリは翌月や1年後に実行される可能性があるからだ。そのポリシーが変わったようにも見えないので、RAG に関連して Prompt Caching の話が出るのは少し意外だ。
私たちも似たようなことをしています。まず文書を h1、h2、h3 の見出しを基準にチャンク化し、チャンクの冒頭にヘッダーをコンテキストとして付けています
仮の例として、元のチャンクが「成人の通常用量は 200mg 錠またはカプセル 1〜2 個を 1 日 3 回」だったとすると、今は
# Fever、## Treatment、区切り線の後に同じ内容が入る、という形ですかなりうまく動いているようで、文書のインデックス作成時に LLM も不要です
これが「ChatGPT は 12 月に遅くなった」という論争の起源かもしれません。その「日付」メタデータが、ChatGPT に対してあまり役に立たないよう知らせていた可能性がある、ということです
まず各ページを要約し、タイトルを付け、下位セクションの一覧を抽出します。次にすべての要約をまとめ、モデルに階層的なインデックスを作るよう依頼します。モデルが文書群全体をツリーとして整理し、クエリ時にはツリー内のパスを追加コンテキストとして結合します
この手法はあまり好みではありません。提示されたシナリオがよくある問題だという点には同意しますが、提案された解決策はぎこちなく感じます
ベクトル埋め込みにはバッグオブワーズ圧縮のような性質があり、最初の改行テキストブロックに過剰にインデックスされる可能性があります。その結果、ベクトルの特定のインデックスが本来よりはるかに 0 に近づきます。量子化を経ると最終的に 0 になり、密ベクトルの精度を大きく失う可能性があります。IDF 検索がある程度補完しますが、十分ではありません
埋め込みを「意味的にブースト」して文書タイトル、要約、抄録などにより近づければ、この「コンテキスト」接頭辞による再現率の利点を得つつ、ベースベクトルを汚染せずに済みます。実装上は加重和です。拡張段階でコンテキストウィンドウに入れる際、文書がマッチしたら要約チャンクも一緒に注入できます。個人的にはずっとすっきりした解法です
Trieve API の “semantic boost” の説明[1]:
semantic_boost: チャンクの埋め込みベクトルを距離フレーズの方向へ移動させるのに有用です。たとえばchunk_htmlがiphoneのチャンクを、distance_phraseにflagship、distance_factorに0.25を使って、「flagship」という語に 25% 近づけることができます。概念的には、chunk_htmlのinnerTextベクトルとdistance_phraseベクトルの間にユークリッド/L2 距離の線を引き、その線に沿ってchunk_htmlベクトルをdistance_factorL2Distanceだけ近づける、または遠ざけるということです[1]:https://docs.trieve.ai/api-reference/chunk/create-or-upsert-...
だとすると、ベクトル DB は単一の LLM、さらには Claude-3.5 Sonnet のような単一バージョンに縛られ、3.5 Haiku や Opus にも移せず、ChatGPT や Llama に移るには再インデックスが必要という意味ではないでしょうか?
最も有用だと思う手法は、チャンクが自分を参照する項目への複数のポインタを持つ連結リスト戦略を実装することです。この作業は手動で行っていますが、特定のノードを参照できる方法の多様性が大きく増えます
別の見方をすればコメントのようなものです。この記事の下にあるすべてのコメントは、元記事を指すポインタと見なせます。あるコメントは元記事との意味的距離が近く、あるコメントは書き手の認識のためにより遠くなります。しかし各コメントに
parent_idを付与すれば、元記事へアクセスする経路が増えますこの手法の例はここで見られます [1]。最終ユーザーがどんなクエリをするかを当てようとせず、単にユーザーに話させて、それをポインタとしてインデックスします。あるオブジェクトを表現できる選択肢は有限ですが、ある表現は中心となるオブジェクトの意味から非常に、非常に、非常に遠く離れています
[1] - https://x.com/yourcommonbase/status/1833262865194557505
小さなデータセットでは最良の答えを得るために 20 万トークンをそのまま入れればよい、という話は私の経験とは合いません
プロンプトが大きくなるほど出力の一貫性が落ち、指示追従も悪くなるケースをよく見ます。25k トークンよりはるかに少ない範囲でもこうしたことが起きているように思います。他の人も経験しているのか、これを避けるよく知られた方法があるのか気になります
興味深い。自分が直面している問題は、RAGで知識チャンクではなく、適用可能なルールを検索すること。文脈に適用できるルールだけを文脈に注入する必要がある。
まだ実験はしていないが、特定のルールが適用可能かどうかを判定する小さな分類器を学習させる方法はうまくいくかもしれないと思っている。メインのLLMは、現在の文脈でそのルールが実際に適用されるかを判断する役割を担う。
例えば、LLMでマルチユーザーのダンジョンゲームをプレイすると仮定してみよう。以前キャラクターがタクシーに関して悪い行いをしたため、ゲームが「タクシーに乗ろうとするたびに追い出される。『あなたが誰であるかは分かっており、タクシー会社の取締役に正式に謝罪するまでは客として受け入れない』」というルールを作ったとする。謝罪すればルールは削除される。タクシー会社の取締役は他のプレイヤーかもしれないし、自分のNPCタクシー部隊が執行するルールを最初に発動した人物かもしれない。
有効なルール数という観点でこれがどの程度スケールするのか、従来型のRAGをどの程度適用できるのかが気になっている。ルールが適用されるかを判断することは、知識チャンクが関連しているかを判断することよりも抽象的で難しい問題に見える。
特に難しくしている核心は、知識検索にはない依存関係のループだ。ルールが適用されるかを識別するには、そのルールを先に検索しなければならない。この問題をどう解けるだろうか?
例のクエリは、LLama 3.1 8Bの助けを借りて書くなら、闇のエルフ軍が接近し、Grimgold Ironfistが絶望的な状況に置かれており、かつてはドワーフ民兵隊の誇り高い一員だったが現在は体力が35%で、インベントリには古びたつるはしと水筒、古いパン、金貨17枚があり、Taxi Guildとの「問題の多い過去」にもかかわらず道端のタクシーを拾おうとしている、といった具合に十分詳しく書ける。
ベクトルストアから取得されるルールの例は、キャラクター名/属性とタクシーおよびTaxi Guildへの言及によってベクトル上の近さが生まれ、検索されるはずだ。
「Taxi GuildはGrimgoldに厳しい罰則を科した。彼がタクシーを拾おうとするたび、即座に車両から追い出される。Guildの布告には『評判の悪い髭のドワーフ、Grimgold Ironfistは、Golden Horse Cab Companyの取締役Thorgrim Stonebeardに正式に謝罪するまで、会員が運行するいかなるタクシーにも乗車できない。これに従わない場合、当社サービスから永久に排除される』と記されている」といったルールだ。
「知識ベースが200,000トークンより小さいなら(約500ページ分)」と言っているが、Anthropicには単にトークナイザーを公開してほしい。推測しなくて済むように。
AI業界全体が一周して、結局TF-IDFに戻ってくる日を待っている。