中級 11 分Stack

ChromaDBとRAGを組み合わせて Mistral

端的な回答

Mistralを使ったローカルRAGで最もシンプルな構成は、Ollama(生成と、bge-m3による埋め込み計算)にファイルモードのChromaDBを組み合わせるもので、サーバーもPyTorchも不要です。モデルは、8〜12 GBのグラフィックカードならministral-3:8b(6.0 GB、Apache 2.0ライセンス、公称コンテキスト長256K)を標準の選択肢にするとよく、16 GBならministral-3:14b、それより大きい容量ならmistral-small3.2:24b(15 GB)が適しています。忘れてはいけない設定はOllamaのコンテキストウィンドウです。文書の各部分がプロンプトに収まるよう、サイズを引き上げてください。

このガイドでは、2つの Python スクリプトと、ご自身のマシンで実行する Mistral モデルを使い、文書アシスタントを一通り構築します。PDF とテキストファイルを分割して ChromaDB にインデックス化し、検索で見つかった文章の該当箇所をモデルに渡すと、モデルが出典を示しながら回答します。また、グラフィックメモリに応じた Mistral モデルの選び方や、RAG が的外れな回答をする原因となる落とし穴も説明します。

著者 Mohamed Meguedmi·更新 2026-09-30·Windows・macOS・Linuxでテスト済み

#構築するもの:Mistralを使った完全ローカルのRAG

RAG(検索拡張生成)とは、自分のドキュメントから質問に関係する箇所を探し、それらをモデルのプロンプトに挿入して、その内容に基づいて回答させる手法です。ここで目指すのは、小さなコマンドラインツールです。1つのスクリプトがドキュメントのフォルダをインデックス化し、もう1つのスクリプトが質問を読み取り、ChromaDBで最も近い5つの箇所を検索して、質問とともにOllama経由でMistralモデルに渡します。そして、回答を表示し、その後に参照したファイルを表示します。何もマシンの外には出ません。Ollamaが生成モデルと埋め込みモデルを提供し、ChromaDBがベクトルをローカルフォルダに保存します。

「Mistral」には二つの意味があります。Mistral AI のオープンウェイトモデル(自分でダウンロードして実行するもの、本ガイドの対象)と、同社がホスティングする API(テキストを同社のサーバーに送信するもの)です。機密文書の場合、「100 % ローカル」の要件を満たすのは前者のみです。

#RAGにはどのMistralモデルを選ぶべきか

ローカルRAGキット

あなたのドキュメント、あなたのAI:あなたのPDF、メモ、メールを扱う信頼性の高いローカルRAG。何もクラウドに送信しません。

  • 永久に利用できるオンラインスペース
  • PDF + ファイル
  • 生涯アップデート

RAG には特有の要件があります。モデルは厳密な指示(「提示された文章だけを根拠に回答する」)に従い、複数の文章を混同せずに読み、フランス語で回答する必要があります。自由な会話の場合に比べて、モデルのサイズの重要性は低くなります。一方、コンテキストに使えるメモリの重要性は高くなります。以下のサイズは、Ollama ライブラリにデフォルトの量子化で表示されるものです。

Ollama ライブラリの Mistral モデル(2026年9月時点の調査)
モデルOllamaでのサイズ公表されているコンテキスト長対象者
ministral-3:3b3.0 GB256K専用GPUのないマシン向け。簡単な応答に適しており、複雑な指示への対応力は低い
ministral-3:8b6.0 GB256KVRAM 8~12 GB のグラフィックスカード、またはメモリ16 GB のノート PC 向けの無難な標準選択
ministral-3:14b9.1 GB256K16GBのカード、または中程度のコンテキストを想定した12GBのカード
mistral-nemo (12B)Ollamaのページを見る128Kより古く、まだ広く使われている代替手段
mistral-small3.2:24b15 GB128K24GBのGPUカードまたは32GB以上の統合メモリが必要です。フォーマットの指示に対して最も安定しています
mistral (7B, バージョン 0.3)4.4 GB32K古いモデル:リソースが非常に限られたマシンに用途を限定

Ministral 3ファミリー(3B、8B、14B)は、Mistral 3の発表に記載されているとおり、Apache 2.0ライセンスで公開されています。Ollamaのページでは、エッジ環境への導入を想定して設計され、幅広いハードウェアで動作すると説明されています。2026年に公開されたMistral Small 4は、Hugging Faceのモデルページ名によると総パラメータ数が1,190億で、サーバー向けのハードウェアを対象としています。個人用マシンで使う候補にはなりません。必要なメモリ量の目安を知るには、当サイトのVRAM計算ツールで、モデル本体のメモリ使用量とコンテキストキャッシュの合計を確認できます。

i
公称のコンテキスト長と実際に使えるコンテキスト長
128Kや256Kというコンテキスト長は、モデルの最大容量を示すもので、設定値ではありません。Ollamaがデフォルトで使用するコンテキストはそれよりはるかに短く、コンテキストを大きくすると追加のメモリを消費します。5つの文章抜粋を使うRAGなら、8,000トークンで十分です。

#技術スタック

生成
Ollamaがポート11434のローカルHTTP API経由で提供するMistralモデル。
Embeddings
bge-m3 を Ollama で提供: ライブラリのページでは、BAAI による多用途・多言語・多粒度の 567M パラメータモデルとして説明されています。PyTorch と sentence-transformers のインストールを回避できます。
ベクトルデータベース
ChromaDB のローカルモード(PersistentClient):フォルダ 1 つ、サーバーなし。Chroma は、Ollama の埋め込み API を呼び出す OllamaEmbeddingFunction というラッパーを提供しています。
ファイルの読み込み
テキストを含むPDFにはpypdfを使い、Markdownとプレーンテキストは直接読み込みます。スキャンされたPDFは画像なので、まず文字認識が必要です。

#環境の準備

  1. 01
    Ollamaをインストールし、モデルを取得する
    Ollama をインストールし、以下の2つのコマンドで生成モデルと埋め込みモデルをダウンロードしてください。
  2. 02
    Python環境を構築
    Python 3.10 以上。仮想環境はプロジェクトの依存関係を分離して管理します。
  3. 03
    ドキュメントを配置する
    PDF、Markdownファイル、テキストファイルを、スクリプトと同じ場所にあるdocs/フォルダにコピーしてください。
モデルと依存関係
ollama pull ministral-3:8b
ollama pull bge-m3

mkdir mon-rag && cd mon-rag
python3 -m venv venv
source venv/bin/activate   # .\venv\Scripts\activate sous Windows
pip install chromadb pypdf requests

#2. ChromaDBにドキュメントをインデックス化

スクリプトは各ファイルを読み込み、段落の区切りでテキストを約1,800文字の文章断片に分割して、Chromaに渡します。ChromaはOllama経由でbge-m3を呼び出し、ベクトルを計算します。重要なのは2点です。各断片には出典を引用できるようにファイル名をメタデータとして保持し、追加処理は断片を1つずつではなく、バッチ単位で行います。

index.py
from pathlib import Path
import chromadb
from chromadb.utils.embedding_functions.ollama_embedding_function import OllamaEmbeddingFunction
from pypdf import PdfReader

ef = OllamaEmbeddingFunction(url="http://localhost:11434", model_name="bge-m3")
coll = chromadb.PersistentClient(path="./chroma_db").get_or_create_collection("mes_docs", embedding_function=ef)

def lire(path: Path) -> str:
    if path.suffix.lower() == ".pdf":
        return "\n\n".join(p.extract_text() or "" for p in PdfReader(str(path)).pages)
    return path.read_text(encoding="utf-8", errors="ignore")

def decouper(texte: str, max_chars=1800):
    """Regroupe des paragraphes entiers jusqu'à max_chars ; un paragraphe trop long est coupé."""
    chunks, courant = [], ""
    for para in (p.strip() for p in texte.split("\n\n")):
        if not para:
            continue
        while len(para) > max_chars:
            if courant:
                chunks.append(courant); courant = ""
            chunks.append(para[:max_chars]); para = para[max_chars:]
        if len(courant) + len(para) + 2 > max_chars and courant:
            chunks.append(courant); courant = ""
        courant = (courant + "\n\n" + para).strip()
    if courant:
        chunks.append(courant)
    return chunks

n = 0
for path in sorted(Path("docs").rglob("*")):
    if path.suffix.lower() not in {".pdf", ".md", ".txt"}:
        continue
    chunks = decouper(lire(path))
    if not chunks:
        print(f"  ! {path.name} : aucun texte extrait (PDF scanné ?)")
        continue
    for i in range(0, len(chunks), 32):  # par lots de 32
        lot = chunks[i:i + 32]
        coll.upsert(
            ids=[f"{path.name}-{i + j}" for j in range(len(lot))],
            documents=lot,
            metadatas=[{"source": path.name}] * len(lot),
        )
    n += len(chunks)
    print(f"  + {path.name} : {len(chunks)} passages")
print(f"Terminé : {n} passages indexés")

ファイル名と文章の区切りごとの番号から作成した識別子を使ってupsertを行うことで、スクリプトを再実行できます。同じフォルダを再インデックスしても、文章の各部分は重複して追加されず、更新されます。ただし、ドキュメントが短くなると、余分になった古い部分がデータベースに残る点に注意してください。大きな変更を加えた場合は、chroma_dbフォルダを削除して再インデックスしてください。文章を区切る際の長さの選び方は、チャンキング戦略のガイドで詳しく説明しています。

#3. 質問:検索後に生成

2番目のスクリプトは質問を組み込み、最も近い5つの文章部分を取得して、プロンプトを組み立てます。プロンプト内の指示が決め手です。取得した文章部分だけに基づいて回答し、情報が不足している場合はその旨を認め、ファイルを引用するよう求めます。num_ctxパラメータはコンテキストウィンドウを拡大します。Ollamaのドキュメントによると、デフォルトのウィンドウは4,096トークンで、OLLAMA_CONTEXT_LENGTH変数またはnum_ctxパラメータで変更できます。400〜500トークンの文章部分が5つあり、さらに指示と回答も含めると、4,096トークンではぎりぎりです。コンテキストウィンドウが短すぎると、警告なしに内容が切り捨てられ、モデルは文章部分の末尾を読まないまま回答してしまいます。

ask.py
import sys, requests
import chromadb
from chromadb.utils.embedding_functions.ollama_embedding_function import OllamaEmbeddingFunction

MODELE = "ministral-3:8b"
ef = OllamaEmbeddingFunction(url="http://localhost:11434", model_name="bge-m3")
coll = chromadb.PersistentClient(path="./chroma_db").get_collection("mes_docs", embedding_function=ef)

SYSTEME = (
    "Tu réponds en français, uniquement à partir des passages fournis. "
    "Si la réponse n'y figure pas, dis-le clairement au lieu de deviner. "
    "Termine chaque affirmation par le nom du fichier source entre crochets."
)

def repondre(question: str, k: int = 5):
    res = coll.query(query_texts=[question], n_results=k)
    passages = list(zip(res["documents"][0], res["metadatas"][0]))
    contexte = "\n\n---\n\n".join(f"[{m['source']}]\n{p}" for p, m in passages)
    r = requests.post("http://localhost:11434/api/chat", json={
        "model": MODELE,
        "stream": False,
        "options": {"temperature": 0.2, "num_ctx": 8192},
        "messages": [
            {"role": "system", "content": SYSTEME},
            {"role": "user", "content": f"PASSAGES :\n{contexte}\n\nQUESTION : {question}"},
        ],
    }, timeout=300)
    r.raise_for_status()
    return r.json()["message"]["content"], sorted({m["source"] for _, m in passages})

if __name__ == "__main__":
    q = " ".join(sys.argv[1:]) or input("Question : ")
    reponse, sources = repondre(q)
    print("\n" + reponse)
    print("\nSources consultées :", ", ".join(sources))
起動する
python index.py
python ask.py "Quel est le délai de préavis prévu au contrat ?"

#モデルのせいにする前に、ChromaDBが返す内容を確認する

回答の質が悪い場合、原因は検索かモデルのどちらかにあります。検索で適切なパッセージを取得できなかったか、モデルがそのパッセージを適切に利用できなかったかです。モデルを呼び出さずに、検索で取得したパッセージとそれぞれの距離を表示すれば、両者を区別できます。適切なパッセージが上位5件に含まれていなければ、チャンクの分割方法を変えるか、キーワード検索またはリランカーを追加してください。適切なパッセージが含まれているのに回答が誤っている場合は、プロンプト、切り詰められたコンテキスト、またはモデルが原因です。結論を出す前に、一段大きなモデルを試してください。

debug.py:テキストの抜粋とその距離を表示する
import sys
import chromadb
from chromadb.utils.embedding_functions.ollama_embedding_function import OllamaEmbeddingFunction

ef = OllamaEmbeddingFunction(url="http://localhost:11434", model_name="bge-m3")
coll = chromadb.PersistentClient(path="./chroma_db").get_collection("mes_docs", embedding_function=ef)
res = coll.query(query_texts=[" ".join(sys.argv[1:])], n_results=8)
for doc, meta, dist in zip(res["documents"][0], res["metadatas"][0], res["distances"][0]):
    print(f"{dist:.3f}  {meta['source']}  {doc[:120]!r}")

#メモリ容量の配分:同時にメモリに収める必要があるもの

RAGでは、生成用モデルとベクトル計算用モデルの2つに加え、生成用モデルのコンテキストキャッシュも共存します。Ollamaは各モデルを必要に応じて読み込み、一方をアンロードしてもう一方のために空きを確保することがあります。メモリに余裕がないと、この切り替えのたびに遅延が生じます。表は3つの構成について、おおよその規模を示しています。モデルの容量はOllamaライブラリの値を使用し、それ以外は当サイトのVRAM計算機で精度を高める必要がある計算値です。

必要なメモリの目安(Ollamaのモデル重み、コンテキスト8192トークン)
構成生成モデルのサイズ追加項目対象カード
ministral-3:8b + bge-m36.0 GBコンテキストキャッシュ、埋め込みモデル(5億6,700万パラメータ、半精度では1GB強)、システムマージン8〜12GB
ministral-3:14b + bge-m39.1 GB同様です。12 GBでは、長いコンテキストが制約要因になります12から16 GB
mistral-small3.2:24b + bge-m315 GB同上。十分な余裕を見込む24 GB以上
→
メモリが不足した場合
まずnum_ctxを小さくしてください(5つの文章の抜粋に対しては、8,192でも十分に余裕があります)。その後、より小さいモデルに切り替えてください。抜粋の数を増やすことも避けてください。抜粋が多すぎると、メモリを圧迫するだけでなく、回答の焦点もぼやけます。

#的外れな回答につながる落とし穴

デフォルトのコンテキスト長が短すぎる
上記を参照してください。num_ctxの値を引き上げないと、最後の部分が切り捨てられます。典型的な症状は、ChromaDBが正しい回答を取得しているにもかかわらず、モデルがその回答を見つけられないと言うことです。
スキャンされたPDFファイル
pypdfは、すでに含まれているテキストしか読み取れません。スキャン文書では空の結果が返され、その旨をスクリプトが表示します。まず、Tesseractのガイドで説明しているOCR処理を文書に施してください。
文脈なしの記述
元の文書から切り離された一節(「期間は30日です」)だけでは、何について述べているのか分かりません。各抜粋の先頭に、文書のタイトルまたはセクション名を付けてください。
ドキュメント内に答えがない質問
「そのことをはっきり伝えてください」という指示がなければ、モデルは自分の知識で情報の空白を埋めます。ファイルに答えが含まれていない質問を必ず試してください。
識別子および正確な用語
契約番号や案件番号は、埋め込みによる検索ではうまく見つかりません。ハイブリッド検索のガイドで説明しているように、キーワード検索を追加してください。
!
信頼する前に確認する
RAGはそのソースを引用しますが、それが正しい回答を保証するわけではありません:重要な判断(契約、数字、期限など)を行うためには、引用されたファイルを開いて確認してください。

#さらに詳しく

労力と効果の比率で順位付けした改善策
改善点労力実施すべきタイミング
取得するパッセージ数(k)を5から8に増やす1行回答に必要な情報が複数の文章に分散している
段落単位ではなく見出し単位でチャンクに分割中程度構造化された文書(ドキュメント類、条文立ての契約書)
BM25とベクトル検索を組み合わせたハイブリッド検索中程度識別子、略語、固有名詞による質問
Reranker (bge-reranker-v2-m3)中程度正しい回答は取得されていますが、5番目以降に並びます
チャットインターフェース (Open WebUI、FastAPI API)状況による他の人々がこのツールを使用する必要があります
定期的なバックアップと再インデックス化少ないドキュメントフォルダは毎週更新されます

各改善方法には、それぞれ専用のガイドがあります。手法を積み重ねるよりも、実際の質問30~50問を使って、改善の前後で再現率を測定してください。コードを書かずに使える完成済みのインターフェースを希望する場合は、コードを書かないRAGのガイドでOpen WebUIとAnythingLLMを紹介しています。

#Mistralを使ったRAGに関するよくある質問

FAQ
ローカルRAGには、どのMistralモデルが適していますか?+
VRAMが8〜12 GBのグラフィックカードなら、ministral-3:8b(Ollamaでは6.0 GB、Apache 2.0ライセンス)がよい出発点です。16 GBならministral-3:14b(9.1 GB)、24 GB以上ならmistral-small3.2:24b(15 GB)が候補になります。モデルを読み込んだ後に残るメモリ容量を基準に選んでください。コンテキスト用の空き容量も必要です。
Ollamaはsentence-transformersの代わりにエンベディングを計算できますか?+
はい:Ollama は embeddings API を公開しており、567M パラメータの多言語モデル bge-m3 を提供しています。ChromaDB は、これを呼び出すための OllamaEmbeddingFunction ラッパーを提供します。利点は、PyTorch を必要とせず、インストールするエンジンを一つにできることです。欠点は、インデックス作成中に Ollama をアクティブな状態に保つ必要があることです。
文書に答えがあるのに、なぜモデルは答えが見つからないと言うのですか?+
よくある原因は二つあります。一つ目は、Ollama のコンテキストウィンドウが短すぎてパッセージが切り捨てられていることです:num_ctx を 8 192 に引き上げてください。二つ目は、検索されたパッセージに回答が含まれていないことです:生成前に ChromaDB が返す内容を確認し、チャンク分割または検索を調整してください。
MistralのAPIをOllamaの代わりに使うことは可能ですか?+
技術的には可能です。ただし、その場合、文書の抜粋が企業のサーバーに送信されるため、機密文書に求められる機密保持の要件に反します。ローカルでの運用を続けるには、Ollamaで実行するオープンウェイトモデルを引き続き使用してください。機密性のない文書ならAPIも利用できますが、その場合は提供元のデータ処理条件を確認してください。
全体を再インデックスせずに、新しいドキュメントを追加するにはどうすればよいですか?+
docs/にコピーしてindex.pyを再実行してください。upsertと固定されたIDにより、既存のチャンクは更新され、新しいチャンクは追加されます。チャンクのサイズや埋め込みモデルを変更した場合は、chroma_dbフォルダを削除し、すべてを再インデックス化してください。古いベクトルは比較できなくなるためです。
このRAGにはGPUが必要ですか?+
必ずしも必要ではありません。ministral-3:3bは最近のプロセッサと8 GBのメモリで動作しますが、応答は遅くなります。一方、インデックス作成は一度だけ実行されます。GPUの効果が主に現れるのは応答です。速度が上がり、より大きなモデルも使えるようになります。購入を決める前に、お使いのマシンで応答時間を測定してください。
このガイドは役に立ちましたか?

ご意見、誤りのご指摘、補足はありますか?ぜひお知らせください。皆さんにとってより良いガイドにするために役立ちます。