見出し画像

LlamaIndex v0.10のQueryingでクエリをやってみる

2024/02/23

※すべての内容は無料で閲覧いただけます。

こちらの公式ドキュメント(v0.10.12)を参考にQueryingについてまとめていきます.

クエリ

これまでの記事で以下のことを行いました。

  • データのロード

  • インデックスの構築

  • インデックスの保存

これで、LLMアプリケーションで最も大事であるクエリに取り組む準備が整いました。

最も単純なクエリは、LLMへのプロンプト呼び出しにすぎません。

クエリは、

  • 質問して回答を取り出す

  • 要約を出力してもらう

  • より複雑な指示を取得

することができます。

より複雑なクエリに対しては、反復・連鎖プロンプト+LLM呼び出し、または複数のコンポーネントにわたる推論のループが含まれることがあります。

やってみる

クエリの一番の基礎は`QueryEngine`です。QueryEngineを取得する最も簡単な方法は、次のようにインデックスを取得してクエリエンジンを作成することです。

query_engine = index.as_query_engine()
response = query_engine.query(
    "Write an email to the user given their background information."
)
print(response)

これまでのことを全てまとめて実装したものはこちらです。ただし、新しいドキュメントからインデックス作成する場合と、保存済みインデックスから再構築する場合で枝分かれがあります。


クエリの段階

クエリはコードの裏で多くのことが行われています。大きく3つの異なる段階で構成されています。

  • 検索(Retrieval)は、クエリに最も関連性の高いドキュメントを`Index`から見つけて返すことです。インデックス作成で記述した通り、最も一般的なタイプの検索は「top-k」セマンティック検索ですが、他にも多くの検索戦略があります。

  • 後処理(Postprocessing)は、取得された`Node`にオプションで再ランク付け、トランスフォーム、またはフィルタリングを行います。例えば、キーワードなどの特定のメタデータが添付されていることを条件付けします。

  • 応答合成(Response synthesis)は、クエリ、最も関連性の高いデータ、およびプロンプトが結合されてLLMに送信され、応答が返されます。

ドキュメントとノードにメタデータを添付する方法はこちらの公式ドキュメントを参考にしてください。

クエリの段階のカスタマイズ

LlamaIndexでは、クエリを詳細に制御できる低レベルの合成APIが用意されています。

次の例では、`top_k`に別の数値を使用し取得したノードが含まれる最小類似スコアに達することを要求する後処理ステップを追加するように検索プログラムをカスタマイズします。
関連する結果がある場合は大量のデータが得られますが、関連する結果が何もない場合はデータが得られない可能性があります。

from llama_index.core import VectorStoreIndex, get_response_synthesizer
from llama_index.core.retrievers import VectorIndexRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.postprocessor import SimilarityPostprocessor

# build index
index = VectorStoreIndex.from_documents(documents)

# configure retriever
retriever = VectorIndexRetriever(
    index=index,
    similarity_top_k=10,
)

# configure response synthesizer
response_synthesizer = get_response_synthesizer()

# assemble query engine
query_engine = RetrieverQueryEngine(
    retriever=retriever,
    response_synthesizer=response_synthesizer,
    node_postprocessors=[SimilarityPostprocessor(similarity_cutoff=0.7)],
)

# query
response = query_engine.query("What did the author do growing up?")
print(response)

対応するインターフェイスを実装することで、独自の検索、応答合成、全体的なクエリロジックを追加することもできます。

実装されているコンポーネントとサポートされている構成の完全なリストはリファレンスを参照すると良いです。

各ステップのカスタマイズについて詳しく見ていきます。

レトリーバ(検索器)の構成

retriever = VectorIndexRetriever(
    index=index,
    similarity_top_k=10,
)

検索器には多種多様なものがあります。こちらのガイドで学ぶことができます。

ノードのポストプロセッサーの構成

取得した`Node`オブジェクトの関連性をさらに向上させる高度な`Node`と拡張があります。これは、LLMコールの時間、回数、コストを削減したり、応答品質を向上させたりするのに役立ちます。

例えば、

  • `KeywordNodePostprocessor`: `required_keywords`と`exclude_keywords`により、ノードをフィルタリングできます

  • `SimilarityPostprocessor`: 類似度スコアに閾値を設定してノードをフィルタリングします。そのため、埋め込みベースの検索プログラムでのみサポートされます。

  • `PrevNextNodePostprocessor`: 取得した`Node`オブジェクトを、Nodeの関係に基づいて追加の関連コンテキストで拡張します。

ノードの後処理の完全なリストは以下のリファレンスに記載されています。

必要なノードのポストプロセッサを構成するには、次の手順を実行します。

node_postprocessors = [
    KeywordNodePostprocessor(
        required_keywords=["Combinator"], exclude_keywords=["Italy"]
    )
]
query_engine = RetrieverQueryEngine.from_args(
    retriever, node_postprocessors=node_postprocessors
)
response = query_engine.query("What did the author do growing up?")

応答合成(response synthesize)の構成

取得者が関連するノードをフェッチした後、`BaseSynthesizer`情報を組み合わせて最終応答を合成します。

次のようにして設定できます。

query_engine = RetrieverQueryEngine.from_args(
    retriever, response_mode=response_mode
)

現時点では、次のオプションがサポートされています。

"""
網羅的に比較する際にこのリストを活用できます。
response_mode=choice[0] など
"""
choices = ['default', 'compact', 'tree_summarize', 'no_text', 'accumulate']
  • default: 取得した各Nodeを順番に調べて、回答を「作成、洗練」します。これにより、ノードごとに個別のLLM呼び出しが行われ、より詳細な回答が得られます。

  • compact: 最大プロンプトサイズ内に収まるできるだけ多くのノードをテキストチャンクを詰め込むことで、各LLM呼び出し中にプロンプトを圧縮します。1つのプロンプトに詰め込むにはチャンクが大すぎる場合には複数のプロンプトを検討して回答を「作成、洗練」します。

  • tree_summarize: 一連の`Node`オブジェクトとクエリを指定して、再帰的にツリーを構築し、ルートノードを応答として返します。要約の目的に適しています。

  • no_text: LLMに送信されるはずだったノードをフェッチ(取得)するために検索プログラムのみを実行し、実際にはノードを送信しません。その後、`response.source_nodes`をチェックすることで調べることができます。レスポンスオブジェクトについてはセクション5で詳しく説明されます。

  • accumulate: 一連の`Node`オブジェクトとクエリを指定して、応答を配列に貯めていきながら、それぞれのテキストNodeチャンクにクエリを適応します。全ての応答を連結した文字列を返します。各テキストNodeチャンクに対して個別に実行する必要がある場合に適しています。

全てのパターンを意味的質問と要約質問に対して応答を見てみました。

結果は明らかな違いはなく、より複雑なドキュメントやクエリに対して変化が見られるのではないかと思います。

以前に意味的質問と要約質問に対してIndexの種類で応答がどう変化するかを確認した実装はこちらの記事を参照ください。


構造化された出力

出力が構造化されていることを確認したい場合があります。クエリエンジンクラスからPydanticオブジェクトを抽出する方法については、以下の「クエリエンジン+Pydantic出力」を参照して下さい。

また、構造化出力ガイド全体も必ず確認して下さい。

独自のクエリパイプラインの作成

複雑なクエリフローを設計する場合は、プロンプト・LLM・出力パーサーから検索器、応答生成機、独自のカスタムコンポーネントに至るまで、さまざまなモジュールにわたって独自のクエリパイプラインを構成できます。

詳細については、クエリパイプラインモジュールガイドを参照してください。


まとめ

  • クエリの際は、indexを取得してクエリエンジンを作成する

  • クエリは、検索、後処理、応答合成の3つのプロセスに分けられる

  • 検索器は多様なものがあり、各検索器はtop_kなどのオプションが設定できる

  • 後処理では、LLMのコール回数や速度向上に有効なキーワードや類似度に基づくノードの指定ができる

  • 応答生成では、response_modeの変更で応答の方向性を指定できる

  • 複雑なクエリフローを自作することができる

次に読む記事

こちらの公式ドキュメントで、どのようなLLMを用いたアプリケーションを構築するか、そしてどのような方法で構築するかについて学びましょう。


支援のお願い

ここまで読んでいただきありがとうございます。「スキ」で反応をいただけると励みになります。

また、継続的な記事の公開のために、支援をしていただけると幸いです。

優良部分に特に内容はありません。

ここから先は

23字

¥ 200

この記事が気に入ったらサポートをしてみませんか?