シンプルRAG開発（LangChain + OpenAI）

# はじめに
LangChainとOpenAI APIを使ってシンプルなRAGを作ってみた。

- 検索対象ドキュメント：ローカルにあるPDFファイル
- embeddingモデル：OpenAIのtext-embedding-ada-002
- ベクトルDB：LangchainのInMemoryVectorStore
- LLM：OpenAIのgpt-4o-mini

# 構成
- 図のような構成で、基本的に[RAG・AIエージェント本](https://gihyo.jp/book/2024/978-4-297-14530-9)で説明されていたものと同じ。
- 検索対象をローカルのPDFファイルにして、ベクトルDBをInMemoryVectorStoreにした点が異なる。
- 流れは以下の通り。
  - ドキュメントをベクトル化して、ベクトルDBに格納。
  - ユーザーの質問をベクトル化して、ベクトルDBを検索。
  - プロンプトにユーザーの質問と検索結果のドキュメントデータを入れ、LLMに入力。
  - LLMが回答を出力。

![doc1_pic1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959159/9010069d-747f-2b71-d59e-2c0d1f072bbe.png)

# 実装

## Pythonと主なライブラリのバージョン
- python==3.10.15
- langchain==0.3.9
- langchain-community==0.3.8
- langchain-core==0.3.21
- langchain-openai==0.2.10

※ OSはMac

## ドキュメントの用意
- 内閣府が公開している月例経済報告のpdfファイルをダウンロードして使うことにする。
  - https://www5.cao.go.jp/keizai3/getsurei/getsurei-index.html
- 今回は9月、10月、11月の3つのpdfファイルを使う。

## OpenAIのAPIキーの設定
- 実行するpythonファイルと同じ階層に.envファイルを作成。
- .envファイルにOpenAIのAPIキーを記載。

```.env
OPENAI_API_KEY="your-api-key"
```

- python-dotenvで環境変数を設定。

```python
from dotenv import load_dotenv

load_dotenv()
```

## ドキュメントの読み込み
- LangChainのPyPDFLoaderを使って読み込む。
- pdfファイルはページ単位で読み込む。

```python
from langchain_community.document_loaders import PyPDFLoader

# pdfファイルのリスト
file_paths = ["./内閣府月例経済報告_202409.pdf", "./内閣府月例経済報告_202410.pdf", "./内閣府月例経済報告_202411.pdf"]

# pdfファイルの各ページを格納するリスト
pages = []

for file_path in file_paths:
    loader = PyPDFLoader(file_path)
    
    for page in loader.load():
        pages.append(page)

```

## ドキュメントのベクトル化とベクトルDBへの格納
- OpenAIのembeddingモデルtext-embedding-ada-002を使ってドキュメントをベクトル化。
- LangchainのInMemoryVectorStoreをベクトルDBとして使う。

```python
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings

vector_store = InMemoryVectorStore.from_documents(pages, OpenAIEmbeddings())

```

## RAG chainの作成
- RAGを実現するchainは、LCEL(LangChain Expression Language)で記載。
  - Prompt template、LLM model、Output parserを繋げる基本の形を採用。
- ChatPromptTemplateでプロンプトのテンプレートを作る。
  - ユーザーの質問が変数questionに入り、ベクトルDBの検索結果が変数contextに格納される。
- ChatOpneAIでLLMの設定をする。
- 検索を行うretriverはvectore_storeのas_retrieverメソッドで設定。
  - as_retrieverはデフォルトで類似検索を行う。
  - as_retrieverの引数search_kwargsを使って、検索方法を指定することができる。
    - `search_kwargs={"k": 3}` は、検索結果を３個に絞っている。


```python
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI

prompt = ChatPromptTemplate.from_template('''
以下の文脈だけを踏まえて質問に回答してください。
                                          
文脈: """
{context}
"""

質問: {question}                                          
''')

model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

retriever = vector_store.as_retriever(search_kwargs={"k": 3})

chain = {
    "question": RunnablePassthrough(),
    "context": retriever,
} | prompt | model | StrOutputParser()

```

## RAGによる回答
- chainのinvokeメソッドを使い、質問をインプット。

```python
query = "11月の月例経済報告の開催日は？"
chain.invoke(query)
```

- 以下のようにドキュメントの情報を元にLLMが回答してくれた。

```
'11月の月例経済報告の開催日は令和6年11月26日です。'
```

![doc1_pic2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959159/ee5492a6-41f3-41f3-bad2-f82380f57144.png)


- 別の質問：9月の製造業での雇用人員判断DIは？
- 回答：

```
'9月の製造業での雇用人員判断DIは－22です。'
```

![doc1_pic3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959159/48c8557a-e3f4-e3c8-34a9-650f1a69865c.png)


# 参考
- LangChainとLangGraphによるRAG・AIエージェント［実践］入門
  - https://gihyo.jp/book/2024/978-4-297-14530-9
- LangChain公式サイト
  - https://python.langchain.com/docs/introduction/