Qwen × RAG × PostgreSQL × pgvectorで作るローカルAIチャットボット
最近、カスタマーサポート用のAIチャットボットを、できるだけクラウドサービスに依存せず、自分のPC上で動かしてみたいと思い、実際に一から作ってみました。
今回の目標はかなりシンプルです。
- 小さなLLMをローカルで動かす
- カスタマーサポートのドキュメントをAIに参照させる
- 質問に対して、そのドキュメントを元に回答させる
- 会話履歴をPostgreSQLに保存する
- RAGに情報がない質問については、AI自身の一般的な知識も使えるようにする
最終的には、
Next.js + Qwen + llama.cpp + PostgreSQL + pgvector + Sentence Transformers + FastAPI
という構成になりました。
この記事では、実際に作りながら経験したこと、うまくいかなかったところ、そしてRAGを作る上で特に重要だと感じたポイントをまとめます。
1. まずはローカルLLMを動かす
今回使ったPCは、
- Intel Core i7-8700
- RAM 約32GB
- GPUなし
- Windows
- Python 3.14
という環境です。
GPUがないので、最初から巨大なモデルを動かすのは現実的ではありません。
そこで、比較的小さなモデルとして、
Qwen3-0.6B-Q8_0.gguf
を使うことにしました。
実行には llama.cpp を使います。
cd /e/CustomerSupport/ai/models
../llama.cpp/llama-server.exe \
-m "Qwen3-0.6B-Q8_0.gguf" \
-c 4096 \
--port 8080
これでローカルに、
http://127.0.0.1:8080/v1/chat/completions
というAPIが立ち上がります。
つまり、クラウドのAI APIを使わなくても、自分のPCからHTTP経由でLLMに質問できる状態になりました。
これは最初の大きな成功ポイントでした。
「本当に自分のPCだけでAIが動いた!」
小さなモデルではありますが、これだけでもかなり面白いです。
2. Next.jsからローカルLLMを呼び出す
次にNext.jsでチャット画面を作りました。
構成はかなりシンプルです。
ブラウザ
↓
Next.js
↓
/api/chat
↓
llama.cpp
↓
Qwen
ブラウザから直接LLMを呼ぶのではなく、Next.jsのAPIを間に入れています。
これにはいくつかメリットがあります。
- LLMのURLをブラウザに公開しなくてよい
- データベース処理をサーバー側でできる
- 後からRAGを追加しやすい
- 認証などを追加しやすい
さらに、llama.cppから返ってくるレスポンスをSSEとしてストリーミングしました。
そのため、AIが文章を生成している途中から画面に表示できます。
Q
Qw
Qwe
Qwen
Qwen is
Qwen is ...
という感じで、ChatGPTのようなストリーミング表示ができます。
3. 会話履歴をPostgreSQLに保存する
次に、単なるチャット画面ではなく、会話そのものを保存するようにしました。
データモデルは主に、
Conversation
Message
です。
例えば、
Conversation
│
├── User message
├── Assistant message
├── User message
└── Assistant message
という構造です。
最初のユーザーメッセージが来たときにConversationを作成し、その後のメッセージをそこに紐付けます。
さらに、
- 新しいチャット
- 過去のチャットを開く
- チャット名を変更
- チャットを削除
といった機能も追加しました。
ここで一つ勉強になったのが、ストリーミングとDB保存の組み合わせです。
ユーザーのメッセージは先に保存できますが、AIの回答は生成途中です。
そのため、
User message
↓
DB保存
↓
LLM開始
↓
ストリーミング
↓
全文完成
↓
Assistant messageをDB保存
という流れにしました。
途中でLLMに問題が起きる可能性もあるので、このような部分は実際のアプリケーションではかなり重要だと感じました。
4. RAGを導入する
ここからが今回の本題です。
普通のLLMだけでは、自分のカスタマーサポート情報を知りません。
例えば、
パスワードを忘れた場合はどうすればいいですか?
と聞いても、自社の実際のルールを知っているわけではありません。
そこでRAGを導入しました。
RAG、つまりRetrieval-Augmented Generationは、簡単に言えば、
「質問に関連する情報を先に検索して、その情報をLLMに渡して回答させる」
仕組みです。
今回の構成は、
ユーザーの質問
↓
Embedding
↓
ベクトル検索
↓
関連するサポート情報
↓
Qwen
↓
回答
です。
これによって、モデルそのものを再学習しなくても、自分のサポート情報をAIに使わせることができます。
5. PostgreSQL + pgvectorを選んだ
RAGでは、文章をベクトル化して保存する必要があります。
最初は専用のVector Databaseを使う方法も考えました。
ただ、すでにPostgreSQLを使っていたので、
「それならPostgreSQLに全部まとめよう」
と考えました。
そこで使ったのが pgvector です。
データ構造として、
KnowledgeDocument
│
├── KnowledgeChunk
├── KnowledgeChunk
└── KnowledgeChunk
を作りました。
KnowledgeDocument は元のドキュメントです。
例えば、
Password Reset Guide
です。
そのドキュメントを小さなチャンクに分割して、
KnowledgeChunk
として保存します。
そして各チャンクにEmbedding Vectorを保存します。
6. Windowsでpgvectorをインストールする
ここは今回かなり苦労した部分です。
最初にPostgreSQLは正常にインストールされていました。
しかし、
SELECT extname, extversion
FROM pg_extension
WHERE extname = 'vector';
を実行すると、vector がありませんでした。
そこでpgvectorをWindows上でビルドすることにしました。
Visual StudioのC++ビルドツールを用意して、
cl.exe
nmake.exe
が使える状態にしました。
PostgreSQLの場所も確認して、
PostgreSQL 18.6
であることを確認しました。
その後、pgvectorのソースを取得して、
set "PGROOT=C:\Program Files\PostgreSQL\18"
nmake /F Makefile.win
nmake /F Makefile.win install
と実行しました。
そして、
SELECT extname, extversion
FROM pg_extension
WHERE extname = 'vector';
をもう一度確認。
結果は、
vector 0.8.1
でした。
やった!ついにPostgreSQLでvectorが使えるようになりました。
Windows環境では、この部分が今回の中でも特に「環境構築」という感じの強い作業でした。
7. Embeddingモデルを選ぶ
次に必要なのがEmbeddingモデルです。
今回は、
sentence-transformers/all-MiniLM-L6-v2
を使用しました。
このモデルは文章を384次元のベクトルに変換します。
例えば、
"I forgot my password."
という文章が、
[0.0069, -0.0630, -0.0720, ...]
のような数値の配列になります。
この数値そのものに意味があるというより、意味の近い文章ほどベクトル空間上で近くなるという考え方です。
例えば、
"I forgot my password."
と、
"How can I reset my password?"
は意味が近いので、Embeddingも近くなることが期待できます。
8. ドキュメントをChunkに分割する
長いドキュメントをそのままEmbeddingするのではなく、いくつかのチャンクに分けました。
最初は、
500 words
50 words overlap
というシンプルな方法を使いました。
つまり、
Chunk 1
1 ───────────────── 500
Chunk 2
451 ──────────────── 950
Chunk 3
901 ─────────────── 1400
という感じです。
これでも動きます。
ただ、実際にRAGを作っていくと、
「単純な文字数や単語数だけで分割するより、文章の意味を考えて分割したほうがいい」
ということが分かってきます。
そこで、段落をある程度維持しながらチャンクを作る方式も試しました。
カスタマーサポートの場合、
パスワードリセットについて
1. ログインページを開く
2. Forgot passwordをクリックする
3. メールアドレスを入力する
4. メールの指示に従う
という手順が途中で分断されるより、できるだけまとまっていたほうが検索結果として使いやすいからです。
9. pgvectorで意味検索する
Embeddingができたら、PostgreSQLに保存します。
今回の検索では、
c.embedding <=> %s::vector
を使ってCosine Distanceを計算しました。
概念的には、
ユーザーの質問
↓
質問Embedding
↓
PostgreSQL
↓
保存されているEmbeddingと比較
↓
意味が近い順に並べる
↓
Top 5
という処理です。
実際に、
I forgot my password. How can I reset it?
と質問したところ、Password Reset Guideが上位に返ってきました。
そのとき、
Similarity: ...
という値も確認できました。
ここで、
「ちゃんと文章の意味を使って検索できている!」
と確認できました。
単純なキーワード検索とは違い、
forgot password
と
password reset
のように、表現が多少違っていても意味的に関連する文章を探せるのがEmbedding検索の面白いところです。
10. FastAPIでRAG APIを作る
EmbeddingモデルをNext.jsの中に直接入れるのではなく、Python側にRAG APIを作りました。
FastAPIを使って、
POST /search
というAPIを作りました。
リクエストは、
{
"query": "I forgot my password. How can I reset it?",
"limit": 5
}
のような形です。
Python側で、
query
↓
MiniLM
↓
384-dimensional embedding
↓
pgvector
↓
similarity search
↓
results
という処理をします。
これによってNext.jsとEmbedding処理を分離できました。
最終的には、
Next.js
↓
FastAPI RAG
↓
Sentence Transformers
↓
PostgreSQL + pgvector
という構造になりました。
11. Next.jsとRAGを接続する
ここでようやく、チャットとRAGがつながりました。
ユーザーが質問すると、
Browser
↓
Next.js /api/chat
↓
RAG API /search
↓
PostgreSQL
↓
関連チャンク
↓
Next.js
↓
Qwen
↓
回答
となります。
Qwenに渡すSystem Promptにも、
Support knowledge:
...
として検索結果を入れました。
つまり、Qwenは質問だけを見るのではなく、
ユーザーの質問
+
関連する社内サポート情報
を見て回答します。
12. 実際にパスワードリセットを質問してみる
実際のテストでは、
I forgot my password. How can I reset it?
と質問しました。
するとAIは、
1. ログインページを開く
2. Forgot Passwordを選択する
3. メールアドレスを入力する
4. メールで送られてきた手順に従う
パスワードリセットリンクは30分で期限切れになります。
という内容を回答しました。
これは登録していたPassword Reset Guideの内容と一致しています。
つまり、
Knowledge Base
↓
Embedding
↓
pgvector
↓
Relevant chunk
↓
Qwen
↓
Answer
という一連のRAGパイプラインが実際に動きました。
これは今回の開発の中でもかなり大きな成功でした。
13. RAGに情報がない質問を試す
次に、
How can I change my shipping address?
と聞いてみました。
この質問についてはKnowledge Baseに情報を入れていません。
最初のRAG検索では、
No relevant support knowledge was found.
という結果になりました。
しかし、今回はAIに完全な制限をかける設計にはしませんでした。
つまり、
RAGに情報がある
↓
会社固有の情報を優先して回答
RAGに情報がない
↓
AI自身の一般知識も利用可能
というHybridな設計です。
これは個人的にも重要だと思っています。
RAGはあくまでAIの知識を補完する仕組みであって、必ずしもAIを完全にKnowledge Baseだけに閉じ込める必要はありません。
ただし、
会社独自のポリシーや価格、手続きなどについては、一般知識で推測してはいけない
というルールは今後さらに強化したい部分です。
14. 「RAGのデータが増えると遅くなるのか?」
途中で気になったのが、
Knowledge Baseが大量になったら検索は遅くなるのか?
という問題でした。
もちろんデータ量が増えれば、何らかの最適化は必要になります。
例えば、
1,000 chunks
10,000 chunks
100,000 chunks
1,000,000 chunks
と増えていけば、すべてのベクトルを毎回比較する方法では効率が悪くなります。
そこでpgvectorには、例えばHNSWのようなVector Indexがあります。
将来的には、
CREATE INDEX "knowledgeChunk_embedding_hnsw_idx"
ON "knowledgeChunk"
USING hnsw ("embedding" vector_cosine_ops);
のようなIndexを導入できます。
ただし、現在のような小さなKnowledge Baseでは、最初からIndexを入れても大きな効果はありません。
この経験から、
「最初から全部を最適化するのではなく、実際のデータ量と測定結果を見て最適化する」
ことも重要だと感じました。
15. RAGの「学習」とは少し違う
今回の仕組みを作っていて、もう一つ重要だと感じたのが、
RAGはLLMそのものを再学習しているわけではない
ということです。
例えばKnowledge Baseに、
Password Reset Guide
を追加しても、Qwenの内部パラメータが変わるわけではありません。
実際には、
質問
↓
Embedding
↓
Knowledge Base検索
↓
関連情報
↓
QwenへのContext
↓
回答
という処理を毎回行っています。
そのため、新しいサポートドキュメントを追加したら、モデルを再学習しなくてもすぐに検索対象にできます。
これはカスタマーサポートシステムにとって非常に便利です。
16. 今回の構成
現時点でのシステムは、ざっくり言うとこうなっています。
┌─────────────────┐
│ Browser │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Next.js │
│ /api/chat │
└───────┬─────────┘
│
┌───────┴────────┐
│ │
▼ ▼
┌────────────┐ ┌─────────────┐
│ RAG API │ │ llama.cpp │
│ FastAPI │ │ Qwen │
└─────┬──────┘ └──────┬──────┘
│ │
▼ │
┌────────────────┐ │
│ Sentence │ │
│ Transformers │ │
└───────┬────────┘ │
│ │
▼ │
┌────────────────┐ │
│ PostgreSQL │◄────────┘
│ + pgvector │
└────────────────┘
会話履歴もPostgreSQLに保存しています。
Conversation
Message
KnowledgeDocument
KnowledgeChunk
という4つの主要なデータ構造があります。
17. 今回作ってみて特に重要だと感じたこと
今回の開発で一番感じたのは、AIアプリはLLMだけでは完成しないということです。
LLMはもちろん重要ですが、
LLM
+
Embedding
+
Vector Search
+
Database
+
Prompt
+
Backend
+
Streaming
が全部組み合わさって初めて、実用的なAIアプリになります。
特にRAGでは、
「どんなモデルを使うか」だけではなく、「どんなデータをどう分割して、どう検索するか」
が非常に重要です。
18. 今後やりたいこと
現時点でも基本的なRAGシステムとして動いていますが、まだ改善できるところはたくさんあります。
今後は、
① Retrievalの精度改善
Similarity thresholdを調整して、
本当に関連する情報
だけをQwenに渡せるようにしたいです。
② Chunkingの改善
現在は段落を意識したChunkingにしていますが、さらに、
Heading
Paragraph
Numbered procedure
Bullet list
などの構造を理解したChunkingにしていきたいです。
③ Source / Citation
AIが回答するときに、
Source: Password Reset Guide
のように、どのKnowledgeから回答したのかを表示できるようにしたいです。
これはカスタマーサポートでは特に重要だと思います。
④ RAG Evaluation
例えば、
Question
Expected source
Expected answer
Retrieved source
Similarity
Actual answer
というテストセットを作って、RAGの品質を定量的に確認したいです。
AIシステムは「なんとなく良さそう」だけではなく、実際にテストして改善することが大切だと思います。
⑤ 大きなモデルが必要か検証する
現在はQwen 0.6Bという小さなモデルを使っています。
今後データ量や要求レベルが上がったとき、
Qwen 0.6B
↓
より大きなローカルモデル
に変更する必要があるかもしれません。
ただ、モデルを大きくする前に、
RAG・Chunking・Prompt・Retrievalをきちんと改善する
ことを優先したいと思っています。
まとめ
今回、ゼロに近い状態からローカルのカスタマーサポートAIを作り始めました。
最終的には、
Next.js
+
Qwen
+
llama.cpp
+
FastAPI
+
Sentence Transformers
+
PostgreSQL
+
pgvector
=
Local RAG Customer Support AI
という構成になりました。
特に印象に残ったのは、単にLLMを起動するだけではなく、
質問
↓
Embedding
↓
Vector Search
↓
Relevant Knowledge
↓
LLM
↓
Streaming Answer
という一連の仕組みを自分でつなげていったことです。
最初は「ローカルでAIチャットを動かしてみよう」というところから始めましたが、実際にPassword Reset Guideを登録して、質問すると関連情報を検索して回答してくれた瞬間に、
「ついに、RAGが本当に動いた!」
という実感がありました。
そして、ここからが本当の意味でのAIシステム開発だと思っています。
モデルを大きくするだけではなく、検索精度、データの構造、Chunking、Prompt、評価方法、レスポンス速度などを一つずつ改善していく。
今回の経験を通して、RAGは「LLMに知識を追加するだけの技術」ではなく、データと検索とLLMを組み合わせて、AIを実際の業務に近づけていくためのシステム設計そのものなのだと感じました。
