設定

語言

為什麼你的 AI Agent 總是失去記憶

L
LemonData
·2026年3月5日·744 次瀏覽
為什麼你的 AI Agent 總是失去記憶

你的 AI Agent 剛和使用者進行了 30 分鐘的對話。他們討論了專案需求、分享了偏好、做出了決定。接著,使用者輸入 /new 來開始一個新的對話。

Agent 試圖將該對話整合到長期記憶中。LLM 呼叫失敗了。可能是 Rate limit、Timeout,或者是模型回傳了純文字而非呼叫所需的 tool。

記憶消失了。三十分鐘的上下文煙消雲散。

這種情況發生的頻率比你想像的要高。我們在 LemonClaw 實例中追蹤了這個問題:在任何單一模型上,記憶整合(memory consolidation)的失敗率約為 15%。對於一個理應是「隱形基礎設施」的功能來說,這是不可接受的。

如果你正在構建產品介面而不僅僅是記憶子系統,請將此頁面與 一鍵式聊天機器人指南 以及 自託管 LemonClaw 指南 搭配閱讀。只有當 Agent 真正存在於一個可用的應用程式中時,記憶的持久性才有意義。

其他框架如何處理這個問題(其實他們沒處理)

大多數 AI Agent 框架將記憶整合視為一個簡單的 LLM 呼叫。如果成功了,那很好;如果失敗了,記憶就丟失了。

LemonClaw 的前身框架對整合和對話使用相同的模型。一次 Claude Sonnet 呼叫花費 $0.003 且耗時 8 秒以上,僅僅是為了總結一段使用者永遠不會看到的對話。當該呼叫失敗(Rate limit、Timeout、模型錯誤)時,框架只會記錄一條警告然後繼續。使用者的上下文就此消失。

nanobot 是另一個受歡迎的框架,也採用相同的架構。單一模型、單次嘗試、沒有 fallback。整合功能甚至沒有 Timeout 設定。緩慢的上游(常見的 Cloudflare 524 錯誤)會阻塞整個對話,直到連線中斷。

這些框架都沒有將整合與主模型分離,也沒有針對記憶操作的 fallback 邏輯。它們無法區分「API 呼叫失敗」與「API 呼叫成功但模型沒有執行要求的操作」。

這些並非極端案例。在任何單一模型上都有 15% 的失敗率,這意味著一個每天執行 100 次整合的框架會丟失 15 次記憶。一週下來,就有 105 個對話會讓 Agent 忘得一乾二淨。

問題比重試邏輯更深層

最顯而易見的修復方法是使用指數退避(exponential backoff)進行重試。我們以前也有這個機制。它能很好地處理暫時性的 HTTP 錯誤:

# Retry loop: 1s → 2s → 4s backoff
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 下無法生成有效的 function calls。API 回傳 200,但內部包裹著 ServiceUnavailableErrorMidStreamFallbackError。你的重試邏輯看到異常,重試同一個模型,結果得到相同的錯誤。

失敗模式 2:模型「成功」了但沒有呼叫 tool。LLM 回傳了一個完全有效的響應。HTTP 200。沒有錯誤。但它沒有使用結構化數據呼叫 save_memory,而是寫了一段純文字總結。你的重試引擎認為這是成功的。整合功能檢查 tool calls,發現沒有,然後就放棄了。

第二種失敗模式更為隱蔽。傳輸層(transport layer)認為一切正常,但業務層(business layer)知道並非如此。對於一個不理解你的 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 時,用相同的 prompt 重試它很少有幫助。切換到另一個具有不同權重和不同 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 集合來遍歷 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 模型則使用另一個 endpoint。Fallback 引擎會為鏈中的每個模型解析正確的 gateway,防止出現將 Anthropic 請求發送到 OpenAI endpoint 等協議不匹配的情況。

這是大多數框架完全忽略的細節。它們假設所有模型都使用相同的協議。在生產環境中,面對跨越 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,如果缺失,則移動到鏈中的下一個模型。

Timeout 包裝器(asyncio.wait_for)也會觸發 fallback。如果模型耗時超過 30 秒(在慢速上游的 Cloudflare 524 錯誤中很常見),該函數會捕捉 TimeoutError 並嘗試下一個模型,而不是無限期地阻塞使用者的對話。

為什麼選擇 Groq 進行整合

記憶整合是一項後台任務。使用者看不到輸出,他們只需要它能運作。這使其成為快速、廉價模型的絕佳候選者。

大多數框架對所有任務都使用相同的昂貴模型。如果你使用 Claude Sonnet 進行對話,你也同樣在使用 Claude Sonnet 進行記憶整合。對於一項產出人類永遠不會閱讀的任務來說,這意味著每百萬 input tokens 花費 $3,且每次整合耗時 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 模型在複雜 prompt 下的 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):

Memory consolidation (archive_all): 56 messages
llama-3.3-70b-versatile → "Failed to call a function"
Falling back → qwen3-32b
qwen3-32b: LLM did not call save_memory, skipping
→ "Memory archival failed, session not cleared."

傳輸層捕捉到了第一次失敗並進行了 fallback。但 qwen3-32b 回傳了文字而沒有呼叫 tool。單層 fallback 無法處理這種情況。這正是所有其他框架會默默丟失記憶的典型場景。

第二次部署(雙層 fallback):

Memory consolidation (archive_all): 60 messages
model=llama-3.3-70b-versatile → success
Memory consolidation done: 60 messages remaining

相同的模型,相同的訊息量。這次第一次嘗試就成功了。Tool calling 失敗的間歇性特質,正是為什麼你需要一個 fallback 鏈而不是單一備份模型的原因。

當主模型確實失敗時,鏈條會捕捉到它:

llama-3.3-70b → tool call failed
→ consolidate() fallback → qwen3-32b
→ qwen3-32b didn't call tool
→ consolidate() fallback → llama-4-scout
→ llama-4-scout didn't call tool
→ consolidate() fallback → gpt-4.1-mini
→ gpt-4.1-mini called save_memory ✓
Memory consolidation done

四個模型嘗試過,記憶已儲存。使用者看到「新對話已開始」,完全不知道發生了這一切。

架構差距

LemonClaw 的記憶系統與其他替代方案的功能對比:

功能 典型的 AI Agent 框架 LemonClaw
整合模型 與對話相同 (昂貴、緩慢) 獨立模型鏈,Groq 加速
失敗處理 記錄警告,丟失記憶 雙層 fallback,深度達 5 個模型
傳輸層 Fallback 重試同一個模型 3 次 跨不同模型的鏈式 fallback
業務邏輯 Fallback Tool call 驗證 + 模型切換
Timeout 保護 無 (Cloudflare 524 阻塞對話) asyncio.wait_for(timeout=30) + fallback
對話截斷 無 (上下文無限增長) 整合後截斷舊訊息
歷史搜尋 HISTORY.md 滾動視窗,可 grep 搜尋
內部模型 不支援 hidden=True 僅限系統模型
循環預防 不需要 (無鏈條) visited 集合防止 A→B→A 循環
Gateway 解析 假設單一 API 格式 具備協議檢測的每模型 gateway

表格中的每一行都代表了我們親身經歷過,或在其他框架的 issue 追蹤器中觀察到的生產環境失敗案例。雙層 fallback、隱藏模型目錄、每模型 gateway 解析、Timeout 觸發的 fallback:這些在 nanobot 或我們研究過的任何其他開源 Agent 框架中都不存在。

我們的經驗教訓

「請求成功」不等於「任務成功」。通用的重試引擎運作在 HTTP 層級。它們無法知道一個帶有有效 JSON 的 200 響應實際上是失敗的,因為模型沒有使用你要求的 tool。業務關鍵型操作需要自己的成功標準和 fallback 邏輯。

小模型的失敗方式與大模型不同。大模型(GPT-4.1, Claude Sonnet)在被要求時幾乎總是會呼叫 tool。快速推理引擎上的小模型有時會生成看起來有效但完全忽略 tool schema 的響應。這不是你能透過 prompt engineering 修復的 bug,而是一個需要架構性緩解的功能差距。

使用生產數據而非合成數據進行測試。我們最初使用 6 條合成訊息進行的測試在每個模型上都通過了。而包含 tool call 歷史、時間戳記和混合語言的真實 60 條訊息對話,在三個 Groq 模型中的兩個上都失敗了。真實數據的複雜性會暴露乾淨的測試數據永遠無法發現的失敗模式。

這也是為什麼 AI API Rate Limiting 指南 在這裡很重要。記憶系統不僅僅需要一個「更好的模型」,它還需要傳輸策略、業務邏輯成功檢查,以及一個不會在供應商發生普通故障時崩潰的 fallback 階梯。


LemonClaw 是一個開源的 AI Agent 框架,內建多模型路由、持久化記憶以及 10 多個聊天平台整合。這裡描述的整個雙層 fallback 系統都包含在開源版本中。在你的伺服器上運行它:github.com/hedging8563/lemonclaw

需要透過一個 API key 存取 300 多個 AI 模型嗎?lemondata.cc 提供對 OpenAI、Anthropic、Google、DeepSeek、Groq 等模型的統一存取。

Share: