Eric TechBlog
AIAI Assistant

Memory Architecture

記憶架構設計,包含長期記憶、情境記憶、短期記憶的三層設計,以及記憶的自動提取、對話反思和四層上下文整合。

設計問題

你上週告訴 AI 助理:

  • 搜尋 email 時先給我摘要,再決定要不要看全文
  • John 是我們正在談合約的外部律師

今天你再問它「幫我找 John 那封提到 deadline 的信」,它卻像第一次認識你一樣,重新把十幾封全文都丟出來。

這就是沒有記憶系統的 AI。每次對話都是一個全新的陌生人;一個 production-ready 的 AI 助理必須能「記得」用戶。

核心問題:如何設計一個記憶系統,讓 AI 能跨對話累積知識、自動學習用戶偏好,同時控制 token 成本?

記憶和檢索在解不同的問題

Retrieval Pipeline 負責找出「哪幾封 email 相關」; 這篇則負責讓系統記得「這位使用者偏好怎麼搜尋、怎麼呈現結果,以及過去哪些互動方式有效」。


記憶的三個維度

人類的記憶有不同的類型,AI 助理也需要:

Semantic Memory

跨所有對話的持久知識:用戶偏好、背景資訊、重要事實。

  • 「用戶搜尋 email 時偏好先看摘要,再決定是否讀全文」
  • 「John 是正在往來合約的外部律師」
  • 「用戶只想看過去 7 天內的投資人 email」

特性:少量、高價值、需要更新和刪除

Episodic Memory

對過去對話的「回憶」,不是逐字記錄,而是經驗摘要。

  • 「上次找合約 deadline 時,先用寄件人 + 日期篩選,再用語意搜尋補漏,效果很好」
  • 「上次搜尋 email 時,先給摘要再讓用戶選要讀哪幾封全文,互動最順」

特性:與特定對話綁定、幫助改進未來回應策略

Working Memory

當前對話的上下文。受限於 LLM 的 context window。

  • 最近 10 則訊息
  • 從舊訊息中語意搜尋出的相關內容,例如「剛才提到的 John、deadline、合約」

特性:每次對話臨時組裝、用完即棄


長期記憶的設計

資料模型

Memory {
  id: string
  title: string       ← 簡短描述,用於搜尋
  content: string     ← 詳細內容
  createdAt: datetime
  updatedAt: datetime
}

刻意保持簡單,只用一個標題和一段文字。不做更複雜的結構(如 key-value、知識圖譜),因為 LLM 擅長處理自然語言,不需要結構化資料。

記憶搜尋

每次用戶發送訊息時,系統搜尋最相關的記憶:

用戶訊息 → 組成語意查詢 → embedding 搜尋所有記憶 → 取 top 3

為什麼限制 3 個?

  • 每則記憶佔用 token
  • 太多記憶可能干擾 LLM(互相矛盾的資訊)
  • 語意搜尋已經保證了最相關的排在前面

記憶注入

搜尋到的記憶以結構化方式注入 system prompt:

<memories>
  <memory id="mem-1">
    用戶偏好:不吃辣,喜歡日料和義大利菜
  </memory>
  <memory id="mem-2">
    用戶的專案:正在開發一個電商平台,使用 Next.js
  </memory>
</memories>

用 XML tag 包裝,讓 LLM 清楚區分「這是記憶」和「這是對話內容」。

記憶自動提取

觸發時機:每次對話的 onFinish 回呼(不阻塞串流)

提取邏輯:把對話內容和現有記憶一起餵給 LLM,讓它判斷需要新增、更新或刪除什麼:

輸入:
  - 本次對話內容
  - 現有的所有記憶

輸出(結構化):
  - additions: [{ title, content }]     ← 新記憶
  - updates: [{ id, title, content }]   ← 更新的記憶
  - deletions: [id]                     ← 要刪除的記憶 ID

為什麼要看現有記憶?

如果不看,LLM 可能:

  • 重複新增已經存在的記憶
  • 新增與現有記憶矛盾的內容
  • 不知道該更新而非新增

保守策略

Prompt 明確指示「Be conservative — only add memories that will genuinely help personalize future conversations.」

不保守會怎樣?

記憶會快速膨脹,裡面充滿低價值的資訊(「用戶今天問了天氣」),搜尋的信噪比下降。

使用輕量模型

記憶提取用最便宜的模型。它不需要複雜推理,只需要判斷「這段對話有沒有值得長期記住的東西」。

衝突解決

如果同一個 ID 同時出現在 updatesdeletions 中(LLM 偶爾會犯這種錯),更新優先:

filteredDeletions = deletions.filter(id =>
  !updates.some(update => update.id === id)
)

情境記憶的設計

Chat Reflection(對話反思)

每次對話結束後,自動生成一份結構化的摘要:

ChatLLMSummary {
  tags: string[]         ← 2-4 個主題標籤
  summary: string        ← 一句話摘要
  whatWorkedWell: string  ← 有效的策略
  whatToAvoid: string     ← 應避免的做法
}

為什麼是這四個欄位?

  • tags:用於搜尋索引。「transformer_architecture」比「machine_learning」更有區分力
  • summary:快速了解對話做了什麼
  • whatWorkedWell:Agent 可以在類似場景中重複成功策略
  • whatToAvoid:Agent 可以避免重蹈覆轍

Prompt 設計的關鍵

好的反思是具體且可操作的

好:"Showing 3 email snippets first helped the user quickly identify the right contract thread"
壞:"Presented the search results well"

好:"Fetching full email bodies too early increased token usage before the user chose which thread mattered"
壞:"Used too many tokens"

壞的反思太抽象,對未來的對話毫無幫助。

反思摘要會被嵌入到對話的搜尋文本中:

chatToText = Title + Summary + WhatWorkedWell + WhatToAvoid + Tags + Messages

每次新對話時,搜尋前 3 個最相關的過去對話,注入 Agent 上下文:

用戶問題 → 語意搜尋過去的對話 → 取 top 3 → 注入 system prompt

這樣 Agent 不僅知道「上次聊過什麼」,還知道「上次什麼策略有效」。


短期記憶的設計

問題

對話可能長達 50 輪。但 LLM 有 token 限制,而且越長的 context 回答品質越差(lost in the middle 問題)。

滑動視窗 + 語意檢索

最近 10 則完整保留(維持對話的連貫性),較舊的 40 則透過語意搜尋找出與當前問題相關的 10 則。

結果:LLM 看到 20 則訊息(10 最近 + 10 語意相關),而非 50 則全量。

為什麼不只保留最近 N 則?

只保留最近的會丟失重要的上下文。例如:

  • 對話一開始設定的背景(「我在做一個電商專案」)
  • 30 分鐘前討論的結論(「我們決定用 PostgreSQL」)
  • 中間提到的約束(「預算是 5 萬美元」)

這些可能不在最近 10 則中,但語意搜尋會把它們找回來。


四層上下文的整合

每一層的 token 預算:

層級數量上限預估 token
長期記憶3 則~300
情境記憶3 段~1,500
舊訊息10 則~2,000
最近訊息10 則~2,000
合計~5,800

加上 system prompt 本身約 1,000 token,總共約 7,000 token 的輸入。這在大部分模型的 context window 中綽綽有餘,同時提供了豐富的上下文。


生產考量

記憶的隱私

記憶包含用戶的個人資訊。在多用戶環境中:

  • 記憶必須嚴格隔離(per-user)
  • 加密儲存
  • 支持用戶刪除(GDPR right to be forgotten)

記憶的品質控制

自動提取的記憶可能有問題(重複、矛盾、錯誤)。需要:

  • 用戶可以查看和編輯記憶的 UI
  • 定期的記憶清理機制
  • 記憶數量上限(避免無限膨脹)

記憶的評估

如何知道記憶系統有沒有用?

  • A/B 測試:有記憶 vs 沒記憶的回答品質
  • 用戶滿意度:記憶是否提升了個人化程度
  • Precision:注入的記憶有多少真的被用到了

後處理的容錯

記憶提取和對話反思都在後處理中執行。它們的失敗不應該影響用戶體驗:

Promise.allSettled([
  extractAndUpdateMemories(...),  ← 失敗了?log 一下,繼續
  reflectOnChat(...)              ← 失敗了?log 一下,繼續
])

allSettled 而非 all,確保互不影響。


重點總結

記憶類型來源生命週期搜尋方式
長期記憶對話自動提取 + 手動新增永久(可更新/刪除)語意搜尋 top 3
情境記憶對話結束自動反思與對話綁定語意搜尋 top 3
短期記憶當前對話訊息對話期間最近 10 + 語意 10
設計決策選擇原因
記憶格式自然語言 title + contentLLM 原生理解,不需要結構化
提取策略LLM 判斷 + 保守原則避免記憶膨脹
提取時機onFinish 異步不阻塞串流
反思結構tags + summary + 策略兼顧搜尋索引和經驗傳承
視窗策略滑動 + 語意搜尋保留連貫性 + 找回重要上下文

Last updated on

On this page