壓軸:RAG 變成 AI 的工具
前五課的零件全部到齊,組成一台會自己翻手冊的客服。跟上一課的差別只有一個但很關鍵: 上一課是我們決定每題都先檢索;這一課把檢索包成 MCP 工具,模型自己決定要不要查、查什麼、查幾段。 選一個問題,看請求怎麼走完整條管線(內容是 notebook 的實測紀錄):
這堂課用到第 1 課的 gateway、第 2 課的 tool calling 迴圈、第 3 課的 FastMCP、第 4 課的 Qdrant、第 5 課的 RAG—— 沒看過前面也能跑,但每一節會指回對應的課。
把檢索包成工具,說明書寫給模型看
docstring 就是給模型看的使用說明——寫清楚「什麼情況該呼叫」,模型的決策品質差很多。 instructions 則是整台伺服器的說明,客戶端可以拿去當 system prompt 的一部分。 知識庫本身(手冊、切段、批次 embedding、記憶體 Qdrant)是上一課的四步濃縮成一格。
到 notebook 的 1️⃣–2️⃣ 節:知識庫、兩個工具三行把 MCP 說明書變成 OpenAI tools,迴圈照第 2 課
這座橋讓任何 MCP 伺服器的工具都能給任何 OpenAI 相容的模型用。兩個實務細節: 工具出錯要把錯誤訊息當 tool 結果餵回去(模型會自我修正),不要讓例外炸掉迴圈; 每一步記進 trace——那是你 debug agent 唯一的眼睛,notebook 把它畫成時間軸。
到 notebook 的 3️⃣–4️⃣ 節:橋接、ask_agent、第一個問題它什麼時候查、查什麼、怎麼合併
這三個行為都不是我們寫的 if-else,是模型讀了說明書與 system prompt 之後的判斷。說明書是有用的: 沒加「query 請用繁體中文」那句之前,它會用英文 "parking" 去搜繁中手冊、漏掉週二公休; 加一句就改掉了。第三個問題則示範小模型對規矩更字面,LEVEL 3 讓你調它。notebook 末尾有輸入框,換你問客服。
到 notebook 的 5️⃣ 節:三個問題的時間軸、換你問同一台伺服器,交給 Claude Code 當 agent
Claude Code 會看到 search_handbook 與 list_sections,問它山茶屋的事它會自己查—— 不用寫任何 agent 迴圈,客戶端本身就是 agent。第 3 課的無狀態協定在這裡兌現: 每個請求自帶一切,跑 3 個副本放在負載平衡器後面,Qdrant 換成正式伺服器讓副本共用同一份索引, 程式碼其他地方一個字不用改。
到 notebook 的 6️⃣ 節:部署與 4.0 的兌現換你動手
加第三個工具 get_section(title) 回傳整段原文,問「把會員制度完整念給我聽」——模型會改用它嗎?說明書的措辭會影響它的選擇。
給 search_handbook 加門檻:最高分低於 0.4 回傳「手冊裡沒有相關內容」而不是硬湊三段。問「有賣牛排嗎」驗證。
刪掉 system prompt 裡「先用工具查手冊」那句重跑——模型還會主動查嗎?比較 mcp.instructions(伺服器方)與 system prompt(客戶端方)誰該負責提醒,想想在 Claude Code 那條路線上你能控制哪一個。
卡住了?每一題在 notebook 末節都有折疊解答——先自己做,再打開對照。
六堂課到此完結:一個網址一把 key(LiteLLM)、讓模型做事(tool calling)、把函式變工具(FastMCP)、 最像什麼(Qdrant)、先翻書再回答(RAG)——然後把它們接成一台會自己查資料的客服。
情境測驗
離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。
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 長這樣,答案漏掉了週二公休。最可能的原因是?
這是本課實測真的踩過的坑:沒加「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——明明是數學題,它卻先去查了手冊。最可能的原因是?
實測 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 那條路線更是連迴圈都不用寫。
實作在 molab 跑(免費)
molab 的登入狀態進不了內嵌框架(瀏覽器的跨站 cookie 保護), 所以 notebook 要在新分頁執行——把它跟本頁並排開,左邊教學照樣對照。
- 登入 molab(GitHub / Google)
- 開啟課程 notebook,Fork 成自己的副本即可編輯
- 從第一格往下全部執行(首次安裝套件約 1 分鐘)——免費 CPU 環境即可,不需要 GPU
不想用 molab?下載 rag-mcp-agent_ext.py 後在自己電腦
uvx marimo edit --sandbox rag-mcp-agent_ext.py,依賴會自動安裝。