
昨天那四個「資料源」是我拿自己的文章切出來的。拆開 DB、API 與 legacy 之前,有個更基本的問題:這些片段,還是對的嗎?
我拿同一份語料問了一題:「T2 這台機器有幾張顯卡?」
Day 12 我更正過:T2 是單張 RTX 4070 Ti 12GB,不是雙卡 24GB,並宣告前面提到「我的雙卡 24GB」的每一處一併更正。
但沒有一個字被改回去。 已發佈的內容凍結,勘誤只能寫在後面的篇章——Day 01 那張機器表到今天還印著「2× RTX 4070 Ti」。同一件事,索引裡 12 段講舊說法、2 段是更正。

圖 1:同一件事換四種問法,取前五筆時有兩種只拿到舊說法。
四種問法的更正都進得了 top-10。但 k 收到 5,四種都只看得到其中一邊,而其中兩種看到的是舊說法。檢索器沒有壞,新舊說法都在索引裡——它只是沒有版本這個概念,也不知道該把哪一邊放前面。
而數量會影響結果——是進到 context 裡的數量。2026 年一份研究把同一段低可信度內容貼進 context 兩次(不換來源、不加資訊),13 顆開放權重模型原本偏好政府與報社的傾向全都翻了:差別不在有幾個來源同意,在那段話出現幾次。
索引裡的 12 比 2 不會原樣搬進 context。數量可能影響取回結果,但本次沒有單獨量過重複數量的影響。
這裡量的是檢索名次,尚未量模型答對率——那是明天的事。
最直覺的修法:分數乘上時間衰減。我掃了六個半衰期。

圖 2:六個半衰期、四題,24 格裡只有一格比不衰減好。
半衰期 6 天時,更正從第 2 名掉到第 180 名。衰減獎勵的是「最近寫的」,不是「最新的版本」——更正住在第 12 天,而第 13 到 24 天有兩百多段跟這題無關的內容,被扣得比它少。
換成硬過濾(只留第 12 天之後)就有效了,更正排到第 1、2、6、2 名——代價是語料少了一半。
一、時間戳寫進 metadata,不會自己去改分數。 我把 Qdrant 裡那批 payload 的日期整批往後推一百年、向量一個字不動,分數逐位相同。ANN 算的就是向量距離,metadata 的數值欄位一格都不進分數。唯一的例外是把 metadata 一起餵去 embedding(LlamaIndex 預設就是)——那改的是被編碼的文字,不是排序規則。
二、先取候選再套衰減,時間權重的射程就是候選池。 我起了一台 Elasticsearch 9.2:一份現行版的相似度刻意壓到約第 500 名,同一條 gauss 衰減,k=100 撈不到它,k=600 才撈得到。ES 的 profile 把話講完了——knn 進 function_score 之前已被改寫成一份長度 k 的清單,清單外的文件不存在,乘什麼都沒用。
⚠️ 這是「先用 ANN 取候選、再套時間衰減」這條路的性質,不是產品做不到:改用 script_score 逐筆算向量相似度,一樣結合得了時間衰減,代價是官方那句「all matched documents are linearly scanned」。
時間權重能調整排序,卻不會自行辨認現行版本;版本與取代關係仍得明確記錄,然後拿它當過濾條件。
落到這份語料就是:Day 01 那張表不必刪,它其他內容還有效——但顯卡那一格要標上「已被 Day 12 取代」,Day 12 那段要標上它取代了誰。查現況只取沒被取代的,查歷史才放行舊版。
**發布日期、生效日期、取代關係是三個欄位,不是一個時間戳。**上面那條 day ≥ 12 只是示範——它的門檻來自我已經知道答案,不是來自資料。
Day 22 立過一條規矩:L1 Parsing 到 L3 Embedding 改一項就要重建索引。那就照做,換一顆 embedding 重跑。

圖 3:三個出口,三種要自己動手驗的東西。
第一趟報 skipped 10,新模型的 embedding 函式被呼叫 0 次,向量與舊模型逐位相同。文件的 key 是內容與 metadata 各自 hash 再 hash 一次(langchain_core/indexing/api.py),沒有 embedding 版本這一格。
解法寫在官方 docstring 裡,只是寫成提示不是警告:force_update。但它只補得了「向量沒重算」那一格——維度也變了的話,它是引信不是解法:那個例外要等到你加上它才炸出來。
同一台狀態機還有兩個出口。cleanup="full" 配上一個四個來源只抓到兩個的 loader,沒抓到的會被當成「已刪除」清掉;換成 scoped_full 刪 0 份——但它保護的只是這一趟完全沒出現的來源。把失效換成「某個來源出現了、只回了三段裡的一段」,scoped_full 跟 full 刪得一樣多:它分得出誰沒來,分不出誰來得不完整。
而 sha1 會跳一句碰撞警告叫你換強一點的演算法;照著換成 sha256,cleanup=None 那一趟索引就從 10 份變 20 份,其他三種不翻倍但整批砍掉重寫一次。
這不是哪一套框架的 bug。LlamaIndex 的 node hash 同樣不含 embedding;Azure 的 indexer 靠來源時間戳判斷改動,於是在 ADLS Gen2 上,單靠改目錄名不會更新底下 blob 的時間戳,也就不會觸發重新索引。只要拿一個代理訊號(內容指紋、來源時間戳)判斷要不要重算,訊號沒涵蓋的東西就是看不見。
而這條路跟上一節的結論打架:版本或生效日期寫進 metadata,那份 metadata 就會進 hash。 在本次的預設 hash 流程下,只加一個欄位,key 就從 fa8442ea… 變成 2dfa535b…,record manager 從此認為那些文件都是新的。
⚠️ 但這不代表你被綁死:source_id_key 可以指向既有欄位,不一定需要修改 metadata。
前面三節的麻煩只發生在「把資料複製一份進索引」那一格。企業資料至少四種形狀,兩種不必進去:
| 資料形狀 | 進索引的是什麼 | 權限從哪來 | 變了怎麼知道 |
|---|---|---|---|
| 非結構文件wiki、SOP、規章 | 全文;表格自成 chunk | ACL 跟著文件進索引 | 你自己的狀態機(上一節) |
| 結構化 DBwarehouse | schema、表描述必要時另建值索引 | 下推原生授權引擎 | 數值現查;schema 與值索引另行同步 |
| 即時系統庫存、額度、產線 | 不進,包成 tool 現查 | 呼叫端帶使用者身分 | 現查,不必知道 |
| Legacy主機、老 DB | 看情況 | 多半只剩粗粒度群組 | 有可用日誌走 CDC;否則定期匯出、比對快照 |
第一列還了 Day 22 欠的表格那筆,正解很土:Uber 在政策問答上試遍幾套 PDF loader 都找不到通用解,最後把來源整批搬到 Google Docs、自己寫 loader——至少在那個 case,決定表格切不切得完整的是來源格式,不是 parser。
第二列那句「必要時」不能省:問題帶到產品名稱時(「冰茶賣多少」——表裡寫的是 Ice Tea 還是 spiced tea?),還得把使用者的叫法對上資料庫裡的值。若走「另外建一份值索引」那條,那份複本就要同步——新鮮度又被請回來了。
第三列的邊界 Microsoft 畫成了產品:federated connector 走 MCP、查詢時才抓、唯讀,送審時每個 tool 都要掛 readOnlyHint。我原本照它 overview 的比較表寫「不支援自訂」,結果那張表到今天還寫著 No,隔壁的 how-to 頁八月中就在教你怎麼建——連微軟自己的文件都有一格沒更正。
至於建圖,判準是來源本身半結構化、而且問題真的要多跳。⚠️ 沒有任何一家寫過「什麼時候不該建圖」,這條是我歸納的,請當假設拿去驗;微軟把 GraphRAG 轉成維護模式與公開 benchmark 的分界,都放在文末。
版本更新得了,權限也得跟著更新——同樣是「來源端改了,不代表索引端已經生效」。
在 prompt 裡寫「這位使用者是 A 部門的,不要回答 B 部門資料」不是權限控制,是禮貌請求。這裡比較兩種常見做法:文件側複製 ACL 進索引,檢索階段就濾掉;DB 側下推原生授權引擎,權限只有一份、不必同步,代價是資料得在受治理的倉庫裡。
不過這一派也有自己的坑,而且官方自己寫了範例:欄位與過濾函式的型別不符時,關掉 ANSI mode 會讓轉型失敗的值靜靜變成 NULL、不報錯;如果那個條件恰好放行 NULL,不該顯示的資料就出去了。
複製那一派的代價,Microsoft 在會進索引的那種 connector 上寫得很清楚:權限改了「可能要 24 小時才反映」,而且只在全量爬取時更新。
fail 的方向也不是天生就對。Kendra 的規則是「沒有 ACL 的文件視為 public」,2026 年的接班人 Bedrock Managed Knowledge Base 反過來寫「缺少權限資訊視為受限」——同一朵雲、同一問題、相反的預設值(後者只對啟用了 ACL 的資料源成立)。兩代倒是講了同一句話:這是過濾,不是身分驗證。
然後是撈多深。權限過濾若發生在檢索之後,可見比例越低剩得越少。

圖 4:取回深度不變,同文件片段共用 ACL 後,湊滿率下降 13.0–38.5 個百分點。
在「每個片段各自決定可見與否」的假設下,要有 95% 把握湊滿 top-5,得撈到 K ≈ 9.15 ÷ p——四種可見比例下,片段獨立模型的模擬湊滿率是 95.0%–99.5%,與設定目標一致——但模擬沿用同一個獨立假設,不能據此保證真實 ACL 下也湊得滿。
同一份文件的片段共用 ACL 時,片段彼此獨立的假設就不成立。 可見比例 10%、同樣取回 92 筆,湊滿率從 95.0% 掉到 63.5%——少了 31.5 個百分點。
不用 GPU,bge-m3 掛 sentence-transformers 跑 CPU 就夠。門檻是一份你知道答案已經改過的題目,三題起步。
⚠️ 但別把「舊版還在索引裡」或「文件全部 skipped」直接當成錯誤——它們都可能是對的。要驗的是三件事:問現況時有沒有拿到現行版本、換過模型之後向量有沒有真的重算、來源漏抓的那一趟有沒有誤刪。
明天 Day 28〈把答案直接餵給它還是答錯:讓不大的模型答得準〉:今天講的是候選池裡有什麼,明天把正確的段落直接塞給 8B、12B——它照樣答錯,而且錯的形狀是完全無視那一段。
咱們明天見。
上面那個維度衝突真的發生時,換掉 collection 只做了一半:record manager 的 namespace 沒跟著換,10 份會全部 skip 進一個空的 collection——索引回報一切正常,庫裡什麼都沒有。
Day 22 引過 Uber Genie 的 helpfulness 48.9%,欠了一句「分母為什麼可疑」。(這不是昨天那個 48.9%——那個是我量的召回率。)它是四顆 Slack 按鈕的使用者自選回報。Uber 逐字定義了那四顆,卻沒有一句話說 48.9% 是怎麼算的——「helpfulness」這個詞全文只出現一次,就在那個百分比旁邊,跟定義按鈕的那一節從頭到尾沒有交集。
同一家公司一年後示範了該怎麼做:uReview 報「平均 65% 的評論在同一次改動裡被處理」時,同一段補了人類基準——人寫的評論只有 51%。48.9% 要能判讀,得先知道分子、分母與回報範圍;人類基準則是另一項有用的比較。
機器 T1(M4 Max 128GB),全程 CPU。語料與切法沿用 Day 25,corpus_sha256 逐位相同。所有數字僅代表這份語料與這組設定。
| 檢索 | day01.md–day24.md / 451 片段 / BAAI/bge-m3(rev 5617a9f6)/ CPU / chunk 300 token |
| 衝突標記 | 只收「主張作者那台 N 卡機是雙卡/24GB」的句子,談別台的不收,stale 是下限 |
| 時間衰減 | score × 0.5 ** ((24 − day) / 半衰期),day 取檔名編號;硬過濾 day ≥ 12。⚠️ 綁在「更正比無關內容舊」這個形狀 |
| 射程實驗 | Elasticsearch 9.2.0 單節點 / 2,000 份 / 8 維 dense_vector + HNSW / function_score 包 knn |
| 索引狀態機 | langchain-core 1.6.1(當期 1.6.2)/ chromadb 1.5.9 / 10 份 / 確定性假 embedding。維度不符的錯誤是 chromadb 丟的,換別的向量庫可能不報 |
| post-filter | 200 個模擬使用者;「成群」=逐 dayNN.md 指派可見性。是模擬不是量測,真實區塊大小自己量 |
| K ≈ 9.15/p | 【推算】Poisson 近似下使 P(X ≥ 5) ≥ 0.95 的 λ* = 9.1535。⚠️ 取 9.15 只有 94.99%;精確二項解在 p=0.1 是 89 |
| 用在哪 | 出處 |
|---|---|
衝突票數與名次、半衰期掃描、硬過濾、四個 IndexingResult 與後續條件、Qdrant 推日期、k 決定射程、post-filter 模擬 |
一手實測,research/day27/(ES 9.2.0 與 Qdrant 1.19 跑在容器裡) |
force_update、cleanup 四模式、source_id_key 吃字串或 callable、metadata 進 hash |
langchain-core 的 index() docstring 與 indexing/api.py |
| LlamaIndex 的 node hash、預設把 metadata 併進要 embedding 的文字;Azure indexer 的時間戳與目錄改名 | 官方文件與原始碼(hash 那半是讀碼推論,metadata 那條實跑過) |
| Uber 放棄 PDF loader 改自寫 Google Docs loader;Genie 四顆按鈕與 48.9%(「helpfulness」全文只出現一次);uReview 的 65% 與基準 51% | uber.com/blog 的 enhanced-agentic-rag(2025-05-29)、genie-…-copilot(2024-10-10)、ureview(2025-08-12)【皆廠商自述】 |
帶值提問要把使用者的叫法對上資料庫的值;另建值索引是其中一條路,複本要填 TARGET_LAG |
Snowflake《Improve literal search…》 |
| ACL 變更可能要 24 小時、只在全量爬取更新 | Microsoft《Search and validate indexed connector content》(08-23)。⚠️ 只涵蓋會進索引的 synced connector |
federated 走 MCP、唯讀(tool 要掛 readOnlyHint)、可自訂 |
Microsoft 的 set-up-custom-federated-connectors(08-17)與 submit-federated-connector(08-29)。⚠️ 同文件集的 overview 比較表(08-13)仍寫不支援自訂,本文取較新的兩頁 |
| 「沒有 ACL 視為 public」/「缺少權限視為受限」、「過濾不是授權」 | Amazon Kendra;Bedrock Managed Knowledge Base 的 ACL-aware retrieval |
row filter 失效要三件事同時成立:欄位與函式參數型別不符、ANSI mode 關閉、條件放行 NULL |
Databricks《Filters and masks》的官方 bug 範例(salary_filter(dept INT) 掛在 STRING 欄位) |
| 「差別不在幾個來源同意,在出現幾次」 | Whose Facts Win? v3。13 顆開放權重模型、合成來源、強制選擇題設定(早期版本對其中一顆有例外) |
| GraphRAG 轉維護模式 | github.com/microsoft/graphrag 的 WARNING(2026-08-14;archived: false,08-21 仍發 v3.1.2) |
三家的時間衰減結構上就在檢索之後:Qdrant 無 prefetch 回 400、Weaviate 稱 post-retrieval rescorer、Milvus 型別寫死 RERANK;ES 的 function_score 是 query,script_score 還能逐筆算向量相似度並結合衰減 |
四家官方文件;Qdrant 的 400 與 ES 包 match_all 對 2,000 筆全算分為一手實測。ES 對代價的原句是 “all matched documents are linearly scanned” |
| 建圖的分界(延伸閱讀) | GraphRAG-Bench(arXiv:2506.05690 v3,ICLR 2026)。同一個小說語料上,MS-GraphRAG(local)的 ACC 在事實檢索輸給加 rerank 的基本 RAG(49.29 對 60.92),複雜推理反超(50.93 對 42.93)、情境摘要拉最開(64.40 對 51.30);換到醫療語料四個難度全輸。⚠️ 全是 ACC、GPT-4o-mini、local 模式(global 分數完全不同);有報導把 HippoRAG2 與 MS-GraphRAG 混成同一欄 |