iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0

昨天我們替 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,很容易把它想成電腦的硬碟或資料庫容量,但兩者並不相同。

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 放得越多,剩下能使用的空間就越少。


二、Input 和 Output 都需要 Token 空間

我們目前的 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 到底有多大?

不同模型的 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 中還有一些不會直接顯示在文章裡的結構資訊。


四、真正占用 Context 的不只聊天文字

假設 Conversation History 是:

[
    {
        "role": "user",
        "content": "我的英文程度是 B1。"
    },
    {
        "role": "assistant",
        "content": "了解,我會以 B1 程度協助你。"
    }
]

不能只計算兩段 content 的字數。

模型實際收到的 Input 還包括:

System Instructions
Message Role
Message Boundary
Conversation Structure

例如 userassistant 的角色資訊,也需要被編碼成模型可以處理的格式。

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。


五、讓 Memora 顯示每一輪的 Token Usage

與其自己根據字數猜測,不如直接查看 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 文件中找到。


六、實際觀察 Token 如何增加

現在可以先輸入一句簡單的話:

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 TokensSession 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 Limit 會發生什麼?

我標題寫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 更大的模型就能解決?

更大的 Context Window 當然可以容納更多內容,但它只能把問題延後,不能讓無限增長的 History 真的變成有限。

只要我們仍然一直執行:

conversation_history.append(...)

卻從來不刪除、不選擇也不整理,History 最後仍然會不斷增加。

而且即使還沒有碰到上限,把所有舊訊息都塞進每一次 Request,也不代表這些內容對目前的回答都有幫助。

例如使用者現在問的是:

請解釋現在完成式。

幾十輪以前討論晚餐吃什麼的內容,可能仍然存在 Conversation History 裡,但不一定值得占用這次的 Context。

所以真正的問題不是:

怎麼讓模型什麼都收進去?

而是:

有限的 Context Window 裡,現在應該放進哪些內容?

這才是 Context Management 要解決的核心問題。


Day 7 小結

今天沒有改變 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 的第一個難題正式出現了:

當所有內容放不下時,我們該留下什麼,又該移除什麼?

Day 08|Token 太多怎麼辦?Sliding Window 與 Context Management

下一篇我們會第一次開始管理 conversation_history,不再讓它無限制增長。

我們會實作:

  • 只保留最近幾輪對話
  • 讓較舊的 Message 離開目前的 Context
  • 控制每次送進模型的 History 大小
  • 比較完整 History 與 Sliding Window 的差異

今天我們看見了 Context Window 的邊界。明天開始,Memora 不只要會保存對話,還要學會在有限的空間裡做出選擇。


上一篇
Day 06|Conversation History 是什麼?讓 AI 接得上前一句
系列文
從 Stateless LLM 到 Agentic Memory:30 天打造會記憶的 AI Agent7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言