設定

言語

なぜあなたのAI Agentは記憶を失い続けるのか(そして、いかにして私たちがそれを解決したか)

L
LemonData
·2026年2月28日·21 回表示
#AIエージェント#メモリ#フォールバック#アーキテクチャ#LemonClaw
なぜあなたのAI Agentは記憶を失い続けるのか(そして、いかにして私たちがそれを解決したか)

AIエージェントが記憶を失い続ける理由(とその解決策)

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

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

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

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

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

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

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

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

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

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

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

明らかな解決策は、指数バックオフを伴うリトライです。私たちはそれを導入していました。一時的な HTTP エラーにはこれで十分対応できます:

# リトライループ: 1秒 → 2秒 → 4秒のバックオフ
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 を呼び出したか?」
    │               いいえ → チェーン内の次のモデルを試行
    │
    ▼
_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つの隠しモデルは、会話の質よりも速度とコストが重要となる、記憶の集約のようなバックグラウンドタスクのためだけに存在しています。

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

トランスポート層:サイクル検知機能付きチェーンフォールバック

リトライエンジンは、無限ループを防ぐために訪問済みセット(visited set)を使用してフォールバックチェーンを巡回します:

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

ゲートウェイの解決も重要です。モデルによって必要な 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 を使っています。それは、人間が読むことのない出力を生成するタスクに対して、入力トークン 1M あたり 3ドルを支払い、1回の集約に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秒以上かかっていました。1回の集約あたりのコストは約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 の会話でテストしました。

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

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."

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

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

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

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

プライマリモデルが失敗したとき、チェーンがそれを救い出します:

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つのモデルを試し、記憶は保存されました。ユーザーには「新規セッションが開始されました」と表示されるだけで、その裏で何が起きたかを知る由もありません。

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

LemonClaw の記憶システムと、他の選択肢との機能比較:

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

この表のすべての行は、私たちが自ら経験したか、あるいは他のフレームワークの課題追跡(Issue tracker)で観察した本番環境での失敗を表しています。2層フォールバック、非表示のモデルカタログ、モデルごとのゲートウェイ解決、タイムアウトによるフォールバック:これらは OpenClaw、nanobot、あるいは私たちが調査した他のオープンソースエージェントフレームワークのどれにも存在しません。

学んだこと

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

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

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


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

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

Share: