昨天我們替 Memora 加入了 Conversation History。每一輪的 User Message 和 Assistant Message 都會被保存,下一次呼叫 API 時,再透過:
input=conversation_history
一起交給模型。
Memora 終於能接得上前一句了,但昨天最後也留下了一個問題:
如果 Conversation History 一直累積,模型真的能全部看完嗎?
答案當然不是。
LLM 每次能處理的內容都有範圍限制,這個範圍就是今天的主題:
Context Window。
Day 2 已經介紹過文字會被切成 Token,所以今天不再重新解釋 Tokenization,而是把焦點放在另一個問題:當 Conversation History 不斷增加時,這些 Token 究竟會如何填滿模型的 Context Window?
看到 Context Window,很容易把它想成電腦的硬碟或資料庫容量,但兩者並不相同。
Context Window 指的是:
模型在一次互動中能夠處理的 Token 範圍。
這裡的關鍵是:
一次互動
它不是 AI 永久可以記住多少資料,也不是整個 Chatbot 從啟動到關閉只能使用多少 Token,而是每一次呼叫模型時,能放進這個 Request 的內容有限。
以目前的 Memora 來說,每次 Request 大致包含:
System Instructions
Conversation History
Current User Message
模型收到這些內容後,還要產生新的Assistant Response。
因此,可以先把 Context Window 想像成一張大小固定的工作桌:
┌─────────────────────────────┐
│ Context Window │
│ │
│ System Instructions │
│ Conversation History │
│ Current User Message │
│ Model Output │
│ │
└─────────────────────────────┘
桌面空間是有限的。Conversation History 放得越多,剩下能使用的空間就越少。
我們目前的 API 呼叫是:
response = client.responses.create(
model="gpt-5-mini",
instructions=SYSTEM_PROMPT,
input=conversation_history
)
其中,模型收到的內容會形成Input。
目前主要包含:
SYSTEM_PROMPT
+
conversation_history
而 conversation_history 裡又包含:
先前的 User Messages
先前的 Assistant Messages
目前的 User Message
模型產生的回答則是 Output。
所以一次模型互動的 Token 使用,可以先簡化成:
Input Tokens
+
Output Tokens
=
Total Tokens
而在規劃 Context Window 時,也不能只考慮 Input。即使輸入勉強塞得進去,仍然要替模型接下來要產生的內容留下空間。
另外,Output 也不一定只有最後顯示在畫面上的文字。以支援 Reasoning 的模型來說,Output Token Usage 還可能包含 Reasoning Tokens。這些細節可以從 API 回傳的 usage 欄位觀察。
不同模型的 Context Window 不一樣,因此不存在所有 LLM 都通用的單一 Token Limit。
我們目前使用的是:
model="gpt-5-mini"
根據目前的 OpenAI 官方模型文件,gpt-5-mini 的規格包括:
Context Window:400,000 Tokens
Max Output Tokens:128,000 Tokens
這裡有兩個不同的限制。
Context Window 描述模型一次能處理的上下文範圍;Max Output Tokens 則限制模型單次最多可以生成多少 Output。實際使用時,Output 除了受到自己的上限限制,也必須考慮目前還剩下多少 Context 空間。
但 400,000 Tokens 並不代表:
400,000 個中文字
也不代表:
400,000 則 Message
因為 Token 和字數不是固定的一對一關係,而且 Request 中還有一些不會直接顯示在文章裡的結構資訊。
假設 Conversation History 是:
[
{
"role": "user",
"content": "我的英文程度是 B1。"
},
{
"role": "assistant",
"content": "了解,我會以 B1 程度協助你。"
}
]
不能只計算兩段 content 的字數。
模型實際收到的 Input 還包括:
System Instructions
Message Role
Message Boundary
Conversation Structure
例如 user 和 assistant 的角色資訊,也需要被編碼成模型可以處理的格式。
OpenAI 的 Token Counting 文件也特別提到,精確的 Input Token 數量會包含表示 Message Role 與邊界所需要的 Formatting Tokens。這些 Token 不一定會直接出現在我們看到的文字欄位中。
因此,下面這兩組 Conversation History 即使 Message 數量一樣,Token 數量也可能差很多:
Conversation A
2 則很短的 Message
Conversation B
2 則各有數千字的 Message
真正決定是否接近 Context Limit 的不是:
len(conversation_history)
而是這次 Request 最後被編碼成了多少 Token。
與其自己根據字數猜測,不如直接查看 API 回傳的 Usage。
Responses API 的 response.usage 會提供:
input_tokens
output_tokens
total_tokens
所以今天直接延續昨天的程式,加入 Token Usage 的觀察功能。
除了顯示每一個 Request 使用多少 Token,我也會增加:
session_total_tokens
把這次程式執行期間用過的 Token 累積起來。
完整程式如下:
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.4")
print("輸入 exit 可以結束對話。")
print("輸入 history 可以查看目前的對話紀錄。")
conversation_history = []
session_total_tokens = 0
while True:
user_input = input("\nYou: ")
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
}
)
sent_message_count = len(conversation_history)
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
}
)
usage = response.usage
session_total_tokens += usage.total_tokens
print("Memora:", assistant_reply)
print("\n--- Token Usage ---")
print("Messages sent:", sent_message_count)
print("Input tokens:", usage.input_tokens)
print("Output tokens:", usage.output_tokens)
print("Total tokens:", usage.total_tokens)
print("Session total tokens:", session_total_tokens)
print("-------------------")
和昨天相比,Conversation History 的邏輯完全沒有改變。今天只新增:
session_total_tokens = 0
記錄目前程式執行期間累積使用的 Token。
在呼叫 API 前,先記錄這次送出了幾則 Message:
sent_message_count = len(conversation_history)
取得 Response 後,再讀取:
usage = response.usage
最後顯示這次 Request 的 Token Usage:
print("Input tokens:", usage.input_tokens)
print("Output tokens:", usage.output_tokens)
print("Total tokens:", usage.total_tokens)
Responses API 回傳的 usage 確實包含 Input、Output 與 Total Token Usage,相關欄位可以在 OpenAI Responses API 文件中找到。
現在可以先輸入一句簡單的話:
You:
我的英文程度是 B1。
可能會看到類似下面的結果:
Memora: 了解,我會以 B1 程度協助你。
--- Token Usage ---
Messages sent: 1
Input tokens: 86
Output tokens: 20
Total tokens: 106
Session total tokens: 106
-------------------
接著輸入:
You:
請給我三個適合的英文單字。
第二次的結果可能變成:
Memora: travel、journey、abroad。
--- Token Usage ---
Messages sent: 3
Input tokens: 125
Output tokens: 24
Total tokens: 149
Session total tokens: 255
-------------------
這裡的數字只是示意,實際結果會受到模型回答、System Prompt 與輸入內容影響。
不過可以觀察到一個重要現象:
第一輪 Input
=
System Prompt
+
User Message 1
到了第二輪:
第二輪 Input
=
System Prompt
+
User Message 1
+
Assistant Message 1
+
User Message 2
所以即使第二次的 User Message 很短,Input Tokens 還是可能比第一輪多。
原因就是:
第二次 Request 重新帶入了第一輪的 Conversation History。
Total Tokens 和 Session Total Tokens 不一樣程式現在會顯示兩個看起來很像的數字:
Total tokens
Session total tokens
但兩者代表的事情不同。
usage.total_tokens 是:
目前這一次 Request 使用的 Input Tokens 與 Output Tokens。
而 session_total_tokens 是我們自己在 Python 裡累加的數字:
從程式啟動到現在,所有 Request 總共使用了多少 Token。
Context Window 限制的是:
單一 Request
並不是整個 Session 的累積總量。
假設:
Request 1:100 Tokens
Request 2:150 Tokens
Request 3:200 Tokens
那麼第三次 Request 的大小是:
200 Tokens
而不是:
100 + 150 + 200 = 450 Tokens
450 Tokens 是整個 Session 到目前為止的累積使用量,不是第三次 Request 占用的 Context Window。
這個差異很重要。只要每一個 Request 都沒有超過 Context Limit,Session 累積使用的 Token 就算遠高於 Context Window,也不代表程式一定會出錯。
Conversation History 帶來的問題,不只是最後可能超過 Context Window。
因為我們每一次都送出完整的:
conversation_history
先前的 Message 會被重複放進後面的 Request。
假設有三輪對話:
第 1 輪:
Message 1
Message 2
第 2 輪:
Message 1
Message 2
Message 3
Message 4
第 3 輪:
Message 1
Message 2
Message 3
Message 4
Message 5
Message 6
第一輪的內容不只被處理一次,而是會出現在:
Request 1
Request 2
Request 3
第二輪新增的內容則會出現在:
Request 2
Request 3
因此,對話越長,每一次 Request 的 Input 越大;而且越早出現的內容,會在後續 Request 中被重複傳送很多次。
如果每一輪訊息長度差不多,單次 Request 的 Input 會隨著對話輪數增加;整段 Conversation 累積處理的 Token,則會比單純的 Message 數量成長得更快。
也就是說,History 還沒真的塞滿 Context Window 之前,我們可能就已經先遇到:
Input Token Usage 增加
API 成本增加
每次要處理的內容增加
無關的舊訊息越來越多
所以 Context Management 並不是等到 API 報錯時才需要處理的問題。
我標題寫Context Window爆掉,是指這次Request想放進模型的內容,已經超過它能處理的範圍。
不同 API、Framework 或設定,可能採取不同做法,例如:
直接拒絕 Request
自動移除前面的內容
截斷部分輸入
以我們目前使用的 Responses API 來說,truncation 預設為 disabled。根據目前的 OpenAI Responses API 文件,如果 Input 超過模型的 Context Window,Request 預設會失敗並回傳 400 錯誤。
也就是說,我們現在的程式不會因為 History 太長,就自動聰明地判斷哪些訊息不重要。
它也不會自動理解:
這一段可以刪掉
那一段必須保留
使用者的英文程度不能忘
剛才的閒聊可以捨棄
對目前的 Memora 來說,Conversation History 仍然只是一個一直增長的 Python List。
既然問題是 Context Window 有限,那是不是選一個 Context Window 更大的模型就能解決?
更大的 Context Window 當然可以容納更多內容,但它只能把問題延後,不能讓無限增長的 History 真的變成有限。
只要我們仍然一直執行:
conversation_history.append(...)
卻從來不刪除、不選擇也不整理,History 最後仍然會不斷增加。
而且即使還沒有碰到上限,把所有舊訊息都塞進每一次 Request,也不代表這些內容對目前的回答都有幫助。
例如使用者現在問的是:
請解釋現在完成式。
幾十輪以前討論晚餐吃什麼的內容,可能仍然存在 Conversation History 裡,但不一定值得占用這次的 Context。
所以真正的問題不是:
怎麼讓模型什麼都收進去?
而是:
有限的 Context Window 裡,現在應該放進哪些內容?
這才是 Context Management 要解決的核心問題。
今天沒有改變 Memora 保存 Conversation History 的方式,而是替它加入 Token Usage 的觀察功能。
我們確認了幾件事:
Context Window
=
模型單次互動能處理的 Token 範圍
Token Usage
不只來自目前的 User Message
每一次 Request 還可能包含:
System Instructions
Previous User Messages
Previous Assistant Messages
Current User Message
Message Structure
Model Output
而且因為我們每一輪都重新送出完整的 Conversation History,Input Tokens 會隨著對話不斷增加。
目前 Memora 的狀態是:
Day 06
保存所有 Conversation History
↓
能接得上上一句
↓
History 持續增長
Day 07
觀察 Token Usage
↓
理解 Context Window 有限
↓
發現不能永遠傳送全部 History
今天最重要的結論是:
Conversation History 讓 AI 記得更多,但 Context Window 決定了它一次能看多少。
因此,Memory Engineering 的第一個難題正式出現了:
當所有內容放不下時,我們該留下什麼,又該移除什麼?
下一篇我們會第一次開始管理 conversation_history,不再讓它無限制增長。
我們會實作:
今天我們看見了 Context Window 的邊界。明天開始,Memora 不只要會保存對話,還要學會在有限的空間裡做出選擇。