注: 以下の翻訳の正確性は検証されていません。AIPを利用して英語版の原文から機械的に翻訳されたものです。
LLMは、ビジネス固有のコンテキストに適用されると非常に強力です。特定のタスクが与えられた場合、最初のステップはほとんどの場合、LLMに提供すべき関連するコンテキストを見つけることです。関連するコンテキストを見つけることは、リトリーバルを利用した生成システムの設計において最も難しい部分であることが多いです。このセクションでは、コンテキストリトリーバルの一般的なアプローチをいくつか紹介します。最適なアプローチはデータの具体的な特性に大きく依存するため、単一の「最良の方法」は存在しません。しかし、ここで紹介するテーマは良い出発点となり、適宜修正や組み合わせが可能です。
新しいモデル世代のコンテキスト長の増加に伴い、意味検索を使用せずにフルコンテキストをプロンプトに渡すことができる場合があります。たとえば、GPT-4oの128kコンテキストウィンドウは300ページ以上のテキストに相当します。アプリケーションのフルコンテキストがこの制限内に収まる場合、検索を使用せずに始めることをお勧めします。
基本的な意味検索を作成するには、次の手順を実行します。
埋め込みに関する詳細は、Palantir提供モデルを使用して意味検索ワークフローを作成するためのドキュメントを参照してください。
リトリーバルに関する詳細は、Palantir提供モデルを使用して意味検索ワークフローを作成するためのドキュメントを参照してください。
AIP関連のツールが、ドキュメントコーパスにあるはずの質問に答えられない場合、まず関連するコンテキストがリトリーブされてプロンプトに渡されたかどうかを調査する必要があります。多くの場合、リトリーバルステップで最も関連性の高いコンテキストが表面化せず、次のステップでLLMが適切に応答できない原因となります。
データとクエリの内容に応じて、リトリーバルを改善するための多くのアプローチが存在します。そのいくつかを以下に示します:
最終的には、特定のユースケースの要件とどれだけの時間を投資するかに依存します。ユースケースによっては、元のシンプルなセットアップが十分に機能することもあります。そうでない場合、HyDEと意味チャンクを追加するだけで済むかもしれません。基本的な実装から始め、必要に応じて機能を追加することをお勧めします。
リトリーバルパフォーマンスを向上させる1つのアプローチはHyDE、すなわち仮想上のドキュメント埋め込みです。基本的なアイデアは、クエリを直接埋め込むのではなく、最初にLLMにこの質問に答える仮想上のチャンクを生成させ、その後でそれを埋め込むことです。これにより、クエリとその回答との非対称性がバランスされます。また、関連する学術論文"Precise Zero-Shot Dense Retrieval without Relevance Labels" ↗も参照できます。
これは、オリジンドキュメントと章をエンコードする特定のフォーマットでチャンクがフォーマットされている特定のケースで特に役立ちます。
たとえば、「動物との衝突にどのように対処するか?」という質問に適切な回答として以下のチャンクを考えてみてください:
Claim Management - Motor: Animal Collision:
動物との衝突に関する保険請求は、通常、A、B、Dタイプの保険に含まれます。ただし、除外事項が適用される場合があります...
まず、LLMに仮想上のチャンクを生成するようにプロンプトします。
Copied!1 2 3 4 5 6 7 8 9 10
あなたは保険専門アシスタントとして、同僚が問い合わせに対して関連する文書を見つけるのを手伝う役割を担っています。 以下のユーザーの問い合わせを与えられています: {query} 以下の形式で仮の段落を作成し、それに答えてください: {Document Name}: {Chapter name}: {Section} > ... {Content} ここで、{Document Name}は該当する文書の名前、{Chapter name}は章の名前、{Section}はセクションの名前、{Content}はセクションの内容を指します。
このプロンプトは次のような応答を返します。
動物事故の管理: 一般条件:
動物との衝突は、通常、包括的な保険パッケージに含まれています...
LLM の応答はすでに「実際の」答えに「近い」ため、その埋め込みはこの実際の答えを含むチャンクに近くなります。関数内のセマンティック検索は次のようになります。
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
async searchChunksByEmbedding(query: string, k: Integer): Promise<Chunk[]> { // 仮説のための完全なプロンプトを作成 const prompt = `...` // 仮説チャンクを生成 const hypothetical = await GPT_4o.createChatCompletion({messages: [{"role": "user", "contents": [{"text": prompt}]}]}) // 結果を埋め込み const embedding = await TextEmbeddingAda_002.createEmbeddings({inputs: [hypothetical]}) // 埋め込みを最近傍探索に使用 const docs = Objects.search() .chunks() .nearestNeighbors(chunk => chunk.vectorProperty.near(embedding, {kValue: k}) .orderByRelevance() .take(k) return docs }
query
と k
を引数に取り、Promise<Chunk[]> 型を返す非同期関数 searchChunksByEmbedding
の宣言。prompt
変数に仮説のためのプロンプト文字列を設定。GPT_4o.createChatCompletion
メソッドを使用して、仮説チャンクを生成。ここで prompt
を使用。TextEmbeddingAda_002.createEmbeddings
メソッドを使用して、生成された仮説チャンクの埋め込みを作成。Objects.search().chunks()
メソッドを使用して、埋め込みを基に最近傍のチャンクを検索。検索結果を関連性順に並べ替え、k
個のチャンクを取得。OpenAI Ada のような汎用埋め込みモデルは、多様なデータの大規模なコーパスでトレーニングされています。ユーザーのユースケースが特定のドメインのコーパス(たとえば製造業)での検索を必要とする場合、検索結果が予想通りに機能しないことがあります。これは、汎用埋め込みモデルが特定のドメインに対して埋め込み空間のごく一部しか使用していないためです。
カスタムモデルをファインチューニングすることは、これらのケースで検索精度を向上させる1つのアプローチですが、はるかに簡単な即時使用可能なソリューションとして、ランク付けされたキーワード検索を使用し、場合によっては一部のLLM前処理を行うことが挙げられます。
これは、Object Storage v2 が動作するインデックスが、クエリに対して既に「関連性」の概念を持っているためです。この関連性は他のチャンクに対して相対的であり、チャンクのドメイン固有のコンテキストを自動的に考慮します。
オブジェクトに対する関数は、オブジェクトクエリの結果を関連性によって並べ替えることをサポートしているため、次のような関数を書くことができます。
Copied!1 2 3 4 5 6 7 8 9
async searchChunksByKeywords(query: string, k: Integer): Promise<Chunk[]> { // `query` というキーワードでチャンクを検索し、関連度の高いものから `k` 個取り出す const chunks = Objects.search() // 検索オブジェクトを取得 .chunks() // チャンクを取得 .filter(chunk => chunk.text.matchAnyToken(query)) // クエリにマッチするチャンクをフィルタ .orderByRelevance() // 関連度順にソート .take(k) // 上位 `k` 個のチャンクを取得 return chunks // 結果のチャンクを返す }
しかし、この方法では意味検索のセマンティック要素が抽象化されます。たとえば、ユーザーが「どうやって鹿との衝突に対処するのか?」と尋ねた場合、そのまま関数に入力すると、動物の衝突全般について話している部分は見つかりません。LLM は、以下で説明するクエリアグメンテーションを通じてセマンティック要素を取り戻すことができます。
クエリの前処理は、返される結果の関連性を最大化するための重要なステップです。本質的には、検索の種類に依存して、ユーザーのクエリをそのコアコンポーネントに蒸留することを目指します。クエリエンリッチメントを考慮することができ、もう1つはクエリエクストラクションです。
ユーザーのクエリとキーワード検索に渡す内容の間に LLM ステップを挿入することで、クエリをより関連性の高いものに蒸留する可能性が生まれます。LLM にストップワードや不要なフレーズ(「...を見つけるのを手伝ってください」など)を削除し、他の関連する単語や同義語を追加するように促すことができます。
次のようなプロンプトを設定することができます:
あなたはユーザーが関連する文書を見つけるのを手助けする保険AIアシスタントです。
そのために、会社の内部データベースでキーワード検索を使用することができます。
以下のユーザーのクエリが与えられた場合: {query}
関連する結果を見つけるための検索用語のリストを作成してください。ストップワードを取り除き、
最も重要な用語には同義語や関連語を追加してください。
答えはカンマで区切られた値のリストとして示してください。
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
def generate_search_terms(query): stop_words = set(["a", "an", "and", "the", "to", "for", "with", "on", "in", "at", "by", "of", "is", "it", "this", "that"]) synonyms = { "insurance": ["coverage", "policy", "assurance"], "claim": ["demand", "request", "application"], "customer": ["client", "user", "policyholder"] } # クエリを単語に分割 words = query.lower().split() # ストップワードを除去 filtered_words = [word for word in words if word not in stop_words] # 同義語と関連語を追加 search_terms = [] for word in filtered_words: search_terms.append(word) if word in synonyms: search_terms.extend(synonyms[word]) # カンマで区切られたリストとして返す return ", ".join(search_terms) # 例 query = "insurance claim for customer" print(generate_search_terms(query))
たとえば、「動物との衝突にどのように対処しますか?」という質問に対するLLMの回答は以下のようになります。
deer, animal, collision, claims, wildlife, accidents, vehicle, damage, car, insurance, policy, coverage, comprehensive, reimbursement
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# このコードは、特定のキーワードのリストを定義しています。 # これらのキーワードは、動物(特に鹿)と車両の衝突に関連する保険請求に関するものです。 # キーワードのリストは、以下の通りです。 keywords = [ "deer", # 鹿 "animal", # 動物 "collision", # 衝突 "claims", # 請求 "wildlife", # 野生動物 "accidents", # 事故 "vehicle", # 車両 "damage", # 損害 "car", # 車 "insurance", # 保険 "policy", # 保険証書 "coverage", # 保険範囲 "comprehensive",# 包括的 "reimbursement" # 払い戻し ]
これは、"deer collisions" に言及していないドキュメントだけでなく、"animal"、"wildlife"、および "accidents" について一般的に言及しているドキュメントをユーザーが見つけることができるようにします。
コードでは:
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
async searchChunksByAugmentedKeywords(query: string, k: Integer): Promise<Chunk[]> { // クエリアグメンテーションのためのフルプロンプトを作成 const prompt = `...` // GPT-4を使用してクエリを拡張 const augmentedQuery = await GPT_4o.createChatCompletion({messages: [{"role": "user", "contents": [{"text": prompt}]}]}) // チャンクを検索し、拡張されたクエリに一致するトークンを持つものをフィルタリング const chunks = Objects.search() .chunks() .filter(chunk => chunk.text.matchAnyToken(augmentedQuery)) .orderByRelevance() // 関連性でソート .take(k) // 上位k個を取得 return chunks // チャンクの配列を返す }
クエリの拡張は、関連順のキーワード検索には効果的です。しかし、意味検索には、ユーザーのクエリの核心を抽出し、意味のない余計な用語(ストップワードなど)を削除し、必要に応じてクエリ用語を レマタイズまたはステミング ↗ する必要があります。
そのためには、クエリ抽出を行い、質問をユーザーの主要な問いに変換します。
たとえば、次のようなプロンプトが考えられます。
ユーザーから与えられたクエリを基にセマンティック検索を行う準備をしています。
与えられたクエリから主要なユーザーアクションを抽出し、不要なストップワードを取り除きます。
以下のユーザークエリを与えられた場合: {query}
アクションを連結して、ピリオドで区切って返します。
たとえば、「動物との衝突をどのように対処しますか?」という質問に対して、LLM は以下のように返答します。
Copied!1
動物の衝突を処理します。
この応答はクエリのセマンティックコンテンツを最大化し、セマンティック検索を実行した後に強力なマッチングの可能性を高めます。上記の例は、ストップワードのみを削除することでも解決できます。
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
async searchChunksByExtractedQuery(query: string, k: Integer): Promise<Chunk[]> { // クエリ拡張のための完全なプロンプトを作成 const prompt = `...` // GPT-4を使用して拡張されたクエリを生成 const augmentedQuery = await GPT_4o.createChatCompletion({messages: [{"role": "user", "contents": [{"text": prompt}]}]}) // 拡張されたクエリから埋め込みを生成 const embedding = await TextEmbeddingAda_002.createEmbeddings({inputs: [augmentedQuery]}) // 埋め込みに最も近いチャンクを検索 const chunks = Objects.search() .chunks() .nearestNeighbors(obj => obj.embeddingProperty .near(embedding, { kValue: k })) .allAsync() // チャンクを返す return chunks }
GPT_4o.createChatCompletion
は、GPT-4を使用してチャットの完了を作成する関数です。TextEmbeddingAda_002.createEmbeddings
は、テキストから埋め込みを生成する関数です。Objects.search().chunks().nearestNeighbors(obj => obj.embeddingProperty.near(embedding, { kValue: k })).allAsync()
は、埋め込みに最も近いチャンクを検索するための連鎖メソッドです。レシプロカルランクフュージョン (RRF) は、複数の検索タイプの結果を 1 つのリストに結合するためのシンプルなアルゴリズムです。本質的には、リスト内での順位が高いほど、ドキュメントに高いスコアを与えます。総スコアは、リスト全体のスコアの合計です。
k
は正則化として機能します。k が高いほど、ドキュメントがリスト内のどこに表示されるかは重要ではなく、リストに表示されること自体が重要になります。
`RRFscore(d ∈ D) = Σ [1 / (k + r(d))]`
`# kは高ランクと低ランクのバランスを取るための定数です。`
`# r(d)はドキュメントの順位/位置です。`
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
public combineResultsWithRRF(vectorSearchResults: Chunk[], keywordSearchResults: Chunk[], k: Integer = 60): Chunk[] { // RRF(Relevance Ranking Function)スコアリング関数を定義 const RRF = (r: number, k: number) => 1 / (r + k); // 各チャンクのスコアを追跡するマップを初期化 // ここでは各Chunkがstring型の主キー "id" プロパティを持つと仮定している const resultMap: Map<string, {chunk: Chunk, score: number}> = new Map(); const combinedResults: Chunk[] = []; const searchResultsList = [vectorSearchResults, keywordSearchResults]; searchResultsList.forEach((searchResults) => { searchResults.forEach((chunk, rank) => { // リスト内の各チャンクのスコアを計算 // そのスコアをマップ内のチャンクの合計に追加 const rrfScore = RRF(rank, k); const chunkData = resultMap.get(chunk.id) || {chunk: chunk, score: 0}; chunkData.score += rrfScore; resultMap.set(chunk.id, chunkData); }); }); // 全てのチャンクをリストに取得 resultMap.forEach((chunkData) => { combinedResults.push(chunkData.chunk); }); // スコアに基づいて降順にソート combinedResults.sort((a, b) => resultMap.get(b.id).score - resultMap.get(a.id).score); return combinedResults; }
完全なハイブリッド検索の実装は、次のようになります。
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
async hybridSearch(query: string, k: Integer, n1: Integer, n2: Integer): Promise<Chunk[]> { // キーワード検索とベクトル検索を並行して開始 const keywordSearchResultsPromise = await searchChunksByKeywords(query, n1) const vectorSearchResultsPromise = await searchChunksByEmbedding(query, n2) // 両方の検索結果を待機して取得 const [keywordSearchResults, vectorSearchResults] = await Promise.all([keywordSearchResultsPromise, vectorSearchResultsPromise]) // RRF(Relevance Ranking Fusion)を用いて結果を再ランク付け const rerankedResults = combineResultsWithRRF(vectorSearchResults, keywordSearchResults) // 上位k件の結果を返す return rerankedResults.slice(k) }