多 Agent Token 優化系列 pt.3:Context 壓縮與摘要 — 打造可無限對話的 AI 系統

在前一篇文章《多 Agent 系統的 Token 用量調優指南》中,我們介紹了 Context 壓縮與摘要作為長期對話穩定性的關鍵策略。本文將深入實作層面,探討如何在真實系統中建構完整的 Context 管理機制,從基礎的滑動視窗到進階的語意壓縮,打造可以「無限對話」的 AI 系統。


Context 累積的問題

為什麼 Context 會爆炸?

在長對話或多輪互動的 AI 應用中,Context(上下文)會隨著對話進行而不斷累積:

Context 累積的指數成長模型:

單輪對話的 Token 消耗:
┌─────────────────────────────────────────────────────────────┐
│  輸入 = System Prompt + 歷史對話 + 當前輸入                  │
│  輸出 = 模型回應                                            │
│                                                             │
│  第 N 輪的輸入 tokens ≈ System + Σ(前 N-1 輪對話) + 當前    │
└─────────────────────────────────────────────────────────────┘

實際數據模擬(假設每輪平均 500 tokens):

輪次    累積 tokens    API 成本(以 Sonnet $3/1M 計)
─────────────────────────────────────────────────────
1       500           $0.0015
5       2,500         $0.0075
10      5,000         $0.015
20      10,000        $0.03
50      25,000        $0.075
100     50,000        $0.15      ← 單次呼叫!
200     100,000       $0.30      ← 接近 Context 上限

問題:
1. 成本線性增長(每輪都重複發送歷史)
2. 延遲增加(處理更多 tokens 需要更多時間)
3. Context Window 耗盡(Claude: 200K tokens 上限)
4. 資訊稀釋(太多歷史可能干擾當前任務)

Context 管理的目標

理想的 Context 管理系統:

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐  │
│  │ 完整對話歷史 │ →  │ 智能壓縮引擎 │ →  │ 精簡 Context │  │
│  │ 50,000 tokens│    │              │    │ 5,000 tokens │  │
│  └──────────────┘    └──────────────┘    └──────────────┘  │
│                                                             │
│  目標:                                                     │
│  ✓ 保留關鍵資訊(決策、結論、重要數據)                    │
│  ✓ 移除冗餘內容(重複、過渡性對話、已解決問題)            │
│  ✓ 維持連貫性(確保模型理解對話脈絡)                      │
│  ✓ 成本可控(壓縮後 tokens 維持在固定範圍)                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

策略一:滑動視窗(Sliding Window)

最簡單直接的 Context 管理方式:只保留最近 N 輪對話。

基礎實作

  1import anthropic
  2from dataclasses import dataclass, field
  3from typing import Optional
  4
  5client = anthropic.Anthropic()
  6
  7@dataclass
  8class Message:
  9    role: str  # "user" or "assistant"
 10    content: str
 11
 12    def to_dict(self) -> dict:
 13        return {"role": self.role, "content": self.content}
 14
 15@dataclass
 16class SlidingWindowConfig:
 17    """滑動視窗配置"""
 18    max_turns: int = 10          # 最多保留幾輪對話
 19    max_tokens: int = 8000       # 最大 tokens 數(估算)
 20    tokens_per_char: float = 0.4 # 中文約 0.5,英文約 0.25
 21
 22
 23class SlidingWindowManager:
 24    """
 25    滑動視窗 Context 管理器
 26
 27    策略:只保留最近 N 輪對話,超過則丟棄最舊的
 28
 29    優點:實作簡單,延遲可預測
 30    缺點:完全丟失早期對話資訊
 31    """
 32
 33    def __init__(self, config: Optional[SlidingWindowConfig] = None):
 34        self.config = config or SlidingWindowConfig()
 35        self.messages: list[Message] = []
 36        self._stats = {
 37            "total_messages": 0,
 38            "dropped_messages": 0
 39        }
 40
 41    def _estimate_tokens(self, text: str) -> int:
 42        """估算文字的 token 數量"""
 43        return int(len(text) * self.config.tokens_per_char)
 44
 45    def _get_total_tokens(self) -> int:
 46        """計算當前所有訊息的 tokens"""
 47        return sum(self._estimate_tokens(m.content) for m in self.messages)
 48
 49    def add_message(self, role: str, content: str):
 50        """添加新訊息"""
 51        self.messages.append(Message(role=role, content=content))
 52        self._stats["total_messages"] += 1
 53        self._trim_if_needed()
 54
 55    def _trim_if_needed(self):
 56        """如果超過限制,移除最舊的訊息"""
 57        # 按輪數限制(一輪 = user + assistant)
 58        while len(self.messages) > self.config.max_turns * 2:
 59            self.messages.pop(0)
 60            self._stats["dropped_messages"] += 1
 61
 62        # 按 token 數限制
 63        while self._get_total_tokens() > self.config.max_tokens and len(self.messages) > 2:
 64            self.messages.pop(0)
 65            self._stats["dropped_messages"] += 1
 66
 67    def get_messages(self) -> list[dict]:
 68        """取得格式化的訊息列表"""
 69        return [m.to_dict() for m in self.messages]
 70
 71    def get_stats(self) -> dict:
 72        return {
 73            **self._stats,
 74            "current_messages": len(self.messages),
 75            "current_tokens": self._get_total_tokens()
 76        }
 77
 78
 79class SlidingWindowChatbot:
 80    """使用滑動視窗的聊天機器人"""
 81
 82    def __init__(
 83        self,
 84        system_prompt: str,
 85        window_config: Optional[SlidingWindowConfig] = None
 86    ):
 87        self.system_prompt = system_prompt
 88        self.window = SlidingWindowManager(window_config)
 89
 90    def chat(self, user_input: str) -> str:
 91        # 添加使用者訊息
 92        self.window.add_message("user", user_input)
 93
 94        # 呼叫 API
 95        response = client.messages.create(
 96            model="claude-sonnet-4-6",
 97            max_tokens=2048,
 98            system=self.system_prompt,
 99            messages=self.window.get_messages()
100        )
101
102        assistant_response = response.content[0].text
103
104        # 添加助手回應
105        self.window.add_message("assistant", assistant_response)
106
107        return assistant_response
108
109
110# 使用範例
111if __name__ == "__main__":
112    bot = SlidingWindowChatbot(
113        system_prompt="你是一個友善的助手。",
114        window_config=SlidingWindowConfig(max_turns=5, max_tokens=4000)
115    )
116
117    # 模擬多輪對話
118    conversations = [
119        "你好,我叫小明",
120        "我喜歡程式設計",
121        "特別是 Python",
122        "我正在學習機器學習",
123        "你能推薦一些資源嗎?",
124        "謝謝你的建議",
125        "對了,你還記得我叫什麼名字嗎?"  # 測試是否記得早期資訊
126    ]
127
128    for user_msg in conversations:
129        print(f"\n👤 User: {user_msg}")
130        response = bot.chat(user_msg)
131        print(f"🤖 Bot: {response[:200]}...")
132        print(f"   [視窗狀態: {bot.window.get_stats()}]")

滑動視窗的限制

滑動視窗的問題:

對話歷史:
Turn 1: "我叫小明,是軟體工程師"     ← 被丟棄
Turn 2: "我在開發一個電商系統"        ← 被丟棄
Turn 3: "使用 Python 和 FastAPI"     ← 被丟棄
Turn 4: "資料庫選擇了 PostgreSQL"    ← 被丟棄
Turn 5: "現在遇到效能問題"            ← 被丟棄
─────────────────────────────────────
Turn 6: "查詢很慢"                    ← 保留(視窗內)
Turn 7: "大概 5 秒才有結果"           ← 保留
Turn 8: "該怎麼優化?"                ← 保留
Turn 9: "索引已經加了"                ← 保留
Turn 10: "還有什麼方法?"             ← 保留(當前)

問題:模型不知道:
- 使用者是誰(小明,軟體工程師)
- 專案背景(電商系統)
- 技術棧(Python, FastAPI, PostgreSQL)

這些關鍵資訊在視窗滑動時被丟棄了!

策略二:漸進式摘要(Progressive Summarization)

將超出視窗的歷史對話壓縮為摘要,保留關鍵資訊。

核心概念

漸進式摘要架構:

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  ┌────────────────────────────────────────────────────────┐│
│  │                    完整歷史摘要                         ││
│  │  "使用者小明是軟體工程師,正在開發電商系統,            ││
│  │   使用 Python/FastAPI/PostgreSQL,                     ││
│  │   目前遇到查詢效能問題,已嘗試添加索引..."              ││
│  │                                 [~500 tokens]          ││
│  └────────────────────────────────────────────────────────┘│
│                           +                                 │
│  ┌────────────────────────────────────────────────────────┐│
│  │                  最近完整對話(5輪)                     ││
│  │  Turn 6-10 的完整內容                                   ││
│  │                                 [~2000 tokens]         ││
│  └────────────────────────────────────────────────────────┘│
│                           =                                 │
│           有效 Context:~2500 tokens(而非 5000+)          │
│           保留了所有關鍵資訊!                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

完整實作

  1import anthropic
  2from dataclasses import dataclass, field
  3from typing import Optional
  4from enum import Enum
  5
  6client = anthropic.Anthropic()
  7
  8
  9class SummarizationModel(Enum):
 10    """摘要使用的模型"""
 11    HAIKU = "claude-haiku-4-5-20251001"    # 便宜快速
 12    SONNET = "claude-sonnet-4-6"           # 平衡
 13
 14
 15@dataclass
 16class ProgressiveSummaryConfig:
 17    """漸進式摘要配置"""
 18    # 視窗設定
 19    recent_turns_to_keep: int = 5       # 保留最近幾輪完整對話
 20
 21    # 摘要觸發條件
 22    summarize_threshold: int = 10        # 超過幾輪時觸發摘要
 23
 24    # 摘要設定
 25    summary_model: SummarizationModel = SummarizationModel.HAIKU
 26    max_summary_tokens: int = 1000       # 摘要最大長度
 27
 28    # 摘要內容指引
 29    summary_focus: list[str] = field(default_factory=lambda: [
 30        "使用者身份和背景",
 31        "主要任務或目標",
 32        "關鍵決策和結論",
 33        "重要的技術細節",
 34        "待解決的問題",
 35        "使用者偏好"
 36    ])
 37
 38
 39@dataclass
 40class ConversationState:
 41    """對話狀態"""
 42    summary: Optional[str] = None           # 歷史摘要
 43    recent_messages: list[dict] = field(default_factory=list)  # 最近的完整訊息
 44    total_turns: int = 0                    # 總對話輪數
 45    last_summary_turn: int = 0              # 上次摘要的輪數
 46
 47
 48class ProgressiveSummarizer:
 49    """
 50    漸進式摘要管理器
 51
 52    核心邏輯:
 53    1. 保留最近 N 輪的完整對話
 54    2. 超過閾值時,將舊對話壓縮為摘要
 55    3. 摘要會隨著對話推進而更新(包含新資訊)
 56    """
 57
 58    def __init__(self, config: Optional[ProgressiveSummaryConfig] = None):
 59        self.config = config or ProgressiveSummaryConfig()
 60        self.state = ConversationState()
 61        self._stats = {
 62            "summarization_count": 0,
 63            "tokens_before_summary": 0,
 64            "tokens_after_summary": 0
 65        }
 66
 67    def _format_messages_for_summary(self, messages: list[dict]) -> str:
 68        """將訊息格式化為摘要輸入"""
 69        formatted = []
 70        for i, msg in enumerate(messages):
 71            role = "使用者" if msg["role"] == "user" else "助手"
 72            formatted.append(f"[{role}]: {msg['content']}")
 73        return "\n\n".join(formatted)
 74
 75    def _create_summary_prompt(self, messages_text: str, existing_summary: Optional[str]) -> str:
 76        """建立摘要提示"""
 77        focus_points = "\n".join(f"- {f}" for f in self.config.summary_focus)
 78
 79        if existing_summary:
 80            return f"""請更新以下對話摘要,整合新的對話內容。
 81
 82現有摘要:
 83{existing_summary}
 84
 85新增對話:
 86{messages_text}
 87
 88請產生更新後的摘要,重點保留:
 89{focus_points}
 90
 91要求:
 921. 保持簡潔,控制在 {self.config.max_summary_tokens} tokens 以內
 932. 使用條列式整理關鍵資訊
 943. 標註重要的變化或新發現
 954. 移除已解決或不再相關的資訊
 96
 97更新後的摘要:"""
 98        else:
 99            return f"""請為以下對話產生摘要。
100
101對話內容:
102{messages_text}
103
104請產生摘要,重點保留:
105{focus_points}
106
107要求:
1081. 保持簡潔,控制在 {self.config.max_summary_tokens} tokens 以內
1092. 使用條列式整理關鍵資訊
1103. 突出關鍵決策和結論
111
112摘要:"""
113
114    def _generate_summary(self, messages: list[dict]) -> str:
115        """使用 LLM 生成摘要"""
116        messages_text = self._format_messages_for_summary(messages)
117        prompt = self._create_summary_prompt(messages_text, self.state.summary)
118
119        response = client.messages.create(
120            model=self.config.summary_model.value,
121            max_tokens=self.config.max_summary_tokens,
122            messages=[{"role": "user", "content": prompt}]
123        )
124
125        self._stats["summarization_count"] += 1
126        return response.content[0].text
127
128    def add_turn(self, user_message: str, assistant_message: str):
129        """添加一輪對話"""
130        self.state.recent_messages.append({"role": "user", "content": user_message})
131        self.state.recent_messages.append({"role": "assistant", "content": assistant_message})
132        self.state.total_turns += 1
133
134        # 檢查是否需要摘要
135        self._check_and_summarize()
136
137    def _check_and_summarize(self):
138        """檢查並執行摘要(如果需要)"""
139        turns_since_summary = self.state.total_turns - self.state.last_summary_turn
140
141        if turns_since_summary >= self.config.summarize_threshold:
142            self._perform_summarization()
143
144    def _perform_summarization(self):
145        """執行摘要"""
146        # 計算要摘要的訊息數量
147        messages_to_summarize_count = len(self.state.recent_messages) - (self.config.recent_turns_to_keep * 2)
148
149        if messages_to_summarize_count <= 0:
150            return
151
152        # 分離要摘要的訊息和要保留的訊息
153        messages_to_summarize = self.state.recent_messages[:messages_to_summarize_count]
154        messages_to_keep = self.state.recent_messages[messages_to_summarize_count:]
155
156        # 記錄壓縮前的 tokens
157        before_tokens = sum(len(m["content"]) for m in self.state.recent_messages) * 0.4
158        self._stats["tokens_before_summary"] += before_tokens
159
160        # 生成摘要
161        print(f"📝 正在壓縮 {len(messages_to_summarize)} 條訊息...")
162        self.state.summary = self._generate_summary(messages_to_summarize)
163
164        # 更新狀態
165        self.state.recent_messages = messages_to_keep
166        self.state.last_summary_turn = self.state.total_turns
167
168        # 記錄壓縮後的 tokens
169        after_tokens = (len(self.state.summary) + sum(len(m["content"]) for m in messages_to_keep)) * 0.4
170        self._stats["tokens_after_summary"] += after_tokens
171
172        print(f"✅ 壓縮完成:{int(before_tokens)}{int(after_tokens)} tokens")
173
174    def get_context_messages(self) -> list[dict]:
175        """取得要發送給 API 的訊息"""
176        messages = []
177
178        # 如果有摘要,作為第一條訊息注入
179        if self.state.summary:
180            messages.append({
181                "role": "user",
182                "content": f"[對話歷史摘要]\n{self.state.summary}\n\n請基於以上背景繼續對話。"
183            })
184            messages.append({
185                "role": "assistant",
186                "content": "我已了解之前的對話背景,請繼續。"
187            })
188
189        # 加入最近的完整訊息
190        messages.extend(self.state.recent_messages)
191
192        return messages
193
194    def get_stats(self) -> dict:
195        """取得統計資訊"""
196        compression_ratio = 0
197        if self._stats["tokens_before_summary"] > 0:
198            compression_ratio = (
199                1 - self._stats["tokens_after_summary"] / self._stats["tokens_before_summary"]
200            ) * 100
201
202        return {
203            **self._stats,
204            "total_turns": self.state.total_turns,
205            "has_summary": self.state.summary is not None,
206            "recent_messages_count": len(self.state.recent_messages),
207            "compression_ratio": f"{compression_ratio:.1f}%"
208        }
209
210
211class ProgressiveSummaryChatbot:
212    """使用漸進式摘要的聊天機器人"""
213
214    def __init__(
215        self,
216        system_prompt: str,
217        summary_config: Optional[ProgressiveSummaryConfig] = None
218    ):
219        self.system_prompt = system_prompt
220        self.summarizer = ProgressiveSummarizer(summary_config)
221
222    def chat(self, user_input: str) -> str:
223        # 取得當前 context
224        messages = self.summarizer.get_context_messages()
225        messages.append({"role": "user", "content": user_input})
226
227        # 呼叫 API
228        response = client.messages.create(
229            model="claude-sonnet-4-6",
230            max_tokens=2048,
231            system=self.system_prompt,
232            messages=messages
233        )
234
235        assistant_response = response.content[0].text
236
237        # 更新摘要器
238        self.summarizer.add_turn(user_input, assistant_response)
239
240        return assistant_response
241
242    def get_current_summary(self) -> Optional[str]:
243        """取得當前摘要"""
244        return self.summarizer.state.summary
245
246
247# 使用範例
248if __name__ == "__main__":
249    config = ProgressiveSummaryConfig(
250        recent_turns_to_keep=3,
251        summarize_threshold=5,
252        summary_focus=[
253            "使用者的身份和角色",
254            "討論的主題和目標",
255            "已做出的決定",
256            "關鍵的技術細節"
257        ]
258    )
259
260    bot = ProgressiveSummaryChatbot(
261        system_prompt="你是一個專業的技術顧問,幫助使用者解決程式問題。",
262        summary_config=config
263    )
264
265    # 模擬長對話
266    conversations = [
267        "你好,我是小明,是一個後端工程師",
268        "我在開發一個電商平台",
269        "使用 Python 和 FastAPI 框架",
270        "資料庫是 PostgreSQL",
271        "現在遇到一個效能問題",
272        "商品列表的查詢很慢",
273        "大概要 5 秒才能返回",
274        "我已經加了索引",
275        "但還是很慢",
276        "你覺得還有什麼優化方向?",
277        "對了,你還記得我用的是什麼框架嗎?"  # 測試摘要是否保留資訊
278    ]
279
280    for i, user_msg in enumerate(conversations, 1):
281        print(f"\n{'='*50}")
282        print(f"Turn {i}")
283        print(f"👤 User: {user_msg}")
284        response = bot.chat(user_msg)
285        print(f"🤖 Bot: {response[:300]}...")
286
287        stats = bot.summarizer.get_stats()
288        print(f"\n📊 Stats: {stats}")
289
290        if bot.get_current_summary():
291            print(f"\n📋 Current Summary:\n{bot.get_current_summary()[:500]}...")

策略三:階層式摘要(Hierarchical Summarization)

對於超長對話或多 Session 的場景,使用多層摘要結構。

架構設計

階層式摘要架構:

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  Level 3: 長期記憶(跨 Session)                            │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ "使用者小明,軟體工程師,偏好 Python,              │   │
│  │  主要專案是電商平台,長期目標是優化系統效能"        │   │
│  │                                    [~200 tokens]    │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│  Level 2: 中期摘要(當前 Session 早期)                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ "本次討論主題:PostgreSQL 查詢優化                  │   │
│  │  已嘗試:添加索引、調整查詢語句                     │   │
│  │  待解決:N+1 問題、連線池配置"                      │   │
│  │                                    [~500 tokens]    │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│  Level 1: 最近完整對話                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ Turn N-4 到 Turn N 的完整內容                       │   │
│  │                                    [~2000 tokens]   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  總 Context: ~2700 tokens(即使對話已超過 100 輪)         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

完整實作

  1import anthropic
  2import json
  3from dataclasses import dataclass, field
  4from typing import Optional
  5from datetime import datetime
  6from pathlib import Path
  7
  8client = anthropic.Anthropic()
  9
 10
 11@dataclass
 12class HierarchicalSummaryConfig:
 13    """階層式摘要配置"""
 14    # Level 1: 最近完整對話
 15    recent_turns: int = 5
 16
 17    # Level 2: Session 摘要
 18    session_summary_threshold: int = 10  # 幾輪觸發 Session 摘要更新
 19    session_summary_max_tokens: int = 800
 20
 21    # Level 3: 長期記憶
 22    long_term_memory_file: Optional[str] = None  # 持久化檔案路徑
 23    long_term_summary_max_tokens: int = 300
 24
 25    # 摘要模型
 26    summary_model: str = "claude-haiku-4-5-20251001"
 27
 28
 29@dataclass
 30class UserProfile:
 31    """使用者輪廓(長期記憶)"""
 32    name: Optional[str] = None
 33    role: Optional[str] = None
 34    preferences: list[str] = field(default_factory=list)
 35    expertise: list[str] = field(default_factory=list)
 36    ongoing_projects: list[str] = field(default_factory=list)
 37    interaction_style: Optional[str] = None
 38    last_updated: Optional[str] = None
 39
 40    def to_summary(self) -> str:
 41        """轉換為摘要文字"""
 42        parts = []
 43        if self.name:
 44            parts.append(f"使用者:{self.name}")
 45        if self.role:
 46            parts.append(f"角色:{self.role}")
 47        if self.preferences:
 48            parts.append(f"偏好:{', '.join(self.preferences)}")
 49        if self.expertise:
 50            parts.append(f"專長:{', '.join(self.expertise)}")
 51        if self.ongoing_projects:
 52            parts.append(f"進行中專案:{', '.join(self.ongoing_projects)}")
 53        if self.interaction_style:
 54            parts.append(f"互動風格:{self.interaction_style}")
 55        return "\n".join(parts) if parts else "(尚無使用者資料)"
 56
 57    def to_dict(self) -> dict:
 58        return {
 59            "name": self.name,
 60            "role": self.role,
 61            "preferences": self.preferences,
 62            "expertise": self.expertise,
 63            "ongoing_projects": self.ongoing_projects,
 64            "interaction_style": self.interaction_style,
 65            "last_updated": self.last_updated
 66        }
 67
 68    @classmethod
 69    def from_dict(cls, data: dict) -> "UserProfile":
 70        return cls(**data)
 71
 72
 73@dataclass
 74class SessionState:
 75    """Session 狀態"""
 76    session_id: str
 77    started_at: str
 78    topic: Optional[str] = None
 79    summary: Optional[str] = None
 80    key_decisions: list[str] = field(default_factory=list)
 81    pending_issues: list[str] = field(default_factory=list)
 82    recent_messages: list[dict] = field(default_factory=list)
 83    turn_count: int = 0
 84
 85
 86class HierarchicalMemoryManager:
 87    """
 88    階層式記憶管理器
 89
 90    三層結構:
 91    - Level 3 (Long-term): 使用者輪廓,跨 Session 持久化
 92    - Level 2 (Session): 當前 Session 的摘要
 93    - Level 1 (Recent): 最近幾輪的完整對話
 94    """
 95
 96    def __init__(self, config: Optional[HierarchicalSummaryConfig] = None):
 97        self.config = config or HierarchicalSummaryConfig()
 98
 99        # Level 3: 長期記憶
100        self.user_profile = self._load_user_profile()
101
102        # Level 2: Session 狀態
103        self.session = SessionState(
104            session_id=datetime.now().strftime("%Y%m%d_%H%M%S"),
105            started_at=datetime.now().isoformat()
106        )
107
108        self._stats = {
109            "session_summaries": 0,
110            "profile_updates": 0
111        }
112
113    def _load_user_profile(self) -> UserProfile:
114        """載入持久化的使用者輪廓"""
115        if self.config.long_term_memory_file:
116            path = Path(self.config.long_term_memory_file)
117            if path.exists():
118                with open(path, "r", encoding="utf-8") as f:
119                    data = json.load(f)
120                    return UserProfile.from_dict(data)
121        return UserProfile()
122
123    def _save_user_profile(self):
124        """儲存使用者輪廓"""
125        if self.config.long_term_memory_file:
126            path = Path(self.config.long_term_memory_file)
127            path.parent.mkdir(parents=True, exist_ok=True)
128            with open(path, "w", encoding="utf-8") as f:
129                json.dump(self.user_profile.to_dict(), f, ensure_ascii=False, indent=2)
130
131    def _update_user_profile(self, messages: list[dict]):
132        """從對話中更新使用者輪廓"""
133        if not messages:
134            return
135
136        messages_text = "\n".join(
137            f"[{'使用者' if m['role'] == 'user' else '助手'}]: {m['content']}"
138            for m in messages
139        )
140
141        current_profile = self.user_profile.to_summary()
142
143        prompt = f"""根據以下對話,更新使用者輪廓。只提取明確提到的資訊,不要推測。
144
145現有輪廓:
146{current_profile}
147
148最近對話:
149{messages_text}
150
151請以 JSON 格式返回更新後的輪廓,只包含有變化的欄位:
152{{
153    "name": "使用者名稱(如果提到)",
154    "role": "職業或角色(如果提到)",
155    "preferences": ["偏好1", "偏好2"],
156    "expertise": ["專長1", "專長2"],
157    "ongoing_projects": ["專案1", "專案2"],
158    "interaction_style": "互動風格描述"
159}}
160
161如果沒有新資訊,返回空物件 {{}}"""
162
163        response = client.messages.create(
164            model=self.config.summary_model,
165            max_tokens=500,
166            messages=[{"role": "user", "content": prompt}]
167        )
168
169        try:
170            # 嘗試解析 JSON
171            response_text = response.content[0].text
172            # 處理可能的 markdown 程式碼區塊
173            if "```json" in response_text:
174                response_text = response_text.split("```json")[1].split("```")[0]
175            elif "```" in response_text:
176                response_text = response_text.split("```")[1].split("```")[0]
177
178            updates = json.loads(response_text.strip())
179
180            if updates:
181                # 更新輪廓
182                for key, value in updates.items():
183                    if value and hasattr(self.user_profile, key):
184                        if isinstance(value, list):
185                            # 合併列表,去重
186                            existing = getattr(self.user_profile, key) or []
187                            setattr(self.user_profile, key, list(set(existing + value)))
188                        else:
189                            setattr(self.user_profile, key, value)
190
191                self.user_profile.last_updated = datetime.now().isoformat()
192                self._save_user_profile()
193                self._stats["profile_updates"] += 1
194                print(f"✅ 已更新使用者輪廓")
195
196        except (json.JSONDecodeError, IndexError) as e:
197            print(f"⚠️ 輪廓更新解析失敗: {e}")
198
199    def _update_session_summary(self):
200        """更新 Session 摘要"""
201        if not self.session.recent_messages:
202            return
203
204        messages_text = "\n".join(
205            f"[{'使用者' if m['role'] == 'user' else '助手'}]: {m['content']}"
206            for m in self.session.recent_messages
207        )
208
209        existing_summary = self.session.summary or "(這是新對話的開始)"
210
211        prompt = f"""請更新對話 Session 摘要。
212
213現有摘要:
214{existing_summary}
215
216新增對話:
217{messages_text}
218
219請產生更新後的摘要,包含:
2201. 主要討論主題
2212. 關鍵決策和結論
2223. 待解決的問題
2234. 重要的技術細節
224
225控制在 {self.config.session_summary_max_tokens} tokens 以內。
226
227更新後的摘要:"""
228
229        response = client.messages.create(
230            model=self.config.summary_model,
231            max_tokens=self.config.session_summary_max_tokens,
232            messages=[{"role": "user", "content": prompt}]
233        )
234
235        self.session.summary = response.content[0].text
236        self._stats["session_summaries"] += 1
237        print(f"✅ 已更新 Session 摘要")
238
239    def add_turn(self, user_message: str, assistant_message: str):
240        """添加一輪對話"""
241        self.session.recent_messages.append({"role": "user", "content": user_message})
242        self.session.recent_messages.append({"role": "assistant", "content": assistant_message})
243        self.session.turn_count += 1
244
245        # 檢查是否需要更新
246        if self.session.turn_count % self.config.session_summary_threshold == 0:
247            # 更新 Session 摘要
248            old_messages = self.session.recent_messages[:-self.config.recent_turns * 2]
249            if old_messages:
250                self._update_session_summary()
251                self._update_user_profile(old_messages)
252                # 只保留最近的訊息
253                self.session.recent_messages = self.session.recent_messages[-self.config.recent_turns * 2:]
254
255    def get_context(self) -> str:
256        """
257        取得階層式 Context
258
259        返回格式:
260        [長期記憶] + [Session 摘要] + [最近對話]
261        """
262        context_parts = []
263
264        # Level 3: 長期記憶
265        profile_summary = self.user_profile.to_summary()
266        if profile_summary != "(尚無使用者資料)":
267            context_parts.append(f"[使用者背景]\n{profile_summary}")
268
269        # Level 2: Session 摘要
270        if self.session.summary:
271            context_parts.append(f"[本次對話摘要]\n{self.session.summary}")
272
273        return "\n\n".join(context_parts)
274
275    def get_messages(self) -> list[dict]:
276        """取得要發送給 API 的訊息"""
277        messages = []
278
279        context = self.get_context()
280        if context:
281            messages.append({
282                "role": "user",
283                "content": f"{context}\n\n請基於以上背景繼續對話。"
284            })
285            messages.append({
286                "role": "assistant",
287                "content": "我已了解背景資訊,請繼續。"
288            })
289
290        messages.extend(self.session.recent_messages)
291        return messages
292
293    def get_stats(self) -> dict:
294        return {
295            **self._stats,
296            "session_turn_count": self.session.turn_count,
297            "recent_messages": len(self.session.recent_messages),
298            "has_session_summary": self.session.summary is not None,
299            "has_user_profile": self.user_profile.name is not None
300        }
301
302
303class HierarchicalMemoryChatbot:
304    """使用階層式記憶的聊天機器人"""
305
306    def __init__(
307        self,
308        system_prompt: str,
309        config: Optional[HierarchicalSummaryConfig] = None
310    ):
311        self.system_prompt = system_prompt
312        self.memory = HierarchicalMemoryManager(config)
313
314    def chat(self, user_input: str) -> str:
315        messages = self.memory.get_messages()
316        messages.append({"role": "user", "content": user_input})
317
318        response = client.messages.create(
319            model="claude-sonnet-4-6",
320            max_tokens=2048,
321            system=self.system_prompt,
322            messages=messages
323        )
324
325        assistant_response = response.content[0].text
326        self.memory.add_turn(user_input, assistant_response)
327
328        return assistant_response
329
330    def get_user_profile(self) -> UserProfile:
331        return self.memory.user_profile
332
333    def get_session_summary(self) -> Optional[str]:
334        return self.memory.session.summary
335
336
337# 使用範例
338if __name__ == "__main__":
339    config = HierarchicalSummaryConfig(
340        recent_turns=3,
341        session_summary_threshold=5,
342        long_term_memory_file="./user_memory.json"
343    )
344
345    bot = HierarchicalMemoryChatbot(
346        system_prompt="你是一個專業的技術顧問。",
347        config=config
348    )
349
350    # 模擬多輪對話
351    test_conversations = [
352        "你好,我是小明",
353        "我是一個 Python 開發者",
354        "主要做後端開發",
355        "最近在學 Rust",
356        "覺得 Rust 的所有權系統很有趣",
357        "想把它用在一個高效能的服務上",
358        # ... 更多對話
359    ]
360
361    for msg in test_conversations:
362        print(f"\n👤 User: {msg}")
363        response = bot.chat(msg)
364        print(f"🤖 Bot: {response[:200]}...")
365
366    print("\n" + "="*50)
367    print("使用者輪廓:")
368    print(bot.get_user_profile().to_summary())
369
370    print("\nSession 摘要:")
371    print(bot.get_session_summary() or "(尚未產生)")

策略四:語意壓縮(Semantic Compression)

不只是摘要,而是智能地識別和保留語意重要的內容。

核心概念

語意壓縮 vs 一般摘要:

一般摘要:
"使用者詢問了 Python GIL 的問題,我解釋了 GIL 的定義和影響..."
→ 壓縮了對話,但丟失了具體細節

語意壓縮:
{
    "entities": ["Python", "GIL", "多執行緒"],
    "facts": [
        "GIL 是 Global Interpreter Lock 的縮寫",
        "GIL 確保同一時間只有一個執行緒執行 Python bytecode",
        "可透過 multiprocessing 繞過 GIL"
    ],
    "user_understanding": "基礎",
    "pending_questions": ["如何判斷何時該用多進程 vs 多執行緒?"]
}
→ 結構化保留關鍵語意單元

完整實作

  1import anthropic
  2import json
  3from dataclasses import dataclass, field
  4from typing import Optional, Any
  5from enum import Enum
  6
  7client = anthropic.Anthropic()
  8
  9
 10class ImportanceLevel(Enum):
 11    CRITICAL = "critical"    # 必須保留
 12    HIGH = "high"           # 高度重要
 13    MEDIUM = "medium"       # 中等重要
 14    LOW = "low"             # 可以捨棄
 15
 16
 17@dataclass
 18class SemanticUnit:
 19    """語意單元"""
 20    content: str
 21    unit_type: str  # "fact", "decision", "question", "preference", "context"
 22    importance: ImportanceLevel
 23    turn_created: int
 24    related_entities: list[str] = field(default_factory=list)
 25
 26    def to_dict(self) -> dict:
 27        return {
 28            "content": self.content,
 29            "type": self.unit_type,
 30            "importance": self.importance.value,
 31            "turn": self.turn_created,
 32            "entities": self.related_entities
 33        }
 34
 35
 36@dataclass
 37class SemanticCompressionConfig:
 38    """語意壓縮配置"""
 39    # 提取設定
 40    extraction_model: str = "claude-haiku-4-5-20251001"
 41
 42    # 保留策略
 43    max_semantic_units: int = 50           # 最多保留幾個語意單元
 44    max_context_tokens: int = 3000         # 重建 context 的最大 tokens
 45
 46    # 重要性衰減
 47    importance_decay_rate: float = 0.1     # 每輪重要性衰減率
 48    min_importance_to_keep: float = 0.3    # 低於此值的單元會被移除
 49
 50    # 最近對話
 51    recent_turns: int = 3
 52
 53
 54class SemanticMemoryStore:
 55    """語意記憶儲存"""
 56
 57    def __init__(self, config: SemanticCompressionConfig):
 58        self.config = config
 59        self.units: list[SemanticUnit] = []
 60        self.entity_index: dict[str, list[int]] = {}  # entity -> unit indices
 61        self.current_turn: int = 0
 62
 63    def add_units(self, units: list[SemanticUnit]):
 64        """添加語意單元"""
 65        for unit in units:
 66            idx = len(self.units)
 67            self.units.append(unit)
 68
 69            # 更新實體索引
 70            for entity in unit.related_entities:
 71                if entity not in self.entity_index:
 72                    self.entity_index[entity] = []
 73                self.entity_index[entity].append(idx)
 74
 75    def decay_importance(self):
 76        """衰減舊單元的重要性"""
 77        for unit in self.units:
 78            age = self.current_turn - unit.turn_created
 79            if age > 0:
 80                # 根據年齡降低重要性
 81                decay = 1.0 - (self.config.importance_decay_rate * age)
 82                decay = max(decay, 0.1)  # 最低保留 10%
 83
 84                # 更新重要性等級
 85                if unit.importance == ImportanceLevel.CRITICAL:
 86                    pass  # CRITICAL 不衰減
 87                elif decay < 0.3:
 88                    unit.importance = ImportanceLevel.LOW
 89                elif decay < 0.6:
 90                    unit.importance = ImportanceLevel.MEDIUM
 91
 92    def prune(self):
 93        """移除低重要性的單元"""
 94        # 按重要性排序,保留最重要的
 95        importance_order = {
 96            ImportanceLevel.CRITICAL: 4,
 97            ImportanceLevel.HIGH: 3,
 98            ImportanceLevel.MEDIUM: 2,
 99            ImportanceLevel.LOW: 1
100        }
101
102        sorted_units = sorted(
103            self.units,
104            key=lambda u: (importance_order[u.importance], -u.turn_created),
105            reverse=True
106        )
107
108        # 保留前 N 個
109        self.units = sorted_units[:self.config.max_semantic_units]
110
111        # 重建實體索引
112        self.entity_index = {}
113        for idx, unit in enumerate(self.units):
114            for entity in unit.related_entities:
115                if entity not in self.entity_index:
116                    self.entity_index[entity] = []
117                self.entity_index[entity].append(idx)
118
119    def get_relevant_units(self, query: str, entities: list[str]) -> list[SemanticUnit]:
120        """取得與查詢相關的語意單元"""
121        relevant_indices = set()
122
123        # 根據實體匹配
124        for entity in entities:
125            if entity in self.entity_index:
126                relevant_indices.update(self.entity_index[entity])
127
128        # 取得相關單元
129        relevant_units = [self.units[i] for i in relevant_indices]
130
131        # 加入所有 CRITICAL 單元
132        for unit in self.units:
133            if unit.importance == ImportanceLevel.CRITICAL and unit not in relevant_units:
134                relevant_units.append(unit)
135
136        return relevant_units
137
138    def to_context_string(self, units: Optional[list[SemanticUnit]] = None) -> str:
139        """將語意單元重建為 context 字串"""
140        units = units or self.units
141
142        if not units:
143            return ""
144
145        # 按類型分組
146        grouped = {
147            "fact": [],
148            "decision": [],
149            "question": [],
150            "preference": [],
151            "context": []
152        }
153
154        for unit in units:
155            if unit.unit_type in grouped:
156                grouped[unit.unit_type].append(unit.content)
157
158        # 格式化輸出
159        parts = []
160
161        if grouped["context"]:
162            parts.append("背景資訊:\n" + "\n".join(f"- {c}" for c in grouped["context"]))
163
164        if grouped["fact"]:
165            parts.append("已確認的事實:\n" + "\n".join(f"- {f}" for f in grouped["fact"]))
166
167        if grouped["decision"]:
168            parts.append("已做的決定:\n" + "\n".join(f"- {d}" for d in grouped["decision"]))
169
170        if grouped["preference"]:
171            parts.append("使用者偏好:\n" + "\n".join(f"- {p}" for p in grouped["preference"]))
172
173        if grouped["question"]:
174            parts.append("待解答的問題:\n" + "\n".join(f"- {q}" for q in grouped["question"]))
175
176        return "\n\n".join(parts)
177
178
179class SemanticCompressor:
180    """語意壓縮器"""
181
182    def __init__(self, config: Optional[SemanticCompressionConfig] = None):
183        self.config = config or SemanticCompressionConfig()
184        self.memory = SemanticMemoryStore(self.config)
185        self.recent_messages: list[dict] = []
186
187    def _extract_semantic_units(self, messages: list[dict]) -> list[SemanticUnit]:
188        """從對話中提取語意單元"""
189        messages_text = "\n".join(
190            f"[{'使用者' if m['role'] == 'user' else '助手'}]: {m['content']}"
191            for m in messages
192        )
193
194        prompt = f"""分析以下對話,提取關鍵語意單元。
195
196對話內容:
197{messages_text}
198
199請以 JSON 格式返回提取的語意單元列表:
200[
201    {{
202        "content": "語意內容",
203        "type": "fact|decision|question|preference|context",
204        "importance": "critical|high|medium|low",
205        "entities": ["相關實體1", "相關實體2"]
206    }}
207]
208
209類型說明:
210- fact: 已確認的事實或資訊
211- decision: 已做出的決定或選擇
212- question: 待解答的問題
213- preference: 使用者的偏好或習慣
214- context: 背景資訊
215
216重要性說明:
217- critical: 核心身份、關鍵決策(必須保留)
218- high: 重要技術細節、主要目標
219- medium: 一般資訊
220- low: 次要細節
221
222只返回 JSON,不要其他說明。"""
223
224        response = client.messages.create(
225            model=self.config.extraction_model,
226            max_tokens=2000,
227            messages=[{"role": "user", "content": prompt}]
228        )
229
230        try:
231            response_text = response.content[0].text
232            # 處理可能的 markdown 程式碼區塊
233            if "```json" in response_text:
234                response_text = response_text.split("```json")[1].split("```")[0]
235            elif "```" in response_text:
236                response_text = response_text.split("```")[1].split("```")[0]
237
238            units_data = json.loads(response_text.strip())
239
240            units = []
241            for data in units_data:
242                unit = SemanticUnit(
243                    content=data["content"],
244                    unit_type=data["type"],
245                    importance=ImportanceLevel(data["importance"]),
246                    turn_created=self.memory.current_turn,
247                    related_entities=data.get("entities", [])
248                )
249                units.append(unit)
250
251            return units
252
253        except (json.JSONDecodeError, KeyError) as e:
254            print(f"⚠️ 語意提取解析失敗: {e}")
255            return []
256
257    def add_turn(self, user_message: str, assistant_message: str):
258        """添加一輪對話"""
259        self.recent_messages.append({"role": "user", "content": user_message})
260        self.recent_messages.append({"role": "assistant", "content": assistant_message})
261        self.memory.current_turn += 1
262
263        # 如果最近訊息超過保留數量,進行語意提取
264        if len(self.recent_messages) > self.config.recent_turns * 2:
265            # 取出要壓縮的訊息
266            messages_to_compress = self.recent_messages[:-self.config.recent_turns * 2]
267            self.recent_messages = self.recent_messages[-self.config.recent_turns * 2:]
268
269            # 提取語意單元
270            print(f"🔍 正在提取語意單元...")
271            units = self._extract_semantic_units(messages_to_compress)
272
273            if units:
274                self.memory.add_units(units)
275                print(f"✅ 提取了 {len(units)} 個語意單元")
276
277            # 衰減和修剪
278            self.memory.decay_importance()
279            self.memory.prune()
280
281    def get_context(self) -> str:
282        """取得語意 context"""
283        return self.memory.to_context_string()
284
285    def get_messages(self) -> list[dict]:
286        """取得要發送給 API 的訊息"""
287        messages = []
288
289        context = self.get_context()
290        if context:
291            messages.append({
292                "role": "user",
293                "content": f"[對話記憶]\n{context}\n\n請基於以上背景繼續對話。"
294            })
295            messages.append({
296                "role": "assistant",
297                "content": "我已了解背景資訊,請繼續。"
298            })
299
300        messages.extend(self.recent_messages)
301        return messages
302
303    def get_stats(self) -> dict:
304        return {
305            "total_semantic_units": len(self.memory.units),
306            "current_turn": self.memory.current_turn,
307            "recent_messages": len(self.recent_messages),
308            "entities_tracked": len(self.memory.entity_index)
309        }
310
311
312class SemanticCompressionChatbot:
313    """使用語意壓縮的聊天機器人"""
314
315    def __init__(
316        self,
317        system_prompt: str,
318        config: Optional[SemanticCompressionConfig] = None
319    ):
320        self.system_prompt = system_prompt
321        self.compressor = SemanticCompressor(config)
322
323    def chat(self, user_input: str) -> str:
324        messages = self.compressor.get_messages()
325        messages.append({"role": "user", "content": user_input})
326
327        response = client.messages.create(
328            model="claude-sonnet-4-6",
329            max_tokens=2048,
330            system=self.system_prompt,
331            messages=messages
332        )
333
334        assistant_response = response.content[0].text
335        self.compressor.add_turn(user_input, assistant_response)
336
337        return assistant_response
338
339    def get_memory_snapshot(self) -> list[dict]:
340        """取得記憶快照"""
341        return [unit.to_dict() for unit in self.compressor.memory.units]
342
343
344# 使用範例
345if __name__ == "__main__":
346    config = SemanticCompressionConfig(
347        recent_turns=2,
348        max_semantic_units=30
349    )
350
351    bot = SemanticCompressionChatbot(
352        system_prompt="你是一個專業的技術顧問。",
353        config=config
354    )
355
356    conversations = [
357        "你好,我叫小明,是後端工程師",
358        "我擅長 Python 和 Go",
359        "最近在開發一個即時通訊系統",
360        "使用 WebSocket 進行雙向通訊",
361        "遇到了連線數擴展的問題",
362        "目前單機只能支撐 10000 連線",
363        "想要擴展到 100000 連線",
364        "你建議用什麼架構?",
365        # 更多對話...
366    ]
367
368    for msg in conversations:
369        print(f"\n👤 User: {msg}")
370        response = bot.chat(msg)
371        print(f"🤖 Bot: {response[:200]}...")
372
373    print("\n" + "="*50)
374    print("記憶快照:")
375    for unit in bot.get_memory_snapshot():
376        print(f"  [{unit['importance']}] ({unit['type']}) {unit['content'][:50]}...")

策略五:混合壓縮系統

結合多種策略的生產級系統。

架構設計

混合壓縮系統架構:

┌─────────────────────────────────────────────────────────────┐
│                    混合壓縮管理器                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  輸入:新的對話訊息                                          │
│         │                                                   │
│         ▼                                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 1. 即時分類                                          │   │
│  │    判斷訊息類型:事實/問題/閒聊/指令                  │   │
│  └─────────────────────────────────────────────────────┘   │
│         │                                                   │
│         ▼                                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 2. 重要性評估                                        │   │
│  │    根據類型和內容判斷保留優先級                      │   │
│  └─────────────────────────────────────────────────────┘   │
│         │                                                   │
│         ▼                                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 3. 儲存策略選擇                                      │   │
│  │    - CRITICAL → 長期記憶                             │   │
│  │    - HIGH     → 語意單元                             │   │
│  │    - MEDIUM   → Session 摘要                        │   │
│  │    - LOW      → 滑動視窗(可丟棄)                   │   │
│  └─────────────────────────────────────────────────────┘   │
│         │                                                   │
│         ▼                                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 4. Context 重建                                      │   │
│  │    根據當前查詢動態組合相關記憶                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

完整實作

  1import anthropic
  2from dataclasses import dataclass, field
  3from typing import Optional, Callable
  4from enum import Enum
  5import json
  6
  7client = anthropic.Anthropic()
  8
  9
 10class MessageType(Enum):
 11    FACT = "fact"           # 事實陳述
 12    QUESTION = "question"   # 問題
 13    INSTRUCTION = "instruction"  # 指令
 14    CHITCHAT = "chitchat"   # 閒聊
 15    FEEDBACK = "feedback"   # 反饋
 16
 17
 18@dataclass
 19class HybridCompressionConfig:
 20    """混合壓縮配置"""
 21    # 模型設定
 22    classification_model: str = "claude-haiku-4-5-20251001"
 23    summarization_model: str = "claude-haiku-4-5-20251001"
 24    main_model: str = "claude-sonnet-4-6"
 25
 26    # 各層容量
 27    recent_turns: int = 3
 28    session_summary_max_tokens: int = 500
 29    semantic_units_max: int = 20
 30    long_term_facts_max: int = 10
 31
 32    # 壓縮觸發
 33    compression_interval: int = 5
 34
 35
 36@dataclass
 37class MemoryLayers:
 38    """記憶層級"""
 39    # Layer 1: 最近對話(完整保留)
 40    recent: list[dict] = field(default_factory=list)
 41
 42    # Layer 2: Session 摘要
 43    session_summary: Optional[str] = None
 44
 45    # Layer 3: 語意單元
 46    semantic_units: list[dict] = field(default_factory=list)
 47
 48    # Layer 4: 長期事實
 49    long_term_facts: list[str] = field(default_factory=list)
 50
 51
 52class HybridCompressionManager:
 53    """
 54    混合壓縮管理器
 55
 56    結合多種策略:
 57    - 滑動視窗:保留最近對話
 58    - 漸進摘要:壓縮 Session 歷史
 59    - 語意提取:保留關鍵語意單元
 60    - 長期記憶:持久化重要事實
 61    """
 62
 63    def __init__(self, config: Optional[HybridCompressionConfig] = None):
 64        self.config = config or HybridCompressionConfig()
 65        self.memory = MemoryLayers()
 66        self.turn_count = 0
 67        self._pending_messages: list[dict] = []  # 待處理的訊息
 68
 69        self._stats = {
 70            "classifications": 0,
 71            "summaries": 0,
 72            "extractions": 0,
 73            "total_tokens_saved": 0
 74        }
 75
 76    def _classify_message(self, message: str) -> tuple[MessageType, float]:
 77        """
 78        分類訊息類型和重要性
 79
 80        Returns:
 81            (類型, 重要性分數 0-1)
 82        """
 83        prompt = f"""分析以下訊息,判斷其類型和重要性。
 84
 85訊息:"{message}"
 86
 87請以 JSON 格式回答:
 88{{
 89    "type": "fact|question|instruction|chitchat|feedback",
 90    "importance": 0.0-1.0,
 91    "reason": "簡短說明"
 92}}
 93
 94類型說明:
 95- fact: 陳述事實或資訊
 96- question: 提問
 97- instruction: 給出指令或要求
 98- chitchat: 閒聊、寒暄
 99- feedback: 對之前回應的反饋
100
101重要性判斷:
102- 1.0: 核心身份、關鍵決策
103- 0.7-0.9: 重要資訊、技術細節
104- 0.4-0.6: 一般資訊
105- 0.1-0.3: 閒聊、過渡性內容
106
107只返回 JSON。"""
108
109        response = client.messages.create(
110            model=self.config.classification_model,
111            max_tokens=200,
112            messages=[{"role": "user", "content": prompt}]
113        )
114
115        self._stats["classifications"] += 1
116
117        try:
118            result = json.loads(response.content[0].text)
119            return MessageType(result["type"]), result["importance"]
120        except (json.JSONDecodeError, KeyError, ValueError):
121            return MessageType.CHITCHAT, 0.5
122
123    def _update_session_summary(self):
124        """更新 Session 摘要"""
125        if not self._pending_messages:
126            return
127
128        messages_text = "\n".join(
129            f"[{'使用者' if m['role'] == 'user' else '助手'}]: {m['content']}"
130            for m in self._pending_messages
131        )
132
133        existing = self.memory.session_summary or ""
134
135        prompt = f"""更新對話摘要。
136
137現有摘要:
138{existing if existing else "(無)"}
139
140新對話:
141{messages_text}
142
143請產生更新後的摘要,控制在 {self.config.session_summary_max_tokens} tokens 以內。
144重點保留:主題、決策、待解決問題。
145
146更新後的摘要:"""
147
148        response = client.messages.create(
149            model=self.config.summarization_model,
150            max_tokens=self.config.session_summary_max_tokens,
151            messages=[{"role": "user", "content": prompt}]
152        )
153
154        self.memory.session_summary = response.content[0].text
155        self._stats["summaries"] += 1
156        self._pending_messages = []
157
158    def _extract_semantic_units(self, messages: list[dict]) -> list[dict]:
159        """提取語意單元"""
160        messages_text = "\n".join(
161            f"[{'使用者' if m['role'] == 'user' else '助手'}]: {m['content']}"
162            for m in messages
163        )
164
165        prompt = f"""從對話中提取關鍵語意單元。
166
167對話:
168{messages_text}
169
170請以 JSON 列表格式返回,每個單元包含:
171[
172    {{
173        "content": "語意內容",
174        "type": "fact|decision|preference",
175        "importance": 0.0-1.0
176    }}
177]
178
179只提取重要性 > 0.6 的單元。只返回 JSON。"""
180
181        response = client.messages.create(
182            model=self.config.summarization_model,
183            max_tokens=1000,
184            messages=[{"role": "user", "content": prompt}]
185        )
186
187        self._stats["extractions"] += 1
188
189        try:
190            text = response.content[0].text
191            if "```" in text:
192                text = text.split("```")[1].split("```")[0]
193                if text.startswith("json"):
194                    text = text[4:]
195            return json.loads(text.strip())
196        except (json.JSONDecodeError, IndexError):
197            return []
198
199    def _check_long_term_fact(self, message: str, msg_type: MessageType, importance: float) -> bool:
200        """檢查是否應加入長期事實"""
201        # 只有高重要性的事實類訊息才加入長期記憶
202        if msg_type == MessageType.FACT and importance >= 0.8:
203            # 避免重複
204            if not any(message in fact or fact in message for fact in self.memory.long_term_facts):
205                if len(self.memory.long_term_facts) < self.config.long_term_facts_max:
206                    self.memory.long_term_facts.append(message)
207                    return True
208        return False
209
210    def add_turn(self, user_message: str, assistant_message: str):
211        """添加一輪對話"""
212        self.turn_count += 1
213
214        # 分類使用者訊息
215        msg_type, importance = self._classify_message(user_message)
216
217        # 檢查是否加入長期事實
218        self._check_long_term_fact(user_message, msg_type, importance)
219
220        # 添加到最近對話
221        self.memory.recent.append({"role": "user", "content": user_message})
222        self.memory.recent.append({"role": "assistant", "content": assistant_message})
223
224        # 添加到待處理佇列
225        self._pending_messages.append({"role": "user", "content": user_message})
226        self._pending_messages.append({"role": "assistant", "content": assistant_message})
227
228        # 檢查是否需要壓縮
229        if self.turn_count % self.config.compression_interval == 0:
230            self._perform_compression()
231
232    def _perform_compression(self):
233        """執行壓縮"""
234        # 計算壓縮前的 token 數
235        before_tokens = sum(len(m["content"]) for m in self.memory.recent) * 0.4
236
237        # 1. 更新 Session 摘要
238        if self._pending_messages:
239            self._update_session_summary()
240
241        # 2. 從舊訊息提取語意單元
242        if len(self.memory.recent) > self.config.recent_turns * 2:
243            old_messages = self.memory.recent[:-self.config.recent_turns * 2]
244
245            new_units = self._extract_semantic_units(old_messages)
246
247            # 合併語意單元,保留最重要的
248            self.memory.semantic_units.extend(new_units)
249            self.memory.semantic_units.sort(key=lambda x: x.get("importance", 0), reverse=True)
250            self.memory.semantic_units = self.memory.semantic_units[:self.config.semantic_units_max]
251
252            # 3. 只保留最近的訊息
253            self.memory.recent = self.memory.recent[-self.config.recent_turns * 2:]
254
255        # 計算壓縮後的 token 數
256        after_tokens = self._estimate_context_tokens()
257        self._stats["total_tokens_saved"] += max(0, before_tokens - after_tokens)
258
259        print(f"✅ 壓縮完成:{int(before_tokens)}{int(after_tokens)} tokens")
260
261    def _estimate_context_tokens(self) -> int:
262        """估算當前 context 的 token 數"""
263        total = 0
264
265        # 長期事實
266        total += sum(len(f) for f in self.memory.long_term_facts) * 0.4
267
268        # 語意單元
269        total += sum(len(u.get("content", "")) for u in self.memory.semantic_units) * 0.4
270
271        # Session 摘要
272        if self.memory.session_summary:
273            total += len(self.memory.session_summary) * 0.4
274
275        # 最近對話
276        total += sum(len(m["content"]) for m in self.memory.recent) * 0.4
277
278        return int(total)
279
280    def get_context(self) -> str:
281        """取得格式化的 context"""
282        parts = []
283
284        # Layer 4: 長期事實
285        if self.memory.long_term_facts:
286            facts = "\n".join(f"- {f}" for f in self.memory.long_term_facts)
287            parts.append(f"[核心資訊]\n{facts}")
288
289        # Layer 3: 語意單元
290        if self.memory.semantic_units:
291            units = "\n".join(f"- {u['content']}" for u in self.memory.semantic_units[:10])
292            parts.append(f"[關鍵記憶]\n{units}")
293
294        # Layer 2: Session 摘要
295        if self.memory.session_summary:
296            parts.append(f"[對話摘要]\n{self.memory.session_summary}")
297
298        return "\n\n".join(parts)
299
300    def get_messages(self) -> list[dict]:
301        """取得要發送給 API 的訊息"""
302        messages = []
303
304        context = self.get_context()
305        if context:
306            messages.append({
307                "role": "user",
308                "content": f"{context}\n\n請基於以上背景繼續對話。"
309            })
310            messages.append({
311                "role": "assistant",
312                "content": "我已了解背景,請繼續。"
313            })
314
315        messages.extend(self.memory.recent)
316        return messages
317
318    def get_stats(self) -> dict:
319        return {
320            **self._stats,
321            "turn_count": self.turn_count,
322            "long_term_facts": len(self.memory.long_term_facts),
323            "semantic_units": len(self.memory.semantic_units),
324            "has_session_summary": self.memory.session_summary is not None,
325            "recent_messages": len(self.memory.recent),
326            "estimated_context_tokens": self._estimate_context_tokens()
327        }
328
329
330class HybridCompressionChatbot:
331    """使用混合壓縮的聊天機器人"""
332
333    def __init__(
334        self,
335        system_prompt: str,
336        config: Optional[HybridCompressionConfig] = None
337    ):
338        self.system_prompt = system_prompt
339        self.manager = HybridCompressionManager(config)
340
341    def chat(self, user_input: str) -> str:
342        messages = self.manager.get_messages()
343        messages.append({"role": "user", "content": user_input})
344
345        response = client.messages.create(
346            model=self.manager.config.main_model,
347            max_tokens=2048,
348            system=self.system_prompt,
349            messages=messages
350        )
351
352        assistant_response = response.content[0].text
353        self.manager.add_turn(user_input, assistant_response)
354
355        return assistant_response
356
357
358# 使用範例與效能測試
359if __name__ == "__main__":
360    config = HybridCompressionConfig(
361        recent_turns=2,
362        compression_interval=3,
363        semantic_units_max=15,
364        long_term_facts_max=5
365    )
366
367    bot = HybridCompressionChatbot(
368        system_prompt="你是一個專業的技術顧問。",
369        config=config
370    )
371
372    # 模擬長對話
373    test_messages = [
374        "你好,我是小明,是資深後端工程師",  # 高重要性事實
375        "我在一家電商公司工作",  # 高重要性事實
376        "今天天氣真好",  # 低重要性閒聊
377        "我們的系統使用微服務架構",  # 中等重要性
378        "主要用 Go 和 Python",  # 高重要性技術事實
379        "最近遇到一個效能問題",  # 問題
380        "資料庫查詢太慢",  # 問題細節
381        "你有什麼建議嗎?",  # 問題
382        "我試過加索引了",  # 反饋
383        "效果不太好",  # 反饋
384        "還有其他方法嗎?",  # 問題
385        "你還記得我用什麼語言嗎?",  # 測試記憶
386    ]
387
388    for i, msg in enumerate(test_messages, 1):
389        print(f"\n{'='*60}")
390        print(f"Turn {i}: {msg}")
391        response = bot.chat(msg)
392        print(f"Bot: {response[:150]}...")
393        print(f"\n📊 Stats: {bot.manager.get_stats()}")
394
395    print("\n" + "="*60)
396    print("最終記憶狀態:")
397    print(f"\n長期事實:{bot.manager.memory.long_term_facts}")
398    print(f"\n語意單元數:{len(bot.manager.memory.semantic_units)}")
399    print(f"\nSession 摘要:{bot.manager.memory.session_summary}")

效能比較與選擇指南

┌─────────────────────────────────────────────────────────────────────┐
│                    Context 壓縮策略比較                              │
├──────────────────┬──────────┬──────────┬──────────┬────────────────┤
│ 策略             │ 壓縮率   │ 資訊保留 │ 實作複雜 │ 適用場景       │
├──────────────────┼──────────┼──────────┼──────────┼────────────────┤
│ 滑動視窗         │ 固定     │ ★★      │ ★        │ 短期對話       │
│ 漸進式摘要       │ 70-90%   │ ★★★★   │ ★★★     │ 長期對話       │
│ 階層式摘要       │ 80-95%   │ ★★★★★  │ ★★★★   │ 跨 Session     │
│ 語意壓縮         │ 85-95%   │ ★★★★★  │ ★★★★★  │ 知識密集型     │
│ 混合壓縮         │ 90-98%   │ ★★★★★  │ ★★★★★  │ 生產環境       │
└──────────────────┴──────────┴──────────┴──────────┴────────────────┘

選擇指南:

1. 簡單聊天機器人(< 20 輪)
   → 滑動視窗

2. 客服系統(20-100 輪)
   → 漸進式摘要

3. 個人助理(跨 Session)
   → 階層式摘要 + 持久化

4. 專業諮詢系統
   → 語意壓縮

5. 企業級應用
   → 混合壓縮系統

最佳實踐清單

Context 壓縮 Checklist:

基礎設定
□ 是否設定了合理的 Context 上限?
□ 是否有壓縮觸發機制(輪數/token 數)?
□ 是否選擇了適合場景的壓縮策略?

摘要品質
□ 摘要是否保留了關鍵資訊?
□ 摘要模型是否選擇了 cost-effective 的選項?
□ 是否有摘要品質驗證機制?

記憶管理
□ 是否區分了不同重要性的資訊?
□ 是否有資訊衰減機制?
□ 長期記憶是否有持久化?

效能監控
□ 是否追蹤壓縮率?
□ 是否監控 Context token 使用量?
□ 是否有回歸測試確保記憶品質?

特殊處理
□ 是否處理了使用者身份資訊?
□ 是否處理了關鍵決策記錄?
□ 是否處理了待解決問題追蹤?

總結

Context 壓縮與摘要是打造「可無限對話」AI 系統的核心技術。本文介紹的各種策略可以根據需求靈活組合:

需求推薦策略
快速上線滑動視窗
長對話支援漸進式摘要
使用者記憶階層式摘要
知識密集場景語意壓縮
生產環境混合壓縮

關鍵原則:

  1. 分層處理:不同重要性的資訊用不同策略
  2. 漸進壓縮:避免一次性大量壓縮造成資訊損失
  3. 語意優先:保留語意完整性比保留原文更重要
  4. 持續監控:追蹤壓縮率和記憶品質

透過合理的 Context 管理,你可以打造出成本可控、體驗優良的長期對話 AI 系統。

Yen

Yen

Yen