昨天我們完成了 Memora 的 Short-term Memory。它能保存目前這段對話、壓縮較舊的內容,並在 Input Token Budget 內建立下一輪需要的 Context。
但只要關閉程式,這些資料還是會消失。
進入 Chapter 3 之後,我們準備讓 Memora 把資訊帶到下一段 Conversation。這時最直覺的做法可能是:
把 conversation_history 全部存進 Database
但這樣就算是 Long-term Memory 了嗎?
還不算。因為聊天紀錄只是記錄「發生過什麼」,真正的 Memory 還必須回答另一個問題:
這段對話裡,有哪些資訊值得在未來繼續保留?
今天先不急著使用 Database,也不讓 LLM 自動判斷。我們會先建立一個最簡單的 memory_candidates,把 Conversation History 和值得保存的 Memory 分開。
假設 Memora 有這段 Conversation:
User:
Hello!
Assistant:
Hello! How can I help you today?
User:
我的英文程度是 B1。
Assistant:
了解,我會用適合 B1 的方式協助你。
User:
我想加強旅遊英文。
Assistant:
沒問題,我們可以從機場和飯店情境開始。
User:
先給我三個機場常用單字。
Assistant:
passport、boarding pass、luggage
如果保存完整 Conversation History,我們會得到:
Hello!
Hello! How can I help you today?
我的英文程度是 B1。
了解,我會用適合 B1 的方式協助你。
我想加強旅遊英文。
沒問題,我們可以從機場和飯店情境開始。
先給我三個機場常用單字。
passport、boarding pass、luggage
這些內容確實都是聊天的一部分,但不代表每一句都值得成為 Long-term Memory。
真正可能在未來繼續有用的資訊是:
使用者的英文程度是 B1。
使用者想加強旅遊英文。
至於:
Hello!
先給我三個機場常用單字。
passport、boarding pass、luggage
比較像是當下的互動內容。它們可以留在 Conversation History 裡,但不一定需要跨 Conversation 保存。
因此,兩者的用途並不相同:
| Conversation History | Memory |
|---|---|
| 記錄對話發生過什麼 | 保存未來仍可能有用的資訊 |
| 通常按照時間排列 | 通常按照事實或意義整理 |
| 包含 User 與 Assistant Message | 不需要保存每一句話 |
| 主要維持目前這段 Conversation | 可能影響未來不同 Conversation |
| 重視完整性 | 重視選擇性 |
Conversation History 比較像逐字稿,而 Memory 比較像從逐字稿中留下的重點。
Day 9 已經替 Memora 加入:
conversation_summary
既然 Summary 也會保留對話重點,它和 Long-term Memory 有什麼差別?
最直接的區分是:
Conversation Summary
回答的是:
「這段對話前面發生了什麼?」
而 Long-term Memory 想回答的是:
Memory
回答的是:
「未來還需要記得使用者的什麼?」
例如一段 Summary 可能是:
使用者正在練習現在完成式。Assistant 已解釋基本用法,
並提供三題練習。使用者答錯第二題,目前正在確認原因。
這份 Summary 對延續目前的對話很有用。但幾天後,使用者開啟另一段 Conversation 時,不一定還需要知道:
上一段對話答錯第二題。
相對地:
使用者的英文程度是 B1。
使用者偏好簡短例句。
使用者希望加強旅遊英文。
更可能在不同 Conversation 中持續影響回答。所以 Summary 和 Memory 都是從對話中減少資訊,但它們的目的不同。
沒有一份適用所有 AI Application 的固定清單。
對購物助手重要的資訊,可能是尺寸、預算與品牌偏好;對健康管理系統重要的資訊,可能是使用者主動提供的目標與限制;對 Memora 來說,比較重要的則是和英語學習有關的資訊。
我們可以先用四個問題判斷:
例如:
我的英文程度是 B1。
未來出題、解釋文法或選擇單字時都可能用到,因此有保存價值。但:
請幫我翻譯上一句。
只和目前 Context 有關,通常不需要跨 Conversation 保存。
例如:
我希望半年後可以用英文完成面試。
即使換一段 Conversation,這個學習目標仍然成立。
但是:
第二題為什麼選 B?
離開當時的題目後,就不知道「第二題」指的是什麼。
例如:
我比較喜歡用簡短例句學文法。
描述了使用者的學習偏好。
相對地,Assistant 產生的一般文法解釋,不一定需要被保存成使用者的 Memory。
例如:
我的 TOEIC 目標是 850 分。
意思相對清楚。
但是:
我英文不太好。
可能代表單字量不足、口說沒有自信,也可能只是使用者當下的感覺。這種資訊在保存前可能還需要更多判斷。
以目前的 Personal English Learning Assistant 為例,可以先做出以下區分:
| 使用者說的內容 | 是否適合成為 Memory | 原因 |
|---|---|---|
| 我的英文程度是 B1 | 適合 | 會影響未來內容難度 |
| 我想練習旅遊英文 | 適合 | 是持續性的學習目標 |
| 我喜歡簡短、直接的解釋 | 適合 | 會影響回答方式 |
| 我的 TOEIC 目標是 850 分 | 適合 | 是明確的學習目標 |
| 幫我翻譯這一句 | 通常不適合 | 只和目前任務有關 |
| 謝謝,我懂了 | 通常不適合 | 是當下的對話回應 |
| 第二個單字是什麼意思 | 通常不適合 | 依賴目前 Context |
| 我的密碼是 123456 | 不應直接保存 | 屬於不必要的敏感資訊 |
這些判斷不是絕對規則。
例如:
我明天要考英文。
明天以前可能很重要,但考試結束後就失去價值。它不像英文程度那樣穩定,也不像「謝謝」那樣完全沒有保存必要。
後面談到 Memory Policy、Importance、Recency 與 Forgetting 時,才會正式處理這些模糊情況。
今天只先建立一個基本觀念:
Memory 應該是經過選擇的資訊,而不是完整聊天紀錄的另一份副本。
memory_candidates目前我們還不讓 LLM 自動判斷哪些資訊值得保存,因為那是明天的內容。
今天先加入一個手動指令:
remember <要保存的內容>
例如:
You:
remember 我的英文程度是 B1。
程式會把:
我的英文程度是 B1。
加入:
memory_candidates
為什麼叫做 Candidate,而不是直接叫做 Long-term Memory?
因為目前這些資料:
還沒有結構化
還沒有寫入 Database
還沒有跨程式保存
還沒有建立 Retrieval
它們只是我們從 Conversation 中挑出的「記憶候選」。
這個命名可以避免太早把一個 Python List 說成完整的 Long-term Memory System。
Day 10 已經建立:
memory = ShortTermMemory(...)
今天保留原本的:
SYSTEM_PROMPT
SUMMARY_INSTRUCTIONS
ShortTermMemory
print_messages
全部不動。
我們只在建立 memory 之後,新增:
memory_candidates = []
因此目前會有兩個不同的區域:
memory.history
負責保存完整 Conversation History。
memory_candidates
只保存被選中的資訊。
remember 與 memories 指令首先修改原本顯示的 Commands:
print(
"Commands: history, context, summary, status, "
"remember <text>, memories, exit"
)
接著加入兩個新指令。
remember <text>將一項資訊標記成 Memory Candidate:
if command.startswith("remember "):
candidate = user_input[len("remember "):].strip()
if not candidate:
print("Usage: remember <text>")
continue
memory_candidates.append(candidate)
print(
"Saved as Memory Candidate:",
candidate
)
user_input = candidate
這裡沒有直接 continue。
原因是 remember 後面的內容除了加入 memory_candidates,也會繼續成為正常的 User Message。
假設輸入:
remember 我的英文程度是 B1。
程式會做兩件事:
加入 Memory Candidate
把「我的英文程度是 B1。」送進目前 Conversation
因此 Conversation History 的 User/Assistant 順序不會被破壞,Memora 也可以正常回應。
memories查看目前的 Memory Candidates:
if command == "memories":
print("\n--- Memory Candidates ---")
if not memory_candidates:
print("(empty)")
else:
for index, candidate in enumerate(
memory_candidates,
start=1
):
print(f"{index}. {candidate}")
print("-------------------------")
continue
保留 Day 10 中 ShortTermMemory Class 以前的程式不變,從建立 memory 的位置開始,修改成以下內容:
memory = ShortTermMemory(
client=client,
model=MODEL,
system_prompt=SYSTEM_PROMPT,
summary_instructions=SUMMARY_INSTRUCTIONS,
max_input_tokens=MAX_INPUT_TOKENS,
max_recent_turns=MAX_RECENT_TURNS
)
# Day 11 新增:
# 暫時保存被手動選中的 Memory Candidate。
memory_candidates = []
print("Memora v0.8")
print(
"Commands: history, context, summary, status, "
"remember <text>, memories, exit"
)
while True:
user_input = input("\nYou: ").strip()
if not user_input:
continue
command = user_input.lower()
if command == "exit":
print("Bye!")
break
if command == "history":
print_messages(
"Full Conversation History",
memory.history
)
continue
if command == "context":
print_messages(
"Last Request Context",
memory.last_context
)
continue
if command == "summary":
print("\n--- Conversation Summary ---")
print(memory.summary or "(empty)")
print("----------------------------")
continue
if command == "memories":
print("\n--- Memory Candidates ---")
if not memory_candidates:
print("(empty)")
else:
for index, candidate in enumerate(
memory_candidates,
start=1
):
print(f"{index}. {candidate}")
print("-------------------------")
continue
if command == "status":
print("\n--- Memory Status ---")
for name, value in memory.get_status().items():
print(f"{name}: {value}")
print(
"Memory candidates:",
len(memory_candidates)
)
print("---------------------")
continue
if command == "remember":
print("Usage: remember <text>")
continue
if command.startswith("remember "):
candidate = user_input[
len("remember "):
].strip()
if not candidate:
print("Usage: remember <text>")
continue
memory_candidates.append(candidate)
print(
"Saved as Memory Candidate:",
candidate
)
# 將 remember 後面的內容視為正常 User Message。
user_input = candidate
memory.add_user_message(user_input)
try:
memory_stats = memory.prepare_context()
response = client.responses.create(
model=MODEL,
instructions=SYSTEM_PROMPT,
input=memory_stats["context_messages"]
)
except Exception as error:
memory.rollback_last_user_message()
print("Request failed:", error)
continue
assistant_reply = response.output_text
memory.finish_turn(
assistant_reply=assistant_reply,
context_messages=memory_stats["context_messages"],
response_tokens=response.usage.total_tokens
)
print("Memora:", assistant_reply)
print("\n--- Memory Status ---")
print(
"Newly summarized messages:",
memory_stats["newly_summarized_count"]
)
print(
"Context messages sent:",
len(memory_stats["context_messages"])
)
print(
"Counted input tokens:",
memory_stats["input_tokens"]
)
print(
"Actual input tokens:",
response.usage.input_tokens
)
print(
"Output tokens:",
response.usage.output_tokens
)
print(
"Summary update tokens:",
memory_stats["summary_token_usage"]
)
print(
"Memory candidates:",
len(memory_candidates)
)
print(
"Session total tokens:",
memory.session_total_tokens
)
print("---------------------")
這次沒有修改 Day 10 的 Context Management。
Responses API 仍然接收由 ShortTermMemory 建立的 Message List,而 memory_candidates 暫時不會被自動加入 Request。OpenAI 官方文件將提供先前 Message 視為管理 Conversation State 的方式;我們現在則開始在 Conversation State 之外,建立另一個準備跨 Conversation 使用的資訊區域。OpenAI Conversation State 文件
啟動程式後,可以先進行一般對話:
You:
Hello!
Memora:
Hello! How can I help you with English today?
這一輪會進入:
memory.history
但不會進入:
memory_candidates
接著輸入:
You:
remember 我的英文程度是 B1。
程式會顯示:
Saved as Memory Candidate: 我的英文程度是 B1。
Memora 仍會正常回答:
Memora:
了解,我會使用適合 B1 程度的方式協助你。
再輸入:
You:
remember 我想加強旅遊英文。
最後查看:
You:
memories
會得到:
--- Memory Candidates ---
1. 我的英文程度是 B1。
2. 我想加強旅遊英文。
-------------------------
如果改成輸入:
You:
history
則會看到完整的 User 與 Assistant Message。
這表示現在的程式第一次把:
Conversation Log
和:
Selected Memory Information
分開了。
看到這裡可能會想直接加入:
input = (
memory_candidates
+ context_messages
)
但今天先不這麼做。
因為目前的 memory_candidates 只是純文字:
[
"我的英文程度是 B1。",
"我想加強旅遊英文。"
]
我們還沒有處理:
這是什麼類型的資訊?
是誰的資訊?
何時建立?
是否仍然有效?
和既有 Memory 是否重複?
應該在什麼情況下取回?
如果每次都把所有 Memory Candidates 塞進 Context,只是把 Conversation History 無限累積的問題換一個地方重演。
所以 Chapter 3 的順序不能跳過:
先決定什麼值得記住
↓
從對話中抽取資訊
↓
轉換成可以管理的資料
↓
建立搜尋與儲存
↓
需要時才取回
Memory 的重點不只是 Save,也包括之後的 Retrieve。
memory_candidates 還缺什麼?現在我們已經建立:
memory_candidates = []
但它仍然有幾個明顯限制。
第一,必須由使用者手動輸入:
remember ...
如果使用者只是自然地說:
我的英文程度是 B1。
程式不會自動判斷這是不是值得保存的資訊。
第二,目前只保存純文字:
"我的英文程度是 B1。"
程式還不知道:
Memory Type
Subject
Value
Created Time
Confidence
第三,重新啟動程式後:
memory_candidates = []
仍然會消失。
第四,Memora 還不會根據新的問題搜尋並取回相關 Memory。
這些限制不是今天的 Bug,而是接下來幾天要依序完成的功能。
今天是 Chapter 3 的起點。我們先不急著把 Conversation History 全部存進 Database,而是先釐清:
聊天紀錄負責保留發生過什麼;Memory 負責保留未來仍可能有用的資訊。
今天新增了:
memory_candidates = []
並透過:
remember <text>
手動選出值得保存的資訊。
目前 Memora 的資料開始分成兩個部分:
Short-term Memory
保存目前 Conversation 的脈絡
以及:
Memory Candidates
保存可能值得跨 Conversation 使用的資訊
這些 Candidate 還不是真正完成的 Long-term Memory,因為它們尚未結構化、持久化,也還不能被搜尋與取回。但我們已經完成最重要的第一步:
不是先決定資料要存在哪裡,而是先決定什麼資料值得被存。
下一篇,我們會拿掉對:
remember ...
的依賴。
當使用者自然地說:
我的英文程度是 B1,我想加強旅遊英文。
Memora 將額外呼叫一次 LLM,判斷這句話裡是否存在值得保存的 Memory Candidate。
也就是讓流程從:
使用者手動指定 Memory
變成:
Conversation
↓
LLM 分析
↓
Memory Candidate
到時候會第一次遇到 Long-term Memory Engineering 的另一個問題:
我們要怎麼讓 LLM 只抽取重要資訊,而不是把每一句話都當成 Memory?