昨天的意圖路由把小林那句話拆出了兩個旗標:it=true、geo=true。我們也說過,這兩個旗標不是判完就丟,而是會一路帶著走、變成下游「該去哪裡找料」的開關。今天就順著這對旗標走進答案的下半場:問題既然同時碰了 IT 與地點兩個領域,那兩路資料怎麼查、查得多快、又怎麼確保「不相干的那一路根本不發」。這一站要回答的,是檢索的延遲與品質怎麼一起顧。
本篇結構:
zip 把兩路並行,延遲壓到較慢那一路先把問題擺清楚。小林那句:
我的員編是 123456,AD 帳號被鎖了,順便想查內湖分行的營業時間。
要答好得湊齊兩種料:
這是兩個獨立的外部查詢——一個打知識庫、一個打地圖,彼此沒有先後依賴。
最直覺的寫法是循序做:先查知識庫、等它回來,再查地圖、再等它回來,兩段等待加總。
| 做法 | 延遲算式 | 結果 |
|---|---|---|
| 循序(一路等完再發下一路) | 180ms + 120ms | 300ms |
並行 zip(兩路同時發) |
max(180ms, 120ms) |
180ms |
假設知識庫檢索花 180ms、地圖查詢花 120ms,循序就是 300ms 的等待;要是哪天又冒出第三路、第四路意圖,延遲還會線性疊上去。對一個「按下送出、不到一秒要有答案」的助理來說,這種把等待一段段串起來的做法是撐不住的。
而且別忘了 Day 5、Day 6 立下的那條鐵律:Portal 是 reactive 的,事件迴圈上絕不空等。既然兩路查詢本來就互不相干,讓它們一前一後排隊等,不只慢,也浪費了非阻塞模型最擅長的事——同時掛著好幾件「在等外部回應」的工作。
zip 把兩路並行,延遲壓到較慢那一路機制其實很簡單:不要一路等完再發下一路,而是兩路「同時發出」,各自非同步進行,再用一個 zip 操作把它們匯合——等到兩路都回來,才把結果合在一起、往下走。
意圖偵測結果(correlation id = a1b2c3d4):it=true, geo=true
┌─ 檢索 IT 知識庫(AD 解鎖步驟) ~180ms ─┐
並行發出 zip( │ │ )
└─ 查地圖(內湖分行營業時間) ~120ms ─┘
│
兩路都回來才續行(等較慢的 180ms)
▼
組裝模型上下文(context injection)
▼
呼叫 LLM 生成回答
關鍵在最後那行:循序要 180+120=300ms,並行只要 max(180, 120)=180ms。zip 的語意是**「等齊」**,所以總延遲不再是兩路相加,而是壓到「較慢那一路」。兩路如此,三路、四路大致也如此——並行讓總延遲主要由最慢那一路主導,而不是把每一路的等待直接相加。這裡得補一句精確的:這條「跟路數關係不大」成立的前提,是下游容量、逾時與並行上限都設好了;路數一多,連線池競爭、下游限流、部分失敗這些成本會浮現,不是無腦加路就一定更快。它也順帶帶出一個得先想好的設計題:萬一某一路掛了,是整題失敗、降級成部分答案,還是改用快取?zip 只講「等齊」,答不了這題,得由產品行為自己定。這對應的還是 reactive 的本質:它本來就是用「資料流怎麼組合」在思考的,把兩條非同步的流 zip 在一起、等齊了再續行,正是它的原生寫法。不需要自己去開執行緒、管同步。
對應到日誌,大概會看到這樣的軌跡:
2026-06-22 09:15:03.420 INFO [req=a1b2c3d4] retrieval : fan-out it-kb + geo (parallel)
2026-06-22 09:15:03.540 INFO [req=a1b2c3d4] retrieval : geo done in 120ms
2026-06-22 09:15:03.600 INFO [req=a1b2c3d4] retrieval : it-kb done in 180ms
2026-06-22 09:15:03.601 INFO [req=a1b2c3d4] retrieval : zip joined → assemble context
注意 geo 先回(120ms),但 zip 不會急著往下走,它會等到較慢的 it-kb 也回來(180ms)才匯合。兩路產出最後一起組裝進模型的上下文,模型才開始生成。Day 12 到 Day 15 那套依賴反轉在這裡又兌現了一次:助理的這套並行流程,完全不在乎底層接的是哪家地圖、哪家 LLM——portal.maps.provider 是 mock 還是 google、檢索器是真向量庫還是離線假資料版,流程一行不動。
並行解決的是「該查的多路怎麼快」,但還有個更前面的問題:哪幾路「該」查?這就是**條件式 RAG(檢索增強生成)**真正的重點。
回到那對旗標。小林這題 it=true、geo=true,所以兩路都發。但如果他只問「內湖分行幾點關門」,路由給出的會是 it=false、geo=true——這時候,知識庫那一路根本不該發。條件式檢索的意思就是:只在偵測到對應意圖時,才發出對應的那一路查詢。
it |
geo |
檢索行為 |
|---|---|---|
true |
true |
fan-out(知識庫 + 地圖)兩路並行 |
false |
true |
只發地圖,知識庫整路跳過 |
true |
false |
只發知識庫,地圖整路跳過 |
false |
false |
兩路都不發,直接讓模型回答 |
這件事很容易被當成「省 token、省一次外部呼叫」的效能優化,但它其實是品質設計。RAG 的一個實務陷阱是:盲目把檢索結果塞進 prompt,無關的內容會稀釋上下文(context)。如果小林只問分行時間,你卻硬塞一段「AD 帳號解鎖步驟」進去,模型反而可能被這段無關的料帶偏、東拉西扯,答案品質不升反降。所以「不該查就不查」省下的不只是成本,更是讓模型專注在真正相關的料上——這是答得準的前提,不是附帶的好處。
這也正好把 Day 21 留下的伏筆收了回來:意圖路由打的旗標準不準,直接決定了這幾路檢索值不值得發。旗標打得太寬,多查一路無關資料、稀釋上下文;打得太窄,漏查該查的料、答不全。意圖判斷與條件式檢索是一組連動的設計——上游判得對,下游才查得準。
這整套並行加條件式的做法,其實就是影分身之術:鳴人要同時辦好幾件事,就放出影分身分頭去做、全部忙完再會合。並行檢索也是——一個分身去查 IT 知識庫、一個去查地圖,同時進行,最慢那個回來才會合(這就是 zip),派幾個分身、時間主要卡在最慢的那一個(前提是後端擋得住這麼多路一起來);而條件式 RAG 是「不相干的那一路,分身根本不派」,既省查克拉,也免得無關情報回來擾亂判斷。
上面一直說「檢索知識庫」,這裡值得補一下 IT 知識庫那一路內部到底怎麼找,因為它跟一般的關鍵字搜尋不一樣。
難題在於使用者的問法千變萬化:「AD 帳號被鎖」和「網域帳戶登不進去」其實是同一件事,但純關鍵字比對會漏掉——字面不一樣。這也正是 Day 21 結尾提到的、樸素語意比對應付不了的那種口語變體,要靠檢索端補上。
做法是 embedding(向量嵌入):用一個 embedding 模型把每段文字轉成一串數字(向量),這串數字代表它的「語意座標」,意思相近的文字,向量距離也相近。於是「找相關文件」就變成「在向量空間裡找最近的鄰居」——用意思找,而不是用字找。
啟動時:種子文件(IT FAQ)── embedding 模型 ──▶ 向量 ──▶ 存入向量庫
查詢時:使用者問題 ── embedding ──▶ 向量 ──▶ similaritySearch 找最相近的 top-K 段落
similaritySearch 回來的那批 top-K 段落,就是要組裝進模型上下文的知識來源。所以「AD 帳號被鎖」即使知識庫裡寫的是「網域帳戶解鎖」,只要兩者語意夠近、向量距離夠近,也找得到。
這一站有兩處取捨值得誠實講。
第一處是向量庫的選型。 目前 PoC(概念驗證)用的是記憶體(in-memory)向量庫,啟動時把種子文件算成向量存進記憶體。
| 面向 | 記憶體向量庫(目前 PoC) |
|---|---|
| 好處 | 零額外基礎設施、PoC 階段最省事 |
| 代價 | 重啟後向量得重算、不跨實例共享、容量受限於記憶體 |
要正式上線、知識量一大,這裡會換成持久化的向量庫——在 GCP 上就是帶 pgvector 的 Cloud SQL for PostgreSQL,或專做向量檢索的 Vertex AI Vector Search。另外,算 embedding 本身要呼叫模型、要 API key,所以離線開發情境用的是另一個假資料版的檢索器——這又是 Day 12 那套「可抽換」的一次體現:把檢索器當成一個可替換的零件,PoC 用記憶體版、離線用假資料版、上線換持久化版,上層流程通通不動。
第二處,回到「不該查就不查」這句話的份量。 把條件式 RAG 當成省錢手段,會低估它。它真正在做的是上下文的品質把關:模型的回答品質,很大程度取決於你餵給它的料乾不乾淨。多餵一段無關的料,省下的那點 token 不重要,被稀釋掉的答案品質才是真代價。所以這道「只在該查時才查」的閘,骨子裡跟 Day 16 到 Day 20 那一整章守門是同一種思路——對往下傳的東西負責,只是守門守的是安全與合規,這裡守的是上下文的相關性。
到這裡,小林那兩路料都齊了:IT 解鎖步驟(附知識庫來源)、內湖分行營業時間,並行查回、組裝進上下文、交給模型生成。但模型不是一口氣把整段答案吐完的——它是一個字一個字慢慢生成的,要是等整段生成完才一次顯示給小林,他得盯著空白螢幕乾等好幾秒。
明天 Day 23,我們來看 Portal 怎麼用 SSE(Server-Sent Events)把答案像打字機那樣邊生成邊吐出來,以及為什麼 reactive 這套「用資料流思考」的架構,天生就是為串流而生的。