昨天做到 Model Routing 之後,我開始比較認真看每一次 AI Request 到底花了多少時間和 Token,結果很快就發現一件以前做 Demo 時不太會注意的事,很多使用者問的問題其實非常接近,但系統每收到一次 Request,還是會重新跑 Retrieval、組 Prompt、呼叫模型,再重新產生一次答案。
如果只有自己測試可能沒什麼感覺,一天幾十次 Request 多花一點 Token 也看不出差多少,可是當系統開始有更多人使用之後,同一類問題可能一天出現幾百次,這時候每一次都完整跑一遍 LLM,就會同時增加 Latency 和 API Cost。
所以 Day 14 我想處理的東西很直接:如果某個問題以前已經算過,而且現在又出現一個非常接近的問題,系統能不能先看看之前有沒有可以重用的結果,再決定需不需要真的呼叫模型。
一開始最直覺的方法就是直接拿使用者輸入的文字當 Key,例如有人問「什麼是 RAG」,系統第一次正常呼叫模型並把答案存起來,下一次如果又有人輸入完全相同的「什麼是 RAG」,就直接把之前的結果拿出來。
這種做法很好理解,而且真的能省 Request,但實際使用時命中率可能沒有想像中高,因為使用者只要改幾個字,系統就會把它當成全新的問題,例如「RAG 是什麼」、「可以解釋 RAG 嗎」和「什麼是 Retrieval-Augmented Generation」,對人來說幾乎都在問同一件事情,傳統字串 Cache 卻會產生三個不同的 Key。
做到這裡就會發現,AI 系統裡的 Cache 跟一般 API Cache 有一點不一樣,我們不只要判斷兩段文字是不是完全一樣,有時候還要判斷它們的意思到底夠不夠接近。
這也是我這次開始測 Semantic Cache 的原因,流程其實跟前面做 RAG 時有一點像,新的問題進來之後先轉成 Embedding,再跟 Cache 裡已經存在的問題做 similarity search,如果相似度高到某個程度,就可以考慮直接回傳之前的結果。
例如 Cache 裡已經有「RAG 是什麼」,現在收到「可以簡單介紹 Retrieval-Augmented Generation 嗎」,兩個句子的文字差很多,但 Embedding 可能會判斷它們語意非常接近,這時候系統就不一定需要重新呼叫一次模型。
但問題也跟著來了,這個 similarity threshold 到底要設多少,如果設太高,很多其實可以共用答案的問題都不會命中;設太低又更危險,兩個看起來很像、細節卻不同的問題可能被當成同一題,最後直接回傳錯誤的舊答案。
所以 Semantic Cache 不能只追求 Hit Rate,我還是得回到前幾天做 Eval 的想法,看看命中之後的答案到底還適不適合現在這個問題。
另一個我很快遇到的問題是資料時效,假設使用者問的是「Python 最新版本是多少」或「今天系統有哪些異常」,這種答案就算兩個問題完全一樣,也不能永遠把昨天的 Cache 拿來用。
因此 Cache 還需要 TTL,也就是每一筆結果可以存活多久,像一些基礎知識可能可以放很久,但即時資料、價格、系統狀態或會變動的文件,就可能只能放幾分鐘甚至完全不適合 Cache。
做到這裡之後我開始覺得,AI Engineering 很多地方都會遇到同樣的情況,一個功能看起來只是「加個快取」,真的進到 Production 之後卻會一路牽到資料時效、正確率、成本和使用情境,最後很難只用一個開關決定全部 Request 要不要 Cache。
把昨天的 Model Routing 跟今天的 Cache 接起來之後,Request 的處理方式就開始有點不一樣了,新問題進來時可以先檢查有沒有適合的 Cache,如果沒有,再判斷這個任務要送到哪個模型,小問題用便宜快速的模型,複雜任務再交給比較強的模型。
這樣做之後,有些 Request 甚至根本不需要進模型,整體 Latency 會更低,Token 使用量也會下降,而真正需要模型推理的 Request,才會繼續走後面的 Model Routing。
以前做 AI Demo 時,我通常只會注意模型回答得好不好,做到第十四天之後,我開始越來越常看模型「有沒有必要被呼叫」,因為當每一次呼叫都會產生延遲和成本時,少做一次其實也是系統設計的一部分。
接下來我想繼續往這個方向看,因為 Cache 雖然可以省掉很多重複 Request,但只要資料更新,舊答案就可能失效,下一個要處理的問題也會變成:系統到底要怎麼知道某一筆 Cache 已經不能用了。