LangChainとStreamlitを用いたシンプルなRAGアプリを作ってみた

### ごあいさつ
こんにちは、MaTTaと申します。
つよつよな記事が溢れている中で若干恐縮なのですが、LangChainを用いたRAGアプリを久しぶりに作ってみましたので記録に残します。


### モチベーション
- RAGのコードは以前書いていたけどノートブック形式(GoogleColab)で止まってたので一応Webアプリ化まではしておこうと思った
- 当時のライブラリのバージョンが今となっては古いみたいなので更新しておきたかった

### アプリ概要
- Dockerで環境構築
- LangChain ver2系
- フロント部分はstreamlitを使用
- CSVファイルに羅列されたテキスト情報をエンべディングしてChromaDBに格納
　- 簡単なQ&Aシステムを想定　
- ベクトルデータは永続的にし、アプリ上で更新も可能
- クエリに対してベクトルDBから検索し、回答をGPT-4oで生成

### Githubレポジトリ
https://github.com/MaTTalv001/LangChainRag_CSV_ver2


インストール手順の詳細はGithubのREADME参照。以下は補足です。

### 用意するデータ
例えばこのような形式のcsvを`data.csv`として用意します。ユーザーのクエリから、近しいデータを検索してGPT-4oで回答するという流れになります。
なお、このサンプルはClaudeでさっと作ったものなので内容の精査はしていません。

| doc_id | text |
|--------|------|
| 1 | Pythonは、インタープリタ型の高水準で汎用的なプログラミング言語です。その設計哲学は、コードの可読性を強調しており、特にインデントによるブロック構造を特徴としています。 |
| 2 | JavaScriptは、ウェブ開発でよく使われる多用途な言語です。動的でインタラクティブなウェブコンテンツの作成を可能にし、クライアント側およびサーバー側の両方で動作します。 |
| 3 | Javaは、クラスベースのオブジェクト指向プログラミング言語で、できるだけ少ない実装依存を持つように設計されています。エンタープライズ規模のアプリケーション構築に広く使用されています。 |
| 4 | C++は、高性能で強力な言語であり、オブジェクト指向の機能を備えたCの拡張です。システムプログラミング、ゲーム開発、リアルタイムシミュレーションでよく使われます。 |


###　ライブラリ
```
langchain==0.2.6
langchain-community==0.2.6
langchain-openai==0.1.14
chromadb==0.5.3
streamlit==1.31.0
```

### 環境変数
きっと`.env`でも問題ないと思いますがstreamlitならでは環境変数を扱う方法があるのでそれを使います。
`.streamlit/secrets.toml`というファイルを作ります。`.gitignore`に含めているのでローカルで各自作成する必要があります。このファイルにOpenAI API キーを記述します。
```
OPENAI_API_KEY = "your_openai_api_key_here"
```
https://docs.streamlit.io/develop/api-reference/connections/secrets.toml


### Streamlit

##### データの読み込みとベクトル化
まず、ベクトルデータの永続保存先を`/app/persistent_storage`として保存します。
そして、`load_and_embed_data()`関数で、CSVデータの読み込み、テキスト分割、エンベディング生成、ベクトルストアへの保存を行います。
`CharacterTextSplitter`で文書を500文字ごと、オーバーラップなしで分割しています。今回は各データが短いので実際には分割は発生していない想定です。

```python:main.py
# Chromaデータベースの設定
CHROMA_PERSIST_DIR = "/app/persistent_storage"

def load_and_embed_data():
    loader = CSVLoader(file_path="/app/data/data.csv")
    documents = loader.load()
    text_splitter = CharacterTextSplitter(chunk_size=500, chunk_overlap=0)
    texts = text_splitter.split_documents(documents)
    
    embeddings = OpenAIEmbeddings()
    vectorstore = Chroma.from_documents(texts, embeddings, persist_directory=CHROMA_PERSIST_DIR)
    vectorstore.persist()
    st.success("データがエンベディングされ、ベクトルデータベースが更新されました。")
```

今回、`text`というカラムしかないので特に意識しなかったのですが、特定のカラムだけをベクトル化したいのであれば下記のようにすると良いようです。
```python
loader = CSVLoader(file_path="/app/data/data.csv", csv_args={'fieldnames': ['text']})
```
https://api.python.langchain.com/en/latest/document_loaders/langchain_community.document_loaders.csv_loader.CSVLoader.html


#### クエリの処理

`OpenAIEmbeddings()`で質問文をベクトル化します。
事前に保存されたChromaベクトルデータベースの読み込み、OpenAIのGPT-4oモデルの初期化を行います。
RetrievalQAチェーンを構築します。つまり、質問文のベクトルと類似する文書をベクトルデータベースから検索し、それをソースとしてGPT-4oで回答を生成します。

```python:main.py
def query_data(query):
    embeddings = OpenAIEmbeddings()
    vectorstore = Chroma(persist_directory=CHROMA_PERSIST_DIR, embedding_function=embeddings)
    
    llm = ChatOpenAI(model_name="gpt-4o", temperature=0)
    qa_chain = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff",
        retriever=vectorstore.as_retriever()
    )
    
    response = qa_chain.run(query)
    return response

```
chain_typeにはいくつか手法がありますが、今回はテキストも短いのでシンプルなstuff(詰め込み方式)を用いています。
1. **"stuff"**
   - 全ての取得したドキュメントを1つのプロンプトにまとめます。
   - 簡単で直接的ですが、モデルのコンテキスト長を超える可能性があります。

2. **"map_reduce"**
   - 各ドキュメントに対して個別に質問し、その後結果を集約します。
   - 長いドキュメントや多数のドキュメントを扱う場合に有効です。
   - 処理時間が長くなる可能性があります。

3. **"refine"**
   - ドキュメントを順番に処理し、各ステップで回答を洗練させていきます。
   - 詳細な回答が必要な場合に適しています。

https://python.langchain.com/v0.2/docs/tutorials/summarization/#stuff


### スクリーンショット
![スクリーンショット 2024-07-07 16.25.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/bfed5a9c-3155-1d86-efa2-9e84bcca8863.png)

下記あたりを参照しているようです。
- Pythonは、インタープリタ型の高水準で汎用的なプログラミング言語です。その設計哲学は、コードの可読性を強調しており、特にインデントによるブロック構造を特徴としています。
- Pythonは、文法がシンプルで読みやすく、初心者にも扱いやすいプログラミング言語です。機械学習や人工知能の分野で広く使用されています。

### おわり
データも少ないですし質問も類似性の判定がつきやすいものなので大したことではないのですが、基本的な構築ができたので今回はよしとします。




参考

https://python.langchain.com/v0.2/docs/introduction/

https://docs.streamlit.io/