7月24日、Iván Palomares Carrascosaが「Stateful vs. Stateless Agent Design: Tradeoffs for Scalable Agentic Systems」と題した記事を公開した。LLMベースのエージェントをプロダクション環境に展開する際、ロードバランサーの設定より先に答えるべき根本的な問いがある。「エージェントの記憶はどこに置くか」だ。この設計判断は、コードレベルの実装にとどまらず、スケーリング戦略全体に影響を及ぼす。
そして、この問いが単純ではない理由がある。ステートフル設計を選んだ瞬間に、「局所的な健忘症(localized amnesia)」と呼ばれるスケーリング固有の障害が生まれるからだ。この問題とそのトレードオフこそが、本記事の最も差別化された論点である。記事では、Groq API経由でLlama 3.1 8B Instantを使った動作可能なコードサンプルを交えながら、2つのパラダイムを詳しく解説している。
ステートレスエージェント:スケールしやすいが、クライアントに負担が集中する
ステートレスエージェントは、各リクエストを完全に独立したものとして処理する。会話が終われば、すべての文脈は忘れ去られる。
def stateless_agent(prompt: str, provided_history: list = None) -> str:
messages = [{"role": "system", "content": "You are a helpful, concise assistant."}]
if provided_history:
messages.extend(provided_history)
messages.append({"role": "user", "content": prompt})
response = client.chat.completions.create(
model=MODEL_ID,
messages=messages,
max_tokens=100
)
return response.choices[0].message.content.strip()
バックエンドにユーザーの記憶を保持しないため、水平スケーリングが容易だ。新規リクエストはどのインスタンスにも振り分けられる。セッション固定(スティッキーセッション)が不要なため、ロードバランサーの設定もシンプルに保てる。
代わりに、マルチターン会話の文脈維持はフロントエンドの責務になる。クライアントは毎回のリクエストに会話履歴全体を付け直さなければならない。これはトークン消費がスノーボール式に増大するという問題を生む。会話ターン数が増えるほど、1リクエストあたりのコストと遅延が線形以上に膨らんでいく。
実際の挙動を確認すると、その制約は明快だ:
--- Turn 2 (Without Client Context) ---
Agent: Unfortunately, I don't have any information about you, including your name.
--- Turn 2 (With Client Context) ---
Agent: Your name is Alice, and you are learning about API infrastructure.
クライアントが履歴を送らなければ、直前の発言すら覚えていない。逆に、フロントエンドがprovided_historyとして履歴を渡せば正確に答えられる。文脈の責任が完全にクライアント側に委譲されていることが、このシンプルな例からも読み取れる。
ステートフルエージェント:クライアントはシンプルになるが、インフラが複雑化する
ステートフルエージェントは記憶の管理を自分で行う。クライアントは最新のプロンプトとセッションIDを送るだけでよく、エージェントがデータベースから過去の会話を取得・更新する。
def stateful_agent(session_id: str, new_prompt: str) -> str:
# 1. DBから既存の会話履歴を取得
cursor.execute("SELECT history FROM agent_memory WHERE session_id=?", (session_id,))
row = cursor.fetchone()
if row:
conversation_history = json.loads(row[0])
else:
conversation_history = [{"role": "system", "content": "You are a helpful, concise assistant."}]
# 2. 新しいプロンプトを追加
conversation_history.append({"role": "user", "content": new_prompt})
# 3. LLM呼び出し
response = client.chat.completions.create(
model=MODEL_ID, messages=conversation_history, max_tokens=100
).choices[0].message.content.strip()
# 4. アシスタントの返答を追記してDBに保存
conversation_history.append({"role": "assistant", "content": response})
cursor.execute('''
INSERT INTO agent_memory (session_id, history)
VALUES (?, ?)
ON CONFLICT(session_id) DO UPDATE SET history=excluded.history
''', (session_id, json.dumps(conversation_history)))
conn.commit()
return response
記事のサンプルコードでは永続化層としてSQLiteのインメモリDBを使っているが、実際のポイントは「エージェント自身がセッションIDをキーにして会話履歴を管理する」という構造にある。
クライアントからはstateful_agent('user_123', 'Hi, I am Bob...')のように呼ぶだけで、2ターン目以降も名前などの文脈が維持される。ツール呼び出しや人間の承認待ちといった非同期ステップを挟む複雑なワークフローでも、セッション単位で状態を保持できるため、処理の中断・再開が自然に扱える点が大きな利点だ。
スケーリング時の落とし穴:「局所的な健忘症」
ステートフル設計の最大の課題が、水平スケーリング時に発生する「局所的な健忘症(localized amnesia)」だ。セッションの会話履歴が特定のインスタンスのローカルストレージにしか存在しない場合、ロードバランサーが別のインスタンスにリクエストを振り向けた瞬間に文脈が失われる。エージェントは突然、直前の会話を知らない状態でユーザーに応答することになる。
これを防ぐには、Redisなどの中央集権型メモリキャッシュをアーキテクチャに組み込み、どのインスタンスからも同一のセッション状態を参照・更新できる構成にする必要がある。これはインフラコストと運用複雑性の増大を意味する。ステートフル設計の「クライアントがシンプルになる」という利便性は、バックエンド側のこのコストと常にセットで評価しなければならない。
設計判断の軸:どちらを選ぶか
| ステートレス | ステートフル | |
|---|---|---|
| 水平スケーリング | 容易 | 複雑(DB/キャッシュ層が必要) |
| クライアント負担 | 大(履歴全送信) | 小(IDと最新プロンプトのみ) |
| トークン消費 | ターン増加で膨張 | DBで管理するため制御しやすい |
| 非同期ワークフロー | 困難 | 適している |
ステートレスはシンプルなスケーリングが最優先の場合に向いており、ステートフルはツール呼び出しや人間の承認待ちを含む複雑なワークフローに適している、と記事は整理している。どちらが「正解」かはユースケース次第であり、この問いに向き合わないまま実装を進めると、後からアーキテクチャを大きく変更することになりかねない。
初期セットアップにはpip install groqが必要で、使用モデルはllama-3.1-8b-instantだ。Groq APIキーの取得および最新の利用制限についてはGroq公式ドキュメントを参照されたい。
詳細はStateful vs. Stateless Agent Design: Tradeoffs for Scalable Agentic Systemsを参照していただきたい。