【０から目指すAIエンジニア】Day7：RAGの基礎

# はじめに
こんにちは、慶應義塾大学理工学部機械工学科3年の金賢佑です！
私は東大発AIベンチャーの株式会社2WINSの元でインターンをしています！

これからの記事では自分がインターンを通して最強のAIエンジニアを目指していきます！
第七回はRAGの基礎を記録しました！

# RAGとは
RAGとは簡単にいうとLLMに答えさせる前に、知識を検索してくれる仕組みのこと！

LLMはあくまで「学習済みの知識」しか知りません！\
なので例えば、\
👨「会社のマニュアルをもとに質問に答えてほしいな....」と思っても\
🤖「何そのマニュアル...知らないんだけど...」\
となってしまいます！

### RAGでは何ができるの？
今回はRAGでは主要なコンポーネントとして5つ紹介します！

`Document loader`：PDFなどの文書を読み込む

`Document transformer`：読み込んだ文書を使いやすいサイズに分割！例えば一万字のPDFを500文字ごとに分ける！（この単位をチャンクって言うよ）\
チャンクに直すことによって\
①大量のテキストデータを小分けにして処理することが可能になり、検索効率が向上する\
②`Embedding`のトークン制限を回避する\
などの利点が挙げられます。

なお、適切なチャンクサイズは200~500トークンと言われていて、小さすぎると意味のなくなりがなくなり、大きすぎると検索をかける際に無関係な文もヒットするなどのデメリットがあります！

`Embedding Model`：チャンクをベクトルに変換する！\
例えば、「この製品は高い耐久性と防水性を備えており、アウトドアでも安心して使えます。」みたいな文章があった時、\
①["この", "製品", "は", ......]のようにテキストがトークン化される\
②損失関数などを用いた事前学習済みの埋め込み行列から、それぞれにベクトルが渡される（ベクトルの数値が似ているものほど、似た意味を持っている）\
③各トークンの平均or特定のトークンでまとめて一つのベクトルに集約される\
④文全体の意味のベクトルが完成する


このことについてはday3でも紹介したこちらの動画をご覧ください！（chapter7くらいまであります）
https://youtu.be/tc8RTtwvd5U?si=ynBpv-bFHHjfwOCz

`Vector store`：ベクトル化したチャンクを保存する！\
こうしてできたベクトルのデータベースは普通のデータベースと違い、似た文を探す、いわゆる「曖昧な検索」が可能になります。\
とっても便利！

`Retriever`：ユーザーの質問に応じて`Vector store`から情報を検索する！\
仕組みとしては例えば\
①ユーザーが質問する👦「地球の直径はどれくらい？」\
②質問文をベクトルに変換！これはさっきやったのと同じ！\
③あらかじめ`Embedding`済みのデータベースに保存されているベクトル群の中で、上記のベクトルと似ているものを抽出！距離が近ければ意味も似ているので、ユークリッド距離を使ったり、ドット積を使います！

ドット積の結果の見方は以下を参考にしてください！

![alt text](<スクリーンショット 2025-04-16 19.10.03.png>)

ちなみに補足ですが、`Retriever`だけだと精度に問題があることがあります。間違った情報を渡してしまうことをハルシネーションと言います。\
こうしたリスクを回避する方法として、`Retriever`が意味的に近いだろうと持ってきたチャンクを再評価してくれる`Reranker`を間に挟むこともあります\
`Reranker`は例えば、`Retriever`が持ってきた10個のチャンクをスコア付けし、最も高いものを選んでくれます！



では実際に一つずつ使ってみましょう！

# Document loader
ではまず初めに`Document loader`を使ってみよう！\
以下のコードを打ってください。

``` python
!pip install langchain-community==0.3.0 GitPython==3.1.43

from langchain_community.document_loaders import GitLoader

def file_filter(file_path: str) -> bool:
  return file_path.endswith('.mdx')

loader = GitLoader(
    clone_url='https://github.com/langchain-ai/langchain',
    repo_path='./langchain',
    branch='master',
    file_filter=file_filter,
)

raw_docs = loader.load()
print(len(raw_docs))
```
こちらのコードではLangChainの公式GitHubリポジトリから`.mdx`ファイルだけを抽出して読み込んでいます！\
最後にデータ件数を表示してみましたが現在では414件あるみたいですね。\
`Document loader`のコードは複雑ですね....\
一応解読すると、

``` python
def file_filter(file_path: str) -> bool:
```
`def file_filter`で関数名を定義。\
`file_path`と言う名前の引数を受け取るよ言う意味。`str`なので引数は文字列。\
`-> bool`この関数は真偽を返す（True or False）と言う意味！

``` python
clone_url='https://github.com/langchain-ai/langchain',
repo_path='./langchain',
branch='master',
file_filter=file_filter,
```
`clone_url='https://github.com/langchain-ai/langchain'`でGitHubからのリポジトリをクローン\
`repo_path='./langchain'`でローカルの保存先を指定\
`branch='master'`でブランチを指定\
`file_filter=file_filter`で上で定義したオブジェクトを引数に渡している！

# Document transformer
続いてはドキュメントをチャンクで分割してみましょう！
``` python
!pip install langchain-text-splitters==0.3.0
```
まずは必要なものをインポートしてください！\
その後テキストをチャンクに分割していきます！

``` python
from langchain_text_splitters import CharacterTextSplitter

text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)

docs = text_splitter.split_documents(raw_docs)
print(len(docs))
```
`chunk_size`っていうのは一つの塊であるチャンクに含める最大文字数（またはトークンの数）のこと！
出力で`WARNING:langchain_text_splitters.base:Created a chunk of size 1281, which is longer than the specified 1000`っていう警告が出ているのは`chunk_size=1000`では分割しきれなかったよ！っていうことです。\
かといって`chunk_size`をあげすぎても分割できる量が少なくなってしまうから難しい...

# Embedding model
次はベクトル化を行ってみます。
``` python
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(model='text-embedding-3-small')

query = 'LangChainでPDFを読み込むにはどうしたらいいですか？'

vector = embeddings.embed_query(query)
print(len(vector))
print(vector[:3])
```
これを実行すると、下のように出力されます。\
今回は長いので3要素しか出力していませんが、実際は1536次元のベクトルが与えられています！（気になった人は[:3]を消してみてください！）
``` text
1536
[-0.011487526819109917, -0.0013641082914546132, 0.013325303792953491]
```
# Vector store & Retriever
続いてベクトル化したドキュメントの保存を行ってみます。\
最初にGithubからコピーしてきたデータを処理しています！
``` python
!pip install langchain-chroma==0.1.4
```
続いて`Chroma`をインポートしてみます！
``` python
from langchain_chroma import Chroma

ob = Chroma.from_documents(docs, embeddings)
```
`Chroma`はLangChainで使える「ベクトルデータベース」の一つ！\
デフォルトではローカル（自分のパソコン）に保存されるよ！

`.from_documents()`はテキストを１つずつ取り出して`embedding`を使ってベクトル化したのちに`Chroma`に保存してくれるショートカット関数！\
「ショートカット関数」とは一連の処理を一行でやってくれる便利な関数のこと！


# Retriever
最後にユーザーの質問に対して関連する情報を取得する`Retriever`を使ってみましょう！

``` python
query = 'LangChainでPDFを読み込むにはどうしたらいいですか？'

context_docs = retriever.invoke(query)
print(len(context_docs))

first_doc = context_docs[0]
print(first_doc.metadata)
print(first_doc.page_content)
```
`first_doc.metadata`にはファイル情報が含まれていて、出典に該当します。\
`first_doc.page_content`には実際の文章の中身が入っています。これがLLMに渡す情報になります。

# まとめ
今回はRAGの主要なコンポーネントについて紹介しました！\
なかなか複雑な文法もあったと思いますが、流れだけでも理解できたら十分だと思います！

お疲れ様でした！
# 参考文献
LangChainとLangGraphによるRAG・AIエージェント〈実践〉入門/西見公宏/吉田真吾/大嶋勇樹