Skip to content
← 文章目次 HCYTLOG / 文章與筆記

讓產品 Agent 記住事情:對話狀態、長期記憶與脈絡預算

SDK 接續一段對話,不等於替產品建立長期記憶。從當次對話、歷史紀錄與整理後的偏好出發,拆開資料責任、記憶更新與模型輸入預算。

本文目錄

讓產品 Agent 記住事情,不是把所有歷史都送進模型。每次呼叫模型前,仍要從當次對話、歷史紀錄與整理後的偏好中,挑出這次需要的內容,確認哪些資料有權使用、哪些資訊仍然有效。這篇把這些責任拆開,讓對話接續與長期記憶可以分別設計。

記得很多,也可能答錯

使用者問:「照我上次的偏好,給我一個短版部署清單。」目前對話說趕時間,三個月前的對話說喜歡詳細說明,昨天又明確要求先給結論。若把歷史依時間全部塞入 prompt,模型會同時看到互相衝突的要求,卻不知道哪一個仍有效。

這個問題包含三個不同動作:從儲存層找到候選、判斷候選與本次請求的關係、決定哪些內容值得佔用模型輸入空間。檢索找到的內容,未必應該送給模型;沒有入選,也不代表應從資料庫刪除。

分開管理對話狀態與記憶資料

SDK 的對話狀態,和產品記憶分開設計

Agent 需要知道上一步呼叫了哪個工具,以及工具回傳什麼。SDK 能幫忙保存這段執行中的對話,但產品還需要處理另外的問題:使用者上週的偏好是否仍有效、哪個租戶可以讀哪段內容,以及更正後要移除哪些舊資料。

OpenAI Agents SDK 可以透過 session 接續對話,或由應用程式保存 result.history;也能選擇服務端管理的對話狀態。通常一段對話選一種接續方式,不要一面重送完整歷史,一面又指定服務端的對話 ID,造成資料重複。官方 TypeScript 範例的 MemorySession 是程序內記憶體,不能把它當成重新啟動後仍存在的產品儲存層。OpenAI:Running agents

Claude Agent SDK 用 resume: sessionId 接續指定對話。多使用者產品必須由後端保存「使用者、產品對話、SDK session」的對應,不能用最近一次 session 猜使用者想接哪段。它預設保存本機對話紀錄,若工作會被派到另一台主機,還要安排狀態儲存與讀取;單有一個 ID 不代表另一台主機已有那份內容。Claude:Sessions

若產品包含程式碼助理,Codex SDK 的 TypeScript 介面提供 startThread()、thread.run() 與 resumeThread(threadId),可接續本機 Codex 工作。這是程式開發任務的執行紀錄,不是跨供應商共用的記憶格式。Codex SDK

因此可以把 SDK 對話狀態放在「目前任務」這一側,產品的歷史檢索與偏好則由自己的資料服務管理。需要給模型看的資料再透過輸入或工具取回,登入身分、資料庫連線等只供後端使用的資訊,留在執行程式的狀態中,不必直接放進 prompt。OpenAI:Local context 與 model context

三層記憶是資料責任,不是三份全文

當次對話層保存目前的對話內容,例如本次目標與剛剛澄清的條件。歷史層保存過去的原始片段,能回答「這個偏好從哪裡來」。洞察層保存整理後的資訊,例如常用的回應格式,必須能回到原始片段,並帶上更新時間與不確定性。

三層資料可以同時取回,再統一整合。同時發出請求,可以讓等待時間重疊,但也要決定部分失敗的行為:當次對話取得成功、洞察服務失敗時,是沿用可用內容並標示缺口,還是停止請求?不能先寫 Promise.all,再把任一失敗都解讀成「沒有記憶」。

把資料放在 Redis 或 PostgreSQL 是儲存選擇,與它屬於哪種記憶責任不同。短期資料可以持久保存以便追查,長期偏好也可以快取;TTL 到期只是快取失效,不代表原始資料已刪除。

互動圖解 · 教學模型 記憶要放進去,但模型可讀的內容有限

先扣除系統提示與輸出保留,再把候選記憶放進剩餘預算。更換排序策略,觀察留下的內容。

先保留 1,000 tokens,剩下 1500 tokens 可放記憶。選入 7 / 10 筆,留有 20 tokens 空間;不夠放的整筆候選會跳過。

已分配 / 總預算
2480 / 2500 tokens
選入記憶
7 筆
剩餘空間
20 tokens
  1. 01
    先保留必要空間完成

    系統提示 450 + 輸出保留 550 tokens。輸出保留不是已生成的內容。

  2. 02
    當次對話優先完成

    對話片段 1、對話片段 2、對話片段 3

  3. 03
    歷史候選完成

    歷史片段 1、歷史片段 2、歷史片段 3、歷史片段 4

  4. 04
    偏好候選略過

    沒有整筆候選能放入剩餘預算

  • 系統提示450 / 2500 tokens

    固定保留

  • 輸出保留550 / 2500 tokens

    不是已使用的輸出

  • 當次對話480 / 2500 tokens

    對話片段 1、對話片段 2、對話片段 3

  • 歷史1000 / 2500 tokens

    歷史片段 1、歷史片段 2、歷史片段 3、歷史片段 4

  • 偏好0 / 2500 tokens

    沒有整筆候選能放入剩餘預算

  • 未分配20 / 2500 tokens

    不為了填滿預算而切碎候選

圖中的資料只用來說明流程,不是實際服務的測試結果。 token 長度與相關性分數是手動指定的。採用整筆選取,不是實際 tokenizer、向量檢索或摘要壓縮;洞察只代表示例偏好紀錄,不推論人格。

先調整可取回的歷史筆數,觀察候選如何增加。接著在「近期優先」與「相關性優先」之間切換,再縮小總脈絡預算。看清楚哪些內容只是進入候選清單,哪些真正進入模型輸入。

圖中的長條與摘要用固定成本展示選擇結果。預算必須先保留系統提示與預期輸出,剩下的部分才分配給對話與記憶。提高歷史筆數卻沒有提高容量時,送入 prompt 的資訊不應無限制成長。這裡沒有執行 embedding,也沒有測量真實 token。

選出本次可用的記憶

排序之前,先排除不該使用的資料

候選至少要包含擁有者、來源片段、更新時間、內容類型與估計 token。查詢時先限定租戶與使用者,不能先對全部資料計算相似度,再希望模型自行忽略別人的內容。

洞察也不能直接升級成已確認事實。模型從兩段話推測「偏好詳細回答」,但本次使用者明確要求短版,就應尊重當次要求,保留洞察只是舊推論的標記。對於互斥事實,留下來源與版本,必要時要求澄清,避免把矛盾句子拼成一個看似一致的摘要。

通過資料邊界後,才討論相關性與時間衰減。教學上可以用 score = relevance × exp(-decay × age) 展示新近性如何改變排序。但 relevance 是排序訊號,沒有經過校準就不是正確機率。使用頻率高也可能只是同一項錯誤被重複取回,不能讓它永久壓過新資訊。

若正式系統使用 pgvector,餘弦距離可作為檢索訊號;近似索引則會用召回率換查詢速度。這是另一個需要量測的取捨,不能因加了索引就宣稱記憶更準確。pgvector 官方文件

預算要包含答案的位置

把模型容量全分給歷史,回應就可能沒有足夠空間。正式預算至少包括系統提示、當次輸入、工具 schema 與結果、記憶、輸出預留及格式誤差:

type Budget = {
  contextLimit: number;
  system: number;
  currentInput: number;
  tools: number;
  outputReserve: number;
  margin: number;
};
type Candidate = { id: string; score: number; tokens: number };

function selectMemory(candidates: Candidate[], budget: Budget) {
  let remaining = Math.max(0,
    budget.contextLimit - budget.system - budget.currentInput
    - budget.tools - budget.outputReserve - budget.margin,
  );
  const selected: Candidate[] = [];
  for (const item of [...candidates].sort((a, b) => b.score - a.score)) {
    if (item.tokens > remaining) continue;
    selected.push(item);
    remaining -= item.tokens;
  }
  return { selected, remaining };
}

這段只負責候選選擇,假設各成本已用目標模型的計數方式估算。如果固定部分已超出容量,回傳零筆記憶還不夠,呼叫端必須拒絕生成或縮減其他輸入。記憶格式化後也要重新計數,因為來源標籤與分隔符會佔用空間。

依分數貪婪選擇容易理解,但不保證取得最佳組合。一段很長的高分內容可能擠掉數段共同回答問題的短文;重複內容也可能佔用兩次預算。正式服務可先移除重複內容、限制單一來源佔比,或將必要事實與補充背景分別分配預算,不必一開始就導入複雜最佳化演算法。

壓縮與更新不能擦掉證據

摘要可以縮短內容,但會遺漏原有措辭、條件與例外。「這次想看短版」若被壓縮成「只接受短答案」,就把暫時需求寫成長期偏好。保存摘要時應保留來源識別碼、產生版本與可更新狀態,讓修正能影響之後的檢索。

刪除或更正記憶也要考慮衍生資料:原文、embedding、摘要、洞察與快取可能位於不同位置。原文更新後,只更新一個 Redis key 不代表其他版本已收斂。需要追蹤更新事件、失敗重送與可觀察的同步狀態,不能把「最終一致」當成會自行發生的承諾。

檢索結果的快取鍵也要包含身分與資料版本。只用查詢文字當 key,兩個人都問「我偏好什麼」時,就可能共用錯誤結果;只加使用者識別碼,則在偏好更正後仍可能命中舊版本。需要決定更新時主動失效,或在可接受時間內透過版本與 TTL 淘汰,並測試這段資料可能不一致的期間。

同一段對話中,同時送出的請求也會改變資料順序。較早送出的請求不一定較早完成,因此不能用生成完成時間直接覆蓋較新的明示偏好。保存事件順序與資料修訂號,讓整合端辨識「晚到的舊結果」。若沒有足夠資訊決定哪個事實有效,留下衝突,比自動合併成新事實更容易追查。

驗證模型實際拿到的內容

測試固定候選與預算,檢查入選內容不超量、來源可追查、其他租戶資料不會出現。再用明確的新偏好覆蓋舊偏好,確認排序與整合不會悄悄恢復過時要求。

查詢品質與回答品質要分開評估。找到相近句子不代表回答有用;召回相關記憶,也不代表模型遵守它。可以記錄候選來源、選入理由、排除原因與預算,不必把完整私人對話寫進監控紀錄。最值得看的結果,是同一請求在相同資料版本下,能否重現那份實際送出的模型輸入。

延伸閱讀

SDK 文件核對日期:2026-10-07。SDK 接續狀態與長期記憶檢索,是兩個需要各自驗證的部分。

本篇屬於「在產品裡打造 Agentic 功能」系列。可以先看產品架構與 Agent 迴圈。

上一篇:LLM 安全防線。下一篇:多家 LLM 怎麼切換。