如果你跟一個 AI 助理聊天聊了很久,它其實會把你們講過的每一句話都記在腦子裡——這樣才能記得你上一句說了什麼。但這件事有個麻煩:講越久,要記的東西就越多,SummarizationMiddleware 就是用來解決這個麻煩的工具。
先想像一個情境:寫筆記本
想像你在幫一場超級長的會議寫筆記,你想把每一句話都逐字寫下來。
一開始還好,但開了兩三個小時之後,你的筆記本已經寫了好幾十頁。這時候會發生兩個問題:
- 筆記本頁數是有限的,寫太多,可能真的會沒紙可以寫
- 每次你想回頭確認之前講了什麼,都要翻過這幾十頁逐字稿,超級花時間
比較聰明的做法是:每隔一段時間,把前面寫的一大堆逐字稿,濃縮成一小段「重點摘要」,只留下最近幾頁的逐字稿保持新鮮,舊的部分就用這段摘要取代。
這正是 AI 助理在長對話裡遇到的問題,而 SummarizationMiddleware 就是那個「幫你把舊筆記濃縮成摘要」的功能。
六個參數,一個一個用筆記本比喻講清楚
from langchain.agents.middleware import SummarizationMiddleware
summarization_middleware = SummarizationMiddleware(
model=model,
trigger=("messages", 20),
keep=("messages", 10),
)
1. model——找誰來幫你濃縮筆記?
沒有預設值,一定要填。
生活例子:濃縮筆記需要找一個「秘書」來重讀你寫的逐字稿,重新整理成精簡版。你可以直接找你原本那位 AI 朋友兼任這個秘書工作(填同一個 model),也可以另外找一個專門負責整理筆記的人。
2. trigger——筆記寫到什麼程度,才叫秘書出來整理?
預設值:None。
生活例子:這是「叫秘書出來工作」的規則。可以用好幾種方式定規則:
# 寫到 50 頁(50 則訊息)就整理
("messages", 50)
# 寫了超過 3000 個字就整理
("tokens", 3000)
# 筆記本用掉 80% 的頁數,或是寫到 100 頁,兩個條件「其中一個」先達到就整理
[("fraction", 0.8), ("messages", 100)]
# 寫超過 4000 字「而且」超過 10 頁,兩個條件「都」要達到才整理
{"tokens": 4000, "messages": 10}
用一個 tuple 代表一條規則,用一個 dict(像 {"tokens": 4000, "messages": 10})代表「這幾條規則要同時成立」(AND),用一個 list 包好幾條規則代表「其中一條成立就好」(OR)。
⚠️ 最容易誤會的陷阱:不填 trigger,秘書永遠不會出現
我實際去翻了原始碼,trigger 預設是 None,而 None 在裡面會被直接轉成空的規則清單——也就是「沒有任何一條規則會被觸發」。
翻譯成白話:如果你沒有填 trigger,這個 SummarizationMiddleware 就等於是一個沒有任何作用的裝飾品,秘書永遠不會被叫出來,筆記本永遠不會被濃縮,跟完全沒加這個功能是一樣的效果。trigger 雖然寫著是「可填可不填」的參數,但實際上不填就等於沒開這個功能,務必記得手動設定。
3. keep——濃縮完之後,最近幾頁要保留原文不動?
預設值:("messages", 20)。
生活例子:秘書把舊的逐字稿濃縮成一段摘要之後,最近的幾頁不會被濃縮,會保持原本逐字稿的樣子留著,因為最近講的內容通常最重要、最需要看到完整細節。預設是保留最近 20 則訊息。
4. token_counter——秘書怎麼知道筆記本寫了「多少」?
預設值:一個內建的估算函式(count_tokens_approximately)。
生活例子:秘書要判斷「現在到底寫了多厚一疊筆記」,預設是用粗估的方式數字數(快,但不是每次都精準)。如果你想要更精確的計算方式,可以自己換一個數字函式進去。
5. summary_prompt——你給秘書的「整理說明書」
預設值:一份內建、寫得很仔細的說明書(要秘書整理出「這段對話的目標是什麼」「重點結論有哪些」「做過哪些事、修改過哪些檔案」「接下來還有什麼要做」)。
生活例子:你不會只跟秘書說「你整理一下」就沒了,你會給他一張說明書,寫清楚「我要的摘要要包含哪些項目」。SummarizationMiddleware 內建就有一份寫得很完整的說明書,如果你想要摘要長得不一樣,可以自己換一份說明書進去。
6. trim_tokens_to_summarize——一次最多給秘書看多少舊筆記?
預設值:4000(字數上限)。
生活例子:就算秘書的工作是整理筆記,你也不能一次把幾百頁全部倒給他,叫他自己想辦法——那這個「整理」的動作本身就會變得超級慢、超級貴。所以會先限制「一次最多給他看 4000 字份量的舊筆記」來整理,超過的部分不會被塞進這次的整理工作裡。設成 None 的話,就是不設上限,全部都塞給他看。
fraction、tokens、messages 到底該選哪一種?
前面一直出現這三種寫法,它們是拿來「衡量筆記本寫了多少」的三把不同的尺。選哪一把尺,會直接影響摘要功能準不準、甚至會不會直接壞掉——這節就是要把三把尺的定義、優缺點、設計理由,一次講清楚。
三把尺的定義
| 尺 | 量的是什麼 |
|---|---|
messages | 講了幾句話——不管每句話長還是短,一句就算一句 |
tokens | 實際寫了多少字——用真正的字數(token 數)去算,長句子佔比較多、短句子佔比較少 |
fraction | 這本筆記本已經寫滿了幾成——先問「這個模型的筆記本總共能寫幾頁」,再算目前寫了佔幾 % |
⚠️ 修正一下:其實只有兩種「單位」,不是三種
上面把它們並列成「三把尺」,但這個說法不夠精確。我去查了原始碼裡 fraction 實際怎麼運作,關鍵是這一行:
threshold = int(max_input_tokens * value)
意思是:如果你設 ("fraction", 0.85),而模型的 max_input_tokens 是 128000,系統會先算出 128000 * 0.85 = 108800,接下來的比對方式,就完全跟你直接設 ("tokens", 108800) 是同一回事。
所以精確地說:
messages是獨立的單位,算「幾句話」,跟字數完全無關tokens才是真正的計量單位,算「幾個字」fraction不是新的單位,它是「幫你自動算出 tokens 數字的小工具」——你負責給它一個百分比,它負責去問模型「你的上限是多少」,兩個相乘算出實際的 tokens 門檻,幫你省下自己猜數字的麻煩
用前面的比喻來說:tokens 是「直接跟秘書說『寫超過 10 萬字就整理』」,fraction 則是「跟秘書說『寫到八成滿就整理』,秘書自己心裡知道這本筆記本總共幾頁,自動幫你換算成字數」——最後秘書其實還是在數字數,只是誰負責把百分比換算成字數的差別而已。
下面還是分開三節講,方便對照著看,但記得:tokens 跟 fraction 骨子裡其實是同一件事,只差在門檻是你自己訂、還是系統幫你自動換算。
如果只用 messages(訊息數)
優點:
- 超級好懂、好預測——「講滿 20 句就整理」,人腦直接就能算
- 不用做任何字數計算,幾乎零成本
- 不需要知道模型的筆記本到底有幾頁,什麼模型都能用
缺點:
- 完全不管每句話的長短。如果某一句話其實是一大包資料(例如某個工具回傳了一長串很長的查詢結果),用
messages算的話它照樣只算「一句」,但它實際佔的份量可能是其他 20 句話的總和 - 因為抓不到「內容到底多大包」,可能你設定的訊息數還沒到,但筆記本其實早就已經寫爆了——這是最需要注意的風險,不只是「不夠精準」,是真的有可能超出模型上限
設計理由:這是三把尺裡「最不精準,但最簡單、最不會出錯」的選項,適合初學或不想處理複雜狀況的情境,我們範例腳本目前用的就是這種。
如果只用 tokens(固定字數)
優點:
- 直接對應到「模型真正在乎的東西」——模型的上限本來就是用字數(token)在算的,不是用「幾句話」在算的,所以這把尺天生比
messages準 - 不需要知道模型的筆記本「總共」有幾頁,只要抓一個「寫超過這個字數就該整理了」的固定數字即可
缺點:
- 這個「固定數字」要你自己去猜、自己去訂,而且換一個模型,這個數字不一定還適用。例如你設定「超過 17 萬字整理」,如果換去一個筆記本只有 8 萬字容量的小模型,還沒等到你設定的門檻,筆記本早就已經寫爆了
- 字數計算預設是「粗估」(前面第 4 個參數
token_counter講過的),不是每次都 100% 精準
設計理由:這是「準確度」跟「不依賴模型資訊」的折衷——比 messages 準,但需要你自己承擔「訂錯數字」的風險,適合你很清楚自己在用哪個模型、也知道它的筆記本大概多大的情境。
如果只用 fraction(佔比)
優點:
- 自動根據模型調整,不用你自己猜數字——同一份
0.85設定,不管換到筆記本大還是小的模型上,都是「寫到 85% 才整理」,自動換算成該模型實際的字數上限 - 最貼近「這個模型到底快不快寫滿了」這個問題的真正答案
缺點,也是最重要的注意事項:
fraction必須依賴「系統知道這個模型的筆記本到底有幾頁」,也就是模型物件要有max_input_tokens這項資訊。我實際測試給你看差別:
from langchain_openai import ChatOpenAI
# 認得的模型名稱,系統知道它的筆記本大小
m1 = ChatOpenAI(model="gpt-4o-mini")
print(m1.profile)
# {'max_input_tokens': 128000, ...} 有資料
# 自架的、或不認得的模型名稱
m2 = ChatOpenAI(model="my-custom-model", base_url="http://localhost:1234/v1")
print(m2.profile)
# None 完全沒有資料
如果這時候你還是設定 trigger=("fraction", 0.85),不會安靜地失效,而是直接丟出錯誤讓程式當掉,原始碼裡的錯誤訊息是這樣寫的:
Model profile information is required to use fractional token limits, and is unavailable for the specified model. Please use absolute token counts instead, or pass
ChatModel(..., profile={"max_input_tokens": ...})
翻成白話:如果你接的是像我們專案裡 .env 那樣的 OpenAI 相容自訂端點(自己指定 MODEL_NAME/MODEL_URL),系統很可能根本不認得這個模型,profile 會是 None,這時候用 fraction 會直接讓程式壞掉,不是「不準」而已。想用 fraction 的話,要嘛換成系統認得的官方模型名稱,要嘛自己手動補資料:
ChatOpenAI(
model="my-custom-model",
base_url="http://localhost:1234/v1",
profile={"max_input_tokens": 32000}, # 自己告訴系統,這本筆記本有幾頁
)
設計理由:fraction 是三把尺裡「最聰明、最能跨模型通用」的選項,這也是為什麼 deepagents 把它設成優先採用的預設值——但它的聰明是建立在「系統要先知道模型的底細」這個前提上,一旦這個前提不成立,它不會退而求其次悄悄改用別的方式,而是直接報錯要求你自己處理。
一張表總結三把尺的取捨
messages | tokens | fraction | |
|---|---|---|---|
| 好懂程度 | 最好懂 | 中等 | 需要多理解一層 |
| 準確度 | 最不準(不管內容大小) | 較準(但門檻要自己訂) | 最準(自動貼合模型上限) |
| 換模型還能用嗎 | 可以,但門檻可能不合理 | 可以,但門檻可能不合理 | 最能跨模型通用 |
| 會不會直接報錯 | 不會 | 不會 | 模型沒有 profile 資訊時會直接報錯 |
主流都怎麼設定 trigger / keep?
前面講的都是「怎麼填」,但實務上大家都填多少?我直接去翻了 LangChain 官方文件跟 deepagents 這個知名開源專案的原始碼,整理出兩種常見配置。
配置一:用「筆記本用掉的比例」來決定(fraction 型)
這是 deepagents 這個專案實際在用的預設值——當它知道你用的模型「筆記本總共有幾頁」的時候(也就是知道模型的 context window 上限),就會自動套用:
trigger = ("fraction", 0.85) # 筆記本寫到 85% 滿,就叫秘書出來整理
keep = ("fraction", 0.10) # 整理完,保留最近 10% 的份量不動
生活例子:這就像是「筆記本快寫完了才整理」——不用太早就急著整理(不然常常整理很浪費時間),但也要在真的寫滿之前留一點緩衝,不要等到完全沒紙寫才動手。
配置二:用「固定字數/頁數」來決定(fixed 型)
如果系統不知道你的模型的筆記本總共有幾頁(例如比較新、比較冷門的模型,沒有登記在案),deepagents 就會退而求其次,改用固定數字當保險:
trigger = ("tokens", 170000) # 寫超過 17 萬字,就叫秘書出來整理
keep = ("messages", 6) # 整理完,只留最近 6 則訊息
生活例子:因為不知道這本筆記本到底有幾頁,乾脆先抓一個「寫太多字一定有問題」的保守數字,而且整理完只留很少的最近訊息,免得猜錯上限,反而讓筆記爆頁。
我們範例腳本用的是更簡單的「訊息數」版本
回頭看我們 stream-example-agent.py 裡用的:
trigger = ("messages", 20) # 累積 20 則訊息就整理
keep = ("messages", 10) # 整理完保留最近 10 則
這種寫法不用管模型的筆記本到底有幾頁,直接用「講了幾句話」當標準,對初學者來說最好懂、最容易預期效果,適合拿來練習;但正式上線的專案,通常會像 deepagents 那樣,改用 fraction(佔筆記本比例)當標準,因為每個模型的筆記本大小不一樣,用固定訊息數不一定準。
一句話總結
SummarizationMiddleware 就是幫 AI 助理內建一位「筆記秘書」:trigger 決定什麼時候叫秘書出來工作(沒設就永遠不會出來)、keep 決定最近幾頁不動、model 決定找誰當秘書、summary_prompt 決定給秘書的整理說明書長什麼樣、token_counter 跟 trim_tokens_to_summarize 則是幫秘書控制工作量,避免整理這件事本身反而變成新的負擔。