iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0

昨天我們完成了 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 分開。


一、保存聊天紀錄,不等於建立 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 比較像從逐字稿中留下的重點。


二、Conversation Summary 也不等於 Long-term 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 來說,比較重要的則是和英語學習有關的資訊。

我們可以先用四個問題判斷:

1. 這項資訊未來還會影響回答嗎?

例如:

我的英文程度是 B1。

未來出題、解釋文法或選擇單字時都可能用到,因此有保存價值。但:

請幫我翻譯上一句。

只和目前 Context 有關,通常不需要跨 Conversation 保存。

2. 離開目前對話後,它還有意義嗎?

例如:

我希望半年後可以用英文完成面試。

即使換一段 Conversation,這個學習目標仍然成立。

但是:

第二題為什麼選 B?

離開當時的題目後,就不知道「第二題」指的是什麼。

3. 這是使用者資訊,還是普通的聊天內容?

例如:

我比較喜歡用簡短例句學文法。

描述了使用者的學習偏好。

相對地,Assistant 產生的一般文法解釋,不一定需要被保存成使用者的 Memory。

4. 資訊是否足夠明確?

例如:

我的 TOEIC 目標是 850 分。

意思相對清楚。

但是:

我英文不太好。

可能代表單字量不足、口說沒有自信,也可能只是使用者當下的感覺。這種資訊在保存前可能還需要更多判斷。


四、Memora 可能記住與不記住的內容

以目前的 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

只保存被選中的資訊。


七、加入 remembermemories 指令

首先修改原本顯示的 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

八、Memora v0.8 的主程式

保留 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 文件


九、實際測試 Conversation 與 Memory 的差別

啟動程式後,可以先進行一般對話:

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

分開了。


十、為什麼不直接把 Memory Candidates 放進 Context?

看到這裡可能會想直接加入:

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,而是接下來幾天要依序完成的功能。


Day 11 小結

今天是 Chapter 3 的起點。我們先不急著把 Conversation History 全部存進 Database,而是先釐清:

聊天紀錄負責保留發生過什麼;Memory 負責保留未來仍可能有用的資訊。

今天新增了:

memory_candidates = []

並透過:

remember <text>

手動選出值得保存的資訊。

目前 Memora 的資料開始分成兩個部分:

Short-term Memory
保存目前 Conversation 的脈絡

以及:

Memory Candidates
保存可能值得跨 Conversation 使用的資訊

這些 Candidate 還不是真正完成的 Long-term Memory,因為它們尚未結構化、持久化,也還不能被搜尋與取回。但我們已經完成最重要的第一步:

不是先決定資料要存在哪裡,而是先決定什麼資料值得被存。

Day 12|讓 LLM 自動抽取 Memory:從對話找到重要資訊

下一篇,我們會拿掉對:

remember ...

的依賴。

當使用者自然地說:

我的英文程度是 B1,我想加強旅遊英文。

Memora 將額外呼叫一次 LLM,判斷這句話裡是否存在值得保存的 Memory Candidate。

也就是讓流程從:

使用者手動指定 Memory

變成:

Conversation
     ↓
LLM 分析
     ↓
Memory Candidate

到時候會第一次遇到 Long-term Memory Engineering 的另一個問題:

我們要怎麼讓 LLM 只抽取重要資訊,而不是把每一句話都當成 Memory?


上一篇
Day 10|Short-term Memory 完成:打造 Stateful Chatbot
系列文
從 Stateless LLM 到 Agentic Memory:30 天打造會記憶的 AI Agent11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言