iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0

昨天我們已經找到了 Memora 接不上前一句的原因。問題不是 LLM 把資訊忘掉了,而是我們每次呼叫 API 時,只送出當下的 user_input,沒有把先前的對話一起放進新的 Request。

所以今天不再重新分析一次 Stateless,而是直接從 Day 5 的程式繼續修改,替 Memora 加入第一種最基本的「記憶」:

Conversation History。

它的概念很直接:把每一輪的對話保存下來,下一次呼叫模型時,再連同新的訊息一起送出去。


一、Conversation History 到底存什麼?

假設我和 Memora 有這樣一段對話:

User:
我的英文程度是 B1。

Assistant:
了解,我會以 B1 程度協助你。

User:
幫我出五題適合我的英文題目。

如果只把所有句子存成純文字,模型可能不知道每一句話是誰說的。因此,Conversation History 不只要保存內容,還要保留每一則 Message 的 role

conversation_history = [
    {
        "role": "user",
        "content": "我的英文程度是 B1。"
    },
    {
        "role": "assistant",
        "content": "了解,我會以 B1 程度協助你。"
    },
    {
        "role": "user",
        "content": "幫我出五題適合我的英文題目。"
    }
]

其中:

role = user

代表這是使用者說的話;而:

role = assistant

則代表這是模型先前的回答。

所以 Conversation History 的本質就是:

一串按照時間順序排列,並保留說話者角色的 Message。


二、從昨天的程式開始修改

昨天的 Memora 已經有:

OpenAI Client
System Prompt
while Loop
exit 指令

今天不更換原本的架構,只加入三個步驟:

1. 建立 conversation_history
2. 把 User 與 Assistant Message 存進去
3. 下一次 Request 時,把整段 History 交給模型

修改後的完整程式如下:

from openai import OpenAI

client = OpenAI()

SYSTEM_PROMPT = """
You are Memora, a personal English learning assistant.

Your goal is to help the user learn English clearly and efficiently.

Guidelines:
- Explain concepts in simple language.
- Keep answers focused on the user's question.
- Use short examples when helpful.
- Avoid unnecessary technical grammar terminology.
"""

print("Memora v0.3")
print("輸入 exit 可以結束對話。")

conversation_history = []

while True:
    user_input = input("\nYou: ")

    if user_input.lower() == "exit":
        print("Bye!")
        break

    conversation_history.append(
        {
            "role": "user",
            "content": user_input
        }
    )

    response = client.responses.create(
        model="gpt-5-mini",
        instructions=SYSTEM_PROMPT,
        input=conversation_history
    )

    assistant_reply = response.output_text

    conversation_history.append(
        {
            "role": "assistant",
            "content": assistant_reply
        }
    )

    print("Memora:", assistant_reply)

和昨天相比,原本的 while Loop、System Prompt 與 API 呼叫方式都還在。真正新增的部分,只有 Conversation History 的建立、更新與傳入。

OpenAI 的 Responses API 可以接受由多則 userassistant Message 組成的 input,因此我們可以用這種方式手動管理 Conversation State。相關格式也可以參考 OpenAI 官方文件


三、這幾行程式做了什麼?

程式啟動時,先建立一個空的 List:

conversation_history = []

這時 Memora 還沒有任何對話紀錄。

當使用者輸入訊息後,先把新的 User Message 加進去:

conversation_history.append(
    {
        "role": "user",
        "content": user_input
    }
)

接著,Day 5原本傳入的是:

input=user_input

今天則改成:

input=conversation_history

因此模型收到的不再只有目前這一句,而是到目前為止保存的整段 Conversation History。模型回答後,我們再把這次回答存進同一個 List:

assistant_reply = response.output_text

conversation_history.append(
    {
        "role": "assistant",
        "content": assistant_reply
    }
)

下一輪開始時,新的 User Message 會繼續加在後面,Conversation History 也就會隨著對話一輪一輪成長。

整個流程可以整理成:

取得 User Message
        ↓
加入 Conversation History
        ↓
將 History 送給 LLM
        ↓
取得 Assistant Message
        ↓
加入 Conversation History
        ↓
進入下一輪

四、重新進行秘密代碼實驗

現在可以重新測試昨天留下來的問題。

先輸入:

You:
我的秘密代碼是 ZQ-731-MANGO,請記住。

Memora:
好的,我記住了你的秘密代碼是 ZQ-731-MANGO。

接著再問:

You:
我的秘密代碼是什麼?

Memora:
你的秘密代碼是 ZQ-731-MANGO。

這一次 Memora 能夠回答,是因為第二次呼叫 API 時,conversation_history 已經包含:

[
    {
        "role": "user",
        "content": "我的秘密代碼是 ZQ-731-MANGO,請記住。"
    },
    {
        "role": "assistant",
        "content": "好的,我記住了你的秘密代碼是 ZQ-731-MANGO。"
    },
    {
        "role": "user",
        "content": "我的秘密代碼是什麼?"
    }
]

模型在產生第二次回答時,真的看得到第一輪的內容。

因此,Conversation History 做的事情可以理解成:

把過去的 Conversation 重新放進現在的 Context。


五、為什麼 Assistant Message 也要保存?

看到這裡可能會有一個疑問:如果只是希望 AI 記得使用者說過什麼,只保存 User Message 不就好了嗎?

考慮下面這段對話:

User:
請給我三個適合 B1 程度的英文單字。

Assistant:
travel
journey
abroad

User:
第二個是什麼意思?

最後一句的「第二個」,指的是 Assistant 上一輪回答裡的 journey

如果 History 只保存 User Message,模型看到的會是:

請給我三個適合 B1 程度的英文單字。
第二個是什麼意思?

它知道自己曾經被要求提供三個單字,卻不知道當時實際回答了哪三個。

因此,完整的 Conversation History 通常會交替保存:

User
Assistant
User
Assistant
...

Conversation State不只包含使用者提供的資訊,也包含模型先前做出的回應。


六、直接查看目前的History

昨天曾經透過 Debug 檢查每一次 Request 裡有什麼。今天也可以加入一個簡單的 history 指令,直接查看目前保存的 Conversation History。

把下面這段程式放在 exit 判斷之後、加入 User Message 之前:

if user_input.lower() == "history":
    print("\n--- Conversation History ---")

    for message in conversation_history:
        print(f"{message['role']}: {message['content']}")

    print("----------------------------")
    continue

也就是:

if user_input.lower() == "exit":
    print("Bye!")
    break

if user_input.lower() == "history":
    print("\n--- Conversation History ---")

    for message in conversation_history:
        print(f"{message['role']}: {message['content']}")

    print("----------------------------")
    continue

conversation_history.append(
    {
        "role": "user",
        "content": user_input
    }
)

聊了幾輪之後輸入:

You:
history

就可能看到:

--- Conversation History ---
user: 我的英文程度是 B1。
assistant: 了解,我會以 B1 程度協助你。
user: 幫我出五題適合我的英文題目。
assistant: 當然可以,以下是五題……
----------------------------

這個 List 就是目前 Memora 保存的Conversation State。


七、現在到底是誰記住了對話?

從使用者的角度來看,Memora 現在確實「記得」上一句了。

不過從系統架構來看,真正保存資料的是:

conversation_history

也就是我們的 Python Application。

每一次 API Request 本身仍然是獨立的,只是 Application 會在呼叫模型之前,重新把先前的 Message 放進 input

實際流程是:

Python Application
保存 Conversation History
        ↓
下一次 Request 重新帶入
        ↓
LLM 看見先前的 Message
        ↓
產生符合上下文的回答

因此,現在 Memora 表現出的「記憶能力」,可以更精確地寫成:

Application State + Context Reconstruction

也就是由 Application 保存狀態,再替模型重建這一輪需要的 Context。


八、程式重新啟動後還記得嗎?

接下來可以做另一個測試。先告訴Memora:

You:
我的英文程度是 B1。

接著確認它還記得:

You:
我的英文程度是多少?

Memora:
你的英文程度是 B1。

然後輸入:

exit

關閉程式,再重新執行:

python chatbot.py

這時候:

conversation_history = []

會重新建立一個空的 List,剛才的對話也就消失了。所以目前的 Memora 可以記住:

同一次程式執行中的對話

但還不能跨越程式重啟或不同 Conversation 保存資訊。這也實際驗證了昨天提過的界線:Conversation History 能讓 AI 記住「這次對話」,但還不是後面要處理的 Long-term Memory。


九、為什麼不直接使用 previous_response_id

昨天提過,Responses API 本身也提供 previous_response_id,另外還有 Conversations API 可以協助管理 Conversation State。

這些方法確實更方便,但目前先不使用,因為後面的章節還會繼續處理:

Sliding Window
Conversation Summary
Context Management

先自己管理 conversation_history,我們才能直接觀察哪些 Message 被保存、哪些 Message 被送進 Context,以及後面該如何刪除、保留或整理它們。

等理解這些底層機制後,再使用 API 或 Framework 提供的功能,就會更清楚它們替我們處理了什麼。


十、Memora 現在算是 Stateful Chatbot 嗎?

Day 5 的 Memora 是:

Current User Input
        ↓
       LLM
        ↓
     Response

今天則變成:

Conversation History
        +
Current User Input
        ↓
       LLM
        ↓
Assistant Response
        ↓
更新 Conversation History

因此,從 Application 的角度來看,Memora 已經從 Stateless Chatbot 跨出了第一步,成為一個最基本的 Stateful Chatbot。

不過新的問題也馬上出現了。我們現在每一次呼叫 API,都會執行:

input=conversation_history

假設聊了 3 輪,History 大約有 6 則 Message;聊了 100 輪,就可能累積到 200 則 Message。

也就是說:

第 1 輪
History 很短

第 10 輪
History 逐漸變長

第 100 輪
History 非常龐大

而 Day 2 已經提過,模型處理的文字最後都會轉換成 Token。

所以現在很自然會產生下一個問題:

Conversation History 可以無限累積嗎?

答案當然不是。


Day 6 小結

今天我們延續昨天的程式,加入了三個關鍵步驟:

保存 User Message
        ↓
把 Conversation History 送給模型
        ↓
保存 Assistant Message

程式裡最重要的改變是:

conversation_history = []

以及:

input=conversation_history

但真正需要理解的並不是 Python List 本身,而是:

Conversation History 會保存先前的 Message,並在下一輪重新把它們帶回 Context。

目前 Memora 的演進來到:

Day 03
Stateless Chatbot
        ↓
Day 04
加入 System Prompt
        ↓
Day 05
理解 Stateless
        ↓
Day 06
加入 Conversation History
        ↓
Stateful Chatbot

今天我們解決了「AI 怎麼接得上上一句」,同時也產生了第一個 Context Management 問題:

如果 Conversation History 一直累積,模型到底能放進多少內容?

這就是明天要討論的主題。

Day 07|Context Window 為什麼會爆?Token Limit 的真相

下一篇,我會把Day 2提過的 Token 再拿回來討論討論,進一步看看:

  • Context Window 到底是什麼?
  • Input 和 Output 都會占用 Context Window 嗎?
  • 為什麼 Conversation History 不可能無限累積?
  • 超過 Context Limit 會發生什麼?
  • 一個看似「記得越來越多」的 Chatbot,為什麼最後反而可能被自己的 History 塞爆?

今天我們讓 Memora 學會記住這次對話。明天開始面對第一個 Memory Engineering 問題:記得越多,真的越好嗎?


上一篇
Day 05|為什麼 LLM API 聊完就忘?理解 Stateless API
下一篇
Day 07|Context Window 為什麼會爆?Token Limit 的真相
系列文
從 Stateless LLM 到 Agentic Memory:30 天打造會記憶的 AI Agent7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言