想起來之後——AI Agent 怎麼越用越懂我
記憶要怎麼從保險庫裡叫出來?七個組件、一條管線,跨 session 召回的完整路徑。
存得到不難。難的是:在我需要的時候,對的那筆記憶會自己浮上來。
—— 做完 Write Track 那天下午,我在終端機前寫下的起點
Write Track 跟 Background Track 兩篇講了記憶怎麼存、怎麼忘,兩條線接上,保險庫會自己整理,但還差臨門一腳——怎麼把它叫回來。
怎麼把記憶叫回來,其實是最早遇到的問題:Write Track 寫完隔天,我開了新對話,試著問 AI「上週那個 OCR 專案進度如何」,它回不出來——資料明明在,卻找不到。那一刻我知道,光存起來是不夠的。
其實 memvault 一開始的念頭,是從 @AI超元域 那邊來的。他在 openclaw 課程提過一句「讓 agent 越用越懂我」,我看完真的感同身受,一直想建立一套輔助工具。後來我從 openclaw 退回 claude code,想說不如就先從 claude code 搭起,建立 memvault 雛形。再後來看到他介紹 LanceDB Pro,也借鑑了其中很多精妙的設計,一點一滴把 memvault 完善起來。真心感謝。
「叫得回」這條路,才是整個記憶系統的核心——這篇會拆解它:七個組件怎麼分工、背後的設計哲學,還有幾條仍在打磨的邊界。這條路徑現在每天在跑,Write Track 寫完那天問不出來的 OCR 進度,現在跨 session、跨幾週問一次就接得上。
先說一下當初的構想
最初我把它想得很單純:收進來、建索引,查的時候找最像的那幾筆就好。做下去一週就發現不夠——「我那個任務做到哪了」「最近聊了什麼」「你覺得我下一步該怎麼走」,這三個問題用同一條路徑去查,都會走偏,得走三條不同的查法。
一、先猜你在問哪種問題
「上週那個 OCR 做到哪了?」「最近聊什麼?」「我在想要不要做 X,你覺得?」——這三種問法得走三條路,第一步是先分清楚你問的是進度、回憶、還是建議,下游的排序權重跟深度才會跟著對。一開始我沒分這一層,結果「任務」這個詞會 match 到三週前的無關對話。
分類走兩層漏斗:先用關鍵字掃一遍,零成本;再算語意向量,幾毫秒;兩個都說不準才叫 LLM 判一次,這一步貴很多。大部分問題在前兩層就分好了,LLM 只在真的搞不清楚時才動。
分對之後,下游每一層的權重會自動調——問進度時時間排序加重、問定義時語意相似度加重、問建議時觸發延伸思考那條分岔。同一個 agent,因為問法不同,拿到的記憶也不同。這就是為什麼換一個新 session 問「上週那個 OCR 搞到哪了」,agent 還接得上——不是它記憶力強,是先判準了你在問什麼。
但「你在問什麼」還能再細一層——同一句「最近怎麼樣」,忙的時候問的是進度,放假的時候問的是回顧,光看字面分不出來。所以分類之上又疊了一層個人化偏好:系統看你最近七天、三十天、九十天比較常問哪一類問題,把這個偏好混進判斷。常翻舊帳的人,「最近怎麼樣」會被偏向回顧;常追進度的人,會被偏向進度,同一句話餵給不同的人,走的路不一樣。
個人化偏好層跟 Background Track 的興趣畫像同源——你看過什麼、停留多久、回頭幾次,都會慢慢勾出一張口味畫像。意圖分類負責「字面在問什麼」,這層負責「你最常想知道什麼」,兩個合起來才接得近真正想要的答案。
二、同時用兩種方式找:意思跟關鍵字
語意搜尋跟關鍵字搜尋各自會漏,所以這一步兩邊同時跑:一邊算語意像不像、一邊算關鍵字匹配度,兩條排行榜出來後用 RRF(倒數排名融合)合起來。
- 只搜語意:問「MLX」,記憶裡寫「Apple Silicon 推理框架」,語意對得上、關鍵字不對,但遇到縮寫、人名、專案代號會失手
- 只搜關鍵字:精準字面匹配很強,換個問法就找不到
RRF 把每個 ranker 的排名換算成分數,誰讓這筆記憶排得越前、貢獻越多分——用排名投票不用分數投票,因為兩種分數尺度不同,直接加會偏掉。
這步純粹是撈候選,找出來的可能有一百筆,下一步才是挑。
三、挑出最對的幾筆:排序疊了十一層
排序把一百筆候選收斂成前 15 筆,靠的是疊起來的十一層訊號,每一層對每筆候選加一個乘數:
- 時間——越新越加分。衰減曲線不是直線往下掉,是 Weibull 形狀:剛存的一兩天衰減慢,一個月後掉比較快,再久就進入低而穩定的尾巴
- 信任度——你手動寫的比 AI 自動生成的加分
- 用過幾次——常被翻的加分,冷門的扣分。常被叫出來看的記憶衰減會慢(偶爾某筆兩三年前的東西還能出線,就是這個邏輯)
- 圖中心性——在知識網路裡被引用多的加分
- 語意接近度——跟問題越像的加分
- 分數下限——太低的直接淘汰
- 去重——兩筆太像的,降分後再比
這 11 層不是隨興疊出來的——每一層對應一個檢索或記憶研究裡的成熟概念:時間衰減、信任、圖中心性、遺忘曲線、語意、噪聲、去重。做的事不是發明新方法,是把十幾篇論文的訊號挑一挑,組成一套適合個人記憶場景的排序,背後有兩條核心設計理念。
第一,不同意圖用不同權重。查事實重信任、查探索重時間、找實體重語意。同一批候選,因為問法不同,排名結果可能整個翻掉。這就是為什麼第一步要先分意圖——意圖錯了,權重就錯,後面排得再精也沒意義。
第二,寫跟讀是閉環,不是兩條獨立管線。Write Track 寫入時會幫每筆記憶打上來源可信度;Read Track 排序時讀這個分數當加權。更關鍵的是反向:每次讀到某筆記憶,系統會回頭把「被讀過」這件事寫進那筆記憶本身,下次它的壽命就會更長。寫時多付一點,讀時少算一點;讀的行為又回頭延長記憶壽命。一般 RAG 沒這條線——它們的向量庫是靜態的,寫進去就不動了。
還在磨:排序穩定性
這套多層排序的難點是各層權重會互相拉扯——動一個,其他幾個指標可能連鎖偏移。目前靠一組 golden query 守著(幾十條已知正解的問題),每次調權重先跑一輪看有沒有退化。大部分情境下跑得穩,但要讓它在所有意圖、所有時間尺度下都不走味,這部分還在持續調校。
四、精挑那一關:注意力閘門
注意力閘門省掉大部分不必要的精排——候選太少(兩三筆)、前幾筆分數已經拉開、所有候選都擠在一個小區間,這三種情況就跳過,不是每個問題都值得全程跑完。
排序排完還有最後一次精排,用的是更貴的模型,把問題跟候選記憶丟在一起聯合打分,比純比對準得多;但每筆都要跑一次模型,慢且貴。
精排模型連續失敗三次,會自動退回去走純排序 10 分鐘,不硬撐——像保險絲。
五、問得深才走的分岔:快思慢想借過來
問進度、問回憶這種確定性高的問題,前面四層就夠了;但「我在想要不要做 X,你覺得」這種問題,Read Track 會多走一條比較慢的分岔——從命中的概念出發,沿著知識圖譜走幾步,把鄰近的記憶也撈回來。這條路徑比較貴,淺問題不走,只有判斷你在問需要延伸思考的問題時才啟動。
單靠「找最像的段落」答不出這種問題什麼有用的東西——agent 只會給通用建議,像網路文章那種。真正有用的答案要從你的脈絡長出來:你過去做過什麼、最近關注什麼、哪些概念跟 X 相連。
這個「快 / 慢」分法不是我想出來的——借自 Kahneman 的《快思慢想》。人的大腦本來就有兩個系統:一個快、直覺、省力;一個慢、推理、費工。記憶系統不該把每個問題都當費力題,也不該把每題都當直覺題,所以 Read Track 把這兩條路並陳,讓問題自己決定走哪邊,不用使用者手選。
問「我在想要不要做 X,你覺得」,拿到的不是公版建議,是帶著你上個月做過 Y、今年提過 Z 的那種答覆——這條路徑的終點。
一個容易混淆的地方
Read Track 裡有兩個「慢」,不是同一件事。
第一個慢——就是上面講的「慢問走慢路」。你問了需要延伸思考的問題,系統當場決定多走一條分岔去圖譜撈鄰居。這條是 Kahneman 雙系統的落地:快問快答、慢問慢答。
第二個慢——叫 Slow Thinker,在背景偷偷跑。它不處理你眼前的這次問答,而是一邊聽你講話一邊猜「你下一個可能會問什麼」,先把可能的答案撈好放在 cache 裡等你。這條的靈感是 Salesforce 的 VoiceAgentRAG。
前者是「當下」,後者是「預測」。剛好兩個都叫慢,但一個在前台、一個在背景,實作也在不同檔案。
六、最後一哩路:打包給 agent 看
排序完挑了前 15 筆,但 agent 的 context 有上限,15 筆全文塞進去可能已經爆了 token,所以最後一步做兩件事:長段落只留摘要、不留全文,這是取捨;依呼叫端是 Hook、API 還是 CLI 排出不同版面,走 API 的給 JSON、走聊天介面的排成卡片,這是整形。
這一步容易被當成裝飾,其實決定了最後 agent 看得到多少。早期直接截前 15 筆丟進 context,常被一個長 block 吃掉一半 token;加了 budget-aware packing 之後,token 利用率從 60% 拉到 85%。再好的排序,擠不進 AI 的腦袋也沒用。
進貨檢驗夠了嗎?出貨還要再一道
只在進貨時檢一次不夠,所以打包給 agent 之前要再過一道濾網——進貨時擋過一次,出貨時再擋一次,雙安檢。
記憶在存進保險庫之前已經過一次安檢,把「忽略前面的指令」「現在改說...」之類想偷渡進來操控 agent 的字串擋在外面,Write Track 那篇講過。但安檢規則會升級,三個月前進來的記憶用的是舊規則;攻擊手法也每個月都在翻新,今天放行的字串,下個月可能就是新型炸彈。
看到危險的句型,出貨這一關會當場改寫或丟掉;背景偷偷預存好的那批答案也走同一道——存的時候濾、要拿出來用之前再濾。麻煩,但餵給 agent 的那一口要乾淨。
七、Read Track 的取捨
6 種意圖分類、11 層 scoring、2 道 rerank gate(attention gate + circuit breaker)、Cascade Recall 三模式(LOCAL / GLOBAL / HYBRID)、budget-aware output formatter——這些拼起來就是現在每天在跑的東西,跨 session 召回、問進度、問建議都走它。每一層都有 golden query 回歸測試守著,Dream Loop 每天凌晨 4 點在背景自動整理記憶。
但它不是無懈可擊。誠實列幾個已知邊界:
- 延遲 vs 召回率——深召回(走圖譜那條)慢,動輒幾百毫秒。只在需要延伸思考的問題才走,是妥協
- 精挑細選那層計算貴——注意力閘門幫忙省掉一些,但該跑的時候還是要跑。當它走保險絲退回的時候,結果品質會看得出差
- 被壓到冰封層的記憶叫不回全文——只剩摘要。法律追溯用沒問題,重建當時脈絡就不夠
- 意圖分類錯了,整條管線跑錯方向——目前靠動態信心閾值降低發生率,事後回退機制還在找更穩的做法
- 自評層擋不住全部幻覺——slow 模式會跑 CRAG 自評,fast 模式省掉這層;代價是 fast 路徑下幻覺可能漏網
每一項都是成本跟效果的交換——系統能跑、也在跑,上面這些是已知邊界,不是隨時會炸的坑。攤出來的意義在於:每個「找到」背後的代價都寫在紙上,不藏起來。
八、貫穿的幾條設計心法
撐住這套系統的不是機制本身,是五條反覆出現的設計心法:快思慢想(問題自己決定要走多深)、寫的時候多做一點換讀的時候少算一步、讀的動作本身會回頭延長記憶壽命、intent 決定一切、trade-off 攤在紙上不藏起來。
- 快思慢想——不是每個問題都要盡全力。問的深淺由問題自己決定,不用使用者手選檔位
- 寫的時候多付一點,查的時候不用算——
trust_score、access_count、三元組雙投影,這些寫入時的「多此一舉」,都是為了查的時候能少跑一層 - 讀的動作會回寫記憶壽命——你每問一次,命中的那筆記憶會被延長有效期。不用的自然褪色,常回訪的自然留下。像走過的路不會長草
- intent 決定一切——不同問法走不同檢索策略、不同排序權重、不同深度。同一個 agent 對不同問題根本是不同的檢索器
- trade-off 要寫在紙上——不能假裝沒有代價。寫下來,自己看得到、之後的我也看得到
九、想起來不夠 — 在你開口前就到位
好的服務生不是只回應點單——他會看到你杯子空了,主動把水倒上。Read Track 有一個在背景偷偷做這件事的傢伙,在你開口之前就先猜、先找、先備好答案。
到這裡為止講的都是「你問了,系統去找」。它做五件事:
- 聽你正在講什麼——盯著當下這場對話,從最近幾句話判斷你接下來最可能問什麼
- 順一條邏輯動線——把猜出來的那幾個可能問題,按照人類自然會問下去的順序排好
- 偷偷先去找答案——挑前幾個最可能的問題,背景跑一次完整的搜尋,搜出來的記憶先放在手邊
- 有節制地動——不是每句話都觸發。對話太短不動、最近講的東西明顯偏離不動、剛剛才預測過不重複動。怕白燒
- 等你真的開口——你問下去那一刻,先去手邊那批備料看看有沒有對得上的;對得上就直接用,省掉一次完整搜尋;對不上就走正常流程,沒損失
這就是料理長備好下一道菜的邏輯。客人還沒點,但前菜上完之後通常會點主菜,先把可能的兩三道備料切好放在台上。客人真的點了,端出來只要幾秒;客人改點別的,那批備料下一桌可能還用得上,最差就是丟掉。
它跟前面講的「快思慢想分岔」很容易搞混。那個是問下去之後當場決定要不要多走一條深路;這個是還沒問之前就已經偷偷在動。一個在前台、一個在背景,剛好兩個都被叫做「慢」,但做的事情完全不同。
對得上的時候,從問到答幾乎沒延遲——備料早就切好了;對不上的時候,那批備料就是浪費。所以「有節制地動」很重要——對話太短不動、跑題不動、剛動過不再動,寧可少猜,不要硬猜。
背景搜出來的那批備料,要進儲藏室之前要先消毒一次,要從儲藏室拿出來給 agent 看之前再消毒一次——前面講的那道雙安檢,這條備料線一樣得走,不能因為是「自己人」備的就鬆懈。
到這裡 Read Track 就是現在的全貌了:聽得懂、找得到、排得對、挑得準、塞得進、來得及。每一條都還在打磨,但骨架已經立起來,每天在跑。
Read Track 盤到這。意圖、找、排、挑、塞、消毒、預取——七個組件、一條管線。
它不會幫我「記住該記住的事」,那是 Write Track 跟 Background Track 的責任。它只負責一件事:在我需要那筆記憶的時候,對的那筆自己浮上來。
開頭那句「存得到不難,難的是讓對的那筆自己浮上來」——現在浮得上來。
架構總覽
Read Track 的入口是 recall(query, context),管線骨架是:QueryClassify(意圖分類)→ Fast Search(快速召回)→ (Cascade Recall(連鎖召回), slow only) → Output Formatter。不論呼叫端是 Hook(鉤子)、MCP(Model Context Protocol)、CLI(命令列)、API(應用介面),走的都是同一條管線,差別只在最後 Output Formatter(輸出格式化器)序列化的格式。Scoring(評分)和 Reranking(重新排序)不是獨立 stage(階段)——它們嵌在 qdrant_search() 裡面(services.py)。
這套 pipeline 借自 10+ 篇研究:HippoRAG 的 PPR、LightRAG 的雙模式檢索、GraphRAG 的三層 KG、HyDE 的雙 task 設計、CRAG 的自評閘、Kahneman 的 fast / slow 分工、VoiceAgentRAG 的預取、Weibull 衰減曲線、RRF 融合、Attention-Residual 的 intent-dependent 權重。下面每個組件都會點出對應出處。
一、Query Router 意圖分類
Query Router 先把每個 query 分進六個 intent(意圖):entity_lookup、factual、conceptual、exploratory、cross_domain、unknown(query_archetypes.py),分類結果決定下游三件事:LayerPlan(分層計畫,查哪幾層)、ScoringConfig(評分設定,11 stages 的權重向量)、RetrievalMode(檢索模式,LOCAL 本地 / GLOBAL 全局 / HYBRID 混合)。
判定走雙軌:
- 關鍵字匹配:
~0ms,純正則 / 詞表命中 - 語意向量:
~5ms,把 query 算成 embedding(嵌入向量)跟 archetype(原型)向量比對
兩邊的信心分數融合,融合後仍低於動態閾值,才觸發 LLM(~500ms)。query_router.py 裡的 QueryClassifyOp 把這整條融合寫成一個 Operator(操作子)——Slow Thinker(慢思考)預取結果也從這一層注入。
Personalized Router:意圖之上的個人偏好層
memvault.query.personalized_router 在 QueryClassifyOp 之後加一層 re-weight,補的是純 archetype 分類的盲點——同樣 query「最近怎麼樣」,對 attention profile 偏 factual 的使用者跟偏 exploratory 的使用者,正解 intent 不同:
- 從
attention_profile拉 7 / 30 / 90 天 intent 分布(與 BG Track 的 Interest Profile 同源,attention_tracker.py寫入) - 對六個 intent 的 archetype 信心分數做 prior 加權:
p_final = p_classify × (1 + α × intent_freq_norm),α 預設 0.25 - 低樣本守門:
profile.sample_count < 50直接 bypass(避免冷啟動偏移) - 輸出
PersonalizedIntent,下游ScoringConfig/RetrievalMode拿這個值,不拿原始 archetype 結果
這條跟 Background Track 的 Interest Profile 是同一筆 attention_profile 在讀。寫入由 BG 算、查詢時 Read 消費——又是一條 Write-Read 閉環。
二、Fast Search:Qdrant Hybrid(混合索引)+ RRF
qdrant_search()(services.py 附近)同時拿 dense(稠密向量,Qwen3 0.6B MLX, 1024d)跟 sparse(稀疏向量,BM25 per-service avgdl)查,這是 Fast Search 必跑的核心,兩邊再用 Reciprocal Rank Fusion(倒數排名融合,RRF)合併:
score_rrf(doc) = Σ_ranker 1 / (k + rank_in_that_ranker)
k 預設 60。用排名投票不用分數投票——dense 跟 sparse 分數尺度完全不同,直接加會被分數大的 ranker(排序器)壓過去。
Scoring 跟 Reranking 都嵌在 qdrant_search() 裡,不是獨立 stage,這個設計是故意的:Scoring 要用到的 access_count、provenance、attention_profile 都在 qdrant_search 的作用域(scope)裡,拉出去成獨立 stage 要多搬很多 state(狀態)。
三、11-Stage Scoring Pipeline
11 個 stage(scoring_pipeline.py)分 intent 給不同權重——entity_lookup 把 SemanticBoost 拉到 0.5、exploratory 把 Recency 拉高、factual 把 Trust 再加重。早期全部 intent 共用一套權重時,factual 跟 exploratory 的 top-10 重疊 8 筆,分 intent 之後才分得開。每個 stage 是一個 ScoringOp(評分操作子,走 Operator 協議):
1. RecencyBoost (時效加權) × (1 + 0.15 × e^(-age/14))
2. ImportanceWeight (信心加權) × (0.7 + 0.3 × confidence)
3. TrustBoost (信任加權) × (1 - 0.3 × (1 - trust))
4. FeedbackBoost (回饋加權) × (1 + 0.15 × tanh(net/3))
5. LengthNorm (長度正規化) ÷ (1 + 0.3 × |log2(len/500)|)
6. WeibullDecay (威布爾衰減) 4-tier: Core 180d · Hot 60d · Warm 30d · Cold 14d
7. PPRBoost (圖中心加權) × (1 + 0.3 × ppr_score) # HippoRAG-inspired
8. SemanticBoost (語意加權) × (1 + 0.3 × cosine_sim)
9. MinScoreGate (分數下限閘) hard filter < 0.10
10. NoiseFilter (雜訊過濾) 7 類 quarantine tag
11. PairwiseDedup (成對去重) cosine > 0.85 → ×0.5 then min_score
Weibull(威布爾分佈)4-tier 由 confidence(信心度)決定——高 confidence 進 Core tier(核心層),慢慢衰減;低 confidence 進 Cold tier(冷層),14 天就掉一半。
access_count(存取次數)會拉長 effective half-life(有效半衰期,最多 10x 上限)——這是 Read 跟 Write 的閉環其中一條:讀的行為回寫 access_count、延長壽命(這條在 Write Track 的 Provenance 段落提過)。
四、Jina v3 Reranker + Attention Gate
Attention Gate(注意力閘門)用三個 skip 規則擋掉不必要的 rerank:
- 候選 ≤ 2 件——直接 return(回傳),不 rerank(重排)
- 主導分數:頭兩筆分差 > threshold(閾值)——結果已經明確,不 rerank
- 緊密集群:所有候選分數都擠在一小區間——rerank 也分不出來,跳過
靈感來自 TurboQuant+ 的 early return(提早回傳)策略。reranker.py 跑的是 Cross-encoder(交叉編碼器)0.6B MLX(rerank_bridge.py),query(查詢)跟 doc(文件)聯合打分,比純雙塔(dual-encoder)準得多,但問題是貴——每筆都要跑一次模型。
Circuit Breaker(斷路器):3 次連續失敗進入 600s recovery,這段時間直接繞過 reranker(重排模型)走純 scoring。
Score Blending(分數融合):預設 0.3 × scoring + 0.7 × rerank,但會隨 intent 變——entity_lookup 的 0.2/0.8 給 rerank 更多權;exploratory 的 0.5/0.5 讓 scoring 回到一半,因為 exploratory 的答案沒有絕對對錯,原始分更重要。
五、Cascade Recall(slow intent only)
Fast path(快速路徑)永遠跑,Cascade 只有 slow intent(慢意圖)才啟動——不是二選一。kg_services.py 裡的 cascade_recall()(連鎖召回)把 Fast Search 的主結果(schema 欄位 cards)跟 Cascade Recall 擴散出來的補充結果(cascade_cards)併入同一個 response(回應)。
這個「fast / slow 二分」骨架來自 Kahneman 雙系統(System 1 快思 / System 2 慢想)。早期版本讓使用者手選 fast 或 slow,後來 refactor 成由 intent 決定(choose_thinking_mode()),但「快慢兩條路」這個分工本身沒換。
內部分兩路:
- GLOBAL(全局):L2 Summary(LLM 預生成的社群摘要)+ L1 Community(Leiden 萊登演算法分群結果)。這兩層不走 scoring pipeline——它們是預先壓縮好的 view(視圖),直接拉就行
- LOCAL(本地):L0 Triple + PPR(Personalized PageRank)Walk(圖走訪)。從命中的實體出發,沿著圖譜走幾步,阻尼 0.85。鄰近的 triple 擴散回來
Mode 由 intent 決定:entity_lookup/factual 走 LOCAL(要精準)、conceptual/exploratory 走 GLOBAL(要鳥瞰)、cross_domain 走 HYBRID(混合,全搜)。
L0/L1/L2 本身不走 scoring。只有 Blocks(記憶塊)層還是走完整 qdrant_search() 的 scoring + reranking——這是為了讓 cascade_cards 裡的 Blocks 部分跟 cards 走一樣的排序邏輯,不會出現 fast 跟 cascade 同一筆記憶分數矛盾的情況。
容易混淆的點——「Cascade Recall 的 slow」跟下一段 panel-end 會提到的「Slow Thinker」不是同一件事。前者是 Kahneman 血統的查詢當下要不要深挖;後者是 VoiceAgentRAG 血統的背景預測下一問、事先撈好放 cache。兩個剛好都叫 slow,實作在不同檔案(kg_services.py vs slow_thinker.py)。
Pipeline 順序的脆弱性
11 個 stage 的順序有依賴關係。RecencyBoost 在 TrustBoost 前面,是因為某次 dream loop 合併後的 trust 會被新舊記憶的時序覆寫,必須先拿時間校正。
依賴順序的 pipeline 天生脆——換兩個 stage 的位置就可能讓 top-k(前 k 筆)全變。目前靠一組迴歸測試守著:幾十條 golden query(黃金查詢)有預期排序,每次動權重前先跑一輪,top-k 差異超過閾值就擋下。現況堪用,要讓它更 robust(穩健)仍在調校中。
六、Output Formatter 與 token 預算
Output Formatter 是管線最後一步——token 預算逼出三件事:cards 跟 cascade_cards 六四分帳、超長 block 只留摘要讓 agent 自己選要不要展開全文、重複段落只留分數最高那筆。
- 主結果
cards跟擴散結果cascade_cards的 budget(token 預算)拆分(通常 60/40) - 超長 block(記憶塊)先 attach(附加)摘要,原文放
expand(展開)欄位讓 agent 自己決定要不要挖 - 重複的段落(cosine > 0.9)只保留分數最高那筆
所有入口(Hook、API、MCP、CLI)走同一條 pipeline(管線),差別只在這裡:format(格式)是序列化參數(text / json / cards),跟上游檢索策略完全無關。這層之前我低估了——直接截 top-15(前 15 筆)丟進 context(上下文),常被一個長 block 吃掉一半 token,後面的記憶全擠不進去;加了 budget-aware packing(預算感知打包)之後,平均利用率從 60% 拉到 85%。
Read-Time Sanitize:與 Write Injection Guard 對稱
Read 端對稱跑一次 memvault.security.read_sanitize,補的是 Write Track 落地前 injection_guard.is_unsafe() 只擋寫入時規則版本的漏洞——三個月前入庫的 block 用的是舊版規則,攻擊樣本也在演化:
- 對所有
cards/cascade_cards在 Output Formatter 之前掃is_unsafe_for_injection()(共用 Write 端的 ruleset,每次部署同步) - 命中規則的 block 走
sanitize_for_injection():危險 token sequence 用[REDACTED]占位,保留語意上下文,不直接整筆丟(避免 top-k 稀釋) - Slow Thinker 的 prefetch cache 同樣套用——進 cache 前濾一次(write-time)、出 cache 給 agent 前再濾一次(read-time),雙層防禦避免 cache 變成繞過 read sanitize 的後門
- 命中事件寫
audit_log,可追溯哪筆舊 block 觸發新版規則 → 反向驅動 Write 端 ruleset 升級
不對稱的安檢是漏的——只在寫入端守,等於假設規則永遠不會升級、攻擊永遠不會演化。Write × Read 雙端各跑一次,才能讓老 block 也吃到新規則。
七、Trade-off 清單
Read Track 沒辦法做到又快又準又完整,每一項都是成本跟效果的交換,只能挑兩個。到現在還沒完全解決的,大概這幾個:
| 問題 | 現況 |
|---|---|
| 延遲 vs 召回率 | slow path(慢路徑)對 p95(95 分位延遲)影響大。現用 intent 閘門,只有真的需要延伸思考的問題才走 |
| Reranker 計算貴 | Attention Gate 擋掉約 30% 的 rerank 請求;Circuit Breaker 失敗隔離 600s |
| Cold / Frozen tier 只剩 summary | 全文被壓縮後叫不回。規劃中的 tier upgrade 可讓高頻被叫的 cold block(冷記憶塊)回升 warm(溫) |
| 意圖誤判 | 整條 pipeline 走錯路。目前靠動態信心閾值降低誤判率 ,但沒有好的事後回退 |
| CRAG 自評漏網 | deep 模式(深度模式)才跑 CRAG;fast 模式(快速模式)省了這一層,代價是 fast 路徑下幻覺可能漏網 |
八、貫穿的設計決策
上面七節的機制底下有五條反覆出現的設計原則:intent 向下游全面派送(intent-driven everywhere)、trust_score 與 access_count 構成的 write-read 閉環、Kahneman fast / slow 落地為 thinking_mode、write 與 read 兩端的 defense in depth、把每條 trade-off 攤在紙上不假裝沒有代價。
- Intent-driven everywhere:
QueryClassifyOp的 intent 向下游派送到 scoring weights、retrieval mode、score blending ratio、cascade layer routing。同一個 query 對不同 intent 走完全不同的路徑 - Write-Read 閉環:
trust_score寫入時由source_tracker打分、讀取時由TrustBoostOp消費;access_count讀取時回寫、下一次讀取影響WeibullDecay的 effective half-life。這條閉環跨 Write / Read Track,不是單向 pipeline - Kahneman fast / slow:
thinking_mode = fast / slow把 Kahneman 雙系統落地。slow 比 fast 多跑 Cascade Recall + CRAG 自評,不是二選一——slow = fast + extras - Defense in depth:Write Track 三 gate 攔截 + injection guard,Read Track attention gate 跳過無意義 rerank + circuit breaker 隔離重排失敗。每層允許下一層 fail 而不整條壞
- Trade-off on paper:延遲 vs 召回、計算成本 vs 品質、冷儲存 vs 全文還原——每條 trade-off 都明寫在 §七,不假裝沒有
九、Slow Thinker:Predictive Prefetch
Slow Thinker 是一條獨立的後台 pipeline(memvault.slow_thinker),不阻塞主 recall(),事件源是 conversation.utterance.appended,靈感來自 Salesforce AI Research 的 VoiceAgentRAG(雙代理:背景 Slow Thinker + 前台 Fast Talker + FAISS speculative cache)——「料理長備好下一道菜」是這個元件的技術名字借來的比喻。
五個元件
- NextQuestionPredictor:消費最近 N=8 句 utterance,呼叫小型 LLM 產出 top-3 預測 query。Prompt 限制 query 必須具體可查(不允許「然後呢」這種空 query)
- PredictionPipeline:5 個 Op 串起——
ContextSampler→QuestionGenerator→QuestionRanker(按對話自然延續度排序)→QuestionDeduplicator(與最近 5 分鐘已預測的去重)→QuestionEmitter。每步可單獨換實作 - SpeculativeFetcher:拿 top-1 / top-2 預測 query 跑完整
recall()(含 11-stage scoring + reranker + cascade,視 intent 而定),結果寫進 Redismemvault:prefetch:{user_id}:{query_hash},TTL 300s。Fire-and-forget,失敗不重試(下一輪 utterance 會再產一批) - Admission Control:5 條過濾規則,全過才放行 prefetch
conversation.length < 3 turns→ skip(樣本太少)topic_drift_score > 0.7→ skip(剛跑題,舊 context 沒參考價值)last_predict_age < 30s→ skip(剛剛才動過,避免反覆 fetch)predicted_query.confidence < 0.4→ skip(小型 LLM 也不確定,硬跑只是燒 token)min_sample_threshold:使用者conversation_count < 10整條 Slow Thinker bypass(冷啟動期不開)
- 注入點:在
QueryClassifyOp內。實際recall()進來時,先看memvault:prefetch:{user_id}:{query_hash},hit 就直接拿 prefetched 結果走 Output Formatter(省掉中間整條 pipeline);miss 就走原本流程,零代價降級
跟 Read-Time Sanitize 的關係
Prefetch cache 是 Sanitize 的雙層套用區——進 cache 前 sanitize_for_injection()(write-time),出 cache 給 agent 前 Output Formatter 再 sanitize 一次(read-time)。Cache 不能是繞過 Read 端 sanitize 的後門,這是設計時就鎖死的不變式(invariant)。
為什麼這個設計
VoiceAgentRAG 的核心觀察是:語音 agent 的 inter-utterance 間隔(人開始講話到講完)足夠跑一次完整 RAG。把這段「人類在講話的時間」拿去 speculatively fetch,hit 的時候 latency 從 ~800ms 砍到 < 100ms(Redis 命中 + format),memvault 的對話介面也吃同一個時間窗。
miss rate 觀測值約 55–65%(top-3 預測命中其中一個),意思是 35–45% 的 prefetch 是純白燒——這是「願意花閒置 token 換 hit 時的零延遲」的明確選擇,所以 Admission Control 的 5 條規則是這個元件的命脈,不是 nice-to-have。
Read Track 三部曲到這收尾:意圖 → 召回 → 排序 → 重排 → 圖譜 → 打包 → 雙端 sanitize → 預測式預取。每一層都有 fallback、有 trade-off、有 golden query 守著,每天在跑。
Read Track 拆完。QueryClassify → fast/slow split → 11-stage scoring → reranker → OutputFormatter → read_sanitize → SlowThinker prefetch loop——七個組件、一條 entry-agnostic pipeline。
它不負責「memvault 有沒有記住該記住的事」,那是 Write Track 跟 Dream Loop 的責任。它只負責一件事:給定 query,把跨 session 對的那筆記憶 surface 上來。
真正花時間調的不是新組件,是 scoring 權重、prefetch 命中率、cascade 邊界判定這些長期參數。三部曲到此。
給 AI Agent 的健檢提示
把所有問題都丟給同一套向量搜尋,等於查事實跟找靈感走同一條路——兩邊都不對。這篇把讀取路徑按問題意圖拆開(分級分類、多訊號排序、動態權重、Attention Gate 省成本…),當一張對照表用。
把下面貼給你的 AI,讓它拿本文這套讀取設計逐條比對你的 RAG/記憶系統:
延伸閱讀
實際影響過這條 Read Track 設計的資源。
| 資源 | 為什麼重要 |
|---|---|
| HippoRAG (NeurIPS'24) | PPR Boost 和從命中實體沿圖走路的策略。Cascade Recall 的 LOCAL mode 基本上是這篇的落地 |
| LightRAG | LOCAL / GLOBAL / HYBRID 的命名跟 intent → mode 對應規則直接借自這邊 |
| Microsoft GraphRAG | L0 / L1 / L2 三層、用 L2 社群摘要做 zero-latency recall 的概念來源。Read 的 GLOBAL 模式吃的就是預先生成好的 L2 view |
| CRAG: Corrective Retrieval-Augmented Generation | Slow path 才跑的自評層。用 CRAG 的 evaluator 在 rerank 後做品質閘——fast 模式為了省 token 不跑這層 |
| Attention-Residual for Intent-Dependent Scoring | 11-stage scoring 裡 intent-dependent 權重的理論依據——不同 intent 調不同信號的權重向量,不是每個問題用同一套分數 |
| Cormack et al. 2009: Reciprocal Rank Fusion | Dense + Sparse 雙 ranker 合併排名的原始做法。Fast Search 的 RRF 融合直接採用這公式,k=60 也是這篇的默認值 |
| TheTom/turboquant_plus | Attention-gated Reranking 的早期 return 策略來自這裡。我的三個 skip 規則(候選太少、分數主導、緊密集群)用它的判斷邏輯重寫 |
| Kahneman — Maps of Bounded Rationality (Nobel Lecture) | Fast / Slow 二分的理論骨架。thinking_mode 本來要使用者自己選,後來重構成由 intent 自動決定,但 System 1 / System 2 的分工本身沒換。Cascade Recall 只在 slow mode 啟動就是這條血統 |
| VoiceAgentRAG (Salesforce AI Research) | Slow Thinker / 預取的靈感來源。雙代理架構(背景 Slow Thinker + 前台 Fast Talker + FAISS cache)對應我這邊的 slow_thinker.py + QueryJournal 預測 + Redis speculative cache |
| win4r / memory-lancedb-pro (LanceDB Pro autoRecall) | memvault 整套設計最早的啟發。autoRecall 的「主動注入」哲學(在 agent 思考前先把相關記憶塞進 context)、五層檢索 + 三層升遷 + Weibull 衰減,都在 memvault 這邊長出各自的對應實作。由 YouTube 頻道 AI超元域 介紹 openclaw 時引入 |
| memvault Write Track — 對話結束之後 | 三部曲第一篇。讀之前要先有東西讀——三道 gate、雙軌寫入、來源履歷怎麼打 |
| memvault Background Track — 存完之後 | 三部曲第二篇。Read 用到的 L1/L2 摘要、PPR、Interest Profile,都是 Background 跑出來的 |