LangChainでPDF検索をやってみた（FAISS、ConversationSummaryMemory、ConversationalRetrievalChain）

## はじめに
LangChainの会話履歴を保存するMemory機能の1つであるConversationSummaryMemoryを検証してみました。LangChainのConversationSummaryMemoryの挙動を確認したい方におすすめです。

## 開発環境
- Windows 11
- Python 3.11.5
- dotenv
- LangChain
- Azure OpenAI Embeddings
- CharacterTextSplitter
- FAISS
- PyPDFLoader

## 実装

### 事前準備
今回は以下の記事で事前にベクトルストアを作成し、そこから回答を生成する方式をとります。

https://qiita.com/tatsuki-tsuchiyama/items/a48d1cc4afce588e1797

私はランニングを行っていることもあり、以下のランニング時計の取扱説明書のPDFをベクトル化しました。

https://download.garmin.com/jp/download/manuals/Forerunner255_OM_JA.pdf


### 必要なパッケージのインストール
以下のコマンドで必要なパッケージをインストールします。

```
pip install langchain
pip install openai
pip install python-dotenv
pip install faiss-cpu
pip install PyPDF2
pip install tiktoken
```

### 環境変数の設定と依存関係の読み込み
.envファイルを作成し、Azure Open AIの環境変数の設定をします。
ご自身の環境に合わせて設定してください。

```.env
# APIキー
OPENAI_API_KEY = "XXXXX"

# エンドポイント
AZURE_OPENAI_ENDPOINT = "XXXXX"

# 使用するOpenAI APIのバージョン
OPENAI_API_VERSION = "XXXXX"
```
　　
必要なライブラリをインポートします。
```test.py
# 環境変数を.envファイルから読み込む
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import AzureChatOpenAI
from langchain.embeddings import AzureOpenAIEmbeddings

from langchain.vectorstores import FAISS
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationSummaryMemory
```

### 使用するモデルの準備
Azure Chat OpenAIとAzure OpenAI Embeddingsを定義します。

```test.py
llm = AzureChatOpenAI(
    temperature=0,
    azure_deployment="gpt-35-turbo-16k",
)

embeddings = AzureOpenAIEmbeddings(
    azure_deployment="text-embedding-ada-002",
    chunk_size=1,
)
```

### FAISSベクトルストアの読み込み

ローカルに保存しているベクトルストアの読み込みを行い、as_retriever()メソッドで検索機能を作成します。

```test.py
vectorstore = FAISS.load_local("./vectorstore", embeddings)
retriever = vectorstore.as_retriever()
```

### ConversationSummaryMemoryの定義

以下のようにConversationSummaryMemoryを定義します。要約にllmを使います。会話履歴と出力のキーを指定しています。
ConversationBufferMemoryは**会話履歴を要約して保持**します。
```test.py
memory = ConversationSummaryMemory(
    llm=llm,
    memory_key="chat_history",
    return_messages=True,
    output_key="answer",
)
```

### chainを作成し、実行

最後にchainを作成し、実行します。質問は3問準備しており、2問目は1問目を詳しく説明する質問、3問目はそれまでの会話をもとに回答できるかを検証する質問に設定しました。

```test.py
chain = ConversationalRetrievalChain.from_llm(
    llm=llm, retriever=retriever, memory=memory
)

results = chain({"question": "心拍変動は計測可能ですか？"})
print("あなた：", results["question"])
print("ボット：", results["answer"])
print("会話履歴：", results["chat_history"])

results = chain({"question": "もっと詳しく説明してください。"})
print("あなた：", results["question"])
print("ボット：", results["answer"])
print("会話履歴：", results["chat_history"])

results = chain({"question": "血中酸素濃度は？"})
print("あなた：", results["question"])
print("ボット：", results["answer"])
print("会話履歴：", results["chat_history"])
```

　　
これまでのPythonコードを上から順に記述し終わったら、Pythonファイルを実行します。
実行結果は以下のようになりました。

```terminal
あなた： 心拍変動は計測可能ですか？
ボット： 申し訳ありませんが、提供された情報には心拍変動を計測する機能についての記述はありません。
会話履歴： [SystemMessage(content='')]
あなた： もっと詳しく説明してください。
ボット： デバイス情報を確認するためには、以下の手順を実行します。
1. デバイスのUPキーを長押しします。
2. [システム]メニューに移動し、[デバイス情報]を選択します。

デバイス情報では、デバイスのユニットID、ソフトウェアバージョン、規制情報、ライセンス契約などの情報が表示されます。

また、電子ラベルの規制および準拠情報を確認するためにも、同じ手順を実行します。

デバイスを充電するためには、以下の手順を実行します。
1. チャージングケーブルのマークの付いた方の端子を、デバイスの裏面にある充電端子に接続します。
2. チャージングケーブルのもう一方の端子を、充電可能なUSBポートに差し込みます。

デバイスを装着する際には、注意が必要です。長期間装着すると、敏感肌やアレルギーを持つ方に炎症やかぶれが生じることがあります。炎症やかぶれが生じた場合は、デバイスを取り外すようにしてください。

以上がデバイス情報の確認方法、充電方法、および装着方法の詳細です。
会話履歴： [SystemMessage(content='The human asks if heart rate variability is measurable. The AI apologizes and states that there is no description of a feature to measure heart rate variability in the provided information.')]
あなた： 血中酸素濃度は？
ボット： はい、Garminデバイスは血中酸素濃度を測定する機能があります。血中酸素トラッキング機能を使用することで、血液中の 酸素のレベルを測定することができます。
会話履歴： [SystemMessage(content='The human asks if heart rate variability is measurable. The AI apologizes and states that there is no description of a feature to measure heart rate variability in the provided information. The human asks for a more detailed explanation. The AI provides step-by-step instructions on how to check device information, charge the device, and wear the device. It also warns about potential skin inflammation or allergies that may occur with prolonged wearing of the device.')]
```

会話履歴が英語で要約されていることが分かります。日本語ではないので、使用する際は注意が必要です。


## おわりに
LangChainの会話履歴を保存するmemory機能の一つであるConversationSummaryMemoryを検証してみました。文書検索に会話履歴を導入する際の選択肢の1つになります。ぜひ、試してみてください。

最後までお読みいただき、ありがとうございました！ 
以下のXで情報発信してます！

https://twitter.com/t___tatsuki

## 参考文献
https://python.langchain.com/docs/modules/memory/types/summary

https://qiita.com/ayoyo/items/07da43bbab6652d37421