本章節要實作滑動視窗與摘要壓縮兩種記憶,並用事實問答量化「省 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
================================================

無標籤

關注作者:

新增評論