【Google Colab対応】Pythonでコードを書きながらRAGの仕組みを理解してみた！

こんにちは。

[株式会社KANGEN Holdings](https://kangen-holdings.co.jp/)の野中康生です！

この記事では、RAGをPythonで実装し、その仕組みを理解する方法を解説します。

RAGを使えば、生成AI（ChatGPTなど）の「情報不足」「間違った情報」を補い、より信頼性の高い回答を生成することができるようになります。

今回は、**Python + Google Colaboratory**を使って、コードを実際に動かしながらRAGを学ぶ内容になっています。

初心者でもわかるように解説しているので、ぜひ最後まで読んでみてください！

# RAGとは?

**RAG（Retrieval-Augmented Generation）** は、**タスクに関連した外部の知識を取り込むことで、LLMの応答性能を向上させる手法**になります。

RAGを使用することで、以下のような効果があります。

- 外部情報に基づいた正確な回答ができる
- LLMが学習していない最新情報にも対応できる

# RAGの利用ステップ

RAGの利用ステップは大きく分けて以下の通りです。

1. **インデックスの作成**
1. **情報の検索**
1. **回答の生成**


# 0. 事前準備（パッケージのインストール）

まずは今回使用するパッケージのインストールを行います。

```
!pip install -q langchain langchain_openai langgraph openai tiktoken chromadb sentence-transformers
```

## パッケージごとの解説

インストールするパッケージの内訳は以下です。

| パッケージ名 | 説明 |
|:-:|:-:|
| langchain  |  RAGの基本パイプラインを作成するためのライブラリ。テキスト分割やベクトル検索が簡単にできる |
| langchain_openai  | OpenAIのGPTモデルを呼び出すためのクライアント。ChatOpenAIなどのモデルを利用可能。  |
| langgraph  | ノードベースで処理フローを組み立てられ、複雑なRAGワークフローを簡単に構築できる。  |
|openai   | OpenAI APIの公式クライアント。  |
|tiktoken   | OpenAIモデルで使われるトークナイザー。 |
|chromadb   | 軽量でローカルでも動作するベクトルDB。 |
| sentence-transformers  | テキストをベクトル化（Embedding）するための高性能モデル群。  |


## OpenAI APIキーの設定

また今回はOpenAIのAPIを使用するので、APIキーをあらかじめGoogle Colab上で環境変数としてセットしておきます。

```python
import os
from google.colab import userdata
os.environ["OPENAI_API_KEY"] = userdata.get('OPENAI_API_KEY')
```

:::note warn
Google Colabの「シークレット」から OPENAI_API_KEY を事前に登録しておきましょう！
:::


# 1. インデックスの作成

RAGの第一歩は、**情報源をベクトル化してインデックスに登録すること**です。

タスクと関連する外部情報からインデックスを作成します。インデックスは以下の手順で作成します。

1. **テキスト抽出**
1. **テキストデータの分割**
1. **分割したデータをベクトル化**
1. **ベクトルの保存**


## 1.1 テキスト抽出

最初に外部情報となる文書からテキストデータを抽出してきます。文書の形式としてはHTML、PDF、Markdownなど様々なものがあります。テキストデータを抽出後、前処理を行います。

今回はサンプルとして、「[株式会社KANGEN Holdingsの私たちについて](https://kangen-holdings.co.jp/about)」からKANGENのミッションのテキストを抽出してきます。

![スクリーンショット 0007-03-20 18.15.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954402/a2b7d203-e85a-499c-b266-54f1cb764dd4.png)

これを以下のようなコード例で、テキストを抽出してきます。

```python
import requests
from bs4 import BeautifulSoup

# 取得したいURL
url = "https://kangen-holdings.co.jp/about"

# ウェブページの内容を取得
response = requests.get(url)
# 文字エンコーディングを自動検出して設定
response.encoding = response.apparent_encoding  

# HTMLを解析
soup = BeautifulSoup(response.text, 'html.parser')

# 会社概要のセクションを抽出
# metaタグからproperty="og:description"を探す
og_description_tag = soup.find("meta", attrs={"property": "og:description"})

# content属性の値を取得する
if og_description_tag and og_description_tag.get("content"):
    text_data = og_description_tag["content"]
else:
    text_data = "og:description が見つかりませんでした"

# 確認
print(text_data)
```

上記のコードから、以下のようなテキストデータが取得できます。

```
私たちは、「世界中に”フェア”を届ける」ことをミッションとし、全てのエンジニアとクライアント企業様が、お互いに信頼し合い、一丸となってビジネスの成長を目指す、そんな”フェア”な世界を作ることを目指しています。 エンジニアにとっての「労働環境日本一の企業になる」こと、 クライアント企業様にとっても「日本一信頼されるパートナーになる」ことで、 全員が気持ちよく働きながら笑顔で満たされる環境を築き上げていき、業界全体の持続的な発展と競争力を高めることを目指しています。私たちとフェアな新しい世界を創っていきましょう。
```

## 1.2 テキストデータの分割

次に先ほど取得したテキストデータを適切なサイズのチャンクに分割します。テキストを分割する目的としては、後で LLM に入力する際の制限を考慮してのことです。

LLMは最大トークン数を超える入力ができないため、適切なサイズに分割する必要があります。

チャンクの単位は、文章、段落、固定長のトークン数などが考えられますが、今回はLangChainのドキュメント「[How to recursively split text by characters](https://python.langchain.com/docs/how_to/recursive_text_splitter/)」と同じくchunk_sizeを`100`、chunk_overlapを`20`として指定しています。

```python
from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,
    chunk_overlap=20
)

docs = text_splitter.create_documents([text_data])
for doc in docs:
    print(doc.page_content)
```

`chunk_size`は、情報のまとまりを区切るために使用します。これにより検索の質が上がりやすくなります。

一方、`chunk_overlap`は文脈をつなげるために使用し、意味が切れずに自然な検索・生成ができるようになります。

これらをうまく調整することで検索結果の質を向上できる可能性が高まります。

出力としては以下のようになります。

```
私たちは、「世界中に”フェア”を届ける」ことをミッションとし、全てのエンジニアとクライアント企業様が、お互いに信頼し合い、一丸となってビジネスの成長を目指す、そんな”フェア”な世界を作ることを目指して
んな”フェア”な世界を作ることを目指しています。
エンジニアにとっての「労働環境日本一の企業になる」こと、 クライアント企業様にとっても「日本一信頼されるパートナーになる」ことで、
全員が気持ちよく働きながら笑顔で満たされる環境を築き上げていき、業界全体の持続的な発展と競争力を高めることを目指しています。私たちとフェアな新しい世界を創っていきましょう。
```

## 1.3 分割したデータをベクトル化

次のステップでは、分割した個々のチャンクを**埋め込みベクトル**に変換します。埋め込みベクトルは文書の特徴をベクトル化したものになります。埋め込みベクトルの作成には、**Transformer**がよく使われます。

Transformerに入力し、最後のトークンの埋め込みベクトルを取得します。
最後のトークンは文書全体の情報が集約されているため、文書全体の特徴を表す埋め込みベクトルとして利用することができるからです。

といいつつ、今回は`sentence-transformers`ライブラリ & `all-MiniLM-L6-v2`モデルを使用して、Transformerモデルでテキスト全体をエンコードし、**平均プーリング**された文の表現を出力するようにします。

https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2

```python
from sentence_transformers import SentenceTransformer

# 埋め込みモデル（モデルサイズが非常に小さいことから'all-MiniLM-L6-v2'を採用）
embedding_model = SentenceTransformer('all-MiniLM-L6-v2')

# ドキュメントからテキストを抽出
texts = [doc.page_content for doc in docs]

# ベクトルに変換
embeddings = embedding_model.encode(texts)

# 最初のチャンクのベクトル
print(embeddings[0])  
```

<details><summary>出力内容</summary>

```
[ 3.92914116e-02  1.11862250e-01  5.00992266e-03 -5.21850288e-02
 -3.12895030e-02 -1.51933990e-02  3.24619152e-02 -1.44369407e-02
 -2.00526528e-02  6.37214538e-03  1.30883574e-01 -1.11483902e-01
  1.88317187e-02  1.58923287e-02 -5.38811497e-02 -5.53715229e-02
 -6.46234825e-02  2.22134870e-03  6.79299235e-03  1.45858694e-02
  1.19994450e-02 -2.00935584e-02  3.22660543e-02  9.20184031e-02
 -3.50432619e-02  1.05260804e-01  4.41651046e-02  2.77042165e-02
  1.17347121e-01  5.96507862e-02 -5.61345592e-02 -5.51652424e-02
 -4.96229529e-03  2.12216415e-02 -4.04818095e-02 -3.13315094e-02
 -3.46040055e-02 -4.33438011e-02  6.50107488e-03 -2.57167928e-02
  2.55414918e-02 -4.22130488e-02  3.13286334e-02 -5.64000309e-02
 -3.68579738e-02 -2.60787774e-02 -7.20004439e-02  7.21883103e-02
 -2.29343846e-02 -2.79763285e-02  2.33546691e-03 -8.25282633e-02
  2.82686129e-02 -4.79340851e-02  3.69610824e-02  9.78722423e-02
  1.85551662e-02 -1.90711729e-02 -3.83855612e-03 -6.28121197e-02
 -1.59390047e-02 -7.55193979e-02  5.36775626e-02  3.65145504e-02
 -2.80682091e-03  6.46582544e-02  4.37740758e-02  4.46910784e-02
 -8.30462649e-02 -1.12027317e-01 -4.43014316e-02 -4.54674549e-02
 -1.45233069e-02 -6.54269084e-02 -3.67945037e-03  2.90383436e-02
 -4.94690090e-02  2.31321175e-02 -9.65449959e-02 -5.16760983e-02
  9.07397568e-02 -2.92205382e-02 -3.03102266e-02 -1.52976559e-02
  1.07570067e-02  1.01024076e-01 -1.14500048e-02 -1.85032841e-02
  3.43986112e-03  1.02992412e-02  1.60272885e-02 -6.06393702e-02
  1.88141987e-02 -2.28376277e-02 -4.20049354e-02  7.69770145e-02
 -4.19208361e-03 -7.46111497e-02 -2.37840060e-02 -4.45606112e-02
  8.55481550e-02 -8.86123814e-03 -2.82628518e-02 -1.50690256e-02
 -1.08411998e-01 -1.63002275e-02  1.34933898e-02 -2.32811570e-02
  5.05386293e-02 -1.53933801e-02 -3.99312340e-02 -9.74386781e-02
  1.52484924e-02 -1.05181843e-01  8.35850835e-03  2.29882114e-02
  4.73459028e-02 -1.98041461e-02  1.55644286e-02 -7.40210637e-02
  3.84026207e-02  4.68048230e-02 -5.96050955e-02 -3.35996435e-03
  3.22830603e-02  4.10948209e-02  4.35830131e-02  1.00714064e-32
 -2.63468660e-02  4.61648330e-02 -7.05774594e-03 -2.88856830e-02
 -4.71424237e-02 -6.10656887e-02  2.89304275e-02  5.37265316e-02
 -5.05893752e-02 -1.06405988e-01 -6.66306019e-02  7.49162957e-02
 -4.15507257e-02 -4.64247838e-02 -4.73801903e-02  2.23269835e-02
  4.64852387e-03  2.64166743e-02  9.19283256e-02 -2.72230022e-02
 -2.43009813e-02 -4.50830609e-02 -3.81171517e-03  2.58711427e-02
  3.51008102e-02 -3.44705395e-02  2.28739111e-03  1.60249602e-02
 -1.47373518e-02 -7.17167510e-03  1.30819269e-02 -1.52976075e-02
  4.64322325e-03 -5.78424335e-02  1.08748106e-02  2.56144386e-02
  7.46475235e-02  4.14529629e-03  4.96587045e-02  6.27528131e-02
  2.95579229e-02  3.18442769e-02 -3.75853553e-02  2.12090407e-02
  1.20393798e-01  1.89501047e-02  2.94666290e-02  3.10339108e-02
 -3.62219140e-02  2.92742997e-02 -5.77534027e-02  3.14335967e-03
 -3.36438194e-02 -2.09109858e-02  4.74018641e-02 -4.41548117e-02
 -3.05575132e-02  7.69354552e-02 -6.36434332e-02 -6.05734959e-02
  1.36804311e-02  8.71877298e-02 -8.95432979e-02  1.00993887e-01
 -6.46422803e-02  7.29523078e-02 -9.19014513e-02  1.47111993e-02
  1.59752574e-02  1.22274216e-02 -1.07634403e-01  3.20132338e-02
 -4.46023233e-02 -1.60819143e-02  8.96345526e-02 -4.31128852e-02
 -7.84371123e-02  5.25203021e-03 -2.05096919e-02 -4.27220995e-03
 -1.22828692e-01 -2.91259922e-02 -3.07560293e-03  4.62796763e-02
  3.27293836e-02  3.94385979e-02  1.50024919e-02 -4.00144979e-02
  2.85686161e-02 -2.40902752e-02 -4.59084623e-02  1.97132281e-03
  9.26966146e-02  1.40533941e-02 -6.45392761e-02 -1.06610343e-32
 -3.53839882e-02 -2.46731564e-03 -4.09915410e-02 -1.67466607e-02
  8.07340350e-03 -9.81601607e-03 -7.70266727e-02  3.67838033e-02
  3.09563056e-02 -7.23059624e-02  3.90614644e-02 -2.22274438e-02
  6.85447082e-02  2.92128325e-02 -2.04414427e-02  3.06807719e-02
  2.22476330e-02  1.19451776e-01 -1.08987428e-02 -2.08038185e-02
  7.29690194e-02 -1.27974346e-01  3.73813547e-02  4.47081700e-02
 -1.53862201e-02  1.90121420e-02  1.30449221e-01 -9.75590125e-02
  7.78734908e-02 -4.60092761e-02  1.72897580e-03 -1.03512183e-02
 -1.67060569e-02  1.25148699e-01 -4.92487550e-02 -1.65686570e-02
  4.27353345e-02 -2.44743247e-02 -1.59120932e-02  5.91599643e-02
 -1.00127451e-01  1.34531362e-02  1.63824931e-02 -1.75465585e-03
 -6.30380660e-02  5.03698969e-03 -9.60489139e-02  1.70854817e-03
 -8.30992684e-02 -1.51539566e-02 -4.07558791e-02  1.14188378e-03
  2.10140757e-02 -5.58720455e-02 -1.28661254e-02  5.88515177e-02
  5.06988987e-02 -3.07911206e-02  4.06731665e-02 -5.93087375e-02
  5.80901373e-03  5.91392256e-03 -9.16384831e-02  9.07909796e-02
  7.49002546e-02 -2.58696135e-02  2.33400278e-02  6.95952028e-02
 -5.30241011e-03 -6.73901737e-02  5.47081232e-02 -3.94963920e-02
 -7.13923648e-02  1.60549786e-02 -6.39664680e-02  7.45690539e-02
  6.02159090e-02  2.82389261e-02  6.46805912e-02  4.27286662e-02
 -2.12361552e-02  2.63698623e-02 -5.10177901e-03 -5.24074994e-02
  1.80916116e-02 -4.43000086e-02 -2.87304744e-02  5.73310591e-02
 -3.54798809e-02  2.08017826e-02 -2.84314696e-02  1.00845993e-01
 -5.50192222e-03 -7.01652542e-02  9.77845863e-02 -4.46708945e-08
 -9.08566043e-02 -1.11696519e-01 -2.98183020e-02  1.14857331e-02
  1.42130330e-02 -1.09643803e-03  3.21614370e-02 -1.34338429e-02
 -1.14500448e-02 -5.67202792e-02 -8.49443302e-03  2.04971414e-02
 -1.62667017e-02  7.01884553e-02  1.36662861e-02  6.16475986e-03
 -7.14837387e-02  2.49699038e-02  2.62199119e-02 -2.29709763e-02
  3.92399840e-02 -1.04648180e-01  2.42702980e-02 -4.89077494e-02
 -7.65372813e-02 -1.50442552e-02  5.52093517e-03  1.76884364e-02
 -4.36577611e-02 -5.58319455e-03 -2.49177404e-02 -9.99957416e-03
  1.24711804e-02 -3.89258936e-02 -6.93054721e-02  4.42961790e-02
  7.52104968e-02  8.04348961e-02 -1.50853880e-02  6.82799071e-02
  5.42298593e-02 -1.11273108e-02 -4.09890413e-02 -6.20485935e-03
  7.41443038e-02 -5.33504672e-02  1.14040472e-01  5.18376939e-02
  5.74332997e-02 -5.04999459e-02 -1.55173298e-02 -1.34439683e-02
  1.74781550e-02 -1.55872488e-02 -3.39122154e-02 -1.99292395e-02
  3.01778056e-02  4.75825705e-02  3.27285342e-02 -1.93755198e-02
  8.83818790e-02  9.23034698e-02  3.76523510e-02  7.24067315e-02]
```
</details>

出力内容を見るとベクトルになっているのが確認できます。

ちなみに日本語の日本語テキストのembeddings計算としては、(モデルサイズは大きいですが) [stsb-xlm-r-multilingual](https://huggingface.co/sentence-transformers/stsb-xlm-r-multilingual) などを使った方がよさそうとのことです。

この辺のembeddingsやモデル比較などはまた別記事に改めたいと思います。

## 1.4 ベクトルの保存

インデックス作成の最後の手順としては、埋め込みベクトルをデータベースに保存することです。

ここで使用するデータベースは埋め込みベクトルを用いた高速類似度検索に対応可能なもので、今回はベクトルデータに特化した高速検索機能を提供している[Chroma](https://docs.trychroma.com/docs/overview/getting-started)を使用します。

```python
import chromadb
chroma_client = chromadb.PersistentClient(path="./chroma_db")

# 各ドキュメントにIDを付ける
ids = [f"id_{i}" for i in range(len(texts))]
collection = chroma_client.get_or_create_collection(name="rag_sample")

# ベクトルを保存
collection.add(
    documents=texts,
    embeddings=embeddings.tolist(),
    ids=ids
)

print("保存完了！")
```

# 2. 情報の検索（Retrieval）

インデックスの作成が終わったら、次は**情報の検索（Retrieval）** を行います。

RAGでは、ユーザーのクエリ（質問）を「ベクトル化」し、あらかじめ登録しておいたインデックス（ベクトルDB）に対して**類似度検索（similarity search）** を行います。類似度が高い文書を取得することで、LLMがより文脈に沿った精度の高い回答を生成できるようになります。

### 類似度検索とは？

`コサイン類似度`などの手法を使い、ユーザーの質問とインデックス内の文書との距離（**類似度**）を計算します。
距離が近い（類似度が高い）順に上位N件を返すことで、「最も関係が深い情報」を取り出せることができます。

コードの例では、ユーザーの質問をベクトルに変換した上で、類似検索を行い、トップ1件を取得するようにしています。

```python
# ユーザーの質問
query = "KANGEN Holdingsの目指していることは？"

# クエリをベクトルに変換
query_embedding = embedding_model.encode([query])

# 類似検索（トップ1件を取得）
results = collection.query(
    query_embeddings=query_embedding.tolist(),
    n_results=1
)

# 最も関連性の高い文を出力
print("検索結果:")
print(results['documents'][0][0])  
```

以下のような結果が出力されました。

```
検索結果:
エンジニアにとっての「労働環境日本一の企業になる」こと、 クライアント企業様にとっても「日本一信頼されるパートナーになる」ことで、
```

# 3. 回答の生成（Generation）

最後に、取得した外部情報(今回の例だとKANGENのミッション)をプロンプトに組み込み、LLMに**回答を生成（Generation）** させます。

ここでのポイントは「RAGあり」と「RAGなし」の比較です。

### RAGなしの場合
- LLMの学習データに依存する
- 最新情報や企業固有情報（KANGEN Holdingsのミッションなど）は知らない場合が多い
- 回答が曖昧

### RAGありの場合

- 外部情報を追加できる
- 特定ドメインの最新情報に即した回答が可能
- 正確性・信頼性が向上する

```python
from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage

# GPTモデルの初期化
chat = ChatOpenAI(model="gpt-4o-mini")

# ユーザーの質問
query = "KANGEN Holdingsのミッションは何ですか？"

# 外部情報（RAGで取得した文書から）
context = results['documents'][0][0]

# ==============================
# ▶ 1. RAGあり（外部情報を含めたプロンプト）
# ==============================
prompt_with_context = f"""
次の情報を参考にして質問に答えてください。

情報: {context}

質問: {query}

回答:
"""

response_with_context = chat.invoke([HumanMessage(content=prompt_with_context)])

# ==============================
# ▶ 2. RAGなし（外部情報を含めないプロンプト）
# ==============================
prompt_without_context = f"""
質問: {query}

回答:
"""

response_without_context = chat.invoke([HumanMessage(content=prompt_without_context)])

# ==============================
# ▶ 結果を表示
# ==============================
print("=== RAG あり（外部情報を利用した回答） ===")
print(response_with_context.content)
print("\n=== RAG なし（モデル単体での回答） ===")
print(response_without_context.content)
```

上記でRAGありのコードとRAGなしのコードを実装した上で、以下で出力結果を比較してみました。

RAGありだと正確なKANGEN Holdingsのミッションの回答が得られています。
一方でRAGがない検索の場合だと、おそらく他社企業のミッションが回答として生成されており、回答としては完全に誤りです。

```
=== RAG あり（外部情報を利用した回答） ===
KANGEN Holdingsのミッションは、エンジニアにとって「労働環境日本一の企業になる」ことと、クライアント企業様にとって「日本一信頼されるパートナーになる」ことです。

=== RAG なし（モデル単体での回答） ===
KANGEN Holdingsのミッションは、健康で持続可能なライフスタイルを促進し、人々の生活の質を向上させることです。具体的には、高品質な水を通じて健康をサポートし、環境に配慮した製品やサービスを提供することにより、より良い未来を築くことを目指しています。お客様やコミュニティとの信頼関係を重視し、教育や情報提供を通じて健康に対する意識を高めることも重要な要素です。
```

## 補足

上記の例では[LangChain](https://python.langchain.com/docs/introduction/)を使いましたが、[LangGraph](https://langchain-ai.github.io/langgraph/tutorials/introduction/)を使っても同様の結果を得ることができます。LangGraphの実装例だと以下のようになります。

```python
from typing import TypedDict
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END

# GPTモデル
chat_model = ChatOpenAI(model="gpt-4o-mini")

# Stateを定義
class State(TypedDict):
    context: str
    query: str
    prompt: str
    response: str

graph = StateGraph(State)

# ノード1: プロンプト作成
def create_prompt(state: State) -> State:
    context = state["context"]
    query = state["query"]

    prompt = f"""
    次の情報を参考にして質問に答えてください。

    情報: {context}

    質問: {query}

    回答:
    """

    return {
        **state,
        "prompt": prompt
    }

# ノード2: 回答生成
def get_completion(state: State) -> State:
    prompt = state["prompt"]
    response = chat_model.invoke(prompt)

    # 元の状態も含めて返す
    return {
        **state,
        "response": response.content
    }

# ノード登録
graph.add_node("create_prompt", create_prompt)
graph.add_node("get_completion", get_completion)

# ノードの流れ定義
graph.add_edge(START, "create_prompt")
graph.add_edge("create_prompt", "get_completion")
graph.add_edge("get_completion", END)

# グラフをコンパイル
compiled_graph = graph.compile()

# 実行用入力
inputs = {
    "context": results['documents'][0][0],
    "query": query
}

output = compiled_graph.invoke(inputs)

# 出力確認
print("回答:")
print(output["response"])
```

```
回答:
KANGEN Holdingsのミッションは、「エンジニアにとっての労働環境日本一の企業になること」と、「クライアント企業様にとって日本一信頼されるパートナーになること」です。
```


# 最後に

今回は、RAGの基礎と実装手順について、Google Colaboratory上で実際に動かしながら学ぶ方法を紹介しました！

RAGを使うことで、ChatGPTなどの生成AIがより信頼性の高い回答を返せるようになり、業務活用や独自のQAシステム構築にも大きな力を発揮します。

もしこの記事が少しでもお役に立てたら、「いいね」やフォローをしていただけると励みになります！ 質問や感想もお気軽にコメントください！

それでは、次の記事でお会いしましょう！