iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0

昨天的意圖路由把小林那句話拆出了兩個旗標:it=truegeo=true。我們也說過,這兩個旗標不是判完就丟,而是會一路帶著走、變成下游「該去哪裡找料」的開關。今天就順著這對旗標走進答案的下半場:問題既然同時碰了 IT 與地點兩個領域,那兩路資料怎麼查、查得多快、又怎麼確保「不相干的那一路根本不發」。這一站要回答的,是檢索的延遲與品質怎麼一起顧

本篇結構:

  • 兩路查詢,循序做就太慢了
  • zip 把兩路並行,延遲壓到較慢那一路
  • 條件式 RAG:不該查的那一路,根本不發
  • 順帶補充:知識庫怎麼「用意思找」
  • 取捨:PoC 的記憶體向量庫,與品質先於省錢

兩路查詢,循序做就太慢了

先把問題擺清楚。小林那句:

我的員編是 123456,AD 帳號被鎖了,順便想查內湖分行的營業時間。

要答好得湊齊兩種料:

  1. IT 知識庫裡的「AD 帳號解鎖步驟」。
  2. 地圖服務查到的「內湖分行營業時間」。

這是兩個獨立的外部查詢——一個打知識庫、一個打地圖,彼此沒有先後依賴。

最直覺的寫法是循序做:先查知識庫、等它回來,再查地圖、再等它回來,兩段等待加總。

做法 延遲算式 結果
循序(一路等完再發下一路) 180ms + 120ms 300ms
並行 zip(兩路同時發) max(180ms, 120ms) 180ms

假設知識庫檢索花 180ms、地圖查詢花 120ms,循序就是 300ms 的等待;要是哪天又冒出第三路、第四路意圖,延遲還會線性疊上去。對一個「按下送出、不到一秒要有答案」的助理來說,這種把等待一段段串起來的做法是撐不住的。

而且別忘了 Day 5、Day 6 立下的那條鐵律:Portalreactive 的,事件迴圈上絕不空等。既然兩路查詢本來就互不相干,讓它們一前一後排隊等,不只慢,也浪費了非阻塞模型最擅長的事——同時掛著好幾件「在等外部回應」的工作。

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.providermock 還是 google、檢索器是真向量庫還是離線假資料版,流程一行不動。

條件式 RAG:不該查的那一路,根本不發

並行解決的是「該查的多路怎麼快」,但還有個更前面的問題:哪幾路「該」查?這就是**條件式 RAG(檢索增強生成)**真正的重點。

回到那對旗標。小林這題 it=truegeo=true,所以兩路都發。但如果他只問「內湖分行幾點關門」,路由給出的會是 it=falsegeo=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 的記憶體向量庫,與品質先於省錢

這一站有兩處取捨值得誠實講。

第一處是向量庫的選型。 目前 PoC(概念驗證)用的是記憶體(in-memory)向量庫,啟動時把種子文件算成向量存進記憶體。

面向 記憶體向量庫(目前 PoC)
好處 零額外基礎設施、PoC 階段最省事
代價 重啟後向量得重算、不跨實例共享、容量受限於記憶體

要正式上線、知識量一大,這裡會換成持久化的向量庫——在 GCP 上就是帶 pgvectorCloud 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 這套「用資料流思考」的架構,天生就是為串流而生的。


上一篇
Day 21|問什麼?去哪裡?
系列文
轉生到全端工程師沒多久就要負責公司的大平台??22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言