我看過的每一個 RAG 教學都做了相同的兩個假設:你有一張 GPU,而且可以呼叫雲端 API。對我所建立的環境而言,這兩個假設都不成立。

我負責公部門的健康資訊系統。整個堆疊必須在機構內部網路中執行——資料不能離開網路——而我拿到的硬體,通常是採購週期兩年前就已定案的設備。實際上就是 Windows Server、僅有 CPU,以及在本地執行的開放權重模型。

於是我建立了一套完全在內部環境執行的 RAG 堆疊:沒有 GPU、沒有雲端、沒有 Docker。它已在 github.com/psychohub/rag-onpremise 開源:使用 ASP.NET Core 9 進行編排、Ollama 負責本地推論、Qdrant 存放向量、Python 處理 ingest pipeline、Mistral 7B 作為 LLM,以及 nomic-embed-text 負責 embeddings。

把這套系統導入正式環境所花的時間,遠比設計階段還要久,因為有五個教學完全沒警告過我的問題。這就是我的實戰報告。

環境與其重要性

在進入正題之前,先清楚說明限制條件,因為這會改變「好」的定義。

堆疊必須在 Windows Server 上執行,而非 Linux 工作站。許多目標機器都無法使用 Docker——可能是因為尚未核准、GPO 政策限制,或是運維團隊早已把所有服務都當成 Windows Service 來管理,不想再多一個容器執行環境。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

教訓一:Qdrant .NET SDK 使用 gRPC。請直接使用 REST。

我一開始嘗試使用官方的 Qdrant .NET SDK。API 乾淨、文件完整,看起來是正確的選擇。但它以一種花了我一天才診斷出問題的方式失敗了——因為錯誤訊息並不明顯:連線會建立、然後斷開,錯誤訊息指向各種原因,唯獨不是真正的問題。

原因在於:SDK 透過 gRPC 與 Qdrant 通訊,但 .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 支援就能部署的能力。在企業或公部門網路中,這是值得的權衡。

教訓二:預設的 HttpClient 逾時時間會讓你的回應失效。

.NET 中的 HttpClient 預設逾時時間是 100 秒。這對大多數 HTTP 工作來說沒問題,但當另一端是 CPU 上運行的 Mistral 7B 時就不行了。

在一台普通的伺服器上(4 vCPU、16 GB RAM),Mistral 7B 產生完整回應需要 60 到 120 秒。第一次執行端對端查詢時成功了,第二次也成功了,第三次模型剛好產生較長的答案,客戶端就在串流中斷時逾時,讓使用者盯著一個通用錯誤,而伺服器卻繼續產生沒人會看到的答案。

解決方法只要兩行程式碼:

// 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,請加入進度指示器。九十秒的靜默看起來就像系統壞掉,即使它正如設計般運作。

教訓三:Ollama 預設只監聽 localhost。

當我從筆電開發環境移到伺服器部署時發現了這個問題:另一台機器上的 .NET 應用程式無法連線到 Ollama。

Ollama 預設綁定 127.0.0.1:11434。這對本地開發來說沒問題,但對 LLM 主機與應用程式主機分開、或應用程式以服務帳戶執行而無法共用 loopback 情境的任何部署來說都沒用。

解決方法是一個環境變數:

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

Enter fullscreen mode Exit fullscreen mode

一旦你知道,這很簡單。陷阱在於 Ollama 無法連線時的錯誤訊息是通用的連線錯誤,而不是「嘿,我只監聽 loopback」。我在檢查綁定之前,花了一個下午查看防火牆規則。

如果你要把 Ollama 部署為 Windows 服務(你很可能應該這樣做),那個環境變數需要在服務層級設定,而非使用者層級。在 PowerShell 提示字元中設定不會影響服務。這是個小細節,卻會花掉真實的時間成本。

教訓四:Python MSI 安裝程式在企業 GPO 下會失敗。請使用 embeddable 套件。

ingest pipeline 是用 Python 寫的。在受 GPO 嚴格控管的 Windows Server 上,標準 Python MSI 無法安裝。它會以各種方式失敗——從完全沒有錯誤訊息的「作業完成」、到連實際管理員帳戶都無法解決的權限錯誤。

解決方法在第一次遇到時並不明顯:使用 embeddable 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 就能正常運作。

這裡沒有什麼困難的地方。只是它沒有被記錄為預設路徑,所以如果你不知道它的存在,你會花兩天時間與一個永遠不會成功的安裝程式搏鬥。

教訓五:提示詞是品質的關鍵所在。

我花了數週時間調整 chunking、embedding 參數與 retrieval top-K,每次只獲得個位數的百分比改善。然後我重寫了提示詞模板,得到了品質的重大躍升,讓之前的所有檢索調整看起來都只是四捨五入的誤差。

我一直徘徊在兩種失敗模式之間:

太嚴格。「只能從上下文中回答。如果上下文中沒有答案,就說你不知道。」模型變得對上下文過敏。它會拒絕回答部分涵蓋的問題、拒絕做出合理的推論,並對任何閱讀相同文件的人類都能回答的問題不斷回應「我不知道」。

太寬鬆。「使用上下文來幫助你回答問題。」模型開始自信地產生幻覺,用看似合理的發明填補檢索到的片段中的空白。在受管制的環境中,這不是品質問題,而是責任問題。

最後有效的版本大致如下:

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

關鍵字是「partially relevant」(允許從不完整的上下文中進行推理)與「be explicit about what the context does and does not say」(強制模型區分它讀到的內容與它推論的內容)。這兩者都不是魔法咒語。但它們一起將平衡點從「拒絕回答」與「編造內容」移到了「能回答時就回答、不能回答時就承認,並告訴你哪個是哪個」。

CPU 推論的實際樣貌

教學忽略的另一件事:數字。以上所有內容都假設你可以接受的延遲。以下是我在實際硬體上測量的結果:

硬體 模型 回應時間
4 vCPU / 16 GB RAM Mistral 7B 60–120 秒
16 vCPU / 32 GB RAM Mistral 7B 20–45 秒
4 vCPU / 8 GB RAM phi3:mini 15–30 秒
GPU 8 GB+ Mistral 7B 3–8 秒

CPU 那一列是我實際部署的伺服器上的持續測量值。GPU 那一列是借來的硬體上單次測試的結果,並非持續的正式環境測量——請將其視為參考點,而非承諾。

有兩件事值得一提。首先,在普通硬體上,phi3:mini 的延遲表現可以與在更好硬體上的 Mistral 7B 競爭。如果你能接受它的品質水準,請在升級硬體之前先降級模型。其次,從 CPU 跳到 GPU 大約有 10 倍的差異。如果你能在環境中放進一張 8 GB GPU,就去做——它會改變互動的可能性。

因為 CPU 延遲就是這樣,儲存庫在 LLM 前面加入了語意快取:傳入查詢與快取查詢之間的餘弦相似度,閾值為 0.92。當使用者詢問與先前查詢語意相近的問題時,他們會在一秒內獲得快取的答案。當他們詢問新問題時,他們會等待模型。在中等忙碌的內部系統上,快取命中率變得足夠高,讓一般使用者體驗感覺合理,即使最壞情況仍然是九十秒。

我透過破壞它學到的一個警告:當你更換 LLM 時,請清除快取。快取的答案會綁定到產生它們的模型上。當你將 Mistral 換成較新的模型時,快取會回傳來自你已不再執行的模型的答案,而使用者會在你之前注意到個性變化。

如果有人今天開始,我會告訴他們什麼

如果你要在受限硬體上建立內部環境 RAG,濃縮版如下:

  • 透過 REST 而非 gRPC 與 Qdrant 通訊。 在企業網路中會減少意外。
  • 明確設定 HTTP 逾時時間。 預設值是為網頁流量設計的,而不是 CPU 上的本地 LLM。
  • 為你的部署而非筆電設定 Ollama 的綁定。 如果你要將它作為服務執行,請在服務層級設定環境變數。
  • 在受限制的 Windows 上使用 Python 的 embeddable 套件。 MSI 在 GPO 下不是你的朋友。
  • 在調整檢索之前先調整提示詞。 Chunking 與 top-K 很重要,但提示詞才是品質水準真正所在的地方。
  • 當你的 LLM 很慢時積極快取,並記得在更換模型時使其失效。
  • 在升級硬體之前先降級模型。 phi3:mini 在 8 GB RAM 上的表現勝過在你買不起的機器上的 Mistral 7B。

這些都不是什麼奇特的事情。這是 RAG 中當教學假設有 GPU、雲端與 Linux 開發環境時被跳過的部分。當你沒有這些時,這就是你必須面對的現實。

我接下來的路線圖是針對西班牙語臨床文本進行適當的 embedding 評估——因為「它能運作」與「它在你的語言與你的語料庫上運作良好」不是同一件事,而我還沒有測量過差距。那會是下一篇文章。


本文描述了我個人開源專案 rag-onpremise 的設計與實作。測量值來自我自己的測試硬體與專案,而非任何特定的機構部署。此處表達的觀點是我個人的。

Hubert García Gordon 在哥斯大黎加公部門負責健康資訊系統,並在 UNED Costa Rica 任教。他維護 rag-onpremise 並撰寫關於受限環境中應用 AI 的文章。