做到 Day 19,Agent 已經能呼叫真實模型,也有輸入、檢索、工具和後端授權邊界。接下來最容易讓資料越積越多的功能,是記憶。
記住「這台測試筆電是 Windows 11」很方便,下次不用再問一次。但如果同一套機制也收下備用碼,或把一句臨時要求當成永久偏好,風險就不會在這次 run 結束時消失。
今天先不接向量資料庫。我做的是一個小型 session memory policy,確認哪些內容可以暫存、哪些必須拒絕,以及另一個 tenant 能不能讀到這份資料。
我會把 memory 分成四類:
| 類型 | 用途 | 預設生命週期 |
|---|---|---|
| 對話歷史 | 保存目前對話的訊息 | request 或 session |
| Working memory | 整理完成當前任務需要的狀態 | session |
| Long-term memory | 跨 session 保存偏好或事實 | 必須有保留與刪除規則 |
| RAG 知識庫 | 系統管理的 SOP 與參考文件 | 由文件治理政策決定 |
它們都可能出現在模型 context,卻不是同一種資料。RAG 文件有來源與 ACL;long-term memory 則需要回答「誰要求記住」「使用者能否查看或刪除」「多久後失效」。如果全部丟進同一個向量資料庫,之後很難追查一段文字到底是 SOP、聊天內容,還是模型自己整理出的筆記。
Day 20 的預設 Live Prompt 會讓模型提出三筆資料:
device = Windows 11 → working memory
secret = 測試用備用碼 → working memory
tone = 永遠使用簡短回答 → long-term preference
第一筆只留在目前 tenant_id + session_id。第二筆命中敏感內容規則,直接拒絕。第三筆雖然不是秘密,我仍不讓它自動成為長期偏好,因為一句聊天內容可能只是當下要求,不代表使用者同意永久保存。
if contains_sensitive_value(record.value):
return blocked("sensitive_memory_write")
if record.kind == "preference":
return blocked("persistent_memory_requires_approval")
這個 regex 只能抓固定測試格式,不是完整的 secret detector。真正的系統還要處理加密、保留期限、刪除 API、使用者查詢權與稽核紀錄。今天先把最重要的預設值寫死:不確定是否該記,就先不要寫入。
只用 session_id 當 key 不夠。兩個 tenant 可能產生相同的 session id,測試環境也可能重複使用固定字串。
因此 store 使用這組 key:
(tenant_id, session_id, memory_key)
回歸測試會另外用 campus-b 讀取 campus-a 的 session-20,結果必須是空集合。這不是完整的資料庫 Row-Level Security,但至少能把隔離條件寫進介面與測試,而不是只靠呼叫端記得加 filter。
Day 20 沿用 Day 11 接好的 ChatOpenAI。Live 實驗會讓模型從我輸入的 Prompt 中提出 memory candidates,再由 memory policy 決定哪些資料可以寫入:
授權讀取 session memory
→ 只組入 model-visible memory
→ Live LLM
→ 產生候選 memory write
→ 敏感資料與保存類型檢查
→ 寫入或等待人工確認
Day 20 的 Live 實驗會讓模型真的把自然語句拆成 memory candidates:裝置是 working memory、回答語氣是 preference,備用碼則交給後端敏感資料政策判斷。模型只負責提案,不能繞過寫入 policy。固定情境仍保留作回歸測試。

這次我改用 gemini-3.5-flash-lite 跑 Live smoke test。測試會真的送出三組 mock Helpdesk 訊息:
search_it_sop,再呼叫 create_ticket,最後只能出現一張 high-priority mock 工單。第一次接 Gemini 時,我沿用 OpenAI-compatible endpoint。Day 13 這種單輪回答可以完成,但 Agent 把工具結果送回模型時失敗了。Gemini 3 的 function call 會帶 thought signature,後續步驟必須原樣送回;原本的相容層沒有替這條 LangChain Agent 路徑保留簽章。我因此改用 langchain-google-genai,交給 Google GenAI SDK 處理多步工具呼叫。
簽章問題修好後,第三個案例又抓到另一個差異:惡意 SOP 被隔離、沒有文件可用時,Gemini 會再次要求查 SOP。這不會造成寫入,卻會浪費模型與工具預算。我把 search_it_sop 限制為每次 Agent 執行只能呼叫一次;工具內部遇到暫時錯誤時的 retry 仍由另一組預算控制,兩者不是同一件事。
新增 middleware 後,正常的「查 SOP、開工單、回答」流程會經過更多 LangGraph super-steps,所以 Live graph 上限調成 32。模型呼叫仍限制 3 次,全部工具合計 4 次,SOP 查詢則是 1 次。graph step、模型呼叫和工具呼叫是不同單位,不能只留一個很大的 recursion limit 就說有執行預算。
修改後三個 Live 案例都通過。實際工具順序、是否建立工單,以及 retrieval injection 的 quarantined trace 都由 assertion 檢查,不是只看回答像不像成功。
python scripts/live_llm_smoke.py
單元測試會確認三件事:working memory 只能由同一個 tenant 與 session 讀取、敏感內容不會寫入、長期 preference 在沒有核准時維持 blocked。頁面 trace 則只顯示欄位名稱和決策,不會把測試備用碼內容寫出來。
python -m unittest discover -s tests -v
目前整合版本共有 140 個 unit tests 通過,Promptfoo 是 25 passed、0 failed、0 errors。Live smoke test 另外是 3 passed,沒有混進 Promptfoo 的固定通過率,因為真實模型的延遲和輸出會受當下服務狀態影響。
下一篇會繼續處理記憶:如果 Agent 把 PII、錯誤的使用者偏好或別人的資料寫進 long-term memory,使用者要怎麼查詢與刪除。
iThome鐵人賽