為什麼你的 AI Agent 總是在遺失記憶(以及我們如何解決它)
你的 AI agent 剛與用戶進行了一場 30 分鐘的對話。他們討論了專案需求、分享了偏好並做出了決策。接著用戶輸入 /new 來開始一個新的對話視窗。
Agent 嘗試將該對話整合到長期記憶中。LLM 調用失敗了。可能是頻率限制(Rate limit)、逾時(Timeout),或者是模型回傳了純文字而非調用必要的 tool。
記憶消失了。三十分鐘的上下文資訊,煙消雲散。
這種情況發生的頻率比你想像的還要高。我們在 LemonClaw 實例中追蹤了這個問題:在任何單一模型上,記憶整合(memory consolidation)的失敗率大約是 15%。對於一個理應是不可見的基礎設施功能來說,這是不可接受的。
其他框架如何處理這個問題(其實他們沒處理)
大多數 AI agent 框架將記憶整合視為一個簡單的 LLM 調用。如果成功了,很好;如果失敗了,記憶就丟失了。
OpenClaw 是最受歡迎的開源 agent 框架,它在對話和整合時使用相同的模型。一次花費 $0.003 且耗時 8 秒以上的 Claude Sonnet 調用,僅僅是為了總結一段用戶永遠不會看到的對話。當該調用失敗時(頻率限制、逾時、模型錯誤),框架只會記錄一條警告然後繼續執行。用戶的上下文就這樣消失了。
另一個熱門框架 nanobot 也有相同的架構。單一模型、單次嘗試、沒有 fallback(回退)機制。整合功能甚至沒有設定逾時。緩慢的上游響應(Cloudflare 524 錯誤很常見)會阻塞整個對話視窗,直到連線斷開。
這些框架都沒有將整合功能與主模型分離,也沒有為記憶操作設計 fallback 邏輯,更無法區分「API 調用失敗」與「API 調用成功但模型未按要求執行」之間的差異。
這些並非極端案例。在任何單一模型上都有 15% 失敗率的情況下,一個每天執行 100 次整合的框架會在其中 15 次遺失記憶。一週下來,就是 105 場被 agent 忘得一乾二淨的對話。
問題的核心不只是重試邏輯
顯而易見的修復方案是使用指數退避(exponential backoff)進行重試。我們以前就有這個機制,它能很好地處理短暫的 HTTP 錯誤:
# 重試迴圈:1s → 2s → 4s 退避
for attempt in range(3):
try:
response = await acompletion(**kwargs)
return await self._collect_stream(response)
except (RateLimitError, APIConnectionError) as e:
await asyncio.sleep(RETRY_DELAYS[attempt])
這能捕捉 429 錯誤和網路波動。但有兩種失敗模式會成為漏網之魚:
失敗模式 1:模型無法執行 tool calling。 某些模型,特別是在快速推論引擎上運行的較小型模型,偶爾會在複雜的提示(prompt)下無法生成有效的函數調用。API 回傳 200 狀態碼,但內部包裹著 ServiceUnavailableError 或 MidStreamFallbackError。你的重試邏輯看到異常,重試同一個模型,然後得到相同的錯誤。
失敗模式 2:模型「成功」回傳但沒有調用 tool。 LLM 回傳了完全正確的回應。HTTP 200,沒有錯誤。但它沒有調用 save_memory 並帶上結構化數據,而是寫了一段純文字總結。你的重試引擎認為這是成功的。整合函數檢查 tool 調用,發現沒有,然後就放棄了。
第二種失敗模式是最隱蔽的。傳輸層認為一切正常,但業務層知道它失敗了。對於一個不理解你的 tool schema 的模型,再多的 HTTP 層級重試都無濟於事。
雙層 Fallback 架構
我們透過在不同層級運行的兩個獨立 fallback 迴圈解決了這個問題:
用戶發送 /new
│
▼
consolidate() ─── 業務層 Fallback
│ 「模型是否調用了 save_memory?」
│ 否 → 嘗試鏈条中的下一個模型
│
▼
_chat_with_retry() ─── 傳輸層 Fallback
│ HTTP 錯誤 → 指數退避
│ 重試耗盡 → 遍歷 fallback 鏈
│
▼
MODEL_MAP fallback 鏈:
llama-3.3-70b ─$0.59/M─→ qwen3-32b ─$0.29/M─→ llama-4-scout ─$0.11/M─→ gpt-4.1-mini ─→ claude-haiku
(394 TPS) (662 TPS) (594 TPS) (可靠) (最後手段)
第一層處理傳輸失敗。第二層處理業務邏輯失敗。Fallback 鏈由這兩層共用,並在中央目錄中定義一次。
這與「重試同一個模型」的方法有本質上的不同。當一個模型無法調用 tool 時,使用相同的提示詞重試鮮少有用。切換到具有不同權重和不同 tool calling 行為的另一個模型則有效得多。
模型目錄:單一事實來源
我們目錄中的每個模型都有一個可選的 fallback 欄位,指向下一個要嘗試的模型:
@dataclass(frozen=True)
class ModelEntry:
id: str
label: str
tier: str
description: str
fallback: str | None = None
hidden: bool = False # 對用戶端的 /model 列表隱藏
MODEL_CATALOG = [
# 用戶可見模型 (用戶可在 16 個模型間切換)
ModelEntry("claude-sonnet-4-6", "Claude Sonnet 4.6", "standard",
"Recommended", fallback="claude-sonnet-4-5"),
ModelEntry("gpt-4.1-mini", "GPT-4.1 Mini", "economy",
"Stable tool calling", fallback="claude-haiku-4-5"),
# 隱藏的整合模型 (僅供內部使用)
ModelEntry("llama-3.3-70b-versatile", "Llama 3.3 70B (Groq)", "economy",
"394 TPS", fallback="qwen3-32b", hidden=True),
ModelEntry("qwen3-32b", "Qwen3 32B (Groq)", "economy",
"662 TPS", fallback="llama-4-scout-17b-16e-instruct", hidden=True),
# ...
]
hidden=True 標記將內部模型排除在面向用戶的 /model 指令之外,但它們仍參與 fallback 鏈。用戶看到 16 個可以切換的模型,而系統實際使用 19 個。這三個隱藏模型專門用於記憶整合等背景任務,在這些任務中,速度和成本比對話品質更重要。
此目錄是所有模型路由的唯一事實來源。在 fallback 鏈中新增一個模型只需增加一行代碼。不需要同步配置文件,不需要更新環境變數,也不需要修改部署腳本。
傳輸層:帶有循環檢測的鏈式 Fallback
重試引擎透過使用已訪問集合(visited set)來遍歷 fallback 鏈,以防止無限迴圈:
async def _chat_with_retry(self, kwargs, original_model):
# 第一階段:在主要模型上進行指數退避
for attempt in range(3):
try:
response = await acompletion(**kwargs)
return await self._collect_stream(response)
except (RateLimitError, APIConnectionError, APIError) as e:
await asyncio.sleep(RETRY_DELAYS[attempt])
except AuthenticationError:
return LLMResponse(content="API key invalid.", finish_reason="error")
# 第二階段:遍歷 fallback 鏈
visited = {original_model}
current = original_model
while True:
entry = MODEL_MAP.get(current)
if not entry or not entry.fallback or entry.fallback in visited:
break
current = entry.fallback
visited.add(current)
# 解析此模型的正確 gateway
gw = self._resolve_gateway_for_model(current)
resolved = self._resolve_model(current, gateway=gw)
fb_kwargs = {**kwargs, "model": resolved}
# 修正目標模型協議的 api_base
if gw and gw.default_api_base:
fb_kwargs["api_base"] = gw.default_api_base
try:
response = await acompletion(**fb_kwargs)
return await self._collect_stream(response)
except Exception:
continue # 嘗試鏈中的下一個模型
return LLMResponse(content="Service unavailable.", finish_reason="error")
visited 集合至關重要。沒有它,像 A→B→A 這樣的鏈條會陷入死迴圈。有了它,引擎會確保每個模型恰好被嘗試一次。
Gateway 解析也很重要。不同的模型需要不同的 API 格式。Claude 模型通過 Anthropic 格式的 gateway(沒有 /v1 後綴)路由;GPT 模型通過 OpenAI 相容的 gateway(有 /v1)路由;Groq 模型使用另一種端點。Fallback 引擎為鏈中的每個模型解析正確的 gateway,防止出現將 Anthropic 請求發送到 OpenAI 端點這類協議不匹配的情況。
這是大多數框架完全忽略的細節。他們假設所有模型都使用相同的協議。在生產環境中,當使用跨越 4 種不同 API 格式的 19 個模型時,這種假設會立刻崩潰。
業務層:Tool Call 驗證
整合函數在其之上添加了自己的 fallback 迴圈:
async def consolidate(self, session, provider, model, **kwargs):
visited = set()
current_model = model
while current_model and current_model not in visited and len(visited) <= 3:
visited.add(current_model)
response = await asyncio.wait_for(
provider.chat(messages=messages, tools=SAVE_MEMORY_TOOL, model=current_model),
timeout=30,
)
if response.has_tool_calls:
# 成功:提取並儲存記憶
args = response.tool_calls[0].arguments
self.write_long_term(args["memory_update"])
self.append_history(args["history_entry"])
return True
# 模型沒有調用 tool —— 嘗試鏈中的下一個模型
entry = MODEL_MAP.get(current_model)
next_model = entry.fallback if entry else None
if next_model and next_model not in visited:
current_model = next_model
continue
return False # 沒有更多 fallback
return False
這捕捉了 _chat_with_retry 回傳成功回應(HTTP 200,內容有效)但模型未調用 tool 的情況。整合函數檢查 has_tool_calls,如果缺失,則移至鏈中的下一個模型。
逾時包裝器 (asyncio.wait_for) 也會觸發 fallback。如果模型處理時間超過 30 秒(在慢速上游的 Cloudflare 524 錯誤中很常見),該函數會捕捉 TimeoutError 並嘗試下一個模型,而不是無限期地阻塞用戶的對話視窗。
為何使用 Groq 進行整合
記憶整合是一項背景任務。用戶看不到輸出,他們只需要它能運作。這使其成為快速且廉價模型的絕佳候選者。
大多數框架將昂貴的模型用於所有任務。如果你在對話中使用 Claude Sonnet,你在記憶整合時也會使用 Claude Sonnet。這意味著每次整合都要花費 $3/M 輸入 token 和 8 秒以上的時間,而處理的任務產出的內容根本沒有人類會去閱讀。
我們將整合功能與對話模型完全解耦。對話使用用戶選擇的任何模型。整合則使用 Groq 託管的專用模型鏈:
| 模型 | 速度 | 輸入成本 | 輸出成本 |
|---|---|---|---|
| llama-3.3-70b-versatile | 394 TPS | $0.59/M | $0.79/M |
| qwen3-32b | 662 TPS | $0.29/M | $0.59/M |
| llama-4-scout-17b-16e | 594 TPS | $0.11/M | $0.34/M |
| gpt-4.1-mini (先前版本) | ~150 TPS | $0.40/M | $1.60/M |
主模型 (llama-3.3-70b) 在約 5 秒內整合 60 條訊息。之前的預設模型 (gpt-4.1-mini) 需要 8 秒以上。每次整合的成本從約 $0.003 降至約 $0.001。
權衡之處在於:Groq 模型在複雜提示下的 tool calling 可靠性較低。這正是雙層 fallback 存在的原因。當 llama-3.3-70b 無法調用 tool 時,qwen3-32b 會接手。如果它也失敗了,llama-4-scout 會嘗試。如果三個 Groq 模型都失敗了,gpt-4.1-mini 會以接近 100% 的 tool calling 可靠性來處理它。
在生產環境中,我們看到主要模型成功率約為 85%。只有不到 2% 的整合會走到 gpt-4.1-mini。總體失敗率:幾乎為零。
生產結果
我們將其部署到兩個 LemonClaw 實例中,並使用真實的 Telegram 對話進行測試。
第一次部署(僅單層 fallback):
記憶整合 (archive_all): 56 條訊息
llama-3.3-70b-versatile → "Failed to call a function"
正在 Fallback → qwen3-32b
qwen3-32b: LLM did not call save_memory, skipping
→ "記憶封存失敗,視窗未清除。"
傳輸層捕捉到了第一次失敗並進行了 fallback。但 qwen3-32b 回傳了文字而沒有調用 tool。單層 fallback 無法處理這種情況。這正是所有其他框架會默默遺失記憶的場景。
第二次部署(雙層 fallback):
記憶整合 (archive_all): 60 條訊息
model=llama-3.3-70b-versatile → 成功
記憶整合完成:剩餘 60 條訊息
同樣的模型,同樣的訊息量。這次第一次嘗試就成功了。Tool calling 失敗的隨機性質正是為什麼你需要一個 fallback 鏈而不是單一備份模型的原因。
當主要模型確實失敗時,鏈條會捕捉它:
llama-3.3-70b → tool call 失敗
→ consolidate() fallback → qwen3-32b
→ qwen3-32b 沒有調用 tool
→ consolidate() fallback → llama-4-scout
→ llama-4-scout 沒有調用 tool
→ consolidate() fallback → gpt-4.1-mini
→ gpt-4.1-mini 調用了 save_memory ✓
記憶整合完成
嘗試了四個模型,記憶獲救了。用戶只會看到「新會話已開始。」,完全不知道幕後發生了這麼多事。
架構差異
LemonClaw 的記憶系統與替代方案的功能對比:
| 能力 | 典型 AI Agent 框架 | LemonClaw |
|---|---|---|
| 整合模型 | 與對話相同 (昂貴且緩慢) | 獨立模型鏈,Groq 加速 |
| 失敗處理 | 記錄警告,遺失記憶 | 雙層 fallback,5 層深度 |
| 傳輸 Fallback | 重試同個模型 3 次 | 跨不同模型的鏈式 fallback |
| 業務邏輯 Fallback | 無 | Tool call 驗證 + 模型切換 |
| 逾時保護 | 無 (Cloudflare 524 阻塞對話) | asyncio.wait_for(timeout=30) + fallback |
| 會話截斷 | 無 (上下文無限增長) | 整合後截斷舊訊息 |
| 歷史搜尋 | 無 | HISTORY.md 滾動窗口,可用 grep 搜尋 |
| 內部模型 | 不支援 | hidden=True 用於僅限系統的模型 |
| 循環預防 | 不需要 (沒有鏈條) | visited 集合防止 A→B→A 迴圈 |
| Gateway 解析 | 假設單一 API 格式 | 具備協議檢測的每模型 gateway |
這張表中的每一行都代表了我們親身經歷過或在其他框架的 issue 追蹤器中觀察到的生產環境失敗案例。雙層 fallback、隱藏模型目錄、每模型 gateway 解析、逾時觸發的 fallback:這些在 OpenClaw、nanobot 或我們研究過的任何其他開源 agent 框架中都不存在。
我們學到了什麼
「請求成功」不等於「任務成功」。 通用的重試引擎在 HTTP 層級運行。它們無法知道一個帶有有效 JSON 的 200 回應實際上是失敗的,因為模型沒有使用你要求的 tool。業務關鍵型操作需要自己的成功標準和 fallback 邏輯。
小模型與大模型的失敗方式不同。 大模型(GPT-4.1、Claude Sonnet)幾乎總是在被要求時調用 tool。快速推論引擎上的小模型有時會生成看起來有效但完全忽略 tool schema 的回應。這不是你能透過 prompt engineering 修復的 bug,而是需要架構層面緩解的能力差距。
使用生產數據而非合成數據進行測試。 我們最初使用 6 條合成訊息的測試在每個模型上都通過了。而包含 tool 調用歷史、時間戳和混合語言的真實 60 條訊息對話,在三個 Groq 模型中的兩個上都失敗了。真實數據的複雜性會暴露乾淨的測試數據永遠無法發現的失敗模式。
LemonClaw 是一個開源 AI agent 框架,內建多模型路由、持久化記憶,以及 10 多個聊天平台整合。此處描述的整個雙層 fallback 系統已在開源版本中發佈。在您自己的伺服器上運行它:github.com/hedging8563/lemonclaw
需要透過一個 API key 存取 300 多個 AI 模型?lemondata.cc 提供對 OpenAI、Anthropic、Google、DeepSeek、Groq 等模型的統一存取。
