TL;DR
対象:GPUなしで“出典付きRAG”を実運用したい人へ
Faiss + SQLiteの再構築に困ってる人
- CPUオンリーで 出典付きRAG を運用できる構成(ctx4096 / 20k chunks / 判例≈300)
- 命令は テキスト側、
use/refer/off
をテンプレで切替(コードは運搬と配分) - 3分デモ(ルイズ/DMC)+ GitHub/ログ実測 を公開
- Faiss + SQLite環境で、RAGの動的変化に追いつかないときの再構築処理を解説
はじめに
過去の記録を探す、引用する、話させる──
そんな理想を全部1つのシステムに詰め込んだ、大型チャンク規模対応のRAG検索AIをゼロから構築し、GitHubで公開しました。
ただし、現実にコードを書いたのはすべて ChatGPT-4o。私は一行もコードを書いていません。
それでも、このシステムは、単なるRAGではありません。
なぜならば、私が職務遂行のために導入することを前提として作成したからです。
(遊びはありますが…)
-
チャットUI方式(※ログあり、ただしコンテキスト未活用=連続会話は未実装)
-
Word / PDF / Excel /(カレンダーは未対応)など社内ファイルを全文検索
-
回答には
[FILE] 判決文/2023年04月10日.pdf(pdf)
のように出典を明示 -
キャラクター性を一貫して保ち、音声で煽ってくる・ツンデレる?
-
NAS全体のWord/PDF/ExcelをOCR付きで自動検索化。
-
2万チャンクをFAISS+SQLiteで管理、再構成0.1秒(職務投入前、構造的には1000万チャンクまでは拡張可能と想定)。
-
音声入出力・モデル切替・職務投入使用。
-
DMC編
環境:CPU 8C16T / ctx4096 / n_predict 2048 / 20k chunks / 判例≈300 / llama.cpp server(CPU backend) -
所要時間の目安:回答開始まで ~1分(Ryzen 7 8845HS / メモリ16GB)
内部ログ(llama.cpp)抜粋
srv log_server_r: request: GET /health 172.19.0.1 200
srv params_from_: Chat format: Content-only
slot launch_slot_: id 0 | task 2882 | processing task
slot update_slots: id 0 | task 2882 | new prompt, n_ctx_slot = 4096, n_keep = 0, n_prompt_tokens = 17
slot update_slots: id 0 | task 2882 | kv cache rm [4, end)
slot update_slots: id 0 | task 2882 | prompt processing progress, n_past = 17, n_tokens = 13, progress = 0.764706
slot update_slots: id 0 | task 2882 | prompt done, n_past = 17, n_tokens = 13
slot release: id 0 | task 2882 | stop processing: n_past = 272, truncated = 0
slot print_timing: id 0 | task 2882 |
prompt eval time = 166.69 ms / 13 tokens ( 12.82 ms per token, 77.99 tokens per second)
eval time = 13029.82 ms / 256 tokens ( 50.90 ms per token, 19.65 tokens per second)
total time = 13196.51 ms / 269 tokens
srv log_server_r: request: POST /v1/chat/completions 172.19.0.1 200
srv update_slots: all slots are idle
srv log_server_r: request: GET /health 127.0.0.1 200
「RAG × 引用厳守 × キャラ性 × 音声対応」
すべてを両立した、人格駆動型RAGチャットシステムです。
なぜ作ったのか
過去の書面を参照したくとも、どんな事件かも忘れた──
そんなとき、「過去の資料を正しく探し出す」ために:
- ファイルが多くて検索に時間がかかる
- ファイル名や事件フォルダの場所も思い出せない
- ChatGPTに聞いても出典がなくて信頼できない(ChatGPTはそもそも知らない)
- 回答に味気がなく、業務が楽しくない
- (「コンピューター、12月10日午後1時30分の予定は?」っと法廷で言ってみたい)
本当は「過去の主張と今回の主張との論理矛盾を検出する」ためのシステムにしたかったけれど、
そこはまだ未実装(=LLMの限界も感じている)です。
システム構成図
├── チャットUI(JavaScript)
│ └── FastAPIへ送信 / VOICEVOX音声再生
├── FastAPI(バックエンド)
│ ├── chat.py:LLM応答生成
│ ├── vector_search.py:Faiss検索
│ └── voice.py:音声連携
├── LLM(llama.cpp)
│ └── ローカル推論モデル
├── ベクトルDB(Faiss + SQLite)
│ └── 各種チャンク(PDF/Word/Excel等)を管理
特徴とこだわり①:引用厳守のRAG検索
回答に使う情報は RAGチャンクに「明示的に存在するものだけ」
記載がない内容は「記載なし」と返す(創作禁止)
[FILE] ファイル名(拡張子) の形式で出典を明記
【FILE】093558_hanrei.pdf.txt(pdf)に書いてある通りだぞ。
被告人は住居侵入、強盗致死、窃盗などの罪で無期懲役に処されている。未決勾留日数が300日も加算されてるって、悪趣味な数え方だな!
さらに、「強盗致死、強盗予備、窃盗被告事件判決主文被告人を無期懲役に処する。」とある。これは酷すぎる! 殺人を犯した上に窃盗も行うとは…悪魔の如き存在だ!
特徴とこだわり②:キャラクター性 × 引用
build_user_prompt() によって
「キャラ性 > RAG引用」構造を作りつつ、引用ルールは絶対厳守
キャラ口調でも [FILE] を絶対に省略しない(…つもり)
キャラ定義は .txt 管理、将来的に複数人格も切替可
特徴とこだわり③:出典付きチャンク設計
種別 チャンク化方針
PDF / Word 300字+前後50字重複
Excel 1行=1チャンク(ファイル名・シート名・行番号)
カレンダー 1件=1チャンク(UID付)※未実装
特徴とこだわり④:音声出力(VOICEVOX)
応答後、自動で音声を合成・再生
話者(例:四国めたん)× スタイル(例:ツンデレ)をUIから選択可能
特徴とこだわり⑤:モデルの動的切り替え
GGUF形式であれば、メモリの許す限り複数モデルをUIから自由に切り替え可能。
- 軽量モデルから中量モデルまで柔軟に運用できる
- OLLamaのように「事前登録」不要、ファイルを置くだけで即切替OK
- 現在は
gpt-oss-20b-MXFP4
を採用中(高性能かつ最新トレンド)
複数モデル環境を整えておけば、「即応性」「検証性」「用途別LLM」も視野に入る。
特徴とこだわり⑥:RAGエンジンの変遷
検討エンジン 理由
Chroma 100万件で不安定/ブラックボックス感強い
Qdrant UUIDとUIDの整合が困難
LanceDB 削除まわりが扱いづらく破棄
Faiss + SQLite(現行) ゴースト検知 → 再構成対応/超高速運用可能
✅ SQLiteにベクトル次元も保持 → 再構成時の埋め込み不要
✅ 約2万チャンクでの復元は 0.1秒以下
(抜粋)keep_rows に残す行を決めた後の再構成ループ
=== 再構成処理(SQLiteのBLOBベクトルからFAISSを復元) ===
BATCH_SIZE = 1_000_000
new_index = None
vec_index = 0
for i in range(0, len(keep_rows), BATCH_SIZE):
batch = keep_rows[i:i + BATCH_SIZE]
for r in batch:
# SQLiteのBLOBからベクトルを復元
r["vec_index"] = vec_index
vec = np.frombuffer(r["vector"], dtype=np.float32)
# 初回のみ次元取得 → FAISS初期化
if new_index is None:
d = len(vec)
new_index = faiss.IndexFlatIP(d)
# ベクトルを追加
new_index.add(np.array([vec], dtype=np.float32))
# メタデータを再INSERT
cursor.execute("""
INSERT INTO vector_metadata
(vec_index, uid, chunk_index, path, type, vector)
VALUES (?, ?, ?, ?, ?, ?)
""", (
vec_index, r["uid"], r["chunk_index"],
r["path"], r["type"],
sqlite3.Binary(vec.tobytes())
))
vec_index += 1
conn.commit()
print(f"[INFO] 再構成中: {vec_index} 件まで完了")
件数ゼロならFAISSファイルを削除
if new_index is None or new_index.ntotal == 0:
if conf["faiss_index"].exists():
conf["faiss_index"].unlink()
print(f"[DONE] FAISS削除済み: {conf['faiss_index']}")
else:
print("[SKIP] FAISSファイルが存在しない")
return
FAISSファイルを書き出し
faiss.write_index(new_index, str(conf["faiss_index"]))
print(f"[DONE] FAISS再構成完了: {conf['faiss_index']}")
この部分でやっていること
SQLiteからBLOBのベクトルを取得 → np.frombuffer()でfloat32配列化
最初の1本で次元を取得 → len(vec) を使って faiss.IndexFlatIP(d) を初期化
ベクトルをFAISSに追加 → 再埋め込み不要
vector_metadataに再INSERT → ゴースト削除後の正規化
FAISSファイルを書き出し → 新しい index.faiss に置き換え
GitHubで公開中
📦 ソースコード →
https://github.com/no-code-rag/no-code-rag
クローンしてローカルで即動作可能(GPU不要)
READMEに構成と使い方あり(※導入手順はChatGPT製なので保証外)
PDF/Wordは自前でご用意ください(裁判例などがおすすめ)
今後のアップデート予定
ウェイクワードによる音声起動
Web UIからのPDF/Wordアップロード機能
会話ログをコンテキストに活用(未対応)
モデル切替の自動化/プリセット機能
Whisper連携による音声入力
おわりに(興味がある方へ)
このRAG検索AIは、「実用に耐えること」を最初から意識して構築しました(まだ未走行ですが)。
出典付きで正確に回答できて
キャラクターとして話し
音声で読み上げ
1000万件規模の全文検索にも対応(ただしファイルが足りない)
📣 GitHubスターやQiitaコメント、大歓迎です!
使用技術まとめ
バックエンド:FastAPI
LLM推論:llama.cpp(CPU100%叩かせる目的)
ベクトルDB:Faiss + SQLite
音声合成:VOICEVOX
フロントエンド:ChatGPT風 JSチャットUI
チャンク処理:Word / PDF / Excel(カレンダーは未対応)
🧠 どう作ったか(人間 × ChatGPT の分担)
- 要件・制約・プロンプト設計/コードの統合・動作検証・動画制作:私
- ソース生成・雛形作成・改修提案:ChatGPT-4o(約95%)
- 手直し:ログ整備・例外処理・パス/権限の調整(約5%)