naive 與 agentic RAG 的唯一架構差異,是「檢索被一條寫死的直線呼叫,還是被一顆會迭代的控制器呼叫」。chunk / embed / 向量庫 / API 兩邊一模一樣。MCP 是插座,不是大腦。
重點摘要(TL;DR)
- 唯一架構差異:naive 是一條直線(查一次→生成);agentic 多一顆會反覆決策的控制器,把檢索當工具反覆呼叫。其他元件兩邊相同。
- MCP 是插座不是大腦:包成 MCP 只是讓 agent「叫得到」;要不要 agentic,看呼叫它的腦有沒有迭代。沒有 MCP 也能 agentic。
- 那顆腦放哪是設計選擇:thin-tool(消費端自帶生成)vs fat-service(平台內部自帶生成+決策 loop)。
- 斷網逼向 fat-service:進場斷網沒有外部強 agent 當腦,只能把本地 LLM(Ollama)焊進服務內部當控制器 ——「外面訓、裡面跑」。
- 只准 443:Ollama 就是普通 HTTP 服務,用既有 Nginx 反向代理從 443 轉到內部 11434,沒特殊做法。唯一必修是把
proxy_read_timeout拉大,否則慢模型的長答案會被 60 秒 timeout 砍成 504。 - 別過度:約 80% 單跳查手冊 naive 就夠;只有約 20% 多跳排障才值得開 agentic loop。
把一份地端 RAG(向量檢索 + 生成)升級成 agentic RAG,到底要不要打掉重練?答案:是加法,不是重寫。本文以某萬人跨國製造集團的 air-gapped 廠區知識庫為情境,拆清楚 naive 與 agentic 唯一的架構差異、MCP 為何是「插座」不是「大腦」、那顆生成加決策的 LLM 該放哪,以及在「公司只准走 443、進場斷網」下怎麼把地端 Ollama 接起來。
一、naive RAG vs agentic RAG:唯一的架構差異
先給結論:兩者差別只有一處 —— 誰在驅動檢索。naive 是程式寫死的直線;agentic 是一顆 LLM 控制器在決策。
| 元件 | naive | agentic | 差異 |
|---|---|---|---|
| chunk + embed | ✅ | ✅ | 完全一樣 |
| 向量庫 | ✅ | ✅(可能多庫/多源) | 幾乎一樣 |
| 檢索(top-k) | 單發 | 可被呼叫多次 | 同一個函式 |
| 生成 LLM(G) | 一次 | 一次(在 loop 尾) | 同一顆模型 |
| 控制器 / loop | ❌ 沒有(直線) | ✅ 有(決定→迭代) | ⭐ 唯一的架構差異 |
| API / MCP | 可有可無 | 可有可無 | 正交,與 naive/agentic 無關 |
- naive:問題 → embed → 向量庫 top-k → 塞 prompt → LLM 生成(帶出處)→ 答。走一次,不回頭。
- agentic:上面那套檢索當成一個工具,上面多一顆 LLM 控制器反覆決定「要不要再查、查什麼、夠了沒」,最後才生成。
- 升級成本是加法:naive → agentic 只是外面包一個決策函式 + 迴圈,embed / retrieve 一行不動。不是重寫。
判準:單跳問題(「A 規格 vs B 規格怎麼分」)一次檢索就答完 → naive。多跳問題(「這個異常 → 可能原因 → 對應處置」要翻兩三本手冊)一次檢索答不全 → 才需要 agent。
二、MCP 是「插座」,不是「大腦」
普通 REST API,LLM agent 不會自己知道怎麼呼叫。MCP 把你的檢索「自我描述」給 LLM(有個工具、參數是什麼、做什麼),agent 才叫得動 —— 這是插座。
但插座不強制 agentic:同一個 MCP tool,agent 可以只按一次(等於 naive),也可以反覆按(agentic)。是按的那顆腦有沒有跑迭代決定 agentic,不是 MCP。反例:你可以有完全不用 MCP 的 agentic RAG(loop 就是一段程式);也可以有 MCP 包好但只被叫一次的 naive 用法。
三、那顆腦放哪:thin-tool vs fat-service
| thin-tool | fat-service | |
|---|---|---|
| 平台提供 | R(檢索)+ 可選 A(格式化 context) | R + A + G + 決策 loop 全包 |
| 生成 / 控制器在哪 | 消費端自帶(外部 agent) | 平台內部(本地 Ollama) |
| 呼叫端要多強 | 要是個強 agent | 可以是笨前端 / 一行 curl |
| 適合 | 有網、消費端本來就強 | 斷網、消費端很笨 |
| 代價 | 平台輕快;智力外包 | 平台變重、每次呼叫可能多輪;智力負載壓平台 |
fat-service 的殺手級性質:決策腦焊死在平台內部,不管誰呼叫、怎麼呼叫,答案水準一致 → 品質可保證、斷網自足、呼叫端極簡。
四、斷網 → 只能 fat-service
- thin-tool 的大腦是外部 agent(在外網)→ 進場斷網就沒腦,agentic 直接死。
- fat-service 的大腦(本地 LLM)包在服務內部 → 整包搬進場,斷網照樣自走 loop。這就是「外面訓、裡面跑」的裡面那顆自走的腦。
五、算力分層 + air-gap 拓撲約束
Ollama 本來就是 HTTP 服務(11434 埠),放哪台跟服務層無關,客戶端把 OLLAMA_HOST 指過去即可,零改碼:
[服務層:輕、CPU] [推論層:重、GPU]
· agent loop(決策) ──內網 HTTP──► Ollama(本地大模型 + embedding 模型)
· 向量庫 + 檢索
· MCP 端點
- 一台 GPU 可餵多個瘦服務節點 → 對「集中一台 GPU、各處放瘦服務」友善。
- ⚠️ air-gap 拓撲:服務機 + GPU 機都必須在同一個隔離網段內;GPU 機不可放外面或雲(那就破 air-gap)。各廠若各自隔離 → 每個隔離域一台 GPU,不可跨域共用。
- ⚠️ 分層不增加算力:拆機只讓服務不跟推論搶 CPU;GPU 的吞吐天花板不變,多節點打同一台仍在 GPU 排隊。
六、只准 443:Nginx 反向代理(唯一必修 = timeout)
公司只准走 443、既有服務都用 Nginx 從 443 轉下來 —— Ollama 同一套 pattern,沒有特殊轉拋。對外 443 → 內部 127.0.0.1:11434:
location /ollama/ {
proxy_pass http://127.0.0.1:11434/; # 尾斜線:剝掉 /ollama/ 前綴再轉
proxy_http_version 1.1;
proxy_set_header Host $host;
# ⚠️ 必修:大模型慢 + agentic 多輪,長答案會超過預設 60s → 504。拉大。
proxy_read_timeout 600s;
proxy_send_timeout 600s;
# 之後若改 streaming(stream:true)才需要:
proxy_buffering off;
# Ollama 預設零認證 —— 最低限度鎖一個 token
if ($http_x_ollama_token != "<SECRET>") { return 403; }
}
三個坑:
| 坑 | 後果 | 解 |
|---|---|---|
proxy_read_timeout 預設 60s |
大模型長答案跑超過 60s → Nginx 切斷回 504 | 拉到 600s(必修) |
proxy_buffering 開著 |
改 streaming 後 token 被憋住不即時吐 | 串流時 off |
| Ollama 零認證 | 掛 443 後任何連得到的人能白嫖 / 打爆 GPU | Nginx 層加認證 |
七、agentic loop 骨架(約 30 行,套現有檢索,核心不動)
def decide(q, scratchpad):
got = "\n".join(f"- {c['title']}" for c in scratchpad) or "(還沒查任何東西)"
prompt = (f"問題:{q}\n目前已查到:\n{got}\n\n"
"決定下一步。資訊不夠→ 回 search + query;已足夠→ 回 answer。只回 JSON。")
d = _post("/api/chat", {"model": GEN_MODEL, "stream": False, "format": "json",
"messages": [{"role": "user", "content": prompt}]})
try:
return json.loads(d["message"]["content"])
except Exception:
return {"action": "answer"} # 護欄:解析壞 → 退化成 naive 單發
def agent_answer(q, max_steps=4): # 護欄:硬上限,防無限迴圈
chunks = load_index()
scratchpad, seen = [], set()
for _ in range(max_steps):
step = decide(q, scratchpad)
if step.get("action") != "search":
break
for c in retrieve(step["query"], chunks, k=3): # 原本的檢索,沒改
key = (c["source"], c["title"])
if key not in seen:
seen.add(key); scratchpad.append(c)
return generate_final(q, scratchpad) # 幾乎就是原本的生成
三護欄:max_steps 硬上限、decide 的 JSON try/except、壞掉退化成 naive。別為此拉重框架(LangGraph / LlamaIndex):斷網場手刻這 30 行剛好,可控、可離線、每行能解釋。
八、結語:先做簡單版,別在承重牆上炫技
所以先做 naive 完全正確,別為了潮上 agentic。等真出現「一次檢索答不出來」的多跳問題,再把那顆會迭代的腦加上去 —— 加法不是重寫。
而斷網場的關鍵,是那顆腦不能借外面的,得焊進自己服務裡:外面訓、裡面跑。只准 443 也不特別 —— Ollama 就是個 HTTP 服務,Nginx 照舊轉,只記得把 timeout 拉大。
延伸閱讀
- 別動不動就上 120B:用甜蜜點階梯找剛好夠用的最小模型 —— 這篇的本地模型選型,直接決定 fat-service 那顆腦要多大
- 從一顆腦到公司 Agent:LLM 全景地圖 —— RAG 在整個 Agent 體系裡的位置
- 一張圖看懂 AI Agent 系統:Loop、Harness、MCP、A2A 差在哪 —— MCP 與 loop 的角色分工
- 本地換腦實戰:CC 撞牆、aider 跑通、選對 agent 更重要 —— 本地模型當主力的實測
發佈留言