← Claudeエージェント実装:深掘り編
LESSON 03 / 06

メモリと状態管理:長期実行エージェント

所要時間 14分 上級レベル

エージェントの実装で最も難しい問題が メモリ管理コンテキストウィンドウは無限ではないし、コストとレイテンシは履歴量に比例します。

メモリの3階層設計

階層 用途 保存場所 持続時間
1. 直近コンテキスト 現在の対話・実行中のタスク messages配列 セッション中
2. セッション要約 長くなった対話のサマリ サマリテキスト セッション終了まで
3. 永続メモリ ユーザー嗜好・繰り返し参照する事実 外部DB 永続

直近コンテキストの管理

# 履歴を「最新N件」で打ち切るシンプル実装

def trim_history(messages, max_tokens=50000):
    # システムプロンプトは保持
    system = messages[0]
    history = messages[1:]

    # トークン数で計算(簡易)
    while estimate_tokens(history) > max_tokens:
        # 最古を削る
        history.pop(0)

    return [system] + history

セッション要約による圧縮

履歴が長くなったら、古い部分をサマリに圧縮 します。

# 古い履歴を要約して圧縮

def compress_history(messages, threshold=80000):
    if estimate_tokens(messages) < threshold:
        return messages

    # 古い履歴(先頭N件)を抜き出す
    old = messages[1:30]  # 最初の30件
    recent = messages[30:]

    # サマリ生成
    summary = call_claude(
        system="次の対話履歴を要点だけ簡潔にまとめてください。",
        messages=old,
    )

    # サマリで置換
    return [
        messages[0],  # システムプロンプト
        {"role": "user", "content": f"【これまでの経緯のサマリ】n{summary}"},
        {"role": "assistant", "content": "了解しました。続きを処理します。"},
    ] + recent

サマリの注意点

  • 重要事実は省略しない:ユーザーIDやエラーコードは要約しても残す
  • 未解決のタスクは強調:「Aは未完了、Bは完了」と明確に
  • ツール呼び出し結果の数値:要約で消えやすい、再現可能なように残す

永続メモリの設計

セッションを跨いで覚えたい情報は 外部DB に保存します。

# メモリ・ストアの実装例(Postgres + JSON)

CREATE TABLE agent_memory (
    user_id TEXT,
    key TEXT,
    value JSONB,
    created_at TIMESTAMP,
    last_accessed_at TIMESTAMP,
    access_count INTEGER,
    importance_score FLOAT
);

# メモリ書き込みツール
{
  "name": "remember",
  "description": "ユーザーに関する事実を記憶する",
  "input_schema": {
    "type": "object",
    "properties": {
      "key": {"type": "string", "description": "メモリのキー"},
      "value": {"type": "string", "description": "記憶する内容"},
      "importance": {"type": "number", "description": "重要度 0.0〜1.0"}
    }
  }
}

# メモリ読み込みツール
{
  "name": "recall",
  "description": "記憶した事実を呼び出す",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {"type": "string", "description": "検索クエリ"},
      "top_k": {"type": "integer", "description": "取得件数", "default": 5}
    }
  }
}

セマンティック検索によるメモリ呼び出し

メモリ件数が増えたら、キーマッチではなく 埋め込みベース検索

# 埋め込み + ベクトルDB での実装

# 書き込み時:
embedding = create_embedding(value)
store_vector(user_id, key, value, embedding)

# 読み込み時:
query_embedding = create_embedding(query)
results = vector_db.search(user_id, query_embedding, top_k=5)
return [r.value for r in results]

メモリの忘却戦略

すべてを永続化するとコストとプライバシーの問題が出ます。

# 自動忘却ルール

1. 時間ベース:30日アクセスがないメモリは削除
2. 重要度ベース:importance < 0.3 のメモリは7日後に削除
3. ユーザー要請:「忘れて」と言われたら即削除
4. プライバシー:機密情報フラグ付きメモリは2時間で自動削除

# 実装
DELETE FROM agent_memory
WHERE last_accessed_at < NOW() - INTERVAL '30 days'
   OR (importance_score < 0.3 AND created_at < NOW() - INTERVAL '7 days')

状態管理のアーキテクチャ

大規模エージェントでは、状態管理を 独立したコンポーネント にします。

class AgentState:
    def __init__(self, user_id, session_id):
        self.user_id = user_id
        self.session_id = session_id
        self.short_term = []  # messages配列
        self.summary = ""     # 圧縮されたサマリ
        self.facts = {}       # このセッション中に確定した事実
        self.pending_actions = []  # 実行待ちアクション
        self.completed_tasks = []  # 完了したタスク

    def to_messages(self):
        # APIに渡す形式に変換
        ...

    def update(self, message, tool_result):
        # 状態を進める
        ...

    def persist(self):
        # DBに永続化
        ...

    @classmethod
    def restore(cls, user_id, session_id):
        # DBから復元
        ...

多ユーザー対応の注意

注意点 対策
メモリの混線 必ず user_id でスコープ
セッション識別 session_id を発行・管理
同時実行 楽観ロック(version)またはRedis lock
レート制御 ユーザー単位のスロットリング

このレッスンのまとめ

「直近コンテキスト → セッション要約 → 永続メモリ」の3階層と、忘却戦略・状態管理の仕組みでエージェントは長期実行に耐えます。次のレッスンでは、プランニングと自己修正のパターンを学びます。

よくある質問

この記事に関連する質問と答えをまとめました。

Q.メモリの3階層とは何ですか?
A.
直近コンテキスト(messages 配列)、セッション要約(圧縮されたサマリ)、永続メモリ(外部DB)の3階層です。各階層の役割を分離することで、長時間動作するエージェントが実装できます。
Q.メモリの忘却戦略の設計は?
A.
時間ベース(30日アクセスなしで削除)、重要度ベース、ユーザー要請、プライバシー区分の4軸で自動忘却ルールを設定するのが定石です。