設定

語言

為什麼你的 AI Agent 總是遺忘記憶

T
TokenLab
·2026年3月5日·1334 次瀏覽
為什麼你的 AI Agent 總是遺忘記憶

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

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

記憶就這樣消失了。三十分鐘的 context,煙消雲散。

這種情況發生的頻率比你想像的要高。在 Hermes Agent 的運行環境中,在我們加入專用的 fallback 機制之前,任何單一模型在進行記憶整合時的失敗率約為 15%。對於一個應該是「隱形基礎設施」的功能來說,這是不可接受的。

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

其他框架如何處理(其實他們不處理)

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

Hermes Agent 的基準版本曾使用與對話相同的模型來進行整合。一次 Claude Sonnet 的調用成本為 $0.003 且耗時 8 秒以上,僅僅是為了總結一段用戶永遠不會看到的對話。當該調用失敗(Rate limit、Timeout、模型錯誤)時,框架只會記錄一條警告並繼續執行。用戶的 context 就此消失。

另一個流行的框架 nanobot 也有同樣的架構。單一模型、單次嘗試、沒有 fallback。整合函數甚至沒有 Timeout 機制。緩慢的上游(Cloudflare 524 錯誤很常見)會阻塞整個 session,直到連線中斷。

這兩個框架都沒有將整合功能與主模型分離。兩者都沒有針對記憶操作的 fallback 邏輯。兩者也都無法區分「API 調用失敗」與「API 調用成功但模型沒有執行要求的指令」。

這些並非極端案例。在任何單一模型都有 15% 失敗率的情況下,一個每天執行 100 次整合的框架會在其中 15 次丟失記憶。一週下來,這意味著有 105 場對話的內容會被 Agent 徹底遺忘。

問題比 Retry 邏輯更深層

顯而易見的修復方法是使用 exponential backoff 進行 retry。我們曾嘗試過,它能很好地處理暫時性的 HTTP 錯誤:

# Retry 迴圈: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。某些模型,特別是在快速 inference 引擎上運行的較小模型,偶爾會在面對複雜 prompt 時無法生成有效的 function calls。API 會回傳 200 狀態碼,但內部包裹著 ServiceUnavailableErrorMidStreamFallbackError。你的 retry 邏輯看到異常,重試同一個模型,結果得到相同的錯誤。

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

第二種失敗模式更為隱蔽。傳輸層認為一切正常,但業務層知道失敗了。對於一個不理解你的 tool schema 的模型,再多的 HTTP 層級 retry 都無濟於事。

雙層 Fallback 架構 (Dual-Layer Fallback Architecture)

我們通過在不同層級運行的兩個獨立 fallback 迴圈解決了這個問題:

用戶發送 /new
    │
    ▼
consolidate() ─── 業務層 Fallback
    │               「模型是否有調用 save_memory?」
    │               否 → 嘗試鏈中的下一個模型
    │
    ▼
_chat_with_retry() ─── 傳輸層 Fallback
    │                    HTTP 錯誤 → exponential backoff
    │                    重試耗盡 → 遍歷 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

retry 引擎使用 visited 集合遍歷 fallback 鏈,以防止無限迴圈:

async def _chat_with_retry(self, kwargs, original_model):
    # 第一階段:在主模型上進行 Exponential backoff
    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}

        # 為目標模型的 protocol 修正 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 等 protocol 不匹配的情況。

這是大多數框架完全忽略的細節。他們假設所有模型都使用相同的 protocol。在生產環境中,當你擁有橫跨 4 種不同 API 格式的 19 個模型時,這個假設會立即破滅。

業務層:工具調用驗證

整合函數在頂層添加了自己的 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 並嘗試下一個模型,而不是無限期地阻塞用戶的 session。

為什麼選擇 Groq 進行記憶整合

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

大多數框架對所有任務都使用相同的昂貴模型。如果你使用 Claude Sonnet 進行對話,你也會使用 Claude Sonnet 進行記憶整合。對於一項產出人類永遠不會閱讀的任務,這意味著每次整合需要花費 $3/M input tokens 且耗時 8 秒以上。

我們將整合功能與對話模型完全解耦。對話使用用戶選擇的任何模型,而整合則使用專用的 Groq 託管模型鏈:

模型 速度 Input 成本 Output 成本
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 條訊息的 session。之前的預設模型(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。總體失敗率:實際上為零。

生產環境結果

我們將此方案部署到兩個 Hermes Agent 運行實例,並使用真實的 Telegram 對話進行了測試。

第一次部署(僅限單層 fallback):

記憶整合 (archive_all):56 條訊息
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):

記憶整合 (archive_all):60 條訊息
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

嘗試了四個模型,記憶成功儲存。用戶看到「新 session 已開始」,完全不知道背後發生了這一切。

架構差距

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

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

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

我們學到了什麼

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

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

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

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


本文描述了來自 Hermes Agent 的可靠性模式。請將此處的架構模式作為記憶持久性、fallback 設計和具備 gateway 感知能力模型路由的參考。

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

分享: