LangChainの新機能Contextual Compression Retrieverを試す
Q&Aチャットボットのようなシステムを作成するとき、ユーザーの問い合わせに関連した情報をプロンプトに埋め込んで精度の高い回答を返す、といった仕組みはもはや一般的だと思います。
その上で、関連情報を取り出す仕組みとしてベクトルDBの利用が一般的になってきていますが、抽出した文章が必ずしも質問に対して適切な情報源になっているとは限らない可能性はあります。類似度から算出して似ていると評価されていても、文脈が違うケースもあったりするのではないでしょうか。
先日(4/21)追加されたContextual Compression Retrieverはまさにこの問題を解決するためのもので、ベクトルDBなどから抽出した情報の評価を行い、更にLLMsを利用して余計な情報を圧縮することで情報量の改善も行うことができる仕組みです。
コード例を試す
そういうわけで、早速下記のページで紹介されているコード例を試してみました。
データソースとしてWikipediaのキングダム解説ページを利用しました。
from langchain.text_splitter import CharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.document_loaders import TextLoader
from langchain.vectorstores import FAISS
def pretty_print_docs(docs):
print(f"\n{'-' * 10}\n".join([f"Document {i+1}:\n\n" + d.page_content for i, d in enumerate(docs)]))
documents = TextLoader('kingdom.txt').load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(documents)
retriever = FAISS.from_documents(texts, OpenAIEmbeddings()).as_retriever()
docs = retriever.get_relevant_documents("信が目指しているものは何ですか?")
pretty_print_docs(docs)
まずベクトルDBのFAISSにデータソースから1000文字ずつ分割した文字列をストアし、単純な類似度検索でドキュメントを抽出してみます。
次にContextualCompressionRetrieverを利用して、抽出した文章をクエリの意図に最適となるよう圧縮します。
from langchain.chat_models import ChatOpenAI
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
llm = ChatOpenAI(temperature=0)
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(base_compressor=compressor, base_retriever=retriever)
compressed_docs = compression_retriever.get_relevant_documents("信が目指しているものは何ですか?")
pretty_print_docs(compressed_docs)
Document 3の結果から、FAISS DBから抽出したデータに対してLLMsによる情報圧縮をかけていることがわかります。
base_compressorにはLLMChainExtractorの他にも、以下のようにフィルタオプションを取ることもできます。
さらにDocumentCompressorPipelineを利用することで、「ドキュメントを小さな塊に分割→冗長なドキュメントを削除→クエリとの類似性に基づいてフィルタリング」といった処理を連続で行わせることができます。
from langchain.document_transformers import EmbeddingsRedundantFilter
from langchain.retrievers.document_compressors import DocumentCompressorPipeline
from langchain.text_splitter import CharacterTextSplitter
splitter = CharacterTextSplitter(chunk_size=300, chunk_overlap=0, separator="\n")
redundant_filter = EmbeddingsRedundantFilter(embeddings=embeddings)
relevant_filter = EmbeddingsFilter(embeddings=embeddings, similarity_threshold=0.76)
# 各フィルタをDocumentCompressorPipelineで繋ぐ
pipeline_compressor = DocumentCompressorPipeline(
transformers=[splitter, redundant_filter, relevant_filter]
)
compression_retriever = ContextualCompressionRetriever(
base_compressor=pipeline_compressor,
base_retriever=retriever
)
compressed_docs = compression_retriever.get_relevant_documents("信が目指しているものは何ですか")
pretty_print_docs(compressed_docs)
所感
LLMChainExtractorを使った場合に、一番理想的な結果が得られましたが、全ての文章にLLMsを適用するため、速度と費用の問題があります。
一方でEmbeddingsFilterなどを組み合わせたパイプラインを利用すれば費用の問題は軽減できますが、期待するほどは情報量を圧縮できないような気がします。
プロンプトに埋め込むコンテキストから冗長な情報を除去/圧縮するというアイデアそのものは理に適っているため、これをいかにLLMsに頼り切らずに実装するか、というところが腕の見せ所になっていきそうです。
現場からは以上です。
この記事が気に入ったらサポートをしてみませんか?