【AIエージェント入門】LangChain・LangGraphによるエージェント開発　その③　RAG入門

どうも。@TM_AIbuchoことおっさんです。
SES企業の社長が開発経験ゼロからAIを学習しています。
是非とも暖かく、時には厳しく見守っていただけると嬉しいです。

# はじめに

AIエージェントの定義は明確ではないですが、一般的には特定の目的に応じて自律的に目標を設定し、タスクを実行していくシステムをAIエージェントといわれています。

LangChainによるAIエージェントを開発してみよう。
開発環境はGoogleColab、言語はPythonを使用しています。

以下書籍を参考にしています。
[LangChainとLangGraphによるRAG・AIエージェント［実践］入門 (エンジニア選書) 単行本（ソフトカバー） – 2024/11/9 西見 公宏 (著), 吉田 真吾 (著), 大嶋 勇樹 (著)](https://amzn.asia/d/2UfTzY9)

# 過去の記事
[【AIエージェント入門】LangChain・LangGraphによるエージェント開発　その①複数のモデルを使ってみる](https://qiita.com/TM_AIbucho/items/33a7b20da151a0b0f016)

[【AIエージェント入門】LangChain・LangGraphによるエージェント開発　その②LangChain基礎知識](https://qiita.com/TM_AIbucho/items/c52abe0f6050a4c42f4b)

# それでは実装していく！

```Python:入力
import os
from google.colab import userdata

os.environ["OPENAI_API_KEY"] = userdata.get("OPENAI_API_KEY")
```

シークレットからAPIキーを読み込みます。

# RAGの実装

みんな大好き、RAGです。Retrievalです。
LLMといえばRAGを思い浮かべる方も多いのではないでしょうか。

プロンプトのコンテキストに情報を追加することでLLMが知らない情報をもとに回答を生成したり、ハルシネーション対策ができます。

```Python:入力
from langchain_openai import ChatOpenAI
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser

model = ChatOpenAI(
   model="gpt-4o-mini",
   temperature=0,
   streaming=True,
   callbacks=[StreamingStdOutCallbackHandler()]
)

prompt = ChatPromptTemplate.from_messages(
    [
        ("system","ユーザーの入力に回答してください"),
        ("human","{text}")
    ]
)

chain = prompt | model | StrOutputParser()
response = chain.invoke({"text": "大谷翔平選手のMVP受賞回数は？"})
```

```text:出力
大谷翔平は2021年と2022年の2回、アメリカンリーグのMVP（最優秀選手）を受賞しています。
```

LLMは最新の情報を知らないので、2024年11月に3回目の受賞をしたことを知りません。けしからんです。

Wikipediaが早速更新されてるので、プロンプトに最新情報を記載します。
https://ja.wikipedia.org/wiki/%E5%A4%A7%E8%B0%B7%E7%BF%94%E5%B9%B3

```Python:入力
prompt = ChatPromptTemplate.from_messages([
    ("system", """大谷翔平選手の新しい情報を踏まえて、ユーザーの入力に回答してください。
新しい情報：\"\"\"史上最高の野球選手の1人として評価されている[2][3]、近代プロ野球では極めて稀なシーズンを通して投手と野手を兼任する「二刀流（英: two-way player）」の選手[4][5]。メジャーリーグベースボール（MLB）/日本プロ野球（NPB）両リーグで「1シーズンでの2桁勝利投手・2桁本塁打」を達成。NPBで最優秀選手を1度受賞、MLBでシーズンMVP（最優秀選手賞）を3度受賞。\"\"\"
"""),
    ("human", "{text}")
])

prompt_template = prompt.messages[0].prompt.template
print(prompt_template)
```


```text:出力
大谷翔平選手の新しい情報を踏まえて、ユーザーの入力に回答してください。
新しい情報："""史上最高の野球選手の1人として評価されている[2][3]、近代プロ野球では極めて稀なシーズンを通して投手と野手を兼任する「二刀流（英: two-way player）」の選手[4][5]。メジャーリーグベースボール（MLB）/日本プロ野球（NPB）両リーグで「1シーズンでの2桁勝利投手・2桁本塁打」を達成。NPBで最優秀選手を1度受賞、MLBでシーズンMVP（最優秀選手賞）を3度受賞。"""

```

プロンプトにいれるコンテキストが作成されました。
この情報をもとに、｛text｝に代入されるユーザーからの質問に回答します。

```python:入力
response = chain.invoke({"text": "大谷翔平のMVP受賞回数は？"})
```

```text:出力
大谷翔平選手はMLBでシーズンMVP（最優秀選手賞）を3度受賞しています。
```

ちゃんと正しい回答を得ることができました。

実際のRAGシステムでは、ドキュメントなどのデータはDBなどで保管されていて、キーワードや類似度で関連文書を検索してるかと思います。

LangChainでもドキュメント読み込みから検索までの一通りの機能が提供されています。
ものは試し！さっそくやってみましょう。

## デザインパターンカタログの読み込み

本書籍の後半でもでてくるAIエージェントのデザインパターンに関する論文を読むこむ。
GoogleColaboで実装しているのでダウンロードしたPDFをアップロードしておく。

[Agent Design Pattern Catalogue: A Collection of Architectural Patterns for Foundation Model based Agents](https://arxiv.org/abs/2405.10467)

```text:入力
!pip install langchain-community pypdf
```

```Python:入力
from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("/content/2405.10467v4.pdf")
pages = loader.load()
print(f"総ページ数: {len(pages)}ページ")
```

```text:出力
総ページ数: 38ページ
```

document_loadersを使ってPDFを読み込みます。38ページもある。これを、検索しやすいように分割します。チャンク分割というやつです。

```Python:入力
from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)

docs = text_splitter.split_documents(pages)
print(f"分割したチャンク数: {len(docs)}チャンク")
```

```text:出力
分割したチャンク数: 292チャンク
```

38ページのPDFが292個に分割されました。次はベクトル化処理をしていきます。
テキストを数値（ベクトル）に変換することで、キーワードが一致していなくても、意味が近い単語、文脈を検索できるようになります。

ベクトル化するためのEmbeddingModelというものがあります。OpenAIのEmbeddingを使います。

```Python:入力
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
```

テキストのベクトル化とはなんぞや？ということで、実際にみてみます。日本語の文章を、コンピューターが理解できるような数字に変換してる模様。

とにかく、**よくわからない数字に変換されている**ということがよく分かると思います。

```python:入力
query = "亀山社中を設立したのはだれ？"

vector = embeddings.embed_query(query)
print(len(vector))
print(vector)
```

```text:出力
1536
[-0.007103362586349249, -0.0834045335650444, 0.008746211417019367, 0.055783823132514954, 0.03573586046695709, -0.03876078873872757, -0.028976714238524437, 0.07326581329107285, -0.03269007056951523, -0.06650666892528534, 0.028288282454013824, 0.031125454232096672, （略）
```



>ベクトル化（Vectorization）とは、テキストや画像などのデータを数値のベクトル（配列）に変換するプロセスのことです。これは以下のような特徴があります：
>
>テキストの意味を数値で表現
文章「犬は散歩が好きです」を例えば [0.2, -0.5, 0.8, ...] といった数百次元の数値の配列に変換します。似た意味の文章は似たベクトルになります。
意味的な検索を可能に
ベクトル同士の類似度を計算することで、「犬の散歩」について書かれた文章を検索したとき、「ペットの運動」といった似た意味の文章もヒットするようになります。
実現方法
>
>
>Sentence-BERTなどの言語モデルを使用
文章を固定長のベクトルに変換
ベクトルはデータベース（ベクトルDB）に保存
>
>
>RAGでの活用
>
>
>質問文をベクトル化
>文書群の中から類似度の高い文書を検索
>検索結果を元に回答を生成
>
>つまり、ベクトル化によって文章の意味を数値化し、それを使って効率的な検索や類似度計算を行うことができるようになります。これがRAGシステムの重要な基盤技術となっています。



はい、次。
せっかくよくわからない数字に変換したので、保存します。ベクトルのデータベースということで、ChromaというVectorStoreというものを使います。

```text:入力
!pip install langchain-community chromadb
```
```python:入力
from langchain_community.vectorstores import Chroma

db = Chroma.from_documents(docs, embeddings)
```
```Python:入力
query = "Passive Goal Creatorってなに？"
retriever = db.as_retriever()
docs = retriever.invoke(query)

print(f"取得した文書数：{len(docs)}\n")
print(f"文書:\n{docs[0].page_content}\n")
```
```text:出力
取得した文書数：4

文書:
Solution: Fig. 3 illustrates a simple graphical representation of passive goal creator. A foundation model-based agent
provides a dialogue interface where users can directly specify the context and problems, which are transferred topassive
goal creator for goal determination. Meanwhile, the passive goal creator can also retrieve related information from
memory, including the repository of artefacts being worked on, relevant tools used in recent tasks, conversation histories,

```


Passive Goal Creatorってなに？
という質問に類似するドキュメントを4つ取得できました。デフォルトでは4つ返すように設定されているようです。

LCELでプロンプト作成から回答までつなげてみましょう。

```Python:入力
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.callbacks import StreamingStdOutCallbackHandler

prompt = ChatPromptTemplate.from_template("""
以下の文脈だけを踏まえて質問に回答してください。回答は必ず日本語でお願いします。

文脈：\"\"\"
{context}
\"\"\"

質問：{question}
""")

model = ChatOpenAI(
   model="gpt-4o-mini",
   temperature=0,
   streaming=True,
   callbacks=[StreamingStdOutCallbackHandler()]
)
```
```Python:入力
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

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

output = chain.invoke(query)
```

```text:出力
Passive Goal Creatorは、ユーザーが対話インターフェースを通じて明示した目標を分析するシステムです。
このシステムは、ユーザーが指定した文脈や問題を受け取り、それに基づいて目標を決定します。
また、関連情報をメモリから取得することもでき、作業中のアーティファクトのリポジトリや
最近のタスクで使用されたツール、会話の履歴などを参照します。
```

Passive Goal CreatorというAIエージェントのデザインパターンについて、英語の論文をもとに日本語で回答してくれました。

# まとめ

### 1.RAGの基本的な実装


OpenAIのAPIを使用して基本的なチャットボットを作成
プロンプトに追加情報を含めることで、LLMの知識を補完する方法を示した
大谷翔平選手のMVP受賞に関する例を用いて、情報更新の重要性を実証


### 2.ドキュメント処理の実装


PDFファイルの読み込み（PyPDFLoader使用）
テキストの分割（RecursiveCharacterTextSplitter使用）
チャンクに分割


### 3.ベクトル化とデータベース


OpenAIのEmbeddingsモデルを使用してテキストをベクトル化
Chromaベクトルデータベースを使用してベクトルを保存
類似度検索の実装


### 4.完全なRAGシステムの構築


LangChainのLCELを使用してコンポーネントを接続
プロンプト、検索、モデル出力を一つのチェーンとして統合
英語の論文から日本語で回答を生成するシステムの実装

この実装により、AIエージェントが外部知識を活用して、より正確で最新の情報に基づいた回答を提供できるようになりました。

次回は改めてLCELの使い方を掘り下げて実践していきます！