AIエージェント入門

AIエージェントのステート管理|Redux的パターン vs イベントソーシング

AIエージェントのステート管理|Redux的パターン vs イベントソーシング

この記事の結論

長時間稼働AIエージェントの状態設計で迷っていませんか?Redux的集中管理とイベントソーシングのメリデメをLangGraphのコード例で徹底比較。監査証跡・障害回復・並行エージェント対応まで解説します。

AIエージェントが長時間稼働するようになって、改めて問われている問題がある。「状態をどこに持つか」だ。

単発の処理ならステートレスで十分だった。しかし人間の代わりに何時間も、何日も動き続けるエージェントでは、文脈の維持・障害からの回復・並行エージェント間の同期が全て「状態設計」にかかってくる。フロントエンドで長年議論されてきたRedux的なアプローチと、近年注目が集まるイベントソーシング——両者の実像を、実際のAIエージェント実装コードとともに検証する。

AIエージェントのステート管理に正解はない。ただし、選択を誤るとデバッグ地獄と障害が待っている。この記事では、具体的なトレードオフをコードで示しながら「どのケースでどちらを選ぶか」を明確にする。

スペック比較:Redux的集中管理 vs イベントソーシング

比較軸 Redux的集中管理(集中ストア) イベントソーシング(イベントログ)
状態の形 現在のスナップショット(単一) イベントの累積ログ(追記型)
デバッグ タイムトラベルデバッグが容易 イベントリプレイで完全再現可能
書き込み 上書き(ミューテーション) 追記のみ(イミュータブル)
スケーラビリティ 書き込みボトルネック発生リスク 高スループット(追記のみ)
監査ログ 別途実装が必要 ログがそのまま監査証跡
コンプレキシティ 低〜中(フロントエンド技術転用) 高(CQRS、プロジェクション設計が必要)
長期実行向き ストア肥大化・一貫性維持が課題 非常に高い(設計次第)
フレームワーク対応 LangGraph, CrewAI, OpenAI Agents SDK Apache Kafka, EventStoreDB + LangGraph

Redux的集中管理で比較する

LangGraphはRedux的な思想に最も近い実装を提供している。グラフの各ノードが「ディスパッチャー」として動作し、共有ステートを更新する。フロントエンドエンジニアなら「useReducer + Context」に近いと考えると理解しやすい。

以下は、LangGraphでカスタマーサポートエージェントのステートを設計した例だ。


# 動作環境: Python 3.11+, langgraph>=0.2.0, langchain-openai>=0.1.0
# pip install langgraph langchain-openai

from typing import TypedDict, Annotated, Sequence
from langgraph.graph import StateGraph, END
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
import operator

# ステート定義(Reduxのstoreに相当)
class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]  # 追記式
    current_intent: str       # 分類されたユーザー意図
    resolved: bool            # 解決済みフラグ
    escalation_count: int     # エスカレーション回数
    session_id: str           # セッション識別子

def classify_intent(state: AgentState) -> dict:
    """意図分類ノード — stateを受け取り差分dictを返す(Reducerに相当)"""
    last_message = state["messages"][-1].content
    # 実際はLLM呼び出し
    intent = "billing" if "料金" in last_message else "technical"
    return {"current_intent": intent}  # ストアのbilling sliceのみ更新

def handle_billing(state: AgentState) -> dict:
    """料金対応ノード"""
    response = AIMessage(content="料金についてお調べします。しばらくお待ちください。")
    return {"messages": [response]}

def should_escalate(state: AgentState) -> str:
    """エッジ関数 — 条件分岐(Reduxのmiddlewareに相当)"""
    if state["escalation_count"] >= 2:
        return "human_handoff"
    return "respond"

# グラフ構築(Reduxのstore configに相当)
builder = StateGraph(AgentState)
builder.add_node("classify", classify_intent)
builder.add_node("billing", handle_billing)
builder.add_conditional_edges("classify", should_escalate, {
    "respond": "billing",
    "human_handoff": END
})
graph = builder.compile()

# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。

ポイントは各ノードが「差分dictを返す」設計だ。stateオブジェクトを直接ミューテーションしない。LangGraphはこの差分をReducerのように適用してnew stateを生成する。Reduxを知っていれば、自然に理解できる。

イベントソーシングで比較する

イベントソーシングの本質は「状態(what)を保存するのではなく、起きたこと(why/how)を保存する」発想だ。状態は「イベントの累積ログを再生した結果」として導出される。

AIエージェントにイベントソーシングを適用すると、こんな設計になる。


# 動作環境: Python 3.11+
# pip install langgraph aiofiles

import asyncio
import json
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Literal

# イベント定義(不変の事実を表す)
@dataclass
class AgentEvent:
    event_id: str
    event_type: Literal[
        "SESSION_STARTED",
        "USER_MESSAGE_RECEIVED",
        "INTENT_CLASSIFIED",
        "TOOL_CALLED",
        "TOOL_RESULT_RECEIVED",
        "RESPONSE_GENERATED",
        "SESSION_ENDED"
    ]
    payload: dict
    timestamp: str
    agent_id: str

class EventStore:
    """イベントを追記専用ログに保存(絶対に上書きしない)"""

    def __init__(self, storage_path: str):
        self.path = Path(storage_path)
        self.path.mkdir(parents=True, exist_ok=True)

    async def append(self, session_id: str, event: AgentEvent) -> None:
        log_file = self.path / f"{session_id}.jsonl"
        async with aiofiles.open(log_file, "a") as f:
            await f.write(json.dumps(asdict(event)) + "n")

    async def replay(self, session_id: str) -> dict:
        """イベントをリプレイして現在の状態を導出(プロジェクション)"""
        log_file = self.path / f"{session_id}.jsonl"
        state = {"messages": [], "current_intent": None, "tool_calls": []}

        async with aiofiles.open(log_file, "r") as f:
            async for line in f:
                event = AgentEvent(**json.loads(line))
                # 各イベントタイプに応じてstateを更新(純粋関数)
                if event.event_type == "USER_MESSAGE_RECEIVED":
                    state["messages"].append(event.payload)
                elif event.event_type == "INTENT_CLASSIFIED":
                    state["current_intent"] = event.payload["intent"]
                elif event.event_type == "TOOL_CALLED":
                    state["tool_calls"].append(event.payload)

        return state

# 使用例
store = EventStore("/var/agent_events")
event = AgentEvent(
    event_id="evt_01j...",
    event_type="USER_MESSAGE_RECEIVED",
    payload={"content": "請求書の確認をお願いします"},
    timestamp=datetime.now(timezone.utc).isoformat(),
    agent_id="cs-agent-001"
)
asyncio.run(store.append("session_abc123", event))

# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。

筆者のおすすめ

正直、「どちらが正解」という話ではない。ユースケースと運用体制で選ぶべきだ。

Redux的集中管理を選ぶべき場合: チームにフロントエンドエンジニアが多い、LangGraph / OpenAI Agents SDKをそのまま使いたい、1セッション数時間程度の中規模エージェント、デバッグ速度を最優先したい——こういった状況では集中管理が圧倒的にやりやすい。LangGraphのチェックポイント機能(AsyncPostgresSaver)と組み合わせれば、pause/resumeも「タイムトラベルデバッグ」も標準機能で賄える。

イベントソーシングを選ぶべき場合: 金融・医療など完全な監査証跡が必要、週単位・月単位で動く長時間エージェント、複数エージェントが並行して同一ドメインを操作する、LLMの意思決定プロセスを事後に完全再現したい——こうした要件があるなら、イベントソーシングの初期コストを払う価値がある。全てのエージェント行動がイベントログに残るため、コンプライアンス対応が格段に楽になる。

ハイブリッドも現実的な選択肢だ。LangGraphをロジック実行エンジンとして使いながら、外部のApache KafkaやEventStoreDBにドメインイベントを書き出す構成は、実運用で増えている。コンプレキシティは上がるが、それぞれの強みを引き出せる。

【要注意】よくある失敗パターンと回避策

失敗1: 全てをメモリ内Dictで管理する

state = {"messages": [], "context": {}} をプロセス内Dictで持つ

⭕ LangGraphのチェックポインター(PostgresSaver/RedisSaver)で外部に永続化する

なぜ重要か: エージェントプロセスが落ちた瞬間、セッション状態が全消失する。長時間エージェントでこれは致命的だ。

失敗2: ミュータブルなステートオブジェクトを複数ノードで共有する

state["messages"].append(msg) をノード内で直接実行する

⭕ 各ノードは差分dictを返し、フレームワークに更新を委ねる

なぜ重要か: 並行実行時に競合状態(race condition)が発生し、メッセージの欠落や重複が起きる。LangGraphがこのパターンを強制しているのには理由がある。

失敗3: イベントログを「ステートのバックアップ」として使う

❌ 定期的にステートをシリアライズしてイベントログに書き込む

⭕ 「何が起きたか(意図・行動・結果)」をドメインイベントとして記録する

なぜ重要か: ステートのスナップショットをイベントログに入れてしまうと、イベントソーシングのメリット(リプレイ・監査・分析)が失われる。イベントは「UserMessageReceived」「ToolCalled」といったビジネス上の事実であるべきだ。

失敗4: 永遠に増え続けるイベントログの設計をしない

❌ 1年間のイベントを1ファイルに蓄積し続ける

⭕ スナップショットを定期的に作成し、古いイベントをアーカイブする

なぜ重要か: 1000万件のイベントをリプレイしてカレントステートを導出しようとすると、起動時間が分単位になる。スナップショット+差分イベントの組み合わせが実用的だ。

参考・出典

AIエージェントのアーキテクチャについては、AIエージェント構築完全ガイドで基本から体系的にまとめています。状態設計が決まったら次はAIエージェント構築ツール徹底比較でフレームワーク選定に進んでください。

AIエージェントの設計・導入に関するご相談は 株式会社Uravation(uravation.com) までお気軽にどうぞ。


この記事はAIgent Lab編集部がお届けしました。

Need help moving from reading to rollout?

この記事を読んで導入イメージが固まってきた方へ

Uravationでは、AIエージェントの要件整理、PoC設計、社内導入、研修まで一気通貫で支援しています。

この記事をシェア

X Facebook LINE

※ 本記事の情報は2026年4月時点のものです。サービスの料金・仕様は変更される可能性があります。最新情報は各サービスの公式サイトをご確認ください。

関連記事