設定

言語

なぜあなたのAI Agentは記憶を失い続けるのか

T
TokenLab
·2026年3月5日·1324 回表示
なぜあなたのAI Agentは記憶を失い続けるのか

あなたのAIエージェントは、ユーザーと30分間の会話を終えたところです。プロジェクトの要件について話し合い、好みを共有し、意思決定を行いました。そこでユーザーが /new と入力し、新しいセッションを開始します。

エージェントはその会話を長期メモリに集約(consolidate)しようとします。しかし、LLMの呼び出しが失敗します。Rate limit(レート制限)、Timeout(タイムアウト)、あるいはモデルが要求されたツールを呼び出す代わりにテキストを返してしまったのです。

メモリは消え去りました。30分間のコンテキストが、霧のように消えてしまったのです。

これは、あなたが思うよりも頻繁に起こります。Hermes Agentのランタイムでは、専用のfallback(フォールバック)を追加する前、メモリ集約の失敗率は単一のモデルで約15%に達していました。目に見えないインフラであるべき機能にとって、これは許容できない数字です。

もしあなたがメモリサブシステムだけでなく、周辺のプロダクト全体を構築しているなら、このページと併せてone-key chatbot guideおよびAI API rate limiting guideも参照してください。メモリの耐久性は、エージェントが実際に使えるアプリケーションの中に存在して初めて意味を持ちます。

他のフレームワークはどう対処しているか(実際にはしていない)

ほとんどのAIエージェントフレームワークは、メモリの集約を単純なLLM呼び出しとして扱います。うまくいけば良し、いかなければメモリは失われます。

Hermes Agentのベースラインでは、会話用と同じモデルを集約にも使用していました。ユーザーが目にすることのないチャットを要約するためだけに、0.003ドルかかり、8秒以上かかる Claude Sonnet の呼び出しを行うのです。その呼び出しが失敗(Rate limit、Timeout、モデルエラー)すると、フレームワークは警告をログに記録して次に進みます。ユーザーのコンテキストは失われます。

もう一つの人気フレームワークである nanobot も、同じアーキテクチャを採用しています。一つのモデル、一回の試行、fallback なし。集約関数にはTimeoutさえ設定されていません。アップストリームが遅い場合(Cloudflareの524エラーはよくあります)、接続が切れるまでセッション全体がブロックされます。

どちらのフレームワークも、集約をメインモデルから分離していません。メモリ操作のためのfallbackロジックもありません。「API呼び出しが失敗した」ことと、「API呼び出しは成功したが、モデルが要求通りに動かなかった」ことを区別することもありません。

これらはエッジケースではありません。単一モデルで15%の失敗率がある場合、1日に100回の集約を行うフレームワークは、そのうち15回でメモリを失います。1週間では、エージェントがすべてを忘れてしまう会話が105回も発生することになります。

問題はリトライロジックよりも深いところにある

明らかな解決策は、指数バックオフ(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エラーやネットワークの瞬断をキャッチします。しかし、2つの失敗モードがこれをすり抜けます:

失敗モード1:モデルが tool calling(ツール呼び出し)を実行できない。一部のモデル、特に高速な推論エンジンで動作する小規模なモデルは、複雑なプロンプトに対して有効な関数呼び出しを生成できないことが時々あります。APIは MidStreamFallbackError に包まれた ServiceUnavailableError を含む200レスポンスを返します。リトライロジックは例外を検知し、同じモデルでリトライしますが、同じエラーが発生します。

失敗モード2:モデルは「成功」するが、ツールを呼び出さない。LLMは完全に有効なレスポンスを返します。HTTP 200。エラーなし。しかし、構造化データを用いて save_memory を呼び出す代わりに、プレーンテキストで要約を書き出します。リトライエンジンはこれを成功と見なします。集約関数はツール呼び出しを確認しますが、見つからないため諦めます。

2番目の失敗モードは非常に厄介です。トランスポート層はすべてがうまくいったと考えますが、ビジネス層はそうではないことを知っています。HTTPレベルのリトライを何度繰り返しても、ツールのスキーマを理解していないモデルを修正することはできません。

2層構造のFallbackアーキテクチャ

私たちは、異なるレベルで動作する2つの独立したfallbackループによってこの問題を解決しました:

ユーザーが /new を送信
    │
    ▼
consolidate() ─── ビジネスレイヤー Fallback
    │               「モデルは save_memory を呼び出したか?」
    │               No → チェーン内の次のモデルを試行
    │
    ▼
_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)                  (信頼性高)        (最終手段)

レイヤー1はトランスポートの失敗を処理します。レイヤー2はビジネスロジックの失敗を処理します。fallbackチェーンは両方のレイヤーで共有され、中央のカタログで一度だけ定義されます。

これは「同じモデルをリトライする」のとは根本的に異なるアプローチです。モデルがツールの呼び出しに失敗した場合、同じプロンプトでリトライしても効果があることは稀です。異なる重みを持ち、異なる 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のモデルを使用しています。3つの隠しモデルは、会話の質よりも速度とコストが重視されるメモリ集約のようなバックグラウンドタスクのためだけに存在します。

このカタログは、すべてのモデルルーティングにおける唯一の真実のソース(Single Source of Truth)です。fallbackチェーンに新しいモデルを追加するには、1行追加するだけです。同期すべき設定ファイルも、更新すべき環境変数も、修正すべきデプロイスクリプトもありません。

トランスポートレイヤー:ループ検知機能付きの連鎖的Fallback

リトライエンジンは、無限ループを防ぐために visited セットを使用してfallbackチェーンを辿ります:

async def _chat_with_retry(self, kwargs, original_model):
    # フェーズ 1: プライマリモデルでの指数バックオフ
    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")

    # フェーズ 2: 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)

        # このモデルに正しいゲートウェイを解決
        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 のようなチェーンで永久にループしてしまいます。これがあることで、エンジンは各モデルを正確に一度ずつ試行します。

ゲートウェイの解決も重要です。モデルによって必要なAPI形式が異なります。Claude モデルは Anthropic 形式のゲートウェイ(/v1 サフィックスなし)を経由します。GPT モデルは OpenAI 互換のゲートウェイ(/v1 あり)を経由します。Groq モデルはまた別のエンドポイントを使用します。fallbackエンジンはチェーン内の各モデルに対して正しいゲートウェイを解決し、Anthropic のリクエストを OpenAI のエンドポイントに送るようなプロトコルの不一致を防ぎます。

これは、ほとんどのフレームワークが完全に無視している詳細です。彼らはすべてのモデルが同じプロトコルを話すと仮定しています。本番環境で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

        # モデルがツールを呼び出さなかった — チェーン内の次を試行
        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、有効なコンテンツ)を返したが、モデルがツールを使用しなかったケースをキャッチします。集約関数は has_tool_calls をチェックし、欠落している場合はチェーン内の次のモデルに移動します。

Timeoutラッパー(asyncio.wait_for)もfallbackをトリガーします。モデルの応答に30秒以上かかる場合(遅いアップストリームでのCloudflare 524エラーで一般的)、関数は TimeoutError をキャッチし、ユーザーのセッションを無期限にブロックする代わりに次のモデルを試行します。

なぜ集約に Groq を使うのか

メモリの集約はバックグラウンドタスクです。ユーザーはその出力を目にしません。ただ、機能すればいいのです。そのため、高速で安価なモデルにとって最適な候補となります。

ほとんどのフレームワークは、すべてに同じ高価なモデルを使用します。会話に Claude Sonnet を使っているなら、メモリ集約にも Claude Sonnet を使います。人間が読むことのない出力を生成するタスクのために、入力 token 100万個あたり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)は、60メッセージのセッションを約5秒で集約します。以前のデフォルト(gpt-4.1-mini)は8秒以上かかっていました。集約あたりのコストは約0.003ドルから約0.001ドルに低下しました。

トレードオフとして、Groq モデルは複雑なプロンプトに対する tool calling の信頼性が低くなります。だからこそ、2層のfallbackが存在するのです。llama-3.3-70b がツールの呼び出しに失敗すると、qwen3-32b が引き継ぎます。それも失敗すれば、llama-4-scout が試みます。3つの Groq モデルがすべて失敗した場合、ほぼ100%の tool calling 信頼性を持つ gpt-4.1-mini が処理します。

本番環境では、プライマリモデルが約85%の確率で成功します。チェーンが gpt-4.1-mini まで到達するのは集約の2%未満です。最終的な失敗率は、実質的にゼロです。

本番環境での結果

これを2つの Hermes Agent ランタイムインスタンスにデプロイし、実際の 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 はツールを呼び出さずにテキストを返しました。単一層のfallbackではこれを処理できませんでした。これは、他のすべてのフレームワークが黙ってメモリを失うのと全く同じシナリオです。

2番目のデプロイ(2層構造の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

4つのモデルが試行され、メモリが保存されました。ユーザーには「New session started.」と表示され、裏で何が起こったのか知る由もありません。

アーキテクチャのギャップ

私たちのメモリシステムと代替案の機能比較:

機能 一般的なAIエージェントフレームワーク 2層構造のFallbackデザイン
集約モデル 会話用と同じ(高価、低速) 独立したモデルチェーン、Groq加速
失敗処理 警告をログ出力、メモリ消失 2層構造Fallback、5段階の深さ
トランスポートFallback 同じモデルを3回リトライ 異なるモデルにわたる連鎖的Fallback
ビジネスロジックFallback なし ツール呼び出し検証 + モデル切り替え
Timeout保護 なし(Cloudflare 524がセッションをブロック) asyncio.wait_for(timeout=30) + Fallback
セッション切り詰め なし(コンテキストが永遠に肥大化) 集約後に古いメッセージを切り詰め
履歴検索 なし HISTORY.md ローリングウィンドウ、grep検索可能
内部モデル サポートなし システム専用モデル用の hidden=True
ループ防止 不要(チェーンがないため) visited セットによる A→B→A ループ防止
ゲートウェイ解決 単一のAPI形式を想定 プロトコル検知機能付きモデル別ゲートウェイ

この表の各行は、私たちが自ら経験した、あるいは他のフレームワークのイシュートラッカーで観察した本番環境での失敗を表しています。2層構造のfallback、非表示のモデルカタログ、モデルごとのゲートウェイ解決、Timeoutトリガーのfallback。これらは、私たちが調査した nanobot やその他のオープンソースエージェントフレームワークには存在しません。

学んだこと

「リクエストが成功した」は「タスクが成功した」と同義ではありません。汎用的なリトライエンジンはHTTPレベルで動作します。200レスポンスと有効なJSONが返ってきても、モデルが求めていたツールを使わなかったために実際には失敗である、ということを知ることはできません。ビジネス上重要な操作には、独自の成功基準と独自のfallbackロジックが必要です。

小規模なモデルは、大規模なモデルとは異なる失敗の仕方をします。大規模モデル(GPT-4.1、Claude Sonnet)は、要求されればほぼ確実にツールを呼び出します。高速な推論エンジン上の小規模モデルは、ツールのスキーマを完全に無視した、一見有効そうなレスポンスを生成することがあります。これはプロンプトエンジニアリングで修正できるバグではなく、アーキテクチャによる緩和が必要な能力のギャップです。

合成データではなく、本番データでテストしてください。6つの合成メッセージを使った初期テストは、すべてのモデルでパスしました。ツール呼び出しの履歴、タイムスタンプ、混合言語を含む実際の60メッセージのセッションは、3つの Groq モデルのうち2つで失敗しました。実際のデータの複雑さは、きれいなテストデータでは決して現れない失敗モードを露呈させます。

これが、AI API rate limiting guide が重要である理由でもあります。メモリシステムに必要なのは、単なる「より良いモデル」ではありません。トランスポートポリシー、ビジネスロジックの成功チェック、そしてプロバイダーの通常の障害で崩壊しないfallbackの梯子が必要なのです。


この記事では、Hermes Agent の信頼性パターンについて説明しました。ここにあるアーキテクチャパターンを、メモリの耐久性、fallback設計、およびゲートウェイ対応のモデルルーティングの参考にしてください。

一つの API key で300以上のAIモデルが必要ですか? tokenlab.sh は、OpenAI、Anthropic、Google、DeepSeek、Groq などへの統合アクセスを提供します。

共有: