設定

語言

為什麼您的 AI Agent 總是丟失記憶(以及我們如何解決這個問題)

L
LemonData
·2026年2月28日·33 次瀏覽
#AI 智能體#記憶#回退機制#架構#LemonClaw
為什麼您的 AI Agent 總是丟失記憶(以及我們如何解決這個問題)

為什麼你的 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 狀態碼,但內部包裹著 ServiceUnavailableErrorMidStreamFallbackError。你的重試邏輯看到異常,重試同一個模型,然後得到相同的錯誤。

失敗模式 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 等模型的統一存取。

Share: