在前一篇文章《多 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 系統的核心技術。本文介紹的各種策略可以根據需求靈活組合:
| 需求 | 推薦策略 |
|---|---|
| 快速上線 | 滑動視窗 |
| 長對話支援 | 漸進式摘要 |
| 使用者記憶 | 階層式摘要 |
| 知識密集場景 | 語意壓縮 |
| 生產環境 | 混合壓縮 |
關鍵原則:
- 分層處理:不同重要性的資訊用不同策略
- 漸進壓縮:避免一次性大量壓縮造成資訊損失
- 語意優先:保留語意完整性比保留原文更重要
- 持續監控:追蹤壓縮率和記憶品質
透過合理的 Context 管理,你可以打造出成本可控、體驗優良的長期對話 AI 系統。
