背景

gmailの返答を自動化するプロジェクトを推進しています。 そこで、RAGですね。もうだいぶ時間が経っていますが、しっかりやっていきたいと思う。 RAG = 「Retrieval-Augmented Generation」 これを理解できればもう仕組みを理解したも同然です。 が一応説明しておきます。

RAGの仕組み

めっちゃ簡単にいうと、

  1. ベクトルデータベースに保存されたナレッジから
  2. 問い合わせと類似するナレッジを検索し引っ張り出し
  3. それを元にGEN AIに回答を作らせる

という流れになります。

1でベクトルデータベースにナレッジを保存する時には、元の自然言語をベクトル表現に変換した物を格納する必要があります。 このベクトル変換器のことを「Embedding model」と言います。

x: 自然言語
f: Embedding model

とすると

y = f(x)

で得られたy(とそれに対応するx)をベクトルDBに格納しておくわけですね。

x_q: 問い合わせが来た時

y_q = f(x_q)

から得られるy_qと類似しているy_1, y_2, y_3, ,,, y_nを検索し、 それに対応するx_1, x_2, x_3, ,,, x_n を出力し、

ans = LLM(x_1, x_2, x_3, ,,, x_n)

とう感じで回答を作らせるという流れです。

Embedding modelsの選定

こちらの記事に書いてある通り、Embedding modelの選定がRAG全体の回答生成性能に大きく影響する。 まあ、chatGPTに聞いた結果これが一番いいのかな。 BAAI/bge-base-en

今回RAGを構築するにあたり使うソフト

vector DB: qdrant embedding model: BAAI/bge-base-en LLM: phi4 14b on ollama (@ rtx 3060 12g)

Langchainなどの統合的なライブラリは使わない。なぜなら自由度が低いから。 vector DBでの検索も普通にREST APIでできるので問題なさそう。

RAG構築までの道のり

  1. ナレッジ集め
  2. vector DBの構築
  3. vector DBへの流し込み(Embedding model経由)
  4. 検索
  5. LLMに流し込むための周辺整理

step1. ナレッジ集め

集めました。chatgptに流し込むといい感じでチャンクを作ってくれます。

step2. vector DBの構築

以下のdocker composeで一発でqdrantが立ち上がります。

version: '3.8'

services:
  qdrant:
    image: qdrant/qdrant
    container_name: qdrant
    ports:
      - "6333:6333"  # REST API
      - "6334:6334"  # gRPC(不要なら省略してもOK)
    volumes:
      - /var/local/qdrant/qdrant_data:/qdrant/storage
    restart: unless-stopped

step3. vector DBへの流し込み(Embedding model経由)

pythonで流し込みます。流し込むためのライブラリをインストール

pip install qdrant-client sentence-transformers

以下のスクリプトで流し込む

from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct, VectorParams, Distance
from sentence_transformers import SentenceTransformer

# 1. Qdrantに接続
client = QdrantClient(host="localhost", port=6333)

# 2. コレクション作成(存在する場合は削除して再作成)
collection_name = "hogehoge"
embedding_dim = 768  # BAAI/bge-base-enの場合

client.recreate_collection(
    collection_name=collection_name,
    vectors_config=VectorParams(size=embedding_dim, distance=Distance.COSINE)
)

# 3. 埋め込みモデルのロード
model = SentenceTransformer("BAAI/bge-base-en")

# 4. チャンクデータを定義(ここでは例として最初の10件)
chunks = [
  "list of knowledge"
]

# 5. 埋め込み生成
vectors = model.encode(chunks).tolist()

# 6. Qdrantに投入(アップサート)
points = [
    PointStruct(id=i, vector=vectors[i], payload={"text": chunks[i]})
    for i in range(len(chunks))
]

client.upsert(collection_name=collection_name, points=points)
print("✅ チャンクの登録が完了しました!")

検索用のスクリプト

from qdrant_client import QdrantClient
from sentence_transformers import SentenceTransformer

# 検索用クエリ
query = "car with child seat and kitchen"

# Qdrant接続
client = QdrantClient(host="localhost", port=6333)

# モデルロード(検索時も同じモデルを使うこと!)
model = SentenceTransformer("intfloat/e5-small")

# embedding に変換
query_vector = model.encode(query).tolist()

# 検索(上位5件を取得)
results = client.search(
    collection_name="japan_campers_knowledge",
    query_vector=query_vector,
    limit=5
)

# 結果を表示
for hit in results:
    print("Score:", hit.score)
    print("Text:", hit.payload.get("text"))
    print("-" * 40)