| 常聽到的理由 | numpy 做得到嗎 | 對 agent 來說 | 站得住嗎 |
|---|---|---|---|
| 查得快 | 做得到,59 條反而更快 | agent 會查好幾次,但每次都遠比一次模型呼叫便宜 | 不成立 |
| 可以用 metadata 篩選 | 做得到,10 行左右 | 篩選條件不該讓 agent 自己決定 | 不成立 |
| 可以新增、刪除文件 | 做得到,要自己管 id | agent 只帶得回條號,兩份文件都有第 3 條就亂了 | 勉強 |
| 服務重啟之後東西還在 | 做不太到 | agent 查不到的文件,它會以為規章裡沒有 | 成立 |
我的結論是:索引在服務活著的時候不會變,就不需要向量資料庫;會變、而且改完要活過重啟,才需要。 條文數量、agent 會查幾次,反而都是最後才要看的。
第四個理由我原本以為最不重要,結果它是唯一站得住的。而且寫到那一格我才發現,這支服務用了向量資料庫五天,從來沒有用到它這個能力。後面照表的順序講。
向量檢索做的事情是:查詢變成一串 1536 個數字,每一條條文也是,找出方向最接近的幾條。向量正規化過的話,這就是一個矩陣乘一個向量:
scores = matrix @ query_vector # (59, 1536) @ (1536,) → 59 個分數
top = np.argsort(-scores)[:3]
agent 讓這件事看起來更重要,因為它一題會查好幾次。大部分問題查一次就交卷,但規章裡沒有答案的問題,它不會自己放棄,會換個說法一直查。Day 21 量過,28 題裡有 3 題一路查到 6 步上限。
就算 6 次,numpy 是 6 × 0.20 毫秒,Chroma 是 6 × 7.65 毫秒,都不到 50 毫秒。Day 26 量過一次模型回答要 1,423 毫秒,agent 每一步都要問一次模型。檢索怎麼換,都動不到這條路上的大頭。
會輸是遲早的,暴力解的成本跟條文數成正比。不過先撞牆的通常是記憶體:一條 1536 維的 float32 向量佔 6 KB,59 條是 354 KB,10 萬條是 614 MB,50 萬條是 3 GB,服務開幾個 worker 就是幾份。這些數字是算出來的,不是量的,今天我沒有去找交叉點。
「只查第八章」「只查這份文件」。Chroma 的查詢收一個 where,numpy 版十行左右。有一個順序問題要注意:先取最像的三條再丟掉不是第八章的,很可能丟完一條都不剩,所以要先篩再取:
scores = matrix @ query_vector
keep = np.array([m["chapter_no"] == 8 for m in metas])
scores = np.where(keep, scores, -np.inf) # 不合格的壓到最低,再照常取前 k 個
top = np.argsort(-scores)[:3]
對 agent 來說,比「做不做得到」更重要的問題是:這個條件由誰決定?
常見的一種做法是把過濾條件開成工具參數,讓模型自己填 chapter=8。我沒有這樣做。我的 agent 手上的 search 只有一個參數 query,條件是呼叫 API 的人在請求裡給的,由服務在 agent 外面夾帶進每一次檢索。agent 不知道自己被限制在第八章,也沒有辦法把限制放寬。
原因是這個條件常常其實是權限:這個使用者只該看到這一份文件。權限交給模型去填,等於請它自己決定要不要守規矩。
索引裡只有一份規章的時候,這不是問題。多一份就是了。
numpy 版要自己做三件事:每一塊有一個穩定的 id(同一份文件攝取兩次要是覆蓋,不是多一份)、刪除時把那幾列拿掉、每次變動之後重建矩陣。都不難,加起來就是資料庫原本替你做的那一層。
agent 在這裡多了一個麻煩。它看到的查詢結果長這樣:
[1] 第 3 條(第一章 總則)
本規則用詞定義如下:一、直屬主管:指員工編制上之直接管理者……
沒有文件名。兩份文件都有第 3 條的時候,agent 分不出來,而它交卷時只帶得回一串條號。服務要把條號換回原文組成出處,只用條號當 key,後進來的那份就會把前一份蓋掉,引用指到另一份文件去。
我的修法是在 agent 外面那一層記帳:每次檢索經過的時候,把(文件, 條號)成對記下來,出處照這個查。服務這一側的引用從此是對的。但模型那一側我沒解決:它看到的兩個「第 3 條」長得一樣,這支 agent 是前面幾篇的實驗證據,我不能改它顯示結果的格式。目前的緩解是請呼叫端帶上文件條件,讓它根本看不到另一份。
這一格我給「勉強」。做得到,但做完你會開始懷疑自己在幹嘛。
numpy 的矩陣活在記憶體裡。服務重啟就沒了,要從原始文件重新算才排得回來。索引只有開機那份規章的話,這沒關係,向量有快取,重算很便宜。
但如果服務活著的時候有人上傳了第二份文件,重啟之後它就不見了。除非你把矩陣和 metadata 存到硬碟、處理寫到一半當機、處理兩個 worker 同時寫。寫到這裡,你就是在寫一個資料庫。
對 agent,文件不見的後果比對寫死的管線更難發現。寫死的管線撈不到東西,答案通常會很怪,人一看就知道。agent 撈不到,會換關鍵字再查幾次,然後認真地告訴你「規章裡沒有這條規定」,Day 21 那幾題規章裡本來就沒有答案的陷阱題,它就是這樣收尾的。它沒有說錯,它只是不知道那份文件曾經在。
所以這是唯一站得住的理由。然後我去確認這支服務有沒有真的用到它。
我寫了一個很普通的測試:上傳第二份文件,等它回 done,再問服務「你現在有幾份文件」。
它說一份。
追下去,上傳的最後一步會重建檢索用的物件,而重建時呼叫的是一段兩個星期前寫給實驗用的程式:資料庫裡的筆數不等於 59,就當成建到一半的壞東西,整個刪掉,從原始文件重建。對實驗這是對的,每一輪都要從同一個起點跑。對服務,這等於每收下一份文件,下一行就把它刪掉。重啟也走同一段。
五天,我有一個向量資料庫,而它唯一站得住的那個理由,被我自己的程式每次都關掉。 這五天裡不管上傳了什麼,agent 查得到的永遠只有第一份。
修法很無聊:服務用自己的 collection,實驗那一顆完全不碰;寫入只剩一條路,開機和上傳都走它;檢索用的物件只從資料庫讀,不再回頭讀原始文件。補了 19 個測試,其中一個是「呼叫端給的文件條件,有被帶進 agent 的檢索」。把舊程式換回去跑,6 個會紅。
有一個測試在舊程式上是綠的:numpy 和 Chroma 在同樣條件下給出同一組結果。兩邊都只看得到第一份文件,一起錯得一模一樣。一致性測試只證明兩邊一致,不證明它們看到的是對的東西。
問自己三個問題,照這個順序:
如果檢索是給 agent 用的,另外再問一題:過濾條件是誰給的? 是權限的話,就不要開成工具參數讓模型填。
大部分教學是反過來問的:先決定用哪個向量資料庫,再想要放什麼進去。
最後一篇。今天的 agent 只查我自己的規章,用它的也只有我自己的 curl。明天把它交出去:用 MCP 把「查規章」和「問一題」包成工具,讓別人的 agent 也能呼叫;再給人一個最小的網頁。Day 28 的上限就是為這一步先裝好的,工具交給別人的 agent 之後,要呼叫幾次就不是我決定的了。今天那個問題也會回來:別人的 agent 呼叫我的檢索工具時,過濾條件該由誰給。