昨天我們替 Memora 加上了 System Prompt,讓原本什麼都能聊的 Chatbot,開始有一個比較明確的角色。
目前它已經知道自己是一個 Personal English Learning Assistant,也知道回答時要盡量簡單、聚焦,必要時提供例子。但有一個問題從Day 3到現在都沒有解決。假設我先告訴它:
You:
我的英文程度是 B1,請記住。
Memora:
好的,我會記住你的英文程度是 B1。
接著馬上問:
You:
我的英文程度是多少?
它卻不一定答得出來。明明上一句才說「我會記住」,為什麼下一句就忘了?
今天我們來把這件事情真正拆開來看。因為理解這個問題之後,下一章才能正式開始替 Memora 加上第一種「記憶」。
這篇的標題叫做「理解 Stateless API」,但這裡要先講得精確一點。
現在有些 LLM API 已經提供保存或延續 Conversation State 的機制。例如 OpenAI Responses API 支援透過 previous_response_id 延續多輪對話,也有 Conversations API 可以保存並管理跨多次 Response 的 Conversation State。
所以嚴格來說:
不是「所有 LLM API 天生都一定 Stateless」,而是我們目前寫出的 Memora 是 Stateless。
這個差別很重要。從 Day 3 開始,我每一次都這樣呼叫模型:
response = client.responses.create(
model="gpt-5-mini",
instructions=SYSTEM_PROMPT,
input=user_input
)
我沒有提供:
Conversation History
previous_response_id
Conversation ID
Database中的歷史訊息
所以站在我們目前這個 Application 的角度,每一次 Request 都是獨立的。
今天要研究的,就是這種最基本的 Stateless 寫法到底發生了什麼!
昨天完成的版本大概是:
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.2")
print("輸入 exit 可以結束對話。")
while True:
user_input = input("\nYou: ")
if user_input.lower() == "exit":
print("Bye!")
break
response = client.responses.create(
model="gpt-5-mini",
instructions=SYSTEM_PROMPT,
input=user_input
)
print("Memora:", response.output_text)
從 Terminal 看起來,它非常像一個正常的聊天程式:
You: Hello!
Memora: Hello! How can I help you with English today?
You: Explain present perfect.
Memora: The present perfect is used when...
You: Give me another example.
Memora: ...
問題就在這裡。
畫面看起來是連續的,不代表送給模型的資料也是連續的。
while Loop 只是讓程式一直跑我們的程式裡有:
while True:
所以回答完之後,程式不會結束,而是繼續等待下一個輸入。
這讓 Terminal 呈現出:
User
↓
AI
↓
User
↓
AI
↓
User
↓
AI
很容易讓人產生一種感覺:
既然程式一直沒有關掉,它應該知道剛剛聊過什麼吧?
但 while Loop 做的事情只有:
讓同一段程式重複執行。
它並沒有自動建立:
conversation_history = [...]
也沒有幫我保存:
User Message 1
Assistant Message 1
User Message 2
Assistant Message 2
所以:
while loop
≠
Conversation History
這是理解目前 Memora 為什麼沒有記憶的第一個關鍵。
假設我第一次輸入:
我的秘密代碼是 ZQ-731-MANGO,請記住。
程式真正執行的是:
response = client.responses.create(
model="gpt-5-mini",
instructions=SYSTEM_PROMPT,
input="我的秘密代碼是 ZQ-731-MANGO,請記住。"
)
這就是 Request 1。
模型看到的資料可以先簡化成:
Instructions:
You are Memora...
Input:
我的秘密代碼是 ZQ-731-MANGO,請記住。
接著模型回答:
好的,我會記住你的秘密代碼是 ZQ-731-MANGO。
目前看起來都沒問題。
接下來我再輸入:
我的秘密代碼是什麼?
程式又重新執行:
response = client.responses.create(
model="gpt-5-mini",
instructions=SYSTEM_PROMPT,
input="我的秘密代碼是什麼?"
)
注意看這一次 Request 裡有什麼。
有:
System Instructions
有:
我的秘密代碼是什麼?
但是沒有:
我的秘密代碼是 ZQ-731-MANGO。
也沒有上一輪 Assistant 的回答。所以真正的情況其實是:
Request 1
────────────────────────
System Prompt
我的秘密代碼是
ZQ-731-MANGO,請記住。
────────────────────────
↓
LLM
↓
Response 1
接著:
Request 2
────────────────────────
System Prompt
我的秘密代碼是什麼?
────────────────────────
↓
LLM
↓
Response 2
Request 2 裡根本沒有 Request 1。
這就是目前 Memora「失憶」的真正原因。
這裡我覺得有一個用詞差異很重要。
我們平常很容易說:
AI 忘記了。
但以目前這個程式來說,更精確的說法其實是:
這一次 Request 根本沒有把上一輪的資訊提供給模型。
模型不是先拿到:
ZQ-731-MANGO
然後把它弄丟。
而是第二次呼叫時,它根本沒有收到這段資訊。
所以目前的架構其實是:
Request 1
Current Input
↓
LLM
↓
Response 1
Request 2
Current Input
↓
LLM
↓
Response 2
而不是:
Request 1
↓
保存
↓
Request 2
↓
帶入過去資料
↓
LLM
這就是 Stateless 在我們目前 Application 裡最直接的樣子。
這裡還有一個滿有趣的陷阱。
假設我第一句說:
我的英文程度是 B1。
第二句問:
我的英文程度是多少?
有時候模型可能會回答:
我不知道,請告訴我你的英文程度。
但也有可能出現一些看起來像是「它還記得」的回答。這時不能只靠輸出結果判斷系統有沒有 Memory。因為 LLM 是生成模型,它有可能根據語言模式猜測,也有可能在資訊不足時產生一個看似合理的答案。
所以要判斷系統到底有沒有保存 State,最可靠的方法不是問:
「模型這次答對了嗎?」
而是直接檢查:
「這次 Request 到底包含了什麼?」
這也是為什麼我在這個系列裡會一直把 Application 和 LLM 分開來看。模型的回答有一定的不確定性,但程式送了什麼資料,是我們可以直接檢查的。
為了讓這件事情更明顯,我可以稍微修改程式,在每次呼叫 API 前把 Request 的主要內容印出來。
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.
"""
request_count = 1
while True:
user_input = input("\nYou: ")
if user_input.lower() == "exit":
print("Bye!")
break
print(f"\n--- Request {request_count} ---")
print("Input:", user_input)
print("-------------------")
response = client.responses.create(
model="gpt-5-mini",
instructions=SYSTEM_PROMPT,
input=user_input
)
print("Memora:", response.output_text)
request_count += 1
接著輸入:
You:
我的秘密代碼是 ZQ-731-MANGO。
Terminal 會顯示:
--- Request 1 ---
Input: 我的秘密代碼是 ZQ-731-MANGO。
-------------------
下一次輸入:
You:
我的秘密代碼是什麼?
則會看到:
--- Request 2 ---
Input: 我的秘密代碼是什麼?
-------------------
這時就非常清楚了。
第二次 Request 裡沒有:
我的秘密代碼是 ZQ-731-MANGO。
所以問題不是 LLM Memory 壞掉,而是:
我們根本還沒有寫 Memory。
既然一直在講 Stateless,那 State 到底是什麼?
在這個 Chatbot 的情境裡,我可以先把 State 理解成:
需要從前一次互動保留下來,並影響後續互動的資訊。
例如:
使用者之前說過什麼
Assistant 之前回答什麼
目前是哪一段 Conversation
使用者的設定
現在做到哪一個任務
最簡單的 Conversation State 可能長這樣:
messages = [
{
"role": "user",
"content": "我的英文程度是 B1。"
},
{
"role": "assistant",
"content": "了解。"
}
]
第二次 User 再說:
幫我出適合我的題目。
我們就把它加入:
messages = [
{
"role": "user",
"content": "我的英文程度是 B1。"
},
{
"role": "assistant",
"content": "了解。"
},
{
"role": "user",
"content": "幫我出適合我的題目。"
}
]
再把這些內容交給模型。
這時第二個 Request 才真正和第一個 Request 產生關係。
也就是:
Stateless
Current Message
↓
LLM
開始變成:
Stateful
Previous Messages
+
Current Message
↓
LLM
這就是明天要真正實作的東西。
Conversation State 其實不一定只有一種保存方式。
例如最簡單的做法,可以存在 Python 記憶體裡:
messages = []
程式關掉之後就消失。
也可以之後存進:
SQLite
PostgreSQL
Redis
讓程式重新啟動後還能讀回來。
另外,一些 LLM API 本身也提供 Conversation State 的管理方式。以目前的 OpenAI Responses API 為例,可以使用 previous_response_id 建立多輪 Conversation,也可以使用 Conversations API 保存並取得跨 Response 的 Conversation State。
也就是說,State 大致可以由不同地方管理:
Application Memory
Database
LLM Provider 的 Conversation API
那為什麼我明天不直接用 previous_response_id 就好?因為這個系列的目標不是只讓 Chatbot「可以聊天」。我們更應該先把底層邏輯弄清楚。
所以 Day 6 會先自己管理:
Conversation History
等理解 State 到底是怎麼被保存和送回模型之後,再來看 API 或 Framework 幫我們包掉了哪些事情。
這裡也要先提醒一件後面重要的事。假設明天我成功建立:
messages = []
然後讓 AI 可以記得:
User:
我的英文程度是 B1。
Assistant:
了解。
User:
我的英文程度是多少?
Assistant:
B1。
這是不是就代表 Memora 已經有 Long-term Memory勒?
還不是。
因為這時我們只是保存:
這一段 Conversation 裡發生過什麼。
而且可能程式一關掉:
messages = []
就全部消失。後面的 Long-term Memory 要解決的是更複雜的問題,例如:
哪些對話值得留下?
跨不同 Conversation 還要記得嗎?
一千則訊息裡現在需要哪一則?
使用者的偏好改變怎麼辦?
哪些資訊應該忘記?
所以接下來的路線會是:
Day 05
Stateless
↓
Day 06
Conversation History
↓
Day 10
Short-term Memory
↓
Day 17
Long-term Memory
↓
Day 29
Agentic Memory
不是「把 messages 存起來」就直接跳到長期記憶。
到今天為止,我們已經有:
Python
LLM API
System Prompt
User Input
Response
while Loop
目前架構是:
┌───────────────────┐
│ System Prompt │
└─────────┬─────────┘
│
│
┌─────────▼─────────┐
│ Current User Input│
└─────────┬─────────┘
↓
LLM API
↓
Response
現在唯一跟「聊天」相關、但我們還沒有做的就是:
Previous Messages
也就是:
┌───────────────────┐
│ System Prompt │
├───────────────────┤
│ Previous Messages │ ← 還沒有
├───────────────────┤
│ Current Message │
└─────────┬─────────┘
↓
LLM
而這一塊,正好就是 Chapter 2 的起點。
Chapter 1 到這裡差不多完成了。
我們從 Day 1 的「AI 到底有沒有記憶」,一路拆到今天,已經可以比較精確地回答這個問題。
目前 Memora 會忘記上一輪,不是因為模型突然失憶,而是因為我們每次呼叫 API 時,只把 Current User Input 放進新的 Request,沒有把 Previous Messages 一起帶進去。
所以目前的架構是:
Request 1
↓
Response 1
Request 2
↓
Response 2
Request 3
↓
Response 3
各自獨立。
另外也要記住,現代 LLM API 本身可能提供延續 Conversation State 的功能,因此「LLM API 一定是 Stateless」這句話並不精確。OpenAI Responses API 目前就支援 previous_response_id,也提供 Conversations API 管理 Conversation State。
但我們目前刻意沒有使用這些機制。因為接下來,我要自己把 State 加進 Memora。
今天的內容總結:
能一直呼叫 LLM,不代表有 Conversation;真正讓多輪對話成立的,是上一輪的資訊有沒有被保存,並再次帶進下一輪。
現在的 Memora 是:
Day 03
LLM API
↓
Stateless Chatbot
Day 04
System Prompt
↓
Prompted Stateless Chatbot
Day 05
看懂 Request
↓
確認它為什麼 Stateless
Chapter 1 到這裡,我們已經做出了一個可以聊天、知道自己該怎麼回答,卻完全沒有 Conversation State 的 AI。
接下來可以開始替它加入第一種真正的「記憶」囉!
明天我們來正式建立:
messages = []
把每一輪:
User Message
Assistant Message
保存下來,再在下一次 Request 時一起提供給模型。
到時候我會重新做今天的秘密代碼實驗:
You:
我的秘密代碼是 ZQ-731-MANGO。
You:
我的秘密代碼是什麼?
看看加入 Conversation History 之後,結果會發生什麼變化。
從明天開始,Memora 終於要從:Stateless Chatbot正式跨出第一步,走向:Stateful Chatbot!