LangChain を使ってローカルファイルを RAG で回答させてみる

# やりたいこと
業務で Azure OpenAI Service を使う機会があり、LangChain を使っていろいろとやってみたので複数回で記事にまとめます。今回は、ローカルのファイル内容を RAG で LLM から回答させる実装を行います。
すでに多くの方が情報発信していますが、その1つとして参考にしてもらえると幸いです！

# 他の記事
1. [Langchain を使って LLM から回答させてみる](https://qiita.com/coitate/items/14b5ee27162ba5344b37)
1. [LangChain を使ってローカルファイルを RAG で回答させてみる](https://qiita.com/coitate/items/d0fa841f9ce7fa579103) ← イマココ
1. [LangChain を使って Azure Cognitive Search のデータを RAG で回答させてみる](https://qiita.com/coitate/items/90ff3a9ecdcea9aa6504)

# 前提
* WSL (Ubuntu 22.04) 上で行います
* Python で実装します
* LLM のモデルには、GPT-3.5 を使用します
* Azure OpenAI Service で gpt-35-turbo, text-embedding-ada-002 モデルのデプロイメントは作成済み
* Azure OpenAI Service を使うため、OpenAI 社のものを使う場合と異なる場合があります
* Azure OpenAI Service は作成済み


# RAG とは
Retrieval Augmented Generation の略。GPT-3.5 のモデルの場合、2021年9月までの情報しか学習されていないので、それ以降の情報について質問しても回答は得られない。そういった場合に、未学習の情報をプロンプトに組み込むことでその情報を踏まえて LLM から回答してもらう手法。
社内文書の内容や FAQ データなどを RAG で回答させるなどのユースケースが多い。

# 実装
# 未学習のデータ
RAG 用に未学習のデータを作成する。テニスに関する内容なのはテニスが趣味だから :tennis:
GPT-3.5 が未学習のデータならなんでも可。`data/test.txt` として保存しておく。

``` test.txt
1. ロジャー・フェデラーは2022年に惜しまれながらプロテニスプレイヤーを引退しました。
2. ノバク・ジョコビッチは2023年の全豪オープン、全仏オープン、全米オープンを勝利し、グランドスラムの勝利数が24回になりました。
```

# パラメータ設定
OpenAI 関連のパラメータを事前に .env ファイルとして定義しておく。コードの中で環境変数として読み込み使用する。今回は分かりやすさのために引数に設定する形にするが、DEPLOYMENT_NAME 以外のパラメータは環境変数に設定されていれば引数で与える必要はない。
`OPENAI_EMBEDDINGS_DEPLOYMENT_NAME` は RAG を行う上で未学習の情報をベクトル化する際に使用するモデル。Azure OpenAI Service だと `text-embedding-ada-002` が推奨されている。

``` .env
OPENAI_API_TYPE=azure
OPENAI_API_VERSION=2023-05-15
OPENAI_API_BASE={YOUR_OPENAI_API_BASE}
OPENAI_API_KEY={YOUR_OPENAI_API_KEY}
DEPLOYMENT_NAME={YOUR_DEPLOYMENT_NAME}
OPENAI_EMBEDDINGS_DEPLOYMENT_NAME={YOUR_EMBEDDINGS_DEPLOYMENT_NAME}
```

# 使用するライブラリ
バージョンは任意のもので可。`pip install -r requirements.txt` でインストールする。

``` requirements.txt
python-dotenv==1.0.0
langchain==0.0.268
openai==0.27.8
tiktoken==0.4.0
chromadb==0.4.10
```

# コード本体

``` rag.py
import os
import dotenv
from langchain.chains import ConversationalRetrievalChain
from langchain.chat_models import AzureChatOpenAI
from langchain.document_loaders import TextLoader
from langchain.embeddings import OpenAIEmbeddings
from langchain.memory import ConversationBufferMemory
from langchain.schema import AIMessage, HumanMessage
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import Chroma

dotenv.load_dotenv()

OPENAI_API_TYPE = os.getenv('OPENAI_API_TYPE')
OPENAI_API_VERSION = os.getenv('OPENAI_API_VERSION')
OPENAI_API_BASE = os.getenv('OPENAI_API_BASE')
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
DEPLOYMENT_NAME = os.getenv('DEPLOYMENT_NAME')
OPENAI_EMBEDDINGS_DEPLOYMENT_NAME = os.getenv(
    'OPENAI_EMBEDDINGS_DEPLOYMENT_NAME')

llm = AzureChatOpenAI(openai_api_version=OPENAI_API_VERSION,
                      openai_api_base=OPENAI_API_BASE,
                      openai_api_type=OPENAI_API_TYPE,
                      openai_api_key=OPENAI_API_KEY,
                      deployment_name=DEPLOYMENT_NAME,
                      temperature=0)

embedding = OpenAIEmbeddings(openai_api_version=OPENAI_API_VERSION,
                             openai_api_base=OPENAI_API_BASE,
                             openai_api_type=OPENAI_API_TYPE,
                             openai_api_key=OPENAI_API_KEY,
                             model=OPENAI_EMBEDDINGS_DEPLOYMENT_NAME)

loader = TextLoader('data/test.txt')
docs = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
documents = text_splitter.split_documents(documents=docs)
vector_store = Chroma.from_documents(documents=documents, embedding=embedding)
memory = ConversationBufferMemory(memory_key='chat_history',
                                  return_messages=True)
qa = ConversationalRetrievalChain.from_llm(
    llm=llm, retriever=vector_store.as_retriever(), memory=memory)

# 作成した Index が k (Default: 4)より少ないと余計なッセージが表示される
# 表示させたくない場合は k を小さくする
# qa = ConversationalRetrievalChain.from_llm(
#     llm=llm, retriever=vector_store.as_retriever(search_kwargs={'k': 1}), memory=memory)

while (True):
    prompt = input('Q: ')

    if prompt == 'q' or not prompt:
        break

    answer = qa.run(prompt)
    print('A: ', answer)

print('-- Chat History --')
for msg in memory.chat_memory.messages:
    if type(msg) == AIMessage:
        role = 'ai'
    elif type(msg) == HumanMessage:
        role = 'human'
    else:
        role = 'system'

    print(role, '\t>> ', msg.content)
```

# 実行してみる
以下のような形で LLM とチャット形式でやりとりできる。`test.txt` の内容を踏まえて回答してくれている！

`Number of requested results 4 is greater than number of elements in index 1, updating n_results = 1` というメッセージは作成した Index が k (Default: 4) より少ない場合に表示されるもの。表示させたくない場合は、k の値を小さくする (上記コード本体のコメントを参照)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1743460/342a0e61-078a-b7f6-69cd-7cf5f293c486.png)

# ということで
LangChain を使って、未学習のデータを RAG で LLM に回答させるコードを実装しました！
次回は、Azure Cognitive Search をベクトルストアにするケースを行います。

# 次回
LangChain を使って Azure Cognitive Search のデータを RAG で回答させてみる


以上です。