Skip to content
Greg's Space
Go back

什麼是 SummarizationMiddleware?

如果你跟一個 AI 助理聊天聊了很久,它其實會把你們講過的每一句話都記在腦子裡——這樣才能記得你上一句說了什麼。但這件事有個麻煩:講越久,要記的東西就越多,SummarizationMiddleware 就是用來解決這個麻煩的工具。

先想像一個情境:寫筆記本

想像你在幫一場超級長的會議寫筆記,你想把每一句話都逐字寫下來。

一開始還好,但開了兩三個小時之後,你的筆記本已經寫了好幾十頁。這時候會發生兩個問題:

  1. 筆記本頁數是有限的,寫太多,可能真的會沒紙可以寫
  2. 每次你想回頭確認之前講了什麼,都要翻過這幾十頁逐字稿,超級花時間

比較聰明的做法是:每隔一段時間,把前面寫的一大堆逐字稿,濃縮成一小段「重點摘要」,只留下最近幾頁的逐字稿保持新鮮,舊的部分就用這段摘要取代。

這正是 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) 是同一回事。

所以精確地說:

用前面的比喻來說:tokens 是「直接跟秘書說『寫超過 10 萬字就整理』」,fraction 則是「跟秘書說『寫到八成滿就整理』,秘書自己心裡知道這本筆記本總共幾頁,自動幫你換算成字數」——最後秘書其實還是在數字數,只是誰負責把百分比換算成字數的差別而已。

下面還是分開三節講,方便對照著看,但記得:tokens 跟 fraction 骨子裡其實是同一件事,只差在門檻是你自己訂、還是系統幫你自動換算。

如果只用 messages(訊息數)

優點:

缺點:

設計理由:這是三把尺裡「最不精準,但最簡單、最不會出錯」的選項,適合初學或不想處理複雜狀況的情境,我們範例腳本目前用的就是這種。

如果只用 tokens(固定字數)

優點:

缺點:

設計理由:這是「準確度」跟「不依賴模型資訊」的折衷——比 messages 準,但需要你自己承擔「訂錯數字」的風險,適合你很清楚自己在用哪個模型、也知道它的筆記本大概多大的情境。

如果只用 fraction(佔比)

優點:

缺點,也是最重要的注意事項:

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 把它設成優先採用的預設值——但它的聰明是建立在「系統要先知道模型的底細」這個前提上,一旦這個前提不成立,它不會退而求其次悄悄改用別的方式,而是直接報錯要求你自己處理。

一張表總結三把尺的取捨

messagestokensfraction
好懂程度最好懂中等需要多理解一層
準確度最不準(不管內容大小)較準(但門檻要自己訂)最準(自動貼合模型上限)
換模型還能用嗎可以,但門檻可能不合理可以,但門檻可能不合理最能跨模型通用
會不會直接報錯不會不會模型沒有 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 則是幫秘書控制工作量,避免整理這件事本身反而變成新的負擔。

資料來源



Previous Post
什麼是 FilesystemMiddleware?
Next Post
什麼是 Logging?