0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ミニチュア版Gemini NoteBookの構築(v0.1)

0
Last updated at Posted at 2026-08-28

この記事でできること

レンタルGPUサーバーに、日本語で質問すると出典つきで答えが返るアプリ——いわば NotebookLM のミニチュア版——を、部品5つで自分の手で組めるようになります。

Google の NotebookLM は便利です。ソースを登録して質問すると、ソースにもとづいた答えが出典つきで返ってきます。ただ、私には登録できないソースがありました。個人情報の入った自分のメモ類です。外部の AI サービスに送りたくないソースは、自分の管理下で動く AI に答えさせる必要があります。

そこで、同じ動きをするアプリを自分で組むことにしました。あわせて、言葉だけ知っていた「RAG」の中身——ソースの分割・ベクトル化・検索・回答生成——を、部品を1つずつ手で並べて確かめます。この記事はその記録です。

  • 対象読者:ChatGPT や NotebookLM は使っていて、RAG の中身をこれから理解したい方/LLM の組み込みに初めて挑戦する方。Python が読めれば大丈夫です
  • 扱えるソース:Markdown ファイルのみ(PDF・Word は今回のバージョンでは非対応。最後の「今後の拡張」で触れます)
  • コスト:検証まるごとで GPU 代 約0.45ドル+保管に月1.4ドル(後述)
  • コード一式:GitHub で公開しています → mini-notebooklm-v0.1

完成イメージ

ブラウザにこんな画面が出ます。左に読み込んだソースの一覧、中央にチャット欄。
スクリーンショット 2026-08-28 9.25.53.png

ソースに書いてあることを聞くと、答えと一緒にどのソースを見たか(出典)が出ます。逆に「富士山の標高は何メートル?」のようにソースに無いことを聞くと、知ったかぶりをせず「資料にありません。」と返します。ここが RAG の良いところで、LLM の記憶ではなく渡したソースだけを根拠に答えさせます。

RAG は4つの手順でできている

RAG(Retrieval-Augmented Generation)と聞くと身構えますが、やっていることは4つだけです。

手順 何をするか 担当する部品
①分割 ソースを500文字ずつに切る(隣と100文字重ねる) 自作の Python
②数値化 切れはしをベクトル(意味を表す数値の並び)に変える bge-m3
③検索 質問文も同じく数値化し、数値が近い切れはしを5つ探す Chroma
④生成 その5つだけを渡して、答えを作らせる qwen3:8b

部品の名前を補足すると、bge-m3 は埋め込みモデル(文をベクトルに変えるモデル)、Chroma はベクトルを保管・検索する DB(ベクトルDB)、qwen3:8b は回答役の LLM です。

肝は手順2〜3です。ソースも質問も同じモデルで数値化するから、数値の近さ=意味の近さとして探せる。ソースに「感想」と書いてあって質問が「レビュー」でも、意味が近ければ数値も近くなるので拾えます。キーワード検索との一番の違いはここです。

そして手順4の「5つだけ」という数が、あとで説明する弱点の原因になります。

部品は5つ。ぜんぶ Pod の中で動く

質問が通る順に並べます。

役割 選んだもの 選んだ理由
画面 HTML React などは入れない。複雑にしない
受付(API サーバ) FastAPI 軽い。エンドポイント3本で足りる
ベクトル化役 bge-m3 日本語対応。qwen3 と同じ Ollama で動かせる
ベクトルDB Chroma Python 数行で使えて中身を覗ける=勉強向き
回答役の LLM qwen3:8b 日本語が得意で、モデル約6GB と手頃

Ollama を選んだ理由は1つで、LLM と埋め込みモデルの2役を1本でまとめて動かせるからです。部品が1つ減ると、壊れる場所も1つ減ります。

構成はこうなります。回答生成もベクトル化も全部この Pod の中で完結するので、外部の AI サービスにはソースを一切送りません

環境:RunPod とネットワークボリューム

自分のPCに NVIDIA の GPU が無いので、レンタルGPUクラウド RunPod を使いました。GPU 付きの仮想マシンを「Pod」と呼び、使った時間だけ課金されます。GPU があるなら自分のPCでも手順はほぼ同じです(Ollama・Chroma・FastAPI を入れるだけ)。VRAM はモデルファイル合計が約7GB なので、12GB クラスあれば同じ構成が動く見込みです(私が確認したのは 24GB の RTX 4090 のみ)。

最初にやることは2つ。

  1. ネットワークボリュームを作る(20GB・月1.4ドル)——ソース・モデル・ベクトルDB をここに置く。Pod 本体は消してもここは残る
  2. Pod を立てる——ボリュームを /workspace にマウントして起動する

コストの実績も書いておきます。RTX 4090 が 0.34ドル/時間で、環境構築から動作確認・ひととおりテストが終わるまで Pod 稼働は合計1時間20分ほど、GPU 代は約0.45ドル(70円くらい)でした。継続コストは保管の月1.4ドルだけ。Pod は使い終わったら必ず消します(消してもボリュームに全部残るので、次回は立て直して起動スクリプトを打つだけ)。消し忘れると 0.34ドル/h の課金が続くので、終了の確認だけは毎回行います。

組み立て:ファイルは6本

10_実装/
├── setup.sh            # Pod 起動後の準備(Ollama 起動〜サーバ起動まで5段階)
├── config/settings.json # 設定値のまとめ(下に全文)
├── rag.py              # RAG の本体(分割・数値化・保存・検索・生成)
├── app.py              # FastAPI(API 3本)
├── upload_docs.py      # PCのソースを Pod へ送るスクリプト
└── web/index.html      # 画面(fetch するだけの素の HTML)

設定値はコードに埋めず、settings.json に集めました。全文でこれだけです。

{
  "llm": {
    "model": "qwen3:8b",
    "temperature": 0.2,
    "system_prompt": "あなたは資料アシスタントです。渡した資料だけを根拠に日本語で答えてください。資料に無いことを聞かれたら「資料にありません」と答えてください。答えの根拠になった資料の内容は正確に引用してください。"
  },
  "embedding": { "model": "bge-m3" },
  "chunk": { "size": 500, "overlap": 100 },
  "search": { "top_k": 5 },
  "server": { "port": 8000 },
  "ollama": { "url": "http://127.0.0.1:11434" },
  "paths": { "docs": "/workspace/docs", "chroma": "/workspace/chroma" }
}

rag.py の中心はこの2つです(抜粋。全体は GitHub にあります)。

読み込み側——ソースを切って、数値化して、Chroma に入れる:

def ingest(src_dir: str) -> dict:
    for md in sorted(Path(src_dir).glob("*.md")):
        text = md.read_text(encoding="utf-8")
        chunks = split_text(text, size=500, overlap=100)   # 1. 分割
        embs = embed(chunks)                               # 2. 数値化(bge-m3)
        collection.add(                                    # 3. 保存(Chroma)
            ids=[f"{md.name}:{i}" for i in range(len(chunks))],
            documents=chunks,
            embeddings=embs,
            metadatas=[{"source": md.name}] * len(chunks),
        )

質問側——質問を数値化して、近い5つを探して、LLM に渡す:

def ask(question: str) -> dict:
    q_emb = embed([question])[0]
    hits = collection.query(query_embeddings=[q_emb], n_results=5)  # 検索
    context = "\n---\n".join(hits["documents"][0])
    answer = ollama_chat(                                            # 生成
        system=SETTINGS["llm"]["system_prompt"],
        user=f"資料:\n{context}\n\n質問: {question}",
    )
    sources = sorted({m["source"] for m in hits["metadatas"][0]})
    return {"answer": answer, "sources": sources}

app.py の API は3本だけ。画面(index.html)が使うのは GET /api/sources(ソース一覧)と POST /api/ask(質問)の2本で、POST /api/ingest(読み込み)は コマンドラインから実行する upload_docs.py 用です。index.html は入力欄と fetch が書いてあるだけの1枚です。

起動とソース投入

Pod に入ったら setup.sh を1回打ちます。段階ごとにログを残す作りにしました。失敗したとき「どこまで進んで、どこで止まったか」が一目で分かるためです。

[1/5] 01:43:12 Ollama を起動する
[2/5] 01:43:38 GPU を確認する
[3/5] 01:43:39 モデルを確認する(無ければダウンロードする)
[4/5] 01:45:18 保存先と Python パッケージを確認する
[5/5] 01:45:41 サーバを起動する

モデル取得(qwen3:8b 約6GB+bge-m3)は約100秒で終わりました。RunPod の回線は速い。

ソースはPCから送ります。

python3 upload_docs.py --url https://<PodのID>-8000.proxy.runpod.net

私は自分のメモ 61本を読み込ませました(1本が数十の切れはしになります)。ブラウザで開くと左に61本が並び、質問すると出典つきで答えが返る。「富士山の標高は?」には「資料にありません。」。ここまで来れば完成です。

1つ正直に書いておきます。この画面と API に認証は付けていません。URL に含まれる Pod の ID はランダムな文字列で推測は現実的に難しく、Pod 自体も使うときだけ立てて終わったら消す前提だからです。もし常時起動で運用するなら、公開する前に認証(Basic 認証やトークン)を足してください。個人情報を含むソースを載せるアプリなので、ここは省略しない方がいい場所です。

なお ollama ps を見ると、qwen3:8b も bge-m3 も PROCESSOR 列が 100% GPU でした。CPU に逃げていると桁違いに遅くなるので、ここは確認する価値があります。

実機でつまずいた6箇所

机上では問題なしでした。実機では6回つまずきました。同じ手順をなぞる人のために全部書きます。

1. ネットワークボリューム対応のデータセンターに、狙った GPU が無い
当初は RTX 3090(0.22ドル/h)の予定が、ボリュームを作れるデータセンターを21箇所調べても 3090 はゼロ。RTX 4090(0.34ドル/h)に変更しました。ボリュームと GPU の在庫は場所単位で紐づくので、先にボリュームの置き場所から決めるのが正解です。

2. API から Pod を立てると SSH に入れない
Pod は起動したのに Connection refused。RunPod 公式イメージの sshd は環境変数 PUBLIC_KEY があるときだけ起動します。Web 画面からのデプロイなら鍵が自動で入りますが、API 経由では自分で env に渡さないと sshd 自体が立ちません。1台目はこれで作り直しました。

3. rsync がイメージに入っていない/ボリュームが chown を拒む
コード転送の rsync が protocol error。イメージに rsync が無いのでまず apt-get install rsync。次に chown の警告が大量に出ました。ネットワークボリュームは所有者変更を許さないので、-a をやめて -rlt(所有者・グループを送らない)にすると静かに通ります。

4. ssh 越しの pkill が自分の接続を切る
古いプロセス掃除に ssh <Pod> "pkill -f 'setup.sh|uvicorn'" と打った瞬間、exit 255 で切断。ssh が実行するコマンドライン自身がパターンに一致して、pkill が自分自身を終了させてしまいます。pkill -f "setup[.]sh" のように角かっこで自己一致を外すのが定石です。

5. Ollama のインストーラが zstd を要求する
setup.sh の1段目で「This version requires zstd for extraction」。イメージに zstd が無いので入れてから再実行。

6. まっさらな Pod では、その zstd のインストールも失敗する
Pod を立て直して再現確認をしたら、今度は zstd の install 自体がこける。素の runpod/pytorch イメージはパッケージ一覧が空で、apt-get update 無しには何も入りません。結局 setup.sh の冒頭に恒久対策を1行足しました。

(command -v zstd >/dev/null && command -v rsync >/dev/null) || (apt-get update -qq && apt-get install -y -qq zstd rsync)

小ネタをもう1つ。qwen3 は「考え中の独り言」(think)を出力に混ぜてくる癖があるので、API 呼び出しで think を無効にし、万一混ざっても取り除くよう先回りしてあります。

組んだ直後の RAG は、意外と弱い

動いた直後にいろいろ質問して、すぐ気づきました。素の RAG には最初から弱点があります

弱点 実際に起きること
ソースをまたぐ集計ができない 「今まで何冊読んだ?」——数えるには全部の切れはしが要るのに、渡すのは5つだけ
言い回しが違うと拾い損ねることがある 意味検索にも限度はある
切れ目で文脈が切れる 1つの話が2つの切れはしに割れると、片方だけ渡って中途半端になる
時系列が分からない 「最近の◯◯は?」——切れはしに日付が無ければ「最近」を判断できない

原因は作りそのものです。手順4で書いたとおり、LLM に渡るのは検索で選ばれた5つの切れはしだけ。61本のソースを全部読んでいるわけではないので、全体を数える・並べる質問には構造的に答えられません。これは触ってみて初めて本当に理解できました。

本家 NotebookLM との比較も正直に載せます。

本家 今回のミニチュア版
出典つき回答 できる できる
ソースの預け先 Google のサーバ Pod 内で完結・外部へ送らない
PDF・音声・動画 できる Markdown のみ
メモ・音声概要 できる 無い
中身の理解 見えない 全部自分のコード

機能では勝負になりません。勝っているのは「ソースが外に出ない」と「中身を全部説明できる」の2点で、今回の目的にはそれで十分でした。

もう1つ、触った日に見つけて直した話を。「資料にありません。」と答えたときにも、出典欄にソース名が並んでいました。検索は「意味が近い順」に必ず何かを拾うので、答えに使っていないソース名がそのまま見えていたのです。嘘ではないけれど紛らわしい。その日のうちに「ソースに無いときは出典を出さない」に直しました。答えの正直さは、出典表示の正直さとセットです。

settings.json の4つの値で育てる

このミニチュア版は、設定値を回して育てられるように作ってあります。

設定値 回すと何が変わるか
system_prompt(指示文) 答えの姿勢。一番効く。「箇条書きで」「日付を添えて」など
chunk.size / overlap(切れはしの大きさ・重なり) 検索の当たりやすさ。外す質問が多ければ小さくして再読み込み
search.top_k(渡す件数) 材料の量。「資料にありません」が多ければ増やし、的外れが混ざれば減らす
llm.model(モデル) 日本語の自然さ・賢さ。8B→14B は品質と速度のトレード

流儀は1つだけ守っています。一度に変えるのは1つ。2つ同時に変えると、どちらが効いたのか永遠に分からなくなります。同じ質問セットを固定して、変更の前後で答えを比べる——地味ですが、これが「育てる」の正体です。

今後の拡張ロードマップ

v0.1 は出発点で、ここから使いながら育てていきます。予定も公開してしまいます。

  • v0.2:画面からのソースの追加・削除(いまはコマンド実行)/PDF 読み込み/実装中に見つけた細かい直し
  • v0.3:メモ機能(画面右に3枚目のパネル)/利用者ごとにソースを分ける——最終的には他の人にも使ってもらい、ソースは各自が自分で登録する形にするのが目標です
  • その先の候補:音声概要/LoRA でのモデル調整/推論サーバを Ollama から SGLang に差し替えて速度比較(どちらも OpenAI 互換 API なので差し替えて比べられます)

逆に言うと、現時点は一人で・使うときだけ Pod を立てる設計です。常時起動・複数人同時利用はまだ範囲外なので、社内導入の土台にする場合はそこから設計が要ります。

まとめ

  • NotebookLM のミニチュア版は、部品5つ・ファイル6本で組めた
  • コストは GPU 代約0.45ドル+保管月1.4ドル。Pod は使うときだけ立て、ソースはボリュームに残す
  • RAG の弱点(集計できない・切れ目で切れる)は、部品から手で組むと構造ごと理解できる
  • ソースは最初から最後まで、外部の AI サービスに一度も送っていない

「動かせる」と「分かる」は別物です。RAG はフレームワークを使えば半日で動きますが、あえて部品を並べて組んだことで、動かせるだけでなく人に説明できるところまで理解が進みました。次は v0.2 です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?