最初に結論を述べます。手元のPDF・メモ・社内資料に「質問したら答えてくれるAI」を、データを一切外部に出さずに、自分のPCの中だけで作れます。 この仕組みを RAG(検索拡張生成) と呼びます。ChatGPTに資料を貼り付けて聞く代わりに、ローカルLLMと組み合わせて完全にプライベートに実現する——それがプライベートRAGです。本記事は、RAGの仕組みを最短で理解し、Ollama だけで動く最小実装から、精度を上げるコツ、本番化への道筋までを、実際にRAGを本番運用しているエンジニアの視点で解説します。
本記事はローカルLLMの始め方ガイドの応用編です。Ollamaの基本はそちらを参照してください。
RAGとは?「カンペを渡して答えさせる」技術
LLMは賢いですが、あなたの手元の資料(社内規程・議事録・個人のメモ)は知りません。これを解決するのがRAGです。発想はシンプルで——質問に関連する文書を検索して見つけ、それを「根拠(カンペ)」としてLLMに一緒に渡し、その根拠に基づいて答えさせる。
ファインチューニング(モデルを追加学習させる)と違い、RAGは知識をその場で注入します。資料が変わってもデータを差し替えるだけでよく、学習し直す必要がありません(この使い分けはRAG vs ファインチューニングで詳説)。
RAGの処理は、3段階です。
1. 準備(一度だけ): 手元の文書を「埋め込み(ベクトル)」に変換して保存する
↓
2. 検索: 質問を同じくベクトル化し、意味が近い文書を探す
↓
3. 生成: 見つけた文書を「根拠」として、質問と一緒にローカルLLMへ渡して答えさせる
この3段すべてを自分のPC内で動かせば、データは外部に一切出ません。これがプライベートRAGの最大の価値です。
最小実装:Ollama の埋め込みAPI + コサイン類似度
「ベクトルDBが必要そう」と身構えるかもしれませんが、少数の文書なら、Ollamaの埋め込みAPIとコサイン類似度だけで作れます。追加のデータベースも不要です。
ステップ1:文書を埋め込み(ベクトル化)する
「埋め込み」とは、文章を**意味を表す数値の配列(ベクトル)**に変換することです。意味が近い文章どうしは、ベクトルも近くなります。Ollama は埋め込み用のモデルを提供しており、ローカルで変換できます。
/** Ollamaのローカル埋め込みAPIで、テキストをベクトルに変換する。通信はlocalhostのみ=データは外に出ない。 */
async function embed(
text: string,
model = "nomic-embed-text",
endpoint = "http://localhost:11434",
): Promise<readonly number[]> {
const res = await fetch(`${endpoint}/api/embeddings`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ model, prompt: text }),
signal: AbortSignal.timeout(30_000), // タイムアウトで止まらない(回復性)
});
if (!res.ok) throw new Error(`embed failed: ${res.status} ${res.statusText}`);
const data = (await res.json()) as { embedding: readonly number[] };
return data.embedding;
}
ステップ2:質問に近い文書を検索する(コサイン類似度)
ベクトルどうしの「近さ」は、コサイン類似度で測ります。これは外部ライブラリなしで書ける、副作用のない純粋関数です。
/** 2つのベクトルの近さ(-1〜1、1に近いほど類似)。純粋関数なので単体テストが容易。 */
function cosineSimilarity(a: readonly number[], b: readonly number[]): number {
if (a.length !== b.length) throw new Error("vector length mismatch");
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denom = Math.sqrt(normA) * Math.sqrt(normB);
return denom === 0 ? 0 : dot / denom; // ゼロ除算を防ぐ(堅牢性)
}
interface Chunk {
readonly text: string;
readonly embedding: readonly number[];
}
/** 質問ベクトルに近い順に上位k件の文書を返す。検索ロジックは生成と分離(SRP)。 */
function retrieveTopK(
queryEmbedding: readonly number[],
chunks: readonly Chunk[],
k: number,
): readonly Chunk[] {
return chunks
.map((c) => ({ chunk: c, score: cosineSimilarity(queryEmbedding, c.embedding) }))
.sort((a, b) => b.score - a.score)
.slice(0, k)
.map((r) => r.chunk);
}
ステップ3:根拠と一緒にローカルLLMへ渡す
検索した文書を「根拠」としてプロンプトに埋め込み、ローカルLLM(始め方ガイドのストリーミングクライアント)に渡します。
function buildPrompt(question: string, context: readonly Chunk[]): string {
const grounding = context.map((c, i) => `[${i + 1}] ${c.text}`).join("\n\n");
// 「根拠にないことは答えない」と明示し、幻覚(でっち上げ)を抑える
return [
"以下の根拠だけに基づいて、質問に日本語で答えてください。",
"根拠に答えがない場合は「資料からは分かりません」と答えてください。",
"",
`# 根拠\n${grounding}`,
"",
`# 質問\n${question}`,
].join("\n");
}
これで、手元の文書に基づいて答えるAIが、完全にローカルで完成します。文書の埋め込み・検索・生成のすべてが localhost で完結し、データは外に出ません。
精度を上げる3つのコツ
最小実装は動きますが、「思ったより的外れな答えが返る」こともあります。RAGの精度は、LLMの賢さではなく『検索の質』でほぼ決まります。すぐ効くコツを3つ。
- チャンク化(文書の分割)を整える——長い文書は、意味のまとまり(見出し・段落)で分割します。粗すぎると根拠がぼやけ、細かすぎると文脈が失われます。隣接チャンクを少し重ねる(オーバーラップ)と安定します。
- 固有名詞はキーワード検索で補う——ベクトル検索は「意味の近さ」は得意ですが、型番・固有名詞・略語の完全一致が苦手です。キーワード検索(全文検索)と併用する「ハイブリッド検索」にすると、精度が大きく上がります。
- 根拠を提示させる——回答に「どの文書を根拠にしたか」(上の例の
[1][2])を含めさせると、誤りに気づけ、信頼性が上がります。
この「検索の質」を本番品質まで突き詰めると、ハイブリッド検索・リランク・評価といった設計が効いてきます。詳しくは本番RAGの落とし穴と精度改善を参照してください。私が手がけた音声接客AIでは、まさにこの検索の質を作り込むことで「専門商材の誤答を構造的に排除」しました。
本番化への道筋:文書が増えたら、業務で使うなら
最小実装は「少数の文書を、自分が使う」段階に最適です。そこから先は、規模と用途で進化させます。
| 段階 | やること |
|---|---|
| 文書が数百〜数千を超える | メモリ上の総当たりから、ベクトルDB(pgvector等)へ。検索が高速・スケーラブルに |
| 複数人・部門で使う | アクセス制御を検索層に組み込む(他人の文書が混ざらないよう構造的に分離) |
| 業務の意思決定に使う | 精度を測る評価の仕組みを用意し、改善を回す |
特に業務利用では、アクセス制御(他人・他部門の文書が検索結果に混ざらない)が必須です。これは「あとから」では危険で、最初から検索条件に組み込む設計が要ります(本番RAGの落とし穴参照)。個人で試す最小実装から、社内全体で使う本番RAGまでは地続きですが、本番には本番の作り込みが必要です。
よくある質問(FAQ)
Q. プライベートRAGとは何ですか?
手元の文書(PDF・メモ・社内資料)に質問できるAIを、データを外部に出さずに作る仕組みです。質問に関連する文書を検索し、それを根拠としてローカルLLMに渡して答えさせるRAG(検索拡張生成)を、すべて自分のPC内で完結させます。機密情報を外部のAIに入力したくない場合に最適です。
Q. データは本当に外部に出ませんか?
文書の埋め込み・検索・生成のすべてをローカル(Ollama等)で動かせば、データは外部に送信されません。通信はlocalhostで完結します。これがプライベートRAGの最大の価値で、機密情報・個人情報・未公開資料を安心して扱えます。
Q. ベクトルデータベースは必要ですか?
少数の文書なら不要です。Ollamaの埋め込みAPIとコサイン類似度(外部ライブラリなしで書ける純粋関数)だけで作れます。文書が数百〜数千を超えて検索が遅くなってきたら、pgvectorのようなベクトルDBへ移行すると、高速でスケーラブルになります。
Q. RAGとファインチューニング、どちらを使うべきですか?
手元の資料に基づいて答えてほしいだけなら、まずRAGです。RAGは知識をその場で注入するため、資料が変わってもデータを差し替えるだけで済みます。ファインチューニング(追加学習)は文体や振る舞いを変えたいときの選択肢で、知識の更新には不向きです。詳しくは「RAG vs ファインチューニング」の記事を参照してください。
Q. 答えが的外れになります。どうすれば精度が上がりますか?
RAGの精度は「検索の質」でほぼ決まります。LLMを賢いモデルに変える前に、(1)文書のチャンク化を整える、(2)固有名詞に弱いベクトル検索をキーワード検索と併用する(ハイブリッド検索)、(3)根拠を提示させる、を試してください。それでも不十分なら、リランクや評価の仕組みといった本番RAGの設計が必要です。
まとめ:ローカルで完結する「自分専用AI」
手元の資料に答えるAIをプライベートに作るために、押さえるべきは次の通りです。
- RAGは『関連文書を検索し、根拠としてLLMに渡して答えさせる』技術——ローカルで完結すればデータは外に出ない。
- 仕組みは3段——埋め込み→検索→生成。すべて自分のPC内で動かせる。
- 最小実装はOllamaの埋め込みAPI+コサイン類似度だけ——少数の文書ならDBも不要。
- 精度の鍵は『検索の質』——チャンク化・ハイブリッド検索・根拠提示で上げる。
- 本番化の道筋がある——ベクトルDB・アクセス制御・評価へと段階的に発展できる。
「社内の資料に答えるAIを、データを外に出さずに業務へ導入したい」——個人で試す最小実装から、アクセス制御と精度評価を備えた本番の社内RAGまで、私は実務で構築しています。お気軽にご相談ください。