#5 記住了,就代表記對了嗎?
當 Agent 終於不再失憶,下一個會遇到的問題是:它會不會一直記錯?
第一季那四篇,我們一路從 Memory 到底是什麼,聊到記憶長什麼樣、該記什麼,還有記憶是怎麼形成、更新跟消失的。走到這裡,Agent 好像終於可以開始累積經驗了。不過先等一下,它累積下來的這些東西,真的都值得相信嗎?
拿前幾篇提過那個 migration 事故繼續往下想。Agent 修改了資料庫結構,結果 Production 炸了。追查後才發現,原來舊版 API 其實還在使用那些被砍掉的欄位。
理想上它應該學到,修改資料庫結構前得先確認還有哪些服務依賴舊欄位。但如果它最後整理出來的結論變成「這個專案絕對不能做 schema migration」,事情就開始不太對勁了。
更麻煩的是,幾個月後舊 API 早就退役,相關依賴也都移除了,結果當你請 Agent 規劃下一次 migration 時,它卻很有把握地回你:「不建議,我記得這個專案做 migration 會出問題。」
它完全沒有失憶,甚至還非常認真地參考了過去的經驗。只是它把一個有條件的教訓,硬生生記成了沒有條件的鐵則。
這就是第二季第一篇想聊的問題。
當大家都在喊 Memory 要有 Reliability,我們究竟是在要求什麼?又要怎麼把「可靠」真正做進系統裡,而不只是當成寫在產品介紹裡的 buzzword?
這篇我會先聚焦在可以被讀取、修改與追溯的外部記憶,像是文字筆記、結構化紀錄跟知識圖譜。至於那些已經被學進模型參數裡的記憶,要怎麼修正跟驗證完全是另外一回事了。
1. 問題其實不只出在回答,也很可能出在「記下來」的那一刻
假設事故剛發生,對話裡有人說:「我猜可能是 API v1 還在使用這個欄位,不過還沒確認。」
Memory 系統整理完,留下的卻是:「API v1 使用這個欄位,導致這次事故。」
表面上看只是把句子縮短,但「我猜」、「可能」、「還沒確認」這些關鍵字全部被吃掉了。一個明明還在待驗證階段的假設,就這樣變成未來 Agent 拿來引用的鐵錚錚的事實。
HaluMem 就是把評估拆到這一層來做。它沒有只盯著最後的答案看,反而分別去檢查記憶的萃取、更新,還有基於記憶的問答。下圖可以特別留意它的檢查點,除了最右側的問答結果,它也往前去抓記憶到底是怎麼被寫入跟修改的。

圖 1. 把檢查點往前移,才能區分錯誤發生在記憶萃取、更新,還是最後的問答。
看到這張圖我自己的感覺是,同一個錯誤的答案,背後可能需要完全不同的修法。拿前面的例子來說,可能是「還沒確認」這四個字在萃取時就被丟掉,也可能是後來已經查明原因,但舊記憶卻沒有跟著更新。如果連錯誤是從哪一步開始的都不知道,其實很容易變成一直在盲調回答的 prompt,根本沒修到真正出問題的地方。
這讓除錯這件事變得有點不一樣了。以前碰到問題,我們大概只會問為什麼模型這次又亂講,現在還得往前追:它到底是這次理解錯,還是早在半個月前就已經把錯的東西存進資料庫了?
所以我自己對可靠度的理解會是,留下來的東西得要有根據,情況改變時能被修正,而且真正要用的時候也得知道現在適不適合用。這也代表我們不能偷懶只在 prompt 最後加一句「請確認答案正確」,而是得老老實實沿著一份記憶的生命週期,看看每個環節到底能做些什麼。
接下來我用同一個 migration 的案例走一次。這算是我目前整理下來的實作起點,不是要當成哪套產品的標準答案。
2. 寫入時,除了存結論,還得留下它憑什麼成立
如果今天打開編輯器準備動手,我第一個會改的地方就是不要讓每份記憶只剩下一個乾巴巴的 content 欄位。
把資料模型建得多複雜其實沒那麼重要,重點是至少得讓系統之後還能追問這句話到底是誰說的、根據什麼,還有適用在什麼場景。
假設後續真的查到呼叫紀錄,確認 API v1 仍然依賴舊欄位,那麼留下的記憶可以長這樣:
要留下的資訊 | 這個案例裡的內容 |
|---|---|
結論 | 在相關依賴移除前,不要刪除 |
適用範圍 | 訂單服務的 Production 環境。 |
成立前提 | 仍有 API v1 client 使用這個欄位。 |
來源 | 事故報告中的相關段落,以及依賴檢查的結果。 |
時間 | 哪一天確認依賴存在;哪一天寫入這份記憶。 |
判斷狀態 | 已根據上述證據確認;前提改變時需要重查。 |
重點真的不在欄位名稱要叫什麼,而是得把「不能刪」和「為什麼不能刪」牢牢綁在一起存。不然幾個月後就算 Agent 記得很清楚,它也不會知道什麼條件改變之後這條限制應該重新去檢查。
不過多存這幾個欄位問題就解決了嗎?其實還差得遠。
假設模型自己通靈生出了一個根本不存在的來源 ID,或是引用的段落根本沒提到這件事,資料表面上看過去還是一樣很完整。所以寫入的時候,我會把檢查拆成兩層。
第一層是程式能直接檢查的死東西。例如把原始訊息編號,要求模型精準指出支持這條記憶的訊息在哪裡。程式確認位置真的存在後,再從原始訊息把片段抓出來,千萬不要讓模型自己通靈重寫一份原文。來源如果找不到,這筆就不能當作有來源的記憶通過。
第二層才是語意的判斷。把候選記憶跟來源片段放一起檢查,重點在確認來源是不是真的支持這個結論,有沒有把「可能」寫成「確定」,或者把一次有效當成永遠有效。這一層可以交給模型輔助,風險高的時候也可以交給人來確認,但我自己是絕對不會把多套一層 LLM 檢查當成什麼真理保證。
以這個例子來說,如果手上只有「我懷疑 API v1 有依賴」,那就該乖乖保留成待查假設,不該急著升級成已確認的事故原因。至於那些會影響 Production 的操作規則,我會要求更直接的依據,比如相關程式碼、測試或執行紀錄,單憑一段寫得看起來很有道理的 reflection 老實說是完全不夠的。
畢竟告訴模型「不要過度推論」,跟實際去檢查它有沒有過度推論,完全是兩個世界的事。而且就算來源能證明某位工程師說過這句話,也不代表那句話就是真理。保留來源是為了日後出事好追查,這跟幫每句話自動蓋上正確印章是兩碼子事。
3. 更新時,存一條新的不代表舊的問題就解決了
幾個月後,Agent 讀到了一筆新的正式紀錄:所有相關 API v1 client 已經退役。
最直覺的解法就是直接新增一條記憶。但原本那條「不能刪除舊欄位」的限制可能還穩穩地躺在資料庫裡。於是你的系統會同時記得 API v1 已經退役,還有因為 API v1 還在所以不能移除舊欄位。
兩條都找得到也都有來源。接著呢?難道讓負責回答問題的 Agent 每次都自己通靈猜一次嗎?
所以我希望更新流程能多做一步。新資訊進來的時候,除了確認要新增什麼,還得順便抓出哪些舊判斷需要重新檢查。
首先得先分清楚「什麼時候知道」跟「什麼時候成立」。假設 9 月才匯入一份 6 月的事故報告,總不能因為它比較晚進資料庫,就順手把 8 月已經完成的修正給蓋掉吧。所以記憶的紀錄必須把事件發生的時間跟寫入的時間分開存,時間不明就保留不明,千萬不要硬把匯入日期塞成事件日期。
Zep 的文件 裡就有區分系統得知資訊的時間,以及事實成立、失效的時間,藉此把變化的歷史留下來。這算是給了處理時效一個很不錯的結構,當然這也不代表每次時間抽取跟失效判斷系統都能自動搞對。
同樣的道理,Production 使用 API v1、Staging 使用 API v2 也不見得是衝突,它們可能單純就是在講不同的環境。因此更新前得先乖乖對齊對象、範圍跟時間,一看到文字不一樣就直接拿新句子去蓋舊句子,老實說非常危險。
接下來得找出誰依賴了那個已經改變的前提。回到剛剛的案例,我們可以把兩份記憶拆開來看:一份記錄「API v1 仍然使用舊欄位」,另一份記錄「因為這項依賴,目前不能移除舊欄位」。很明顯第二份的成立完全依賴第一份。
一開始其實不一定要搞個很大的知識圖譜,就算是普通資料表,也完全可以替第二份記憶保留它依賴了哪筆紀錄的哪個版本。當第一份出現新證據的時候,系統就能順著找到第二份,把它標成需要重新確認。這個連結真正的用途是讓更新能確實找到該影響的地方,如果單純只是為了讓系統架構圖看起來很厲害而去建,那真的大可不必。
不過這些連結也不會自己長出來。我會先讓 Agent 自己提議依賴關係,然後對重要的規則再人工確認一次。這當然還是有漏掉的可能,不是加了個 depends_on 欄位就能神奇地找出所有隱含影響。
而且重新確認也不等於可以直接宣佈現在所有的 migration 都安全。API v1 退役頂多只能推翻「因為它仍在使用」這個理由,至於其他服務有沒有依賴、這次變更本身到底安不安全,仍然得另外好好檢查。
還有一個很容易踩坑的地方,就是那些曾經轉述過它的地方。假設那條限制早就被整理進「部署注意事項」或是某份週報的摘要裡了,如果原始記憶改對了,摘要卻還在傻傻地說不能移除舊欄位,之後 Agent 一從摘要讀取資料,舊結論馬上又會像鬼打牆一樣跑回來。
因此除了記下結論依賴哪個前提,我也會記錄摘要當初是根據哪些來源版本產生的。來源被修改的時候,衍生的摘要得先標為需要重建,在重新產生之前,絕不能再把它當作最新的現況交出去。前提改變要重新判斷結論,原文改變要重新檢查轉述,總不能我們在這邊辛苦改好了一張卡,卻放任其他地方繼續幫它傳錯話。
但除了找出哪些記憶需要更新,還有一個很現實的問題:這次修改本身,有沒有順手把原本還有效的資訊也一起改壞了?
TrustMem 則是把驗證重點放在「一次記憶變更」上。它沒有單純拿修改後的內容問「這樣對不對」,它會把新輸入、相關記憶的修改前後狀態,還有系統實際做了哪些修改全部放在一起來看。下圖可以先看上半部中間的 Memory Transition Verifier。裡面的 Coverage、Preservation、Faithfulness,我覺得可以直接看成三個問題:該補的有沒有補?原本還有效的有沒有保留?新寫的有沒有超出證據?

圖 2. 檢查一次記憶修改,不只看新資訊是否寫入,也看舊資訊是否被改壞,以及新增內容是否有根據,本文聚焦上半部的 Overview。
套回 migration 的例子,更新時總不能只確認「API v1 已退役」成功寫入,還得確定系統沒有順手把其他有效的部署限制給刪了,或者自己通靈補出一句「現在所有 migration 都安全」。
這樣一來,可靠度就不再只是最後答案上的一個抽象分數,而是可以扎扎實實放進每次修改流程裡的具體檢查。
當然啦,這裡借用的是它的檢查思路。原論文其實也有利用驗證訊號來訓練記憶更新策略,不能天真地以為「多加一道 LLM 檢查」就是在重現整套方法。但它點出了一件很核心的事,就是這次回答正確,完全不代表改記憶的過程裡沒有留下其他的坑。
4. 讀取時,相關不代表現在可以直接照做
做到這裡可能會覺得,記憶有來源、有狀態也會更新了,應該差不多了吧?其實還有最後一段路要走,就是這些資訊得真的影響 Agent 怎麼使用記憶才算數。
假設資料庫早就把舊限制標成已失效,但送進 Context 的時候卻只剩下「不要移除 customer_legacy 欄位」這句話,那前面辛苦做的那些事,根本沒有傳遞到 Agent 手上。
所以我會把讀取流程改成先在授權範圍內找候選,再檢查它們的適用範圍、有效狀態跟版本,單純只把最相關的幾段文字選出來絕對是不夠的。能由程式確定的條件就直接篩選,需要語意判斷的部分再交給 Agent,而且同時要把判斷需要的證據一起丟過去。
最後交出去的東西,大概會長這樣:
目前可確認的資訊:API v1 的相關 client 已退役。
歷史限制:過去曾因為 v1 的依賴禁止移除舊欄位,但這個理由已不再適用。
仍待檢查的事:是否還有其他服務依賴該欄位,目前沒有足夠資訊。
這跟單純丟一句「API v1 已退役」差非常多,因為它同時交代了哪些舊判斷已經不能繼續沿用,而且清楚講明了什麼還不知道。
STALE 研究的正好是這類問題。除了測試 Agent 能不能察覺舊記憶失效,它還考驗當問題依舊帶著過時假設時,Agent 會不會照單全收,以及新的狀態能不能真的改變它後續給出的建議。下圖用了一個很生活的情境。使用者原本提到每天騎腳踏車上班,後來卻說自己腿受傷了。後一句並沒有直接要求「請修改我的通勤記憶」,但這時候 Agent 已經不能再毫無保留地沿用原本的通勤假設了。

圖 3. 新訊息可能讓另一條記憶不再適用。評估不只要確認 Agent 察覺變化,也要確認它能抵抗過時前提,並調整後續建議
看這張圖時,我會特別留意最右側。直接問「他現在還每天騎車上班嗎」,跟請 Agent「規劃這週的通勤方式」,測的根本不是同一件事。前者只是在看它能不能辨認狀態改變,後者才真正去看它會不會把變化應用在建議裡。
回到 migration 的案例也是一樣。Agent 能夠流利地回答「API v1 已退役」,不代表它在規劃變更時,就會真的停止引用那條早就失去依據的限制。
我們得確認的,其實是它知道世界改變後,接下來的判斷有沒有真的跟著變。
為了處理這類問題,論文裡另外提出了一個 CUPMem 原型,在寫入時先判斷舊狀態應該保留、替換、標為失效,還是保留為尚未確定;讀取的時候再根據這些狀態來限制使用方式,不再是把新舊片段一股腦全丟給回答模型,放任它臨時去猜到底哪個能用。
我自己的感覺是,知道舊答案不能用了,不代表就已經知道新答案。假設舊限制被撤銷,但目前還沒完成新的依賴檢查,系統應該讓 Agent 知道還需要查證,而不是默默退回舊答案,更不能把沒查到限制當成可以放心執行。尤其遇到會改動真實系統的操作,Memory 確實可以提示該檢查什麼,但絕不能取代執行前的必要驗證。
5. 要怎麼知道改完真的比較可靠?
走到這裡,系統多了來源、狀態、版本跟一堆更新流程,但這也很可能只是把系統搞得很龐大複雜而已。到底有沒有比較可靠,老實說最後還是得測。
LongMemEval 早就已經把知識更新、時間推理,以及資訊不足時能不能承認不知道這幾件事,都納入了長期記憶評估。所以大家其實早就開始關心這些問題了,我覺得真正值得注意的是,要怎麼把這些能力拆成自己產品裡可以反覆跑的重現測試。
如果要動手做,我會先把前面的案例寫成一段固定時間線,而不會一開始就好高騖遠去追求一個很大的綜合分數。可以先提供事故初期的猜測,再給出確認結果讓系統形成記憶,接著加入 API 退役的證據,最後開一個全新的 session 重新要求它規劃 migration。
這個新 session 不帶前面整段對話,只給它正常會拿到的工具與記憶。這樣才比較看得出來,剛剛的更新到底有沒有真的留在 Memory 裡,而不是模型剛好還看得到上一句對話而已。
我會先驗收這幾件事:
測試情境 | 什麼行為才算通過 |
|---|---|
原文只說「可能是 API v1,還沒確認」 | 可以留下待查假設,但不能寫成已確認原因。 |
已有證據確認依賴,後來又確認依賴解除 | 新 session 不再把舊依賴當成目前的禁令,也不因此省略其他必要檢查。 |
較晚匯入的是較早的事故文件 | 不因匯入時間較新,就覆蓋後來已確認的變更。 |
修改後重新產生摘要,再從摘要查詢 | 舊限制不會以現行規則的身分跑回來。 |
已知舊答案失效,但新狀態還不明 | 先查證或追問,不沿用舊預設,也不自行補出確定答案。 |
而且不能只看最後回答,每個階段都得留下可以檢查的結果。像是萃取後存了什麼、更新時改了哪幾筆、這次讀取到底送出了哪些版本,還有 Agent 最後究竟做了什麼。否則要是這一題剛好矇對了,我們根本不知道那條錯誤記憶是不是其實還躺在那裡。同樣地,修改 migration 限制的時候,也要確認旁邊無關的有效規則沒有被無辜改壞。
如果再往實際一點的場景走,我還會分開記錄哪條記憶被找到、哪條真的進了 Context、Agent 的計畫有沒有反映它,還有最後任務到底有沒有完成,因為這幾個根本是完全不同的事。例如 Agent 的回答引用了那條限制,卻在下一個工具呼叫直接跳過檢查,這絕對不能算它真的學會了。反過來說,任務最後成功了,也不能只因為某條記憶剛好出現在 Context 裡,就宣稱是這條記憶帶來的改善。
要判斷有沒有幫助,我會在可比較的模型、工具、任務跟資源條件下,測試沒有長期記憶、直接提供歷史資料,以及使用這套 Memory 流程的差別。除了看任務結果,也要看重複犯錯、過時資訊被採用的情況、多問了多少不必要的問題,還有實際花了多少時間跟成本。同一個情境還得重跑或換個問法,通過一次往往只代表那一次剛好通過。
畢竟可靠度不能單靠「什麼都不記、什麼都反問」來換。我們要的是該留下的資訊留得住、該修正的能修正,該用的時候真的能幫上忙,而不是養出一個永遠不敢做事的 Agent。
結語:可靠的記憶不是永遠不改口
回頭看,這篇其實也沒有要大家去換個多大的模型,或是把儲存架構整個打掉重練。
第一步其實可以做得很小。挑一種真的會反覆用到的記憶,保留它的來源與成立條件,讓修改能實際影響後續的取用,再寫出幾個「這樣才算過」的測試。先讓一個完整的案例跑得通,再逐步擴大就好。
這也是我持續研究 Memory Reliability,並放回 Cairn 實作裡對照的方向。比起在規格表上多列幾項「可信」功能,我更在意我們能不能具體回答:一條記憶被修正之後,下一次 Agent 的判斷到底有沒有跟著改?有些是現在就能加上的工程機制,有些確實還需要實驗,能把這兩者分清楚,本身也是系統可靠度的一部分。
第一季我們主要在探討 Agent 怎麼把過去留下來。到了第二季,真正需要追問的其實是當過去不再適用,它有沒有辦法發現,並且有根據地改口。沒有記憶的 Agent 每次都得重新學,但沒有修正能力的 Agent 反而會把同一個錯誤當成經驗一直傳下去。我想要的 Agent 不用永遠記得自己說過什麼,但它必須知道自己為什麼相信,以及什麼時候應該不再相信。
這篇我們先假設大家都沒有惡意,純粹只是記錯、理解錯,或是沒有跟上世界的變化。下一篇就要再往深一點走了:如果有人知道 Agent 會相信自己的記憶,並且刻意讓錯誤的東西被留下來呢? 也歡迎大家寫寫最近遇到的問題
作者:Chi