LLM 記憶壓縮實戰:滑動視窗 vs 摘要記憶,用答對率與 Token 成本量化取捨
本章節要實作滑動視窗與摘要壓縮兩種記憶,並用事實問答量化「省 token vs 記得住」的取捨
一、滑動視窗記憶
實作只保留最近 N 輪的 SlidingWindowMemory,並驗證在 Context Window 中「忘掉久遠的事」
"""SlidingWindowMemory:只保留最近 N 輪對話(FIFO)(W2)。"""
class SlidingWindowMemory:
def __init__(self, system=None, max_turns=3):
"""max_turns:保留最近幾輪(1 輪 = 1 user + 1 assistant)。"""
self.system = system
self.max_turns = max_turns
self._messages = [] # 內部保留完整歷史(除錯用)
def add_user(self, text):
self._messages.append({"role": "user", "content": text})
def add_assistant(self, text):
self._messages.append({"role": "assistant", "content": text})
def build_messages(self):
"""只回傳最近 max_turns 輪 = 最多 2*max_turns 則訊息。"""
window = self._messages[-2 * self.max_turns:]
# 確保以 user 開頭(避免 assistant 開頭導致 API 報錯)
while window and window[0]["role"] != "user":
window = window[1:]
return window
def __len__(self):
return len(self._messages)
接著寫一個測試用例
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from memory.llm import chat
from memory.window import SlidingWindowMemory
SYSTEM = "你是助理,用繁體中文簡短回答。"
mem = SlidingWindowMemory(system=SYSTEM, max_turns=2) # 只記最近 2 輪
script = [
"我對甲殼類過敏,請記住。", # 埋在最早——會被滑出去
"我最喜歡的顏色是藍色。",
"幫我推薦一部電影。",
"再推薦一首歌。",
"我對什麼過敏?", # 回頭考:早已滑出視窗
]
for user in script:
mem.add_user(user)
reply, _ = chat(mem.build_messages(), system=SYSTEM)
mem.add_assistant(reply)
print(f"你 > {user}\nAI > {reply}\n (視窗內 {len(mem.build_messages())} 則)\n")
執行測試
python demos/w2_window_probe.py
你 > 我對甲殼類過敏,請記住。
AI > 好的,我會記住您對甲殼類過敏。
(視窗內 2 則)
你 > 我最喜歡的顏色是藍色。
AI > 知道了,您最喜歡的顏色是藍色。
(視窗內 4 則)
你 > 幫我推薦一部電影。
AI > 如果您喜歡藍色,或許您會喜歡《藍色是最溫暖的顏色》(Blue Is the Warmest Color),這是一部法國電影,講述了一段深刻的愛情故事。
(視窗內 4 則)
你 > 再推薦一首歌。
AI > 推薦您聽周杰倫的《青花瓷》,這是一首融入中國傳統元素的現代流行音樂,歌詞優美,旋律動聽。
(視窗內 4 則)
你 > 我對什麼過敏?
AI > 要確定您對什麼過敏,需要進行具體的過敏原檢測。這通過皮膚點刺試驗、血液中的IgE測試或其他相關的過敏檢查來進行。如果您有過敏症狀,建議咨詢醫生或過敏科專家並進行專業檢測。
(視窗內 4 則)
可以觀察得到最早的對話内容已經不見了
二、摘要壓縮記憶
實作由滑窗 + 滾動摘要,當—舊對話滑出視窗時,則并入一段由 LLM 維護的摘要。
在下面新增滑動窗口功能
"""SummaryMemory:近期用滑動視窗保留原文,滑出的舊對話併入滾動摘要。"""
from memory.llm import chat
SUMMARIZE_SYSTEM = (
"你是對話摘要器。根據『既有摘要』與『新增對話』,輸出一段更新後的摘要。"
"務必保留:使用者的個人資訊、偏好、限制(如過敏/預算)、已達成的結論與待辦。"
"用繁體中文,條列或短段落,控制在 150 字內。只輸出摘要本身。"
)
class SummaryMemory:
def __init__(self, system=None, max_turns=3, summary_model="claude-haiku-4-5"):
self.system = system
self.max_turns = max_turns
self.summary_model = summary_model
self._messages = [] # 尚未被摘要的近期訊息
self.summary = "" # 滾動摘要
def add_user(self, text):
self._messages.append({"role": "user", "content": text})
def add_assistant(self, text):
self._messages.append({"role": "assistant", "content": text})
self._maybe_compress() # 每完成一輪就檢查是否要壓縮
def _maybe_compress(self):
"""超出視窗的最舊一輪 → 併入摘要。"""
keep = 2 * self.max_turns
while len(self._messages) > keep:
# 取出最舊一輪(user + assistant)
overflow = self._messages[:2]
self._messages = self._messages[2:]
self.summary = self._roll_summary(self.summary, overflow)
def _roll_summary(self, prev_summary, overflow_msgs):
convo = "\n".join(f"{m['role']}: {m['content']}" for m in overflow_msgs)
prompt = (
f"【既有摘要】\n{prev_summary or '(無)'}\n\n"
f"【新增對話】\n{convo}\n\n請輸出更新後的摘要。"
)
summary, _ = chat(
[{"role": "user", "content": prompt}],
system=SUMMARIZE_SYSTEM, model=self.summary_model, max_tokens=300,
)
return summary.strip()
def build_system(self):
"""把摘要併入 system。"""
base = self.system or ""
if self.summary:
return f"{base}\n\n【先前對話摘要】\n{self.summary}"
return base
def build_messages(self):
return list(self._messages)
def __len__(self):
return len(self._messages)
修改下前面的 w2_window_probe.py
from memory.summary import SummaryMemory
reply, _ = chat(mem.build_messages(), system=mem.build_system())
再執行一次 python demos\w2_window_probe.py
得到結果如下:
你 > 我對甲殼類過敏,請記住。
AI > 好的,已記住您對甲殼類過敏。
(視窗內 2 則)
你 > 我最喜歡的顏色是藍色。
AI > 了解,您喜歡藍色。
(視窗內 4 則)
【摘要更新】
用户对甲壳类过敏,需避免接触。
(視窗內 4 則)
你 > 幫我推薦一部電影。
AI > 好的,根據您喜歡藍色這一喜好,我推薦您觀看《海洋奇緣》(Moana),這是一部充滿藍色海洋風情的動畫電影,故事感人且富有冒險精神。
(視窗內 4 則)
【摘要更新】
用户对甲壳类过敏,需避免接触。用户最喜欢的颜色是蓝色。
(視窗內 4 則)
你 > 再推薦一首歌。
AI > 既然您喜歡藍色,我推薦您聽《藍色大門》这首歌。這是一首由蘇打綠演唱的中文流行歌曲,歌詞中充滿詩意與浪漫,帶有藍色調的 情感色彩。
(視窗內 4 則)
【摘要更新】
用户对甲壳类过敏,需避免接触;最喜欢的颜色是蓝色。根据用户喜好,推荐用户观看充满蓝色海洋风情的动画片《海洋奇缘》(Moana) 。
(視窗內 4 則)
你 > 我對什麼過敏?
AI > 您對甲壳類過敏。
(視窗內 4 則)
可以看到,即使過敏資訊早已滑出視窗,因為它被寫進了摘要,模型這次答得出來。
在下面我們來做對比實驗
=== 全量拼接 BufferMemory ===
Q:我對什麼過敏? 期望[甲殼類] → ✅ 你對甲殼類食物過敏。在外用餐時,請務必注意食物成分,並告知餐
Q:我的每餐預算多少? 期望[一千] → ✅ 你的每餐預算是一千日圓。
Q:我住在哪附近? 期望[京都車站] → ✅ 你住在京都車站附近的旅館。
Q:我叫什麼名字? 期望[小明] → ✅ 你叫小明。
=== 滑動視窗 max_turns=2 ===
Q:我對什麼過敏? 期望[甲殼類] → ❌ 作为人工智能,我无法直接知道您的个人健康状况,包括您对什么过
Q:我的每餐預算多少? 期望[一千] → ❌ 您的每餐预算取决于您的个人经济状况、饮食习惯、所在地区的物价
Q:我住在哪附近? 期望[京都車站] → ❌ 您住在您自己家中或所选择的居住地附近。由于我们没有具体信息,
Q:我叫什麼名字? 期望[小明] → ❌ 您的名字不是由我来决定的,而是您自己的身份标识。如果您忘记了
=== 摘要壓縮 max_turns=2 ===
Q:我對什麼過敏? 期望[甲殼類] → ✅ 您對甲殼類食物過敏。
Q:我的每餐預算多少? 期望[一千] → ❌ 您的每餐預算是1000日圓。
Q:我住在哪附近? 期望[京都車站] → ✅ 您住在京都車站附近。
Q:我叫什麼名字? 期望[小明] → ✅ 您的名字是小明。
最後可以看到
- 全量拼接:答對率最高,但 token 最貴 —— 對話輪數多了仍舊會撞牆。
- 滑動視窗:token 最省,但久遠之前的題目大量答錯(只記得最近 2 輪)。
- 摘要壓縮:token 使用介於中間,多數久遠事實依靠摘要保存,取得成本與品質的平衡點。
但如果事實很細碎、又只在特定時刻才需要,摘要仍舊可能失效。
================================================
策略 總input token 答對
------------------------------------------------
全量拼接 13262 4/4
滑動視窗 2382 0/4
摘要壓縮 5999 3/4
================================================