昨天我們替 Memora 加入了 Token Usage 的觀察功能,也發現 Conversation History 不能永遠全部放進 Context。
目前的程式每一輪都會執行:
input=conversation_history
這代表無論過去的訊息是否仍然和現在有關,都會被重新送給模型。
今天要做的事情,就是第一次真正管理 Context:
Conversation History可以繼續保存,但不代表每一次都要全部送給模型。
我們會從最簡單的 Sliding Window 開始,只選擇最近幾輪對話放進目前的 Context。
Day 6 建立的:
conversation_history = []
同時負責兩件事情:
保存完整對話
把完整對話送給模型
只要 History 還很短,這樣做沒有太大問題。但當對話越來越長,就應該把這兩個責任分開。
今天開始,我們會有兩組不同的資料:
conversation_history = 這次程式執行中的完整對話紀錄
以及:
context_messages = 這一次真正要送給模型的 Message
架構會從:
Conversation History
↓
全部送給 LLM
改成:
Conversation History
↓
Context Selection
↓
Context Messages
↓
LLM
這裡的 conversation_history 可以繼續保留完整資料,但模型每次只會看見經過選擇的 context_messages。
這就是最基本的Context Management。
Sliding Window 的概念很簡單:
只保留最近一段範圍內的內容,當新的內容進來時,整個範圍就向前移動。
假設我們規定模型只能看到最近三輪已完成的對話。一開始的 History 是:
Turn 1
User 1
Assistant 1
Turn 2
User 2
Assistant 2
Turn 3
User 3
Assistant 3
這些內容都還在 Window 裡。
當 Turn 4 出現時,模型看到的會是:
User 1
Assistant 1
User 2
Assistant 2
User 3
Assistant 3
User 4
這裡包含最近三輪完成的對話,再加上目前的 User Message。
等 Turn 4 完成,接著進入 Turn 5,Window 就會向前移動:
User 2
Assistant 2
User 3
Assistant 3
User 4
Assistant 4
User 5
最舊的:
User 1
Assistant 1
仍然可以保存在完整的 conversation_history 裡,但不再出現在這一次送給模型的 Context 中。
因此 Sliding Window 的「滑動」指的是:
舊內容離開目前的 Context
新內容進入目前的 Context
而不一定代表舊資料已經從 Application 裡被刪除。
最直接的寫法可能是:
context_messages = conversation_history[-6:]
也就是只取最後六則 Message。
但這裡有一個容易忽略的問題。
當使用者輸入新的訊息後,Conversation History 可能是:
User 1
Assistant 1
User 2
Assistant 2
User 3
Assistant 3
User 4
這時一共有七則 Message。
如果直接取得最後六則:
conversation_history[-6:]
結果會變成:
Assistant 1
User 2
Assistant 2
User 3
Assistant 3
User 4
Context 是從 Assistant 1 開始的,但它對應的 User 1 已經不在裡面。
模型會看到一個沒有問題、卻突然出現的回答。雖然 API 可能仍然接受這組 Message,但 Conversation 的語意結構已經不完整。
所以今天不直接按照 Message 數量亂切,而是以一組完整的:
User Message
Assistant Message
作為一個 Turn。
這樣 Window 在移除舊內容時,會一次移除完整的一輪,不會只留下其中一半。
先設定要保留幾輪已完成的對話:
MAX_RECENT_TURNS = 3
接著建立一個 Function:
def build_turn_window(history):
current_user_message = history[-1]
completed_messages = history[:-1]
recent_messages = completed_messages[
-MAX_RECENT_TURNS * 2:
]
return recent_messages + [current_user_message]
呼叫這個 Function 時,新的 User Message 已經被加進 conversation_history,所以:
history[-1]
一定是目前這一輪的 User Message。
剩下的:
history[:-1]
則是先前已經完成的 User 與 Assistant Message。
因為每一輪包含兩則 Message,所以最近三輪就是:
MAX_RECENT_TURNS * 2
最後再把目前的 User Message 加回來:
recent_messages + [current_user_message]
這樣建立出來的 Window 就會是:
最近三輪完成的對話
+
目前的 User Message
而且永遠不會從一個失去問題的 Assistant Message 開始。
以最近三輪為範圍,已經可以避免 History 無限增長,但它仍然不是精確的 Token 控制。
因為三輪對話可能非常短:
User:
Hello.
Assistant:
Hi!
也可能每一輪都有數千字。
所以:
3 Turns
不代表固定數量的 Token。
比較完整的做法應該同時考慮兩個限制:
最多保留幾輪
+
最多允許多少 Input Tokens
今天先設定一個刻意偏小的 Input Token Budget:
MAX_INPUT_TOKENS = 1000
這不是 gpt-5-mini 真正的 Context Window 上限,而是我們替 Memora 設定的 Application Budget。
刻意把數字設小,是為了方便觀察 Sliding Window。如果直接使用模型完整的 Context 上限,可能需要輸入非常大量的文字,才看得出 Window 發生變化。
實際產品中的 Budget 通常還需要考慮:
System Prompt
預計保留的 Output 空間
成本
延遲
應用情境
所以不應該直接把模型的完整 Context Window 全部分配給 Input。
Day 7使用:
response.usage.input_tokens
查看 Request 完成後實際用了多少 Input Tokens。
但如果希望在送出 Request 之前控制 Context,就需要先知道目前的 context_messages 有多大。
OpenAI Responses API 提供 Input Token Counting:
client.responses.input_tokens.count(
model=MODEL,
instructions=SYSTEM_PROMPT,
input=messages
)
它可以接受與 responses.create() 相同的 Input 結構,並回傳模型實際會收到的 Input Token 數量。相關格式可以參考 OpenAI 官方 Token Counting 文件。
先建立一個 Function:
def count_input_tokens(messages):
result = client.responses.input_tokens.count(
model=MODEL,
instructions=SYSTEM_PROMPT,
input=messages
)
return result.input_tokens
接著把剛才的 Turn-based Window 再加上一層 Token 檢查:
def build_context_window(history):
current_user_message = history[-1]
completed_messages = history[:-1]
recent_messages = completed_messages[
-MAX_RECENT_TURNS * 2:
]
context_messages = recent_messages + [
current_user_message
]
while True:
input_tokens = count_input_tokens(
context_messages
)
if input_tokens <= MAX_INPUT_TOKENS:
return context_messages, input_tokens
if len(context_messages) == 1:
raise ValueError(
"目前的 User Message 本身已超過 Input Token Budget。"
)
context_messages = context_messages[2:]
一開始,Function 會先取得最近三輪對話。
如果沒有超過 MAX_INPUT_TOKENS,就直接回傳:
return context_messages, input_tokens
如果超過 Budget,就移除最舊的一整輪:
context_messages = context_messages[2:]
因為最前面的兩則 Message 是一組完整的:
User
Assistant
所以每次移除兩則,可以維持 Conversation 的 Turn 結構。接著重新計算Token,直到符合 Budget 為止。
現在把這個 Sliding Window 接進昨天的程式。
除了原本的 history 指令,我再新增一個:
context
用來查看上一個 Request 真正送給模型的 Message。
完整程式如下:
from openai import OpenAI
client = OpenAI()
MODEL = "gpt-5-mini"
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.
"""
MAX_RECENT_TURNS = 3
MAX_INPUT_TOKENS = 1000
print("Memora v0.5")
print("輸入 exit 可以結束對話。")
print("輸入 history 可以查看完整對話紀錄。")
print("輸入 context 可以查看上一次送出的 Context。")
conversation_history = []
last_context_messages = []
session_total_tokens = 0
def count_input_tokens(messages):
result = client.responses.input_tokens.count(
model=MODEL,
instructions=SYSTEM_PROMPT,
input=messages
)
return result.input_tokens
def build_context_window(history):
current_user_message = history[-1]
completed_messages = history[:-1]
recent_messages = completed_messages[
-MAX_RECENT_TURNS * 2:
]
context_messages = recent_messages + [
current_user_message
]
while True:
input_tokens = count_input_tokens(
context_messages
)
if input_tokens <= MAX_INPUT_TOKENS:
return context_messages, input_tokens
if len(context_messages) == 1:
raise ValueError(
"目前的 User Message 本身已超過 Input Token Budget。"
)
context_messages = context_messages[2:]
while True:
user_input = input("\nYou: ")
if user_input.lower() == "exit":
print("Bye!")
break
if user_input.lower() == "history":
print("\n--- Full Conversation History ---")
for message in conversation_history:
print(
f"{message['role']}: "
f"{message['content']}"
)
print("---------------------------------")
continue
if user_input.lower() == "context":
print("\n--- Last Request Context ---")
if not last_context_messages:
print("目前還沒有送出任何 Request。")
else:
for message in last_context_messages:
print(
f"{message['role']}: "
f"{message['content']}"
)
print("----------------------------")
continue
conversation_history.append(
{
"role": "user",
"content": user_input
}
)
try:
context_messages, counted_input_tokens = (
build_context_window(
conversation_history
)
)
except ValueError as error:
print("Error:", error)
conversation_history.pop()
continue
last_context_messages = context_messages.copy()
skipped_message_count = (
len(conversation_history)
- len(context_messages)
)
response = client.responses.create(
model=MODEL,
instructions=SYSTEM_PROMPT,
input=context_messages
)
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--- Context Management ---")
print(
"Stored history messages:",
len(conversation_history)
)
print(
"Context messages sent:",
len(context_messages)
)
print(
"Older messages skipped:",
skipped_message_count
)
print(
"Counted input tokens:",
counted_input_tokens
)
print(
"Actual input tokens:",
usage.input_tokens
)
print(
"Output tokens:",
usage.output_tokens
)
print(
"Session total tokens:",
session_total_tokens
)
print("--------------------------")
這次最重要的改變,是 API 的 input 不再使用完整 History:
input=conversation_history
而是改成:
input=context_messages
現在資料流變成:
conversation_history
↓
build_context_window()
↓
context_messages
↓
Responses API
conversation_history 仍然保存完整對話,而 context_messages 只負責這一次模型真正需要看到的內容。
現在可以依序進行幾輪對話。
第一輪:
You:
我下個月要考TOEIC。
第二輪:
You:
今天想複習現在完成式。
第三輪:
You:
請給我三個 B1 程度的單字。
第四輪:
You:
請解釋 journey 和 travel 的差別。
這時第四輪送出的 Context 仍然包含:
我下個月要考TOEIC。
因為它還位於最近三輪已完成的對話中。
接著進入第五輪:
You:
我下個月要考什麼考試?
此時 Window 已經向前移動。這次送給模型的內容大致是:
User:
今天想複習現在完成式。
Assistant:
...
User:
請給我三個 B1 程度的單字。
Assistant:
...
User:
請解釋 journey 和 travel 的差別。
Assistant:
...
User:
我下個月要考什麼考試?
最早的:
User:
我下個月要考TOEIC。
以及對應的 Assistant Message,已經離開目前的 Context。
可以輸入:
context
直接檢查上一個 Request 真正送出了哪些 Message。
再輸入:
history
則會發現完整的 History 裡仍然保留:
user: 我下個月要考 TOEIC。
因此這時候會出現一個很重要的差別:
Application 還保存這段資料
不代表:
模型在這次 Request 中看得到這段資料
資料是否存在,和資料是否進入目前 Context,是兩件不同的事情。
從 Python Application 的角度來看,沒有。
因為完整的內容仍然存在:
conversation_history
但從目前這一次模型呼叫的角度來看,離開 Window 的 Message 確實不再能影響回答。
目前架構是:
完整 Conversation History
├── 最近的 Message
│ ↓
│ 進入 Context
│ ↓
│ 模型看得見
│
└── 較舊的 Message
↓
暫時不送出
↓
模型看不見
這也是為什麼判斷 AI 是否「記得」一件事情,不能只看資料有沒有存在某個 List 或 Database 裡。
它還必須在正確的時間被放回 Context,才能真正影響模型的輸出。
目前 Sliding Window 使用的選擇規則非常單純:
越新的 Message 越優先。
也就是只依靠 Recency 決定哪些內容進入 Context。
加入 Sliding Window 後,每次 Request 不再隨著完整 History 無限制增長。
原本是:
第 1 輪:傳送全部 History
第 10 輪:傳送全部 History
第 100 輪:仍然傳送全部 History
現在則是:
完整 History 繼續增長
↓
只選最近幾輪
↓
檢查 Input Token Budget
↓
必要時再移除最舊的一輪
↓
送給模型
因此 Sliding Window 可以幫助我們:
限制每次 Request 的 History 範圍
控制 Input Token 數量
避免無限累積的舊訊息全部進入 Context
保留最近對話的連續性
這是最簡單、也最容易實作的 Context Management 方法。
Sliding Window 的優點是簡單,但它的缺點也很直接:
它只知道一段訊息夠不夠新,不知道這段訊息重不重要。
例如:
Turn 1:
我的英文程度是 B1。
接下來可能只是幾輪普通的單字問答。
當 Window 向前移動後:
英文程度是 B1
這個對後續學習很有用的資訊,可能和一般閒聊一樣被移出 Context。
Sliding Window 不會判斷:
這是使用者的重要設定
也不會判斷:
這只是暫時性的對話
它只會按照時間順序移動。
所以它解決的是:
Context 放不下所有 Message
卻同時產生另一個問題:
舊 Message 離開 Context 後,
其中的重要資訊也一起消失了。
到這裡,我們可以對 Context Management 有一個更完整的理解。
它不只是:
conversation_history[-6:]
而是一個選擇過程:
完整資料有哪些?
↓
這次回答需要哪些?
↓
哪些內容可以放進 Token Budget?
↓
最後送給模型哪些 Message?
今天使用的策略是:
Recency
+
Turn Boundary
+
Input Token Budget
也就是優先保留最近的完整對話,並確保 Input 不超過我們設定的範圍。
後面的 Memory Retrieval、Importance Score 和 Memory Policy,實際上都會繼續回答類似的問題:
在有限的 Context 裡,現在最值得放進去的是什麼?
不過我在Chapter 2想要先解決的問題還只是舊的 Conversation 離開 Sliding Window 後,有沒有辦法不要完全失去它提供的資訊勒?,所以目前還不用跳到那麼遠。
今天我們第一次替 Memora 加入真正的 Context Management。
原本的架構是:
conversation_history
↓
全部送給模型
現在則變成:
conversation_history
↓
Sliding Window
↓
Token Budget Check
↓
context_messages
↓
LLM
我們沒有刪除完整的 Conversation History,而是把:
保存過什麼
和:
這次要看什麼
分成兩個不同的責任。
目前的 Sliding Window 會:
保留最近三輪完整對話
加入目前的 User Message
檢查 Input Token Budget
超過時移除最舊的一整輪
這讓 Memora 不再把所有 History 無限制塞進 Context,也避免從孤立的 Assistant Message 開始。
但 Sliding Window 仍然有一個明顯限制:
舊對話一旦離開 Window,模型就看不到其中的重要資訊。
如果直接把所有舊內容放回來,Context 又會再次變大;如果完全不放,先前的資訊就會消失。
那能不能不要保留每一句原文,而是把舊對話整理成一份比較短的筆記?
這就是明天要討論的問題囉~
下一篇我們來讓 Memora 把離開 Sliding Window 的舊對話整理成 Conversation Summary。
新的 Context 將不再只有:
Recent Messages
而是:
Conversation Summary
+
Recent Messages
+
Current User Message
今天我們讓 Memora 學會在有限的空間裡只看最近的對話。明天它要開始學著替過去做筆記囉!!