AI 互動教室 ‹ 學 LLM 應用開發
下載 .py 開啟實戰 notebook ↗ 留言回報
CAPSTONE · LITELLM × FASTMCP × QDRANT

壓軸:RAG 變成 AI 的工具

前五課的零件全部到齊,組成一台會自己翻手冊的客服。跟上一課的差別只有一個但很關鍵: 上一課是我們決定每題都先檢索;這一課把檢索包成 MCP 工具,模型自己決定要不要查、查什麼、查幾段。 選一個問題,看請求怎麼走完整條管線(內容是 notebook 的實測紀錄):

LLMnemotron-3.5-lightning · 經 LiteLLM
MCP Clientagent 迴圈
FastMCP 伺服器search_handbook()
Qdrantembedding + 最近鄰
選一個問題開始。

這堂課用到第 1 課的 gateway、第 2 課的 tool calling 迴圈、第 3 課的 FastMCP、第 4 課的 Qdrant、第 5 課的 RAG—— 沒看過前面也能跑,但每一節會指回對應的課。

01 · 架構

把檢索包成工具,說明書寫給模型看

mcp = FastMCP("山茶屋知識庫", instructions="回答顧客關於山茶屋貓咪咖啡廳的任何問題之前,先用 search_handbook 查手冊。") @mcp.tool def search_handbook(query: str, top_k: int = 3) -> list[dict]: """在山茶屋店務手冊裡做語意搜尋,回傳最相關的段落(含相似度分數 0–1)。 回答任何關於營業時間、規定、貓咪、會員、停車、活動的問題前都應先呼叫。""" return [{"title": h.payload["title"], "score": round(h.score, 3), "text": h.payload["text"]} for h in retrieve(query, top_k)] @mcp.tool def list_sections() -> list[str]: """列出手冊的所有章節標題。想知道手冊涵蓋哪些主題時呼叫。""" return [c["title"] for c in chunks]

docstring 就是給模型看的使用說明——寫清楚「什麼情況該呼叫」,模型的決策品質差很多。 instructions 則是整台伺服器的說明,客戶端可以拿去當 system prompt 的一部分。 知識庫本身(手冊、切段、批次 embedding、記憶體 Qdrant)是上一課的四步濃縮成一格。

到 notebook 的 1️⃣–2️⃣ 節:知識庫、兩個工具
02 · 橋接與迴圈

三行把 MCP 說明書變成 OpenAI tools,迴圈照第 2 課

def mcp_to_openai_tools(tools): return [{"type": "function", "function": {"name": t.name, "description": t.description, "parameters": t.input_schema}} for t in tools] # agent 迴圈裡唯一的改動:工具透過 MCP Client 呼叫 res = await c.call_tool(tc.function.name, json.loads(tc.function.arguments)) msgs.append({"role": "tool", "tool_call_id": tc.id, "content": json.dumps(res.data, ensure_ascii=False)})

這座橋讓任何 MCP 伺服器的工具都能給任何 OpenAI 相容的模型用。兩個實務細節: 工具出錯要把錯誤訊息當 tool 結果餵回去(模型會自我修正),不要讓例外炸掉迴圈; 每一步記進 trace——那是你 debug agent 唯一的眼睛,notebook 把它畫成時間軸。

到 notebook 的 3️⃣–4️⃣ 節:橋接、ask_agent、第一個問題
03 · 觀察決策

它什麼時候查、查什麼、怎麼合併

合併兩個章節「週二中午想去順便停車」→ 一次查 query="週二 中午 停車",答案引「營業時間」+「交通與停車」兩個來源。
換一個工具「手冊有哪些章節?」→ 不做語意搜尋,改呼叫 list_sections
不用工具「1+1 等於多少?」→ 多半零次工具呼叫直接答 2;偶爾會字面地先查一次 1+1 再說手冊裡沒有。

這三個行為都不是我們寫的 if-else,是模型讀了說明書與 system prompt 之後的判斷。說明書是有用的: 沒加「query 請用繁體中文」那句之前,它會用英文 "parking" 去搜繁中手冊、漏掉週二公休; 加一句就改掉了。第三個問題則示範小模型對規矩更字面,LEVEL 3 讓你調它。notebook 末尾有輸入框,換你問客服。

到 notebook 的 5️⃣ 節:三個問題的時間軸、換你問
04 · 上線

同一台伺服器,交給 Claude Code 當 agent

if __name__ == "__main__": mcp.run(transport="http", host="0.0.0.0", port=8000) # 然後 claude mcp add --transport http shancha http://localhost:8000/mcp

Claude Code 會看到 search_handbooklist_sections,問它山茶屋的事它會自己查—— 不用寫任何 agent 迴圈,客戶端本身就是 agent。第 3 課的無狀態協定在這裡兌現: 每個請求自帶一切,跑 3 個副本放在負載平衡器後面,Qdrant 換成正式伺服器讓副本共用同一份索引, 程式碼其他地方一個字不用改。

到 notebook 的 6️⃣ 節:部署與 4.0 的兌現
05 · 實戰

換你動手

LEVEL 1

加第三個工具 get_section(title) 回傳整段原文,問「把會員制度完整念給我聽」——模型會改用它嗎?說明書的措辭會影響它的選擇。

LEVEL 2

search_handbook 加門檻:最高分低於 0.4 回傳「手冊裡沒有相關內容」而不是硬湊三段。問「有賣牛排嗎」驗證。

LEVEL 3

刪掉 system prompt 裡「先用工具查手冊」那句重跑——模型還會主動查嗎?比較 mcp.instructions(伺服器方)與 system prompt(客戶端方)誰該負責提醒,想想在 Claude Code 那條路線上你能控制哪一個。

卡住了?每一題在 notebook 末節都有折疊解答——先自己做,再打開對照。

六堂課到此完結:一個網址一把 key(LiteLLM)、讓模型做事(tool calling)、把函式變工具(FastMCP)、 最像什麼(Qdrant)、先翻書再回答(RAG)——然後把它們接成一台會自己查資料的客服。

06 · 驗收

情境測驗

離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。

Q1 情境題

你照 LEVEL 1 加了第三個工具 get_section(title),可是問「把會員制度完整念給我聽」,模型還是呼叫 search_handbook、只拿到節錄。最佳做法是?

docstring 就是給模型看的使用說明——LEVEL 1 解答實測:說明書裡「完整唸出/全文/整段」那句就是模型選 get_section 的依據,把那句拿掉它多半退回 search_handbook。A 是玉石俱焚——其他所有問題都失去語意搜尋;B 能動,但把決策搬回自己寫死的規則,而且 Claude Code 那條路線根本沒有你的迴圈可加 if-else;D 說明書寫不清楚,多大的模型都只能用猜的。

Q2 錯誤診斷

你的 search_handbook docstring 沒寫 query 該用什麼語言。顧客問「我週二中午想去,順便停車,要注意什麼?」,trace 長這樣,答案漏掉了週二公休。最可能的原因是?

step 1 · tool → search_handbook {'query': 'parking'} step 2 · answer 建議停在對面的饒河停車場…(隻字未提週二公休)

這是本課實測真的踩過的坑:沒加「query 請用繁體中文」之前,模型會用英文 parking 去搜繁中手冊、漏掉週二公休;docstring 補上那一句就改掉了——說明書的一句話能直接改變模型行為。A 手動跑一次 retrieve("停車") 就能排除,索引沒壞;C 治標不治本,問題出在 query 的語言不是段數;D 截斷會看到 finish_reason='length' 的半句話(第 1 課),不是完整卻漏來源的回答。

Q3 情境題

你自己寫 agent 迴圈接 MCP 工具。某次模型呼叫 get_section 帶了不存在的章節標題,工具拋出例外。迴圈應該怎麼處理?

這正是 notebook 的 ask_agent 裡那行 except 的用意:錯誤訊息餵回去,模型看到「沒有這個章節,可用 list_sections 查看標題」就會自己換工具重試——所以 LEVEL 1 的 ToolError 訊息刻意寫成給模型的指引。A 把一個可自癒的小錯變成整段對話陣亡;B 又把決策搬回自己手上,模型明明會修;C 最危險——每個 tool_call 都必須有對應的 tool 訊息,跳過會讓對話缺一塊,模型根本不知道工具失敗了。

Q4 錯誤診斷

顧客問客服「1+1 等於多少?」,偶爾會看到這樣的 trace——明明是數學題,它卻先去查了手冊。最可能的原因是?

step 1 · tool → search_handbook {'query': '1+1'} step 2 · answer 1+1 等於 2。(來源:手冊中未找到相關章節)

實測 nemotron-3.5-lightning 多數時候直接答 2,但偶爾會字面地遵守「先查手冊」去搜一次 1+1 再說手冊裡沒有——該修的是提示詞:加一句「跟店務無關的問題直接回答,不用查手冊」(LEVEL 3 的驗證方法之一)。B 是誤解,給它自由它多數時候直接答 2;C 症狀相似但原因不同——本課迴圈沒有強制工具呼叫,強制的話會是每次、不會是偶爾;D 只是把「偶爾」固定成某一種行為,規矩本身沒改,不算修好。

Q5 情境題

客服上線後流量變大,你想跑 3 個伺服器副本放在負載平衡器後面。目前程式用的是 QdrantClient(":memory:")。應該怎麼做?

第 3 課的無狀態協定在這裡兌現:每個請求自帶一切,伺服器天生就能水平擴展——唯一的共享狀態是向量索引,換成正式 Qdrant(如 QdrantClient("http://qdrant:6333"))讓副本共用,程式碼其他地方一個字不用改。A 在解決一個不存在的問題,無狀態就是不需要 session;B 跑得起來但有副作用——每個副本開機都要重算一次 embedding,手冊一更新三份索引就開始各自漂移;D 搞混了角色——迴圈是客戶端的事,伺服器副本上沒有迴圈,Claude Code 那條路線更是連迴圈都不用寫。

HANDS-ON · MOLAB

實作在 molab 跑(免費)

molab 的登入狀態進不了內嵌框架(瀏覽器的跨站 cookie 保護), 所以 notebook 要在新分頁執行——把它跟本頁並排開,左邊教學照樣對照。

  1. 登入 molab(GitHub / Google)
  2. 開啟課程 notebook,Fork 成自己的副本即可編輯
  3. 從第一格往下全部執行(首次安裝套件約 1 分鐘)——免費 CPU 環境即可,不需要 GPU

不想用 molab?下載 rag-mcp-agent_ext.py 後在自己電腦 uvx marimo edit --sandbox rag-mcp-agent_ext.py,依賴會自動安裝。