これまで読んだRAGチュートリアルは、すべて同じ2つの前提を置いています。GPUがあり、クラウドAPIを呼び出せることです。私が構築する環境では、どちらの前提も誤りです。

私は公共部門の医療情報システムを担当しています。スタックは機関内のインフラで実行する必要があり、データはネットワーク外に出せません。また、入手できるハードウェアは調達サイクルで2年前に調達されたものです。実際にはWindows Server、CPUのみ、そしてオープンウェイトモデルをローカルで実行することになります。

そこで、私は完全にオンプレミスで動作するRAGスタックを構築しました。GPU、クラウド、Dockerは不要です。GitHubのgithub.com/psychohub/rag-onpremiseでオープンソースとして公開しています:オーケストレーションにASP.NET Core 9、ローカル推論にOllama、ベクトルにQdrant、インジェストパイプラインにPython、LLMにMistral 7B、埋め込みにnomic-embed-textを使っています。

本番環境への導入は、設計よりも時間がかかりました。どのチュートリアルも警告していなかった5つの問題が発生したからです。これが現場レポートです。

環境、そしてなぜそれが重要なのか

教訓に入る前に、制約について正確に理解しておく価値があります。なぜなら、それが「良い」の定義を変えるからです。

スタックはLinuxワークステーションではなくWindows Serverで動作する必要があります。対象マシンの多くではDockerは利用できません。承認されていない、GPOポリシーで制限されている、あるいは運用チームがすでにすべてをWindowsサービスとして実行しており、コンテナランタイムを追加することは誰も所有したがらない新しい運用領域になるためです。GPUは理想です。その間はCPU推論で動作させる必要があります。

これは珍しいことではありません。公共部門、医療、従来型エンタープライズ環境では、これが標準的な現実です。また、インターネット上のほとんどのRAGコンテンツが静かに前提としている現実でもあります。

システムの全体像:

Documents (PDF / Word / Excel)
    │
    ▼
[ Python ingest ]
    ├─ Text extraction  (pdfplumber, python-docx, openpyxl)
    ├─ Chunking         (500 tokens, 50 overlap)
    ├─ Embeddings       (nomic-embed-text via Ollama)
    └─ Store            (Qdrant, cosine similarity)
                                                      │
User query                                            │
    │                                                 │
    ▼                                                 │
[ ASP.NET Core 9 API ]  ────────────────────────────  ┘
    ├─ 1. Embed the question
    ├─ 2. Retrieve top-K chunks from Qdrant
    ├─ 3. Assemble prompt with context
    ├─ 4. Call Mistral 7B via Ollama
    └─ 5. Return answer + cited sources

Enter fullscreen mode Exit fullscreen mode

教訓1: Qdrant .NET SDKはgRPCを使用する。RESTを直接使用する。

最初に試したのは、公式のQdrant .NET SDKでした。クリーンなAPIで、よくドキュメント化されており、正しい選択のように感じました。しかし、診断に1日かかる形で失敗しました。失敗は明らかではありませんでした。接続は確立された後、切断され、エラーメッセージは実際の原因以外を指していました。

原因:SDKはQdrantとgRPCで通信し、.NETアプリケーションとQdrantインスタンス間のネットワークパスはHTTP/1.1のみでした。gRPCにはHTTP/2が必要です。中間プロキシまたはロードバランサーが接続をダウングレードし、SDKは正常に劣化せず、ただ失敗しました。

修正はSDKを完全にスキップし、HttpClientでQdrantのREST APIに直接通信することでした:

// Not this — the SDK uses gRPC under the hood
// var client = new QdrantClient(new Uri(url));

// This — plain REST works everywhere
var response = await _httpClient.PostAsync(
    $"{qdrantUrl}/collections/{collection}/points/search",
    content);

Enter fullscreen mode Exit fullscreen mode

QdrantのREST APIはRAGワークロードに十分な機能があります。型安全性や一部の使いやすさを失いますが、すべてのネットワークホップでHTTP/2サポートについて議論することなくデプロイできる能力を得られます。企業や公共部門のネットワークでは、これは良いトレードオフです。

教訓2: デフォルトのHttpClientタイムアウトはレスポンスを殺す。

.NETのHttpClientはデフォルトで100秒のタイムアウトです。これはほとんどのHTTP作業には問題ありません。しかし、相手側がCPUで動作するMistral 7Bの場合には問題になります。

控えめなサーバー(4 vCPU、16 GB RAM)では、Mistral 7Bが完全なレスポンスを生成するのに60〜120秒かかります。エンドツーエンドのクエリを初めて実行したときは動作しました。2回目も動作しました。3回目にモデルが長い回答を生成したところでクライアントがタイムアウトし、ストリームの途中で切断され、サーバーが誰も見ることのない回答を生成し続けている間に、ユーザーは一般的なエラーを眺めることになりました。

修正は2行です:

// Not this — 100s default, will cut you off
var client = new HttpClient();

// This — set the ceiling explicitly, above your worst-case
var client = new HttpClient { Timeout = TimeSpan.FromSeconds(300) };

Enter fullscreen mode Exit fullscreen mode

数値自体は最悪の場合よりも重要ではありません。CPUでローカルLLMを呼び出す場合は、実際のハードウェアで最悪の場合を測定し、それより余裕を持ってタイムアウトを設定してください。また、その上にUIを構築する場合は、プログレスインジケーターを配置してください。90秒の沈黙は、システムが正しく設計された通りに動作している場合でも、壊れたシステムのように見えます。

教訓3: Ollamaはデフォルトでlocalhostのみをリッスンする。

これは、ラップトップでの開発からサーバーへのデプロイに移行し、別のマシン上の.NETアプリケーションがOllamaに到達できないことがわかったときに発見しました。

Ollamaは、初期状態で127.0.0.1:11434にバインドします。ローカル開発には問題ありません。LLMホストがアプリケーションのホストと別の場合、あるいはアプリケーションが対話型ユーザーとループバックコンテキストを共有しないサービスアカウントで実行される場合のデプロイには役に立ちません。

修正は環境変数です:

$env:OLLAMA_HOST = "0.0.0.0:11434"
ollama serve

Enter fullscreen mode Exit fullscreen mode

一度知ってしまえば簡単です。罠は、Ollamaが到達不能な場合のエラーメッセージが、一般的な接続エラーであり、「私はループバックのみをリッスンしています」というものではないことです。バインドを確認する前に、ファイアウォールルールを読み込んで午後を費やしました。

OllamaをWindowsサービスとしてデプロイする場合(おそらくそうすべきです)、その環境変数はユーザーではなくサービスレベルで設定する必要があります。PowerShellプロンプトで設定しても、サービスには影響しません。小さな詳細ですが、実際の時間的コストです。

教訓4: Python MSIインストーラーは企業GPO下で失敗する。埋め込み可能パッケージを使用する。

インジェストパイプラインはPythonです。企業グループポリシーオブジェクトがインストーラーの実行を制御するロックダウンされたWindows Serverでは、標準のPython MSIはインストールできませんでした。サイレントな「操作完了」でディスク上に何もない状態から、実際の管理者アカウントでも解決できない昇格に関する大声のエラーまで、さまざまな形で失敗しました。

最初は明らかではない修正: 埋め込み可能Pythonパッケージを使用します。これはZIPファイルであり、インストーラーではないため、GPOサーフェスのほとんどを回避できます。

セットアップはインストーラーよりも少し手動です:

  1. python.orgからpython-3.x.x-amd64-embed.zipをダウンロードします。
  2. フォルダーに展開します — C:\Python311\、どこでも。
  3. そのフォルダーでpython3xx._pthを開き、import site行のコメントを解除します。これがないと、pipは動作しません。
  4. get-pip.pyをダウンロードし、そのフォルダーからpython get-pip.pyを実行します。
  5. そこから、pip install -r requirements.txtは正常に動作します。

ここで難しいことはありません。ただ、デフォルトのパスとして文書化されていないため、存在を知らなければ、決して成功しないインストーラーと2日間戦うことになります。

教訓5: プロンプトは品質の大部分が存在する場所である。

私はチャンク化、埋め込みパラメーター、検索top-Kの調整に数週間を費やし、毎回1桁台のパーセントの改善を得ました。そして、プロンプトテンプレートを書き直したところ、検索調整が丸め誤差のように見えるほどのレスポンス品質のステップチェンジを得ました。

私が振動し続けていた2つの失敗モード:

制限が厳しすぎる。 「コンテキストからのみ回答せよ。コンテキストに回答が含まれていない場合は、知らないと言う。」モデルはコンテキストにアレルギーになりました。部分的にカバーされている質問に答えることを拒否し、合理的な推論を行うことを拒否し、同じドキュメントを読んでいる人間なら答えられる質問に対して「知りません」を連発しました。

寛容すぎる。 「コンテキストを使用して質問に答えるのを助けよ。」モデルは自信を持って幻覚を起こし始め、検索されたチャンクのギャップを、もっともらしく聞こえる創作で埋め始めました。規制された環境では、それは品質の問題ではありません。責任問題です。

最終的に機能したものは、大まかに:

Answer BASED on the provided context.
If the information is partially relevant, use it and be explicit
about what the context does and does not say.
Only if there is absolutely nothing related to the question,
say so clearly.
Do NOT invent data that is not in the context.

Enter fullscreen mode Exit fullscreen mode

重要だったキーワードは「部分的に関連する」(不完全なコンテキストからの推論を許可する)と「コンテキストが何を言っていて、何を言っていないかを明示する」(モデルに、読んだものと推論したものを区別させる)でした。どちらも魔法の呪文ではありません。しかし、組み合わせることで、「回答を拒否する」と「でっち上げる」のバランスを、「答えられる場合は答え、答えられない場合は先送りし、どちらかを伝える」に移しました。

CPU推論の実際の様子

チュートリアルがスキップするもう一つのこと:数値です。上記はすべて、許容できるレイテンシを前提としています。実際に私が持っていたハードウェアで測定した結果は以下の通りです:

Hardware Model Response time
4 vCPU / 16 GB RAM Mistral 7B 60–120 seconds
16 vCPU / 32 GB RAM Mistral 7B 20–45 seconds
4 vCPU / 8 GB RAM phi3:mini 15–30 seconds
GPU 8 GB+ Mistral 7B 3–8 seconds

CPUの行は、私が実際にデプロイしているサーバーでの継続的な測定値です。GPUの行は、借りたハードウェアでの単一のテストであり、継続的な本番測定値ではありません。その行は参考点として捉え、約束として捉えないでください。

2つ指摘すべき点があります。まず、控えめなハードウェアでのphi3:miniは、はるかに優れたハードウェアでのMistral 7Bとレイテンシで競合します。品質基準が許すなら、ハードウェアをアップグレードする前にモデルをダウングレードしてください。次に、CPUからGPUへのジャンプは約10倍です。環境に8 GB GPUを1つ導入できるなら、そうしてください。可能なインタラクションが変わります。

CPUレイテンシがそうであるため、リポジトリにはLLMの前にセマンティックキャッシュが含まれています。受信クエリとキャッシュされたクエリの間のコサイン類似度で、しきい値は0.92です。ユーザーが以前のクエリと意味的に近いものを尋ねた場合、キャッシュされた回答を1秒未満で取得します。新しいものを尋ねた場合、モデルを待ちます。適度に忙しい内部システムでは、キャッシュヒット率が十分に高くなり、最悪の場合でも90秒かかったとしても、平均的なユーザーエクスペリエンスは合理的だと感じられました。

壊して学んだ警告が1つあります:LLMを変更したらキャッシュをクリアする。キャッシュされた回答は、それを生成したものに固定されています。Mistralを新しいモデルに切り替えると、キャッシュは現在実行していないモデルからの回答を返し、ユーザーはあなたが気づく前に性格の変化に気づくでしょう。

今日から始める人に伝えたいこと

制約のあるハードウェアでオンプレミスRAGを構築する場合、圧縮版は以下の通りです:

  • QdrantとはgRPCではなくRESTで通信する。 企業ネットワークでの驚きを減らします。
  • HTTPタイムアウトを明示的に設定する。 デフォルトはWebトラフィック用に設計されており、CPU上のローカルLLM用ではありません。
  • Ollamaのバインドをラップトップではなくデプロイメント用に設定する。 サービスとして実行する場合は、サービスレベルで環境変数を設定します。
  • ロックダウンされたWindowsではPythonの埋め込み可能パッケージを使用する。 MSIはGPOの下では味方ではありません。
  • 検索の前にプロンプトを調整する。 チャンク化とtop-Kは重要ですが、プロンプトは品質基準が実際に存在する場所です。
  • LLMが遅い場合は積極的にキャッシュし、モデルを変更する場合は無効化することを忘れない。
  • ハードウェアをアップグレードする前にモデルをダウングレードする。 8 GB RAMでのphi3:miniは、手が届かないマシンでのMistral 7Bを上回ります。

これらはどれも珍しいものではありません。チュートリアルがGPU、クラウド、Linux開発ボックスを前提とする場合にスキップされるRAGの一部です。それらがない場合、これがあなたが対峙する現実です。

私の次のロードマップは、スペイン語の臨床テキストでの適切な埋め込み評価です。「動作する」と「あなたの言語で、あなたのコーパスでうまく動作する」は同じではなく、私はまだギャップを測定していないからです。それが次の記事です。


この記事は、私の個人的なオープンソースプロジェクトrag-onpremiseの設計と実装について説明しています。測定値は、私自身のテストハードウェアと私自身のプロジェクトからのものであり、特定の機関のデプロイメントからのものではありません。ここに記載されている見解は私自身のものです。

Hubert García Gordonはコスタリカ公共部門の医療情報システムに携わり、UNED Costa Ricaで教鞭を執っています。彼はrag-onpremiseを維持し、制約のある環境での応用AIについて執筆しています。