RAGを作って公式のドキュメントを隅々まで知り尽くした自分専用の有識者を用意すれば、学習がめちゃくちゃ捗るんじゃね?というアイデアを検証した。
題材はDuckDB。DuckDBの公式ドキュメントはMarkdownで管理されていて、GitHubのリポジトリから直接取得できる。
私自身はDuckDBを使ったことがなく完全に未知のツール。
これからAIを使い倒すなら絶対に避けられないツールだろうし、効果の検証にもなって一石二鳥だと思った。
結論:めちゃくちゃよかった
やってよかったと思う点は大きく3つ。
全体把握が先にできる
普通はGetting Startedとかで最小のユースケースから手を動かし、ドキュメントを読み進める毎に少しずつ全体像が見えてくる感じだけど、RAGがあると「なぜこの概念が必要なのか」「他のツールとどう違うのか」みたいな質問を最初にぶつけることができた。
疑問をその場で潰せる
手を動かしていて詰まったとき、検索して情報を探す時間がほぼなくなる。RAGがドキュメントの該当箇所を引いて即答してくれた。
ハルシネーションを疑わなくてよい
プロンプトに「根拠となるURLも同時に示すこと」と指示しておいたので、回答には必ずURLが付いてくる。公式ドキュメントはもちろん、RAGに情報がない場合はWebSearchでブログ記事なども参照して回答してくれた。
情報の出どころが確認できると安心感が全然違った。
実際にやったこと
RAGの作り方は後述する。ここでは実際にどう使ったかを書く。
ハンズオンを作ってもらった
まずはじめにエージェントにはこう依頼した。
以下の条件で勉強会を開きたい。まずは全体の構成を考えて
受講者の想定:バックエンドエンジニア(データ分析の知識なし)
目的:DuckDBの基礎から学び、業務で実際に使ってもらう
時間:3時間
出てきた構成を見ながら、「この章はなぜ必要なのか」「他のツールと比べてDuckDBはどういう立ち位置なのか」「DuckDBとデータレイクハウスとの関係は」といった質問をエージェントに投げた。
RAGが関連ドキュメントを引きながら答えてくれるので、全体像を把握しながら構成を編集していった。
構成が固まったら台本を書いてもらい、さらに質疑応答を繰り返した。
- DuckDBって本番環境に使える? → 使い方による
- セキュリティってどうなってる → ユースケース毎のリスクは…
- RDBMSとの比較は分かるけど何故Pandasとも比較してる? → データ分析の文脈では使われ方がよく似ていて…
みたいな感じで疑問を感じた箇所をどんどん資料に加えていった。
疑問がどんどん潰せてサクサク手が動く感覚
個人的にRAGを作って一番大きな変化が「疑問を潰す速度が圧倒的に違う」だった。
AIエージェントを使う前は納得できる答えが見つかるまでGoogleで検索していた。AIエージェントを使いだすと、今度はエージェントが嘘を言っていないかの確認のためにGoogleを使うようになった。RAGを使うと、根拠としてエージェントが提示したURLを確認するだけでいい。
ただし嘘がない訳ではない
例えばデータの中からランダムで10%をベルヌーイ施行を使ってサンプル抽出するケースで、シード値を指定する書き方をエージェントはこのように回答した。
USING SAMPLE 10% (bernoulli, 47)
ところが実際に提示されたURLを読んでもこのような使い方は出てこなかったので「これどこから持ってきた例?」と聞いてみたところ、ドキュメントの6番目の例をベルヌーイサンプリングに応用した、と回答してきた。
SELECT *
FROM tbl
USING SAMPLE 20% (system, 377);
このようにドキュメントの記載から回答を組み立てる過程でエージェント推測が入る以上、例え公式のURLから引用してきているからといって鵜呑みにする訳にはいかなかった。ただしURLが提示されるので裏取りはすごく楽になった。
RAGの作り方
ここからは実際にRAGを作る過程を記載する。
もっと難しいものだと思っていたが、実際やってみるとそんなに難しくなかった。
公式ドキュメントをまるごとローカルに落とす
サイトマップからURLを列挙し、それぞれに対応するMarkdownファイルのRaw URLを組み立ててダウンロードする、というスクリプトを書いた。
# 1. サイトマップから /docs/current/ 以下のURLを全件取得
urls = [url for url in parse_sitemap("https://duckdb.org/sitemap.xml")
if "/docs/current/" in url]
# 2. 各URLをGitHubのRaw URLに変換してMarkdownを保存
for url in urls:
raw_url = to_raw_url(url)
markdown = requests.get(raw_url).text
save(markdown, local_path(url))
ChromaDBでベクトルインデックスを作る
次にファイルのテキストをチャンクに分割し、インデックスを構築した。
今回は分かりやすくH2 / H3 ヘッダー単位で分割した。ヘッダーが連続して文がないケースがノイズになったので、セクションが100文字未満だった場合は前のチャンクに結合するようにした。
SQLiteのようにローカルで動いてくれるChromaDBを選択した。埋め込みモデルはよく分からなかったのでAIに聞いたら「対象ドキュメントが英語だけなのでmulti-qa-MiniLM-L6-cos-v1が軽くていいんじゃないか、とおすすめされたので採用した。
最終的には403ファイル・2561チャンクのインデックスができた。
検索してみる
検索のコードはザックリこんな感じ
import chromadb
from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction
ef = SentenceTransformerEmbeddingFunction(model_name=EMBED_MODEL)
client = chromadb.PersistentClient(path=str(CHROMA_DIR))
collection = client.get_collection(COLLECTION_NAME, embedding_function=ef)
results = collection.query(
query_texts=[query],
n_results=NUMBER_OF_RESULT,
include=["documents", "metadatas", "distances"],
)
検索結果の類似度はこのように評価した(コサイン類似度、という指標らしい)。
scores = [1 - d / 2 for d in distances]
実際に質問を投げて、こんな感じで結果が返ってくるように実装した。
$ python search.py "how does DuckDB handle out-of-memory" --top 3
[1] internals/storage.md
## Buffer Manager
score: 0.712
url: https://duckdb.org/docs/current/internals/storage
DuckDB uses a buffer manager to manage memory. When the amount of data exceeds...
[2] guides/performance/environment.md
## Memory Limit
score: 0.681
url: https://duckdb.org/docs/current/guides/performance/environment
The memory_limit setting controls the maximum amount of memory...
キーワードが一致しなくても意味的に近い節が返ってくる。out-of-memory という単語がドキュメントに出てこなくても、memory management・buffer manager・memory_limit といったページがちゃんとヒットすることが確認できた。
今回採用したモデルは複数の言語に対応していないので、クエリは英語で渡さなければならない。そのためエージェントに渡すプロンプトには以下の2点を書いたところ、ちゃんと動作するようになった。
- 質問は日本語から英語に翻訳すること
-
ASOF JOIN・SUMMARIZEなどの固有名詞は翻訳せずそのまま使うこと