設定

言語

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

L
LemonData
·2026年3月5日·753 回表示
なぜあなたの AI Agent は Memory を失い続けるのか

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

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

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

これは、あなたが思うよりも頻繁に起こります。私たちは LemonClaw のインスタンス全体でこれを追跡しました。記憶の集約は、単一のモデルにおいて約15%の失敗率を記録していました。目に見えないインフラであるべき機能にとって、これは許容できない数字です。

もしあなたが記憶サブシステムだけでなく、それを取り巻くプロダクト全体を構築しているなら、このページを one-key chatbot guideself-hosted LemonClaw guide と併せて読んでください。記憶の耐久性は、エージェントが実際に使えるアプリケーションの中に存在して初めて意味を持ちます。

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

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

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

nanobot という別の人気フレームワークも、同じアーキテクチャです。1つのモデル、1回の試行、フォールバックなし。集約関数にはタイムアウトすらありません。アップストリームが遅い場合(Cloudflare の524エラーはよくあります)、接続が切れるまでセッション全体がブロックされます。

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

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

問題はリトライロジックよりも根深い

明らかな解決策は、指数バックオフを伴うリトライです。私たちもそれを導入していました。これは一時的な 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 エラーやネットワークの瞬断をキャッチします。しかし、2つの失敗モードがすり抜けてしまいます。

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

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

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

2層構造のフォールバックアーキテクチャ

私たちは、異なるレベルで動作する2つの独立したフォールバックループによってこれを解決しました。

ユーザーが /new を送信
    │
    ▼
consolidate() ─── ビジネス層のフォールバック
    │               「モデルは save_memory を呼び出したか?」
    │               No → チェーン内の次のモデルを試行
    │
    ▼
_chat_with_retry() ─── トランスポート層のフォールバック
    │                    HTTP エラー → 指数バックオフ
    │                    すべてのリトライを使い果たした → フォールバックチェーンを辿る
    │
    ▼
MODEL_MAP フォールバックチェーン:
    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 フィールドがあります。

@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 コマンドからは内部モデルを隠しつつ、フォールバックチェーンには参加させることができます。ユーザーには切り替え可能な16個のモデルが見えますが、システムは19個を使用しています。3つの隠しモデルは、会話の質よりも速度とコストが重要な記憶の集約などのバックグラウンドタスク専用に存在します。

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

トランスポート層:サイクル検知機能付きの連鎖的フォールバック

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

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: フォールバックチェーンを辿る
    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 のようなチェーンで永久にループしてしまいます。これがあることで、エンジンは各モデルを正確に1回ずつ試行します。

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

これは、ほとんどのフレームワークが完全に無視している詳細です。彼らはすべてのモデルが同じプロトコルを話すと仮定しています。4つの異なる API 形式にまたがる19のモデルを運用する本番環境では、その仮定はすぐに崩壊します。

ビジネス層:ツール呼び出しの検証

集約関数は、その上に独自のフォールバックループを追加します。

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  # これ以上のフォールバックなし

    return False

これは、_chat_with_retry が成功したレスポンス(HTTP 200、有効なコンテンツ)を返したが、モデルがツールを使用しなかったケースを捕捉します。集約関数は has_tool_calls をチェックし、見当たらない場合はチェーン内の次のモデルに移動します。

タイムアウトのラッパー(asyncio.wait_for)もフォールバックのトリガーとなります。モデルが30秒以上かかる場合(遅いアップストリームでの Cloudflare 524 エラーでよく見られます)、関数は TimeoutError をキャッチし、ユーザーのセッションを無期限にブロックする代わりに次のモデルを試します。

なぜ集約に Groq を使うのか

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

ほとんどのフレームワークは、すべてに同じ高価なモデルを使用します。会話に Claude Sonnet を使っているなら、記憶の集約にも Claude Sonnet を使います。人間が読むことのない出力を生成するタスクのために、入力トークン 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 モデルは複雑なプロンプトに対するツール呼び出しの信頼性が低くなります。だからこそ、2層のフォールバックが存在するのです。llama-3.3-70b がツール呼び出しに失敗すると、qwen3-32b が引き継ぎます。それも失敗すれば llama-4-scout が試みます。3つの Groq モデルすべてが失敗した場合、ツール呼び出しの信頼性がほぼ100%の gpt-4.1-mini が処理します。

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

本番環境での結果

これを2つの LemonClaw インスタンスにデプロイし、実際の Telegram の会話でテストしました。

最初のデプロイ(単層フォールバックのみ):

記憶の集約 (archive_all): 56メッセージ
llama-3.3-70b-versatile → 「関数の呼び出しに失敗しました」
フォールバック中 → qwen3-32b
qwen3-32b: LLM が save_memory を呼び出しませんでした。スキップします。
→ 「記憶のアーカイブに失敗しました。セッションはクリアされません。」

トランスポート層は最初の失敗をキャッチしてフォールバックしました。しかし、qwen3-32b はツールを呼び出さずにテキストを返しました。単層フォールバックではこれに対処できませんでした。これは、他のすべてのフレームワークが黙って記憶を失うのと全く同じシナリオです。

2度目のデプロイ(2層フォールバック):

記憶の集約 (archive_all): 60メッセージ
model=llama-3.3-70b-versatile → 成功
記憶の集約完了:残り60メッセージ

同じモデル、同じメッセージ量。今回は1回目の試行で成功しました。ツール呼び出しの失敗が断続的であることこそが、単一のバックアップモデルではなくフォールバックチェーンが必要な理由です。

プライマリモデルが失敗した場合、チェーンがそれを捕捉します:

llama-3.3-70b → ツール呼び出し失敗
→ consolidate() フォールバック → qwen3-32b
→ qwen3-32b ツールを呼び出さなかった
→ consolidate() フォールバック → llama-4-scout
→ llama-4-scout ツールを呼び出さなかった
→ consolidate() フォールバック → gpt-4.1-mini
→ gpt-4.1-mini save_memory を呼び出した ✓
記憶の集約完了

4つのモデルを試し、記憶は保存されました。ユーザーには「新しいセッションが開始されました。」と表示され、裏で何が起こったのか知る由もありません。

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

LemonClaw の記憶システムと代替案の機能比較:

機能 一般的なAIエージェントフレームワーク LemonClaw
集約モデル 会話と同じ(高価、低速) 独立したモデルチェーン、Groq による加速
失敗への対処 警告をログに記録、記憶を失う 2層フォールバック、最大5モデル
トランスポート層のフォールバック 同じモデルを3回リトライ 異なるモデル間での連鎖的フォールバック
ビジネス層のフォールバック なし ツール呼び出しの検証 + モデル切り替え
タイムアウト保護 なし(Cloudflare 524 がセッションをブロック) asyncio.wait_for(timeout=30) + フォールバック
セッションの切り詰め なし(コンテキストが無限に増大) 集約後に古いメッセージを切り詰め
履歴検索 なし HISTORY.md ローリングウィンドウ、grep 検索可能
内部モデル 非対応 システム専用モデルのための hidden=True
ループ防止 不要(チェーンがないため) visited セットによる A→B→A ループの防止
ゲートウェイの解決 単一の API 形式を想定 プロトコル検知機能を備えたモデルごとのゲートウェイ

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

学んだこと

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

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

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

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


LemonClaw は、マルチモデルルーティング、永続記憶、10以上のチャットプラットフォーム統合を内蔵したオープンソースのAIエージェントフレームワークです。ここで説明した2層フォールバックシステム全体が、オープンソース版に搭載されています。ご自身のサーバーで実行してください:github.com/hedging8563/lemonclaw

1つの API キーで300以上のAIモデルが必要ですか? lemondata.cc は、OpenAI、Anthropic、Google、DeepSeek、Groq などへの統合されたアクセスを提供します。

Share: