LangChain

【Python】LangChain: RAGでLLMと外部データを連携する方法

【Python】LangChain: RAGでLLMと外部データを連携する方法

LangChainを用いたRAG(Retrieval-Augmented Generation)でLLMと外部データを連携する方法を解説します。

LangChainによるRAG(Retrieval-Augmented Generation)

LangChainは、大規模言語モデル(LLM: Large Language Model)の活用を容易にするオープンソースのPythonライブラリです。

LangChainには、PromptTemplateやOutputParser、Chainなどの機能を提供し、LLMの操作を簡略化しますが、その中でも重要な機能がRAG(Retrieval-Augmented Generation)です。このRAGにより、LLMと外部データを連携が可能になります。

この記事では、LangChainを用いたRAGでLLMと外部データを連携する方法を紹介します。

RAG(Retrieval-Augmented Generation)

まずは、RAG(Retrieval-Augmented Generation)の概要について説明します。

RAGは、外部データから必要な情報を探索し、LLMの生成プロセスに組み込むことで回答の精度を向上させる技術です。Retrievalは「検索、取得」、Augmentedは「拡張された、増強された」、Generationは「生成」という意味があります。

ChatGPTが登場して間もない時期は、学習に含まれていない直近の情報に関する回答はできませんでしたが、RAGにより大幅に回答の精度は向上しました。ただし、事実に基づかない情報の生成(ハルシネーション)を完全に防ぐことはできないので注意が必要です。

LangChainによるRAGの流れ

RAGは、以下の流れで実現されます。

LangChain RAGの流れ
  1. Load:データソースからデータを取り込みます。
  2. Transform:テキストを前処理して文や単語などに分割するなど、データをインデックス化しやすいように変換します。
  3. Embedding:データをベクトル情報に変換し、これを用いて意味的な類似性を計算できるようにします。
  4. Vector Store:テキストとベクトル情報を検索しやすいように保存します。
  5. Retrievers:Vector Storeからユーザーの問い合わせに関連する情報を検索し、LLMと連携します。

上記の流れにより、質問に最も関連性のある情報を迅速に見つけ出し、LLMと連携して正確な回答を生成します。

例として、社員が部門の残業状況を調べるケースを想定して、回答までの流れを見てみましょう。上記で紹介した流れでA社のVectorStoreが用意されているものとします。

LangChain Retrieversの動作概要

今回のケースで、A社やXX部門の情報は個別企業の情報のため、ChatGPT等の一般的なLLMに情報を含まれません。

そこで、A社の社員から質問があった際に、Retrieversが関連する情報をVector Storeから検索し、質問内容に付与してLLMに問い合わせます。これにより、LLMの回答精度の向上が期待できます。

上記が、RAGを実現するまでの流れでした。以降で、LangChainによるRAG実装の例を紹介していきます。

LangChainにおけるRAG関連機能と実装

LangChainには、RAGを実現するための各種機能が提供されています。以降では、それぞれの基本的な使い方について説明し、RAG実装の流れを紹介していきます。

本記事で使用するバージョンは以下の通りです。バージョンが異なる場合は動作しないことがあります。

  • Python: 3.11.5
  • langchain: 0.2.11
  • langchain-core: 0.2.26
  • langchain-community: 0.2.10
  • langchain-text-splitters: 0.2.2
  • langchain-openai: 0.1.16
  • langchain-chroma 0.1.2
  • pypdf: 4.3.1
  • beautifulsoup4: 4.12.3
  • wikipedia: 1.4.0

Document Loader

Document Loaderは、外部のドキュメントやデータソースからデータを取得するための機能群です。Document Loadersの公式サイトの情報はこちらを参照してください。

色々な種類のLoaderがありますが、基本的なLoaderの使い方を紹介していきます。

LangChain Document Loader

TextLoader:テキストファイルを読み込む

テキストファイルを読み込む場合には、TextLoaderを使用します。

from langchain_community.document_loaders import TextLoader

# TextLoaderを用意する
loader = TextLoader("../data/index.txt", encoding="utf-8")

# データを読み込む
document = loader.load()

print(document)
【実行結果】
[Document(metadata={'source': '../data/index.txt'}, page_content='LangChainとは\nLangChainとは、大規模言語モデル(LLM: Large Language Model)を活用するためのオープンソースPythonライブラリです。公式サイトは、こちらを参照してください。\n\nLangChainは、LLMを用いた複雑なタスクを実行するア
...(途中省略)...
した。ただし、アップデート時には、これらのパッケージ間での依存関係や整合性を考慮し、各バージョンの互換性にも注意が必要です。各パッケージがどのように連携するかは公式ドキュメントを参照するなどして確認するようにしてください。')]

TextLoaderは、インスタンス化の際にテキストファイルのパスやエンコーディングを指定し、データの読み込みにはloadメソッドを使用します。

返却値は、Documentクラスのインスタンスで、メタデータはmetadataに、テキスト内容はpage_contentに格納されます。

DirectoryLoader:ディレクトリ内のファイルを読み込む

ディレクトリ内のファイルをまとめて読み込みたい場合には、DirectoryLoaderを使用します。

from langchain_community.document_loaders import DirectoryLoader, TextLoader

# DirectoryLoaderを用意する
loader = DirectoryLoader(
    "../data",
    glob="*.txt",
    recursive=True,
    loader_cls=TextLoader,
    loader_kwargs={"encoding": "utf-8"},
)

# データを読み込む
documents = loader.load()

print(documents)
【実行結果】
[Document(metadata={'source': '..\\data\\index.txt'}, page_content='LangChainとは\nLangChainとは、大規模言語モデル(LLM: Large Language Model)を活用するためのオープンソースPythonライブラリです。公式サイトは、こちらを参照してください。\n\nLangChainは、LLMを用いた複雑なタスクを実行する
...(途中省略)...
Document(metadata={'source': '..\\data\\index1.txt'}, page_content='LangChainの基本的な使い方\nここでは、LangChainの基本的な使い方を紹介したいと思います。\n\n本記事で紹介する環境は以下バージョンでの動作結果です。バージョン更新が速いため最新バージョンではうまく動作しない可能性があります
...(途中省略)...
LangChainにはさまざまな機能があります。これらの機能についても、今後の記事更新や新たな記事で詳しく解説したいと考えています。\n\n皆さんも、ぜひ色々と調べてLangChainを使ってみてください。')]

DirectoryLoaderは、指定したディレクトリ内のファイルを一括で読み込みます。今回例では、dataディレクトリ内のindex.txtindex1.txtを対象としています。

DirectoryLoaderの引数は以下のようなものがあります。

引数概要説明
glob読み込むファイルのパターンを指定します。例えば「*.txt」とすることで、指定されたディレクトリ内のすべてのテキストファイルが対象になります。
recursiveディレクトリの検索を再帰的に行うかどうかを指定します。
loader_cls各ファイルを読み込む際に使用するDocument Loaderクラスを指定します。
loader_kwargsloader_clsで指定したDocument Loaderクラスに渡す追加の引数を設定します。

読み込みはloadメソッドで行い、結果はDocumentのリストとして返されます。

PyPDFLoader:PDFファイルを読み込む

PDFファイルを読み込む場合には、PyPDFLoaderを使用します。事前にpypdfパッケージをインストールしてください。

pip install pypdf

PyPDFLoaderは、以下のように使用します。

from langchain_community.document_loaders import PyPDFLoader

# PDFLoaderを用意する
loader = PyPDFLoader("../data/sample.pdf")

# データを読み込む
document = loader.load()

print(document)
【実行結果】
[Document(metadata={'source': './data/sample.pdf', 'page': 0}, page_content='LangChain とは  \nLangChain とは、大規模言語モデル (LLM: Large Language Model) を活用するためのオー\nプンソース Pythonライブラリです。公式サイトは、 こちらを参照してください。  \nLangChain は、 LLMを用いた複雑
...(途中省略)...
の研究チームによる「 Attention is All You Need 」という非常に有名な論文で紹介されまし\nた。  \n従来の再帰的ニューラルネットワーク (RNN)やLSTM(Long Short Term Memory) などで\n不得意であった長い依存関係の問題を解決し、その後の BERTやGPT等の現在の主流技\n術の登場に貢献しています。  \n ')]

PyPDFLoaderの使い方は、TextLoaderと同様です。

WebBaseLoader:Webサイトの情報を取得する

Webサイトのデータを読み込む場合には、WebBaseLoaderを使用します。Webスクレイピングには、Beautiful Soupが必要ですので、事前にインストールしてください。

pip install beautifulsoup4

WebBaseLoaderは、以下のように使用します。

from langchain_community.document_loaders import WebBaseLoader

# WebBaseLoaderを用意する
loader = WebBaseLoader("https://tech.nkhn37.net/profile/")

# データを読み込む
document = loader.load()

print(document)
【実行結果】
[Document(metadata={'source': 'https://tech.nkhn37.net/profile/', 'title': 'ホッシーの自己紹介|Python Tech', 'description': 'ホッシーのプロフィール プロフィール ※キャラクターデザイン ゼイルン様 愛知県生まれの30代サラリーマンのホッシーとい', 'language': 'ja'}, page_content='\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nホッシーの自己紹介|Python Tech\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nPython Tech ~Python学習サイト~
...(途中省略)...
\nエキスパートPythonプログラミング\nPythonデータサイエンスハンドブック\nPythonによるディープラーニング\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\nHOME\n\n\n\n\n\n\n自己紹介\n\n \n\n\n \n\n\n\n\n\nプライバシーポリシー\n免責事項\n2021–2024\xa0\xa0Python Tech\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n')]

WebBaseLoaderには、読み込み対象となるWebサイトのURLを指定します。今回は私のサイトのプロフィールページを指定してみました。データを取得手順は、TextLoaderなどの他のDocument Loaderと同様です。

WikipediaLoader:Wikipediaの情報を取得する

Wikipediaからデータを読み込むには、WikipediaLoaderを使用します。事前にwikipediaパッケージをインストールしてください。

pip install wikipedia

WikipediaLoaderは、以下のように使用します。

from langchain_community.document_loaders import WikipediaLoader

# WikipediaLoaderを用意する
loader = WikipediaLoader(query="langchain")

# データを読み込む
documents = loader.load()

# 結果を表示
print(len(documents))
print([d.page_content[:100] for d in documents])
【実行結果】
5
['LangChain is a framework designed to simplify the creation of applications using large language mode', 'Retrieval augmented generation (RAG) is a type of information retrieval process. It modifies interac', 'In natural language processing, a sentence embedding refers to a numeric representation of a sentenc', 'DataStax, Inc. is a real-time data for AI company based in Santa Clara, California. Its product Astr', 'Prompt injection is a family of related computer security exploits carried out by getting a machine ']

今回は"langchain"に関連するWikipediaの情報を取得してみました。WikipediaLoaderは、検索クエリをquery引数に指定してデータを取得します。

デフォルトでは英語の記事(lang="en")が対象です。結果として5つの記事が取得されました。言語を日本語に変更するには、lang="ja"を指定します。

from langchain_community.document_loaders import WikipediaLoader

# WikipediaLoaderを用意する
loader = WikipediaLoader(query="langchain", lang="ja")

# データを読み込む
documents = loader.load()

# 結果を表示
print(len(documents))
print([d.page_content[:100] for d in documents])
【実行結果】
1
['LangChain(ラングチェイン)は、大規模言語モデル(LLM)を使ったアプリケーションソフトウェアの作成を簡素化するように設計されたフレームワークである。言語モデル統合フレームワークとして、Lan']

この例では、日本語のWikipediaには"langchain"に関するページが1つ見つかりました。

Text Splitter

Text Splitterは、読み込んだドキュメントを分割し、Embeddingで扱いやすく加工するツールです。これは、RAGにおける「Transform」に対応します。Text Splitterの公式サイトの情報はこちらを参照してください。

LangChain Text Splitters

LangChainには、基本クラスとしてTextSplitterがあり、これを継承した様々なクラスが提供されています。以下で主要なクラスの使い方を紹介します。

【プロパティ】

名称説明
chunk_sizeチャンクの最大サイズ
chun_overlapチャンク間の重複文字数
length_functionチャンクサイズを計測する関数(デフォルトはlen

【メソッド】

名称説明
text_split文字列を分割してリストを返す
split_documentsドキュメントリストを分割してリストを返す

chunk_sizeは分割する際の最大サイズで、chunk_overlapはチャンク間の重複部分のサイズです。length_functionは、チャンクサイズの計測に使用する関数で、カスタム関数を渡すことも可能です。

CharacterTextSplitter:文字数で分割する

CharacterTextSplitterは、文字数を基準にドキュメントを分割します。

from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import CharacterTextSplitter

# TextLoaderを用意する
loader = TextLoader("../data/index.txt", encoding="utf-8")

# データを読み込む
document = loader.load()

# Character Text Splitterを用意する
text_splitter = CharacterTextSplitter(
    chunk_size=200,
    chunk_overlap=100,
    add_start_index=True,
    separator="。",
)

# 読み込んだデータを分割する
split_document = text_splitter.split_documents(document)

print(len(split_document))
print(split_document)
【実行結果】
21
[Document(metadata={'source': '../data/index.txt', 'start_index': 0}, page_content='LangChainとは\nLangChainとは、大規模言語モデル(LLM: Large Language Model)を活用するためのオープンソースPythonライブラリです。公式サイトは、こちらを参照してください'), Document(metadata={'source': '../data/index.txt', 'start_index': 88}, page_content='公式サイトは、こちらを参照してください。\n\nLangChainは、LLMを用いた複雑なタスクを実行するアプリケーションを構築することを目的としており、様々なツールとの統合、タスク指向の設計、拡張性とカスタマイズ性といったことを特徴としています。\n\nまた、オープンソースプロジェクトとして、多くの開発者や研究者がコミュニティを通じて貢献し、ライブラリは常に改善されています'), 
...(途中省略)..
Document(metadata={'source': '../data/index.txt', 'start_index': 2232}, page_content='バージョン0.1.0以降では、上記のようにパッケージ分割されることで開発や保守がしやすく、ユーザーにとってもパッケージ選択をしやすく柔軟性が増しました。ただし、アップデート時には、これらのパッケージ間での依存関係や整合性を考慮し、各バージョンの互換性にも注意が必要です。各パッケージがどのように連携するかは公式ドキュメントを参照するなどして確認するようにしてください')]

CharacterTextSplitterの使用には、chunk_sizechunk_overlapを設定します。add_start_index=Trueにより、分割時の開始インデックスがメタデータに含まれます。また、分割の基準となるセパレータをseparator引数を使って指定できます。

今回の例では21個のチャンクに分割されました。今回は、chunk_size=200chunk_overlap=100として半分ぐらい重複するように設定してみました。実際の結果を見ても一部重複部分が表れていることが分かります。

RecursiveCharacterTextSplitter:複数の区切り文字で分割する

RecursiveCharacterTextSplitterは、複数のセパレータを指定して分割します。

from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

# TextLoaderを用意する
loader = TextLoader("../data/index.txt", encoding="utf-8")

# データを読み込む
document = loader.load()

# Recursive Character Text Splitterを用意する
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,
    chunk_overlap=100,
    add_start_index=True,
    separators=[",", "、", "。"],
)

# 読み込んだデータを分割する
split_document = text_splitter.split_documents(document)

print(len(split_document))
print(split_document)
【実行結果】
22
[Document(metadata={'source': '../data/index.txt', 'start_index': 0}, page_content='LangChainとは\nLangChainとは、大規模言語モデル(LLM: Large Language Model)を活用するためのオープンソースPythonライブラリです。公式サイトは、こちらを参照してください。\n\nLangChainは、LLMを用いた複雑なタスクを実行するアプリケーションを構築することを目的としており、様々なツールとの統合、タスク指向の設計'), Document(metadata={'source': '../data/index.txt', 'start_index': 94}, page_content='、こちらを参照してください。\n\nLangChainは、LLMを用いた複雑なタスクを実行するアプリケーションを構築することを目的としており、様々なツールとの統合、タスク指向の設計、拡張性とカスタマイズ性といったことを特徴としています。\n\nまた、オープンソースプロジェクトとして、多くの開発者や研究者がコミュニティを通じて貢献し、ライブラリは常に改善されています。LangChainを用いることで'), 
...(途中省略)...
Document(metadata={'source': '../data/index.txt', 'start_index': 2246}, page_content='、上記のようにパッケージ分割されることで開発や保守がしやすく、ユーザーにとってもパッケージ選択をしやすく柔軟性が増しました。ただし、アップデート時には、これらのパッケージ間での依存関係や整合性を考慮し、各バージョンの互換性にも注意が必要です。各パッケージがどのように連携するかは公式ドキュメントを参照するなどして確認するようにしてください。')]

RecursiveCharacterTextSplitterでは、separators引数で複数の区切り文字リストを指定します。複数のセパレータを指定することで、より柔軟な分割が可能です。今回の例では22個のチャンクに分割されました。

TokenTextSplitter:トークン数で分割する

TokenTextSplitterは、トークン数を基準に分割します。トークン数は使用するモデルごとに決まります。

from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import TokenTextSplitter

# TextLoaderを用意する
loader = TextLoader("../data/index.txt", encoding="utf-8")

# データを読み込む
document = loader.load()

# Token Text Splitterを用意する
text_splitter = TokenTextSplitter(
    chunk_size=200,
    chunk_overlap=100,
    add_start_index=True,
    encoding_name="cl100k_base",
)

# 読み込んだデータを分割する
split_document = text_splitter.split_documents(document)

print(len(split_document))
print(split_document)
【実行結果】
19
[Document(metadata={'source': '../data/index.txt', 'start_index': 0}, page_content='LangChainとは\nLangChainとは、大規模言語モデル(LLM: Large Language Model)を活用するためのオープンソースPythonライブラリです。公式サイトは、こちらを参照してください。\n\nLangChainは、LLMを用いた複雑なタスクを実行するアプリケーションを構築することを目的としており、様々なツールとの統合、タスク指向の設計、拡張性とカスタマイズ性といったことを特徴としています。\n\nまた、オープンソースプロジェクトとして、多くの開発者や研究者'), Document(metadata={'source': '../data/index.txt', 'start_index': 148}, page_content='構築することを目的としており、様々なツールとの統合、タスク指向の設計、拡張性とカスタマイズ性といったことを特徴としています。\n\nまた、オープンソースプロジェクトとして、多くの開発者や研究者がコミュニティを通じて貢献し、ライブラリは常に改善されています。LangChainを用いることで、開発者はLLMを最大限に活用してアプリケーションを構築できます。\n\nLangChainの理解に必要な概念\nLangChainを扱っていく'), 
...(途中省略)...
Document(metadata={'source': '../data/index.txt', 'start_index': -1}, page_content='�では、上記のようにパッケージ分割されることで開発や保守がしやすく、ユーザーにとってもパッケージ選択をしやすく柔軟性が増しました。ただし、アップデート時には、これらのパッケージ間での依存関係や整合性を考慮し、各バージョンの互換性にも注意が必要です。各パッケージがどのように連携するかは公式ドキュメントを参照するなどして確認するようにしてください。')]

TokenTextSplitterは、encoding_name引数でトークン分割方法を指定します。例えば、"cl100k_base"はOpenAIのGPTモデルのトークン化方法に対応しています。今回の例では19個のチャンクに分割されました。

Embedding

Embeddingは、テキストを数値ベクトルへ変換し、Vector Storeに蓄積するための機能です。Document Loaderで読み込んだデータをText Splitterで分割した後にベクトル化を行います。Embeddingの公式サイトの情報はこちらを参照してください。

LangChain Embedding

LangChainでは、基本クラスとしてEmbeddingクラスがあり、これを継承した様々なクラスが提供されています。

【メソッド】

名称説明
embed_query単一の文字列を対象にベクトル化する
embed_documents複数の文字列を対象にベクトル化する

embed_queryは、単一のテキストを、embed_documentsは、複数のテキストを対象にします。

OpenAIEmbeddings:OpenAIの埋め込みモデル

Embeddingのモデルには様々な種類がありますが、ここではOpenAIのOpenAIEmbeddingsを使用します。使用するには、langchain-openaiパッケージが必要です。

pip install langchain-openai

OpenAIのAPI使用にはキーが必要です。APIキーについては「OpenAI APIの使い方の基本」や「LangChainで大規模言語モデル(LLM)を活用する」を参考にしてください。今回は、config.iniに設定して使用します。

config.ini

[OPENAI]
key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

OpenAIEmbeddingsを使用した埋め込み(ベクトル化)は以下のようにします。

import configparser

from langchain_community.document_loaders import TextLoader
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import TokenTextSplitter

# コンフィグファイルからキーを取得する
config = configparser.ConfigParser()
config.read("config.ini")

# TextLoaderを用意する
loader = TextLoader("../data/index.txt", encoding="utf-8")

# データを読み込む
document = loader.load()

# Token Text Splitterを用意する
text_splitter = TokenTextSplitter(
    chunk_size=200,
    chunk_overlap=100,
    add_start_index=True,
    encoding_name="cl100k_base",
)

# 読み込んだデータを分割する
split_documents = text_splitter.split_documents(document)

# 埋め込みモデルを用意する
embeddings_model = OpenAIEmbeddings(api_key=config["OPENAI"]["key"])

# 読み込んだデータをベクトル化する
embed = embeddings_model.embed_documents([d.page_content for d in split_documents])
print(len(embed))
print(embed)
【実行結果】
19
[[-0.007674966007471085, -0.0046662986278533936, -0.011643846519291401, -0.0388936847448349, 0.013503627851605415, 0.004646083805710077, -0.012209867127239704, 0.03091549128293991, 0.007937761023640633, 
...(途中省略)...
-0.02785574644804001, -0.01018467452377081, 0.012009980157017708, -0.010409531183540821, -0.01772397942841053, -0.010144994594156742, -0.012261290103197098]]

OpenAIEmbeddingsは、APIキーを渡してモデルを作成し、embed_documentsメソッドで分割したドキュメントをベクトル化します。結果として、各ドキュメントがベクトル化されていることが確認できます。

Vector Stores(Chroma)

ベクトル化したデータを蓄積するVector Storeとしては、Chroma、Pinecone、FAISS、Lanceなどがあります。ここでは、手軽で汎用性の高いChromaを使用したベクトルデータの蓄積方法を紹介します。Vector Storeの公式サイトの情報はこちらを参照してください。

LangChain Vector Store

Chromaにベクトル情報を保存する(InMemory)

Chromaは、ベクトル(高次元の数値配列)を効率的に保存・検索するためのベクトルデータベースです。コサイン類似度やドット積を使用してベクトル間の類似性を計算します。

Chromaの使用にはインストールが必要です。

pip install langchain-chroma

以下は、Embeddingで生成したベクトル情報をChromaに蓄積する例です。InMemory(メモリ上)にVector Storeを構築しています。

import configparser

from langchain_chroma import Chroma
from langchain_community.document_loaders import TextLoader
from langchain_openai.embeddings import OpenAIEmbeddings
from langchain_text_splitters import TokenTextSplitter

# コンフィグファイルからキーを取得する
config = configparser.ConfigParser()
config.read("config.ini")

# TextLoaderを用意する
loader = TextLoader("../data/index1.txt", encoding="utf-8")
# データを読み込む
document = loader.load()

# Token Text Splitterを用意する
text_splitter = TokenTextSplitter(
    chunk_size=200,
    chunk_overlap=100,
    add_start_index=True,
    encoding_name="cl100k_base",
)

# 読み込んだデータを分割する
split_documents = text_splitter.split_documents(document)

# 埋め込みモデルを用意する
embeddings_model = OpenAIEmbeddings(api_key=config["OPENAI"]["key"])

# ChromaにInMemoryで格納する
db = Chroma.from_documents(split_documents, embedding=embeddings_model)

# Chromaを検索する
query = "LangChainとは何ですか?"
docs = db.similarity_search(query)

print(len(docs))
print(docs)
【実行結果】
4
[Document(metadata={'source': '../data/index1.txt', 'start_index': -1}, page_content='デルの簡単な使い方について紹介しました。LangChainにはさまざまな機能があります。これらの機能についても、今後の記事更新や新たな記事で詳しく解説したいと考えています。\n\n皆さんも、ぜひ色々と調べてLangChainを使ってみてください。'), Document(metadata={'source': '../data/index1.txt', 'start_index': 5241}, page_content='することで、APIコールが省略され、コスト節約とレスポンス向上が期待できます。\n\nLangChainの様々な機能\nLangChainには他にも様々な機能があります。以下のページも必要に応じて参考にしてください。\n\nMemoryで情報を保存・管理する\nPromptTemplateで柔軟にプロンプトを定義する\nOutputParserで出力形式を制御する\nChainを使用してワークフローを実行する\n\nまとめ\nPythonで大規模言語モデルを活用する際に使用できるLangChainの概要について解説しました。\n\nLangChainは、Model(モデル'), Document(metadata={'source': '../data/index1.txt', 'start_index': 0}, page_content='LangChainの基本的な使い方\nここでは、LangChainの基本的な使い方を紹介したいと思います。\n\n本記事で紹介する環境は以下バージョンでの動作結果です。バージョン更新が速いため最新バージョンではうまく動作しない可能性がありますがご了承ください。\n\nPython: 3.11.5\nlangchain: 0.2.11\nlangchain-core: 0.2.26\nlangchain-community: 0.2.10\nlangchain-openai: 0.1.16\nLangChainのインストール\nLangChainは、pipを使用してインストールすることができます。以下のpip installでライブラリをインストールしてください。\n\npip install langchain lang'), Document(metadata={'source': '../data/index1.txt', 'start_index': 5517}, page_content=')、Prompt(プロンプト)、Memory(メモリー)、Index(インデックス)、Chain(チェイン)、Agent(エージェント)といった構成要素で成り立っており、LLMを用いた複雑なタスクを開発することを便利にしてくれます。\n\nこの記事では、概要とOpenAIのモデルの簡単な使い方について紹介しました。LangChainにはさまざまな機能があります。これらの機能についても、今後の記事更新や新たな記事で詳しく解説したいと考えています。\n\n皆さんも、ぜひ色々と調べてLangChainを使ってみ')]

Chromaでは、from_documentsメソッドに分割したベクトル化対象のドキュメントと、埋め込みモデルを指定します。これにより、対象ドキュメントをモデルでベクトル化しつつ蓄積できます。

検索時には、similarity_searchに検索対象の文字列を指定します。今回の例では、指定した「"LangChainとは何ですか?"」に類維持するドキュメントとして4件ヒットしました。

ベクトル情報をファイル出力して利用する(persist_directory

InMemoryのVector Storeはプログラム終了時に消えてしまいます。永続的に保存するには、persist_directoryを使用して保存先を指定します。

# Chromaにファイルに格納する
db = Chroma.from_documents(
    split_documents,
    embedding=embeddings_model,
    persist_directory="./chroma_db",
)

persist_directoryに保存先を指定すると、chroma_dbディレクトリが生成され、sqliteのデータやバイナリファイルが生成されます。

chroma_dbの結果

保存したデータを再利用する際は以下のように読み込みます。

import configparser

from langchain_chroma import Chroma
from langchain_openai.embeddings import OpenAIEmbeddings

# コンフィグファイルからキーを取得する
config = configparser.ConfigParser()
config.read("config.ini")

# Chromaを読み込む
db = Chroma(
    persist_directory="./chroma_db",
    embedding_function=OpenAIEmbeddings(openai_api_key=config["OPENAI"]["key"]),
)

# Chromaを検索する
query = "LangChainとは何ですか?"
docs = db.similarity_search(query)

print(len(docs))
print(docs)
【実行結果】
4
[Document(metadata={'source': '../data/index1.txt', 'start_index': -1}, page_content='デルの簡単な使い方について紹介しました。LangChainにはさまざまな機能があります。これらの機能についても、今後の記事更新や新たな記事で詳しく解説したいと考えています。\n\n皆さんも、ぜひ色々と調べてLangChainを使ってみてください。'), Document(metadata={'source': '../data/index1.txt', 'start_index': 5241}, page_content='することで、APIコールが省略され、コスト節約とレスポンス向上が期待できます。\n\nLangChainの様々な機能\nLangChainには他にも様々な機能があります。以下のページも必要に応じて参考にしてください。\n\nMemoryで情報を保存・管理する\nPromptTemplateで柔軟にプロンプトを定義する\nOutputParserで出力形式を制御する\nChainを使用してワークフローを実行する\n\nまとめ\nPythonで大規模言語モデルを活用する際に使用できるLangChainの概要について解説しました。\n\nLangChainは、Model(モデル'), Document(metadata={'source': '../data/index1.txt', 'start_index': 0}, page_content='LangChainの基本的な使い方\nここでは、LangChainの基本的な使い方を紹介したいと思います。\n\n本記事で紹介する環境は以下バージョンでの動作結果です。バージョン更新が速いため最新バージョンではうまく動作しない可能性がありますがご了承ください。\n\nPython: 3.11.5\nlangchain: 0.2.11\nlangchain-core: 0.2.26\nlangchain-community: 0.2.10\nlangchain-openai: 0.1.16\nLangChainのインストール\nLangChainは、pipを使用してインストールすることができます。以下のpip installでライブラリをインストールしてください。\n\npip install langchain lang'), Document(metadata={'source': '../data/index1.txt', 'start_index': 5517}, page_content=')、Prompt(プロンプト)、Memory(メモリー)、Index(インデックス)、Chain(チェイン)、Agent(エージェント)といった構成要素で成り立っており、LLMを用いた複雑なタスクを開発することを便利にしてくれます。\n\nこの記事では、概要とOpenAIのモデルの簡単な使い方について紹介しました。LangChainにはさまざまな機能があります。これらの機能についても、今後の記事更新や新たな記事で詳しく解説したいと考えています。\n\n皆さんも、ぜひ色々と調べてLangChainを使ってみ')]

Collectionを使用して効率的にデータを扱う

Chromaでは、データを整理するためにCollectionを使用します。Collection名をcollection_name引数に指定してデータを管理することで、効率的にデータを検索できます。なお、デフォルトではcollection_name="langchain"となっています。

# Chromaにファイルに格納する
db = Chroma.from_documents(
    split_documents,
    embedding=embeddings_model,
    collection_name="collection_1",
    persist_directory="./chroma_db",
)

検索時には、collection_nameを指定することで対象Collectionを指定して検索可能です。

# Chromaを読み込む
db = Chroma(
    persist_directory="./chroma_db",
    collection_name="collection_1",
    embedding_function=OpenAIEmbeddings(openai_api_key=config["OPENAI"]["key"]),
)

このようにcollection_nameを指定することで、複数のデータセットを整理して管理できます。

Retriever

これまで外部データを取得し、Vector Storeを蓄積する方法を紹介しました。ここからは、RAGの中心となるRetrieverの使い方について説明します。Retrieverの公式サイトの情報はこちらを参照してください。

LangChain Retrievers

今回は、架空の会社「あいうえおカンパニー」の社長プロフィールを使用してVector Storeを構築し、LLMがその情報を使って質問に答えられるかを確認します。

外部データの準備

以下は、架空の会社「あいうえおカンパニー」の社長プロフィールデータです。このデータはChatGPTを使用して生成しています。

profile.md

# 社長プロフィール

## 名前
**山田 太郎 (Yamada Taro)**

## 役職
**代表取締役社長**

## 会社名
**あいうえおカンパニー**

## 生年月日
**1978年5月10日**

## 出身地
**東京都渋谷区**

## 学歴
- **2000年** 東京大学 経済学部 卒業
- **2005年** ハーバードビジネススクール MBA 取得

## 経歴
- **2000年:** 大手商社に入社し、営業部門でキャリアをスタート。国内外のビジネス展開をリードし、業績を大幅に向上させる。
- **2005年:** MBA取得後、外資系コンサルティング会社に転職し、戦略コンサルタントとして活躍。複数の業界において企業変革を支援。
- **2010年:** あいうえおカンパニーに参画し、マーケティング部門の統括に任命される。革新的なプロモーション戦略を展開し、ブランドの認知度を大幅に向上させる。
- **2015年:** 取締役に昇進し、経営戦略全般を担当。新規事業の立ち上げやM&Aに積極的に取り組む。
- **2020年:** 代表取締役社長に就任。デジタルトランスフォーメーションとグローバル展開を推進し、会社の成長を加速させる。

## 専門分野
- 経営戦略
- グローバルマーケティング
- デジタルトランスフォーメーション
- M&A

## 趣味
- 読書(ビジネス書、歴史書)
- ゴルフ
- 旅行(特にヨーロッパと東南アジア)

## 座右の銘
**「挑戦なくして成長なし」**

山田太郎氏は、革新的なリーダーシップとグローバルな視野を持ち、あいうえおカンパニーを次の成長ステージへと導く原動力となっています。

以下のコードでChromaのVector Storeを構築します。

import configparser

from langchain_chroma import Chroma
from langchain_community.document_loaders import TextLoader
from langchain_openai.embeddings import OpenAIEmbeddings
from langchain_text_splitters import TokenTextSplitter

# コンフィグファイルからOpenAI用のキーを取得する
config = configparser.ConfigParser()
config.read("config.ini")

# TextLoaderを用意する
loader = TextLoader("./profile.md", encoding="utf-8")

# データを読み込む
document = loader.load()

# Token Text Splitterを用意する
text_splitter = TokenTextSplitter(
    chunk_size=100,
    chunk_overlap=20,
    add_start_index=True,
    encoding_name="cl100k_base",
)
# 読み込んだデータを分割する
split_documents = text_splitter.split_documents(document)

# 埋め込みモデルを用意する
embeddings_model = OpenAIEmbeddings(api_key=config["OPENAI"]["key"])

# Chromaにファイルに格納する
db = Chroma.from_documents(
    split_documents,
    collection_name="AIUEO_Company",
    embedding=embeddings_model,
    persist_directory="./chroma_db",
)

上記までで外部データから構築したVector Storeの準備が整いました。

Retrieverを使用したRAGの実装

Retrieverを使用してRAGを実装する方法を以下に示します。(参考:Conversational RAG

import configparser

from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains.retrieval import create_retrieval_chain
from langchain_chroma import Chroma
from langchain_core.prompts import (
    ChatPromptTemplate,
    HumanMessagePromptTemplate,
    SystemMessagePromptTemplate,
)
from langchain_openai import ChatOpenAI
from langchain_openai.embeddings import OpenAIEmbeddings

# コンフィグファイルからOpenAI用のキーを取得する
config = configparser.ConfigParser()
config.read("config.ini")

# llmの設定
llm = ChatOpenAI(openai_api_key=config["OPENAI"]["key"])

# Chromaを読み込む
db = Chroma(
    persist_directory="./chroma_db",
    collection_name="AIUEO_Company",
    embedding_function=OpenAIEmbeddings(openai_api_key=config["OPENAI"]["key"]),
)

# Retrieverの設定
retriever = db.as_retriever()

# プロンプトテンプレートを作成する(システムプロンプトにRetrieverを質疑応答に組み込む)
prompt_template = ChatPromptTemplate.from_messages(
    [
        SystemMessagePromptTemplate.from_template(
            "あなたは質疑応答タスクのアシスタントです。"
            "以下のコンテキストを使用して質問に答えてください。"
            "\n\n"
            "{context}"
        ),
        HumanMessagePromptTemplate.from_template("{input}"),
    ]
)

# Chain作成
question_answer_chain = create_stuff_documents_chain(llm=llm, prompt=prompt_template)
rag_chain = create_retrieval_chain(
    retriever=retriever, combine_docs_chain=question_answer_chain
)

# 問い合わせの実行
result = rag_chain.invoke(
    {"input": "あいうえおカンパニーの社長の名前を教えてください。"}
)
print(result)
print(result["answer"])

print("=====")
result = rag_chain.invoke(
    {"input": "山田太郎氏の経歴と専門分野、座右の銘を教えてください。"}
)
print(result)
print(result["answer"])

print("=====")
result = rag_chain.invoke({"input": "山田太郎氏の趣味について教えてください。"})
print(result)
print(result["answer"])
【実行結果】
あいうえおカンパニーの社長の名前は山田太郎(Yamada Taro)です。
====================
山田太郎氏は、東京都渋谷区出身で、2000年に東京大学経済学部を卒業後、大手商社に入社し営業部に配属されました。その後、2005年にハーバードビジネススクールでMBAを取得しました。

山田太郎氏の専門分野はM&A(合併・買収)であり、その分野での豊富な経験を持っています。

また、山田太郎氏の座右の銘は「挑戦なくして成長なし」という言葉です。この言葉からも、彼の革新的なリーダーシップや成長志向がうかがえます。
====================
山田太郎氏の趣味は、読書(ビジネス書、歴史書)、ゴルフ、旅行(特にヨーロッパと東南アジア)です。

上記実行結果を確認してみると、外部ドキュメントとして渡した情報をもとにLLMが回答を生成できていることが分かります。

RAGのためのChainは、create_retrieval_chainでRetrieverとLLMを接続し、質問に対する回答を行っています。なお、Retrieverは上記で作成してChromaのas_retrieverメソッドを使用して取得しています。

PromptTemplateのSystemMessageとして、以下のようにRetrieverの文脈{context}を参照するように指定します。

# プロンプトテンプレートを作成する(システムプロンプトにRetrieverを質疑応答に組み込む)
prompt_template = ChatPromptTemplate.from_messages(
    [
        SystemMessagePromptTemplate.from_template(
            "あなたは質疑応答タスクのアシスタントです。"
            "以下のコンテキストを使用して質問に答えてください。"
            "\n\n"
            "{context}"
        ),
        HumanMessagePromptTemplate.from_template("{input}"),
    ]
)

create_retrieval_chainの内部では、Retrieverを使って質問に関連する情報をVector Storeから検索し、{context}部分に埋め込んでLLMへ渡されます。これにより、外部ドキュメントに関連する回答が得られるわけです。

実際の実行時には、invokeメソッドで{input}に設定する質問を渡し、結果はresult["answer"]で取得します。以上が、LangChainを用いたRAGの実装の流れです。

まとめ

LangChainを用いたRAG(Retrieval-Augmented Generation)でLLMと外部データを連携する方法を解説しました。

RAGは、外部データから必要な情報を検索し、それを質問に組み込むことで回答の精度を向上させる技術です。これにより、一般のLLMで対応できない特定の情報に関するAIを実装できます。

RAGを実現するには、外部データからVector Storeを作成し、Retrieverで質問に関連する情報を検索してLLMと連携します。この記事では、その一連の手順を追って説明しました。

企業や個人のデータをLLMと連携させ、固有の回答を生成できるRAGは、今後様々な場面で重要となる技術です。生成AIの技術は急速に進化していますが、RAGの基礎を理解してもらいたいと思います。