AIエージェントの「ダムコア、スマートエッジ」アーキテクチャ
多くのプロダクションのエージェントシステムは、テストや置き換えが難しい中央に知能が集中して失敗しています。この記事では「ダムコア、スマートエッジ」の原則を紹介します。オーケストレーターはステートレスな状態機械であり、ドメイン知能は交換可能なスペシャリストノードに配置されます。この設計により、テスト容易性、コスト効率、置き換え可能性が向上し、結合が減少し、因果工学による科学的な改善が可能になります。
多くのエージェントシステムがプロダクションで失敗する理由は、知能が中央に集中し、テスト、置き換え、推論が困難になることにあります。オーケストレーターはルーティング、ドメイン知識の保持、メモリ管理、ツール選択、出力整形を同時に行いすぎています。何かが壊れたとき、問題を切り分けられません。要件が変わったとき、正確に更新できません。システム全体を動かさなければなりません。
これはモデルの品質問題ではありません。これらのチームは劣ったLLMを使っているわけではなく、アーキテクチャ上の誤りに直面しているのです。この誤りは非常に一般的で、命名する価値があり、明確な対抗原則があります。
その原則は「ダムコア、スマートエッジ」です。作業を順序付ける中央ノードであるオーケストレーターは、ほぼステートレスで、制御フローのみをエンコードすべきです。知能は周辺部、つまりドメイン境界を持つスペシャリストノードに属し、それらは独立して推論、テスト、置き換えが可能です。これは単なるスタイル上の好みではありません。システムが独立して進化する必要のある複数のドメイン、ツール、責任を持つ場合に有用なトポロジーです。
スマートコアが失敗する理由
知能を集中化する衝動は理解できます。強力なモデルが一つあり、一つのシステムプロンプトを書き、すべてのツールを与えて解決させます。デモではうまくいきます。モデルは複数の関心事を同時に扱うことができ、成功パスはきれいに見えます。
しかしプロダクションでは、次の4つの方法でこれを破ります。
第一に、絡まり合い。オーケストレーターがルーティングロジックとドメイン知識の両方をエンコードすると、一方を変更するともう一方に影響します。新しいツールはプロンプトの書き換えを意味し、新しいドメインルールはルーティングの再テストを意味します。システムはアーキテクチャ上の境界を持たない結合を生み出します——長くて壊れやすい自然言語の命令文字列だけです。
第二に、テスト不可能性と不透明性。五つのことを同時に行うLLMプロンプトのユニットテストは書けません。エンドツーエンドの評価を実行し、創発的な動作が安定しているか観察するしかありません。エージェントが誤動作したとき、ルーティング、ドメイン推論、ツール選択、出力整形のどこで失敗したかを知る必要があります。スマートコアはこれらすべてを混同し、回帰はプロダクションまで見えず、その後はブラックボックスを懐中電灯でデバッグすることになります。
第三に、コスト増幅。スマートコアエージェントは、すべてのコンテキストトークン(完全な履歴、すべてのツールスキーマ、すべてのドメインルール)を同じパスで毎ステップ送信します。マルチステップワークフローでは、現在のステップに必要ないかもしれないコンテキストに繰り返し支払います。ダムコアはスペシャリストにルーティングし、スペシャリストは必要なものだけを受け取ります。モデルルーティングの経済規模(数百万回の呼び出し)では、経済性は急速に複利効果を生みます。
原則の定義
構造的周辺化の原則:複数のドメインやツール境界を持つエージェントシステムでは、自律的判断の能力はグラフのエッジに最大限分散されるべきであり、コアは状態遷移とルーティング契約のみを担当します。
これは新しい考えではなく、古いシステム工学がエージェントグラフに現れたものです。Unixパイプが機能するのは、各プログラムが一つのことを行い、シンプルで安定したインターフェースで通信するからです。マイクロサービスが機能するのも(機能する場合)同じ理由です。インターネットのコアプロトコルは意図的にシンプルで、知能はエンドポイントにあります。ダムコア、スマートエッジはこの同じロジックをエージェントグラフに適用します。
オーケストレーターの仕事は正確にスコープされます:型付き入力を受け取り、どのスペシャリストが処理するかを決定し、ディスパッチし、結果を待ち、型付き出力を発行します。ドメイン知識をエンコードせず、永続メモリを保持せず、コンテンツについて判断を下しません。それはステートマシンであり、ステートマシンとして読めるべきです。
対照的に、スペシャリストはそのドメイン境界内で自律的です。ResearchSpecialistはソースのクエリ方法、信頼性の評価方法、調査結果の統合方法を知っています。CodeReviewSpecialistはコードベースの慣習、チェックすべき障害モード、出力のフォーマット方法を知っています。それぞれ独立してプロンプト、ファインチューニング、交換、評価が可能です。オーケストレーターは何が変わったかを知る必要がありません。
「ダム」の実際の意味
「ダムコア」は正確な用語であり、軽蔑的ではありません。オーケストレーターは依然として、CLASSIFY_INTENTステップでLLMを使用できます——受信リクエストを型付きインテントに分類することは、言語理解の正当な使用です。しかし、そのインテントの内容について推論したり、ドメインヒューリスティックを適用したり、ドメイン内のエッジケースの処理方法について判断を下したりしてはなりません。
有用なテスト:オーケストレーターのシステムプロンプトからすべてのドメイン固有の語彙を削除し、汎用のプレースホルダーに置き換えても、ルーティングロジックは機能しますか?「はい」なら、コアは適切にダムです。ルーティングが「紛争のある請求を持つ返金リクエスト」の理解に依存している場合、ドメイン知識がコアに漏れており、結合問題があります。
オーケストレーターのシステムプロンプトは、交通管制官のマニュアルのように読めるべきです——フロー、優先順位、障害処理に関するルールであり、貨物についての意見はありません。スペシャリストのプロンプトはエキスパートブリーフのように読めるべきです——深く、意見が強く、狭い範囲です。
これはメモリアーキテクチャも支配します。オーケストレーターからアクセス可能な共有グローバルメモリは悪い兆候です。各スペシャリストはタスクの期間中、自分のワーキングメモリを所有すべきです。永続メモリ——ユーザー設定、以前の会話の要約、学習した事実——はスペシャリストがオンデマンドで取得すべきであり、オーケストレーターのコンテキストにプリロードされるべきではありません。オーケストレーターはセッション識別子を渡し、メモリダンプは渡しません。
置き換え可能性テスト
ダムコア、スマートエッジの実用的な証明は、私が置き換え可能性テストと呼ぶものです。任意のスペシャリストノードを——プロンプトベースの実装を決定論的アルゴリズム、より小さなモデル、または異なるスペシャリスト実装に交換する——際に、オーケストレーターや隣接するスペシャリストを変更せずにできるべきです。爆発半径はそのスペシャリスト境界内にとどまるべきです。
交換に、交換されるノードの外部の変更が必要な場合、システムには隠れた結合があります。最も一般的な原因:オーケストレーターがスペシャリスト出力の内部構造を解析または依存していること。修正は常に同じです——エッジでPydanticスキーマを定義し、出力時に検証し、オーケストレーターに生テキストではなく型を消費させることです。
ここで因果工学が登場します。ノードを交換し、システム動作への孤立した影響を観察できるとき、システムに対する因果的なハンドルを持てます。制御実験を実行できます:同じオーケストレーター、同じ隣接スペシャリスト、異なるスペシャリストB。出力品質のデルタはスペシャリストBのみに帰属できます。これにより、直感や祈りではなく科学的にエージェントシステムを改善できます。
実際の適用
LangGraphでの実装パターンは直接的です。オーケストレーターを型付き状態(ルーティングメタデータ、セッション識別子、結果スロットのみを運ぶTypedDictまたはPydanticモデル)を持つグラフとして定義します。グラフの各ノードは、狭くスコープされた入力を受け取り型付き出力を返すスペシャリスト関数です。エッジは制御フローをエンコードし、ノードは知能をエンコードします。
以下はLangGraph v0.4の最小ダムコアオーケストレーターの例です:
from typing import Literal
from pydantic import BaseModel
from langgraph.graph import StateGraph, END
class OrchestratorState(BaseModel):
user_input: str
intent: str = ""
specialist_result: str = ""
session_id: str = ""
async def classify_intent(state: OrchestratorState) -> dict:
intent = await llm_classify(state.user_input)
return {"intent": intent}
def route_by_intent(state: OrchestratorState) -> Literal["research", "code_review", "fallback"]:
mapping = {"research": "research", "code_review": "code_review"}
return mapping.get(state.intent, "fallback")
async def research_specialist(state: OrchestratorState) -> dict:
result = await run_research_agent(state.user_input, state.session_id)
return {"specialist_result": result.model_dump_json()}
async def code_review_specialist(state: OrchestratorState) -> dict:
result = await run_code_review_agent(state.user_input, state.session_id)
return {"specialist_result": result.model_dump_json()}
graph = StateGraph(OrchestratorState)
graph.add_node("classify", classify_intent)
graph.add_node("research", research_specialist)
graph.add_node("code_review", code_review_specialist)
graph.add_node("fallback", lambda s: {"specialist_result": "I can't help with that."})
graph.set_entry_point("classify")
graph.add_conditional_edges("classify", route_by_intent)
graph.add_edge("research", END)
graph.add_edge("code_review", END)
graph.add_edge("fallback", END)
app = graph.compile()オーケストレーターに含まれていないものに注意してください:ドメイン語彙、ツールスキーマ、メモリ検索、出力整形ロジックはありません。route_by_intent関数は純粋なマッピングです。明日新しいスペシャリスト(例えば、legal_review)を追加する場合、ノードとマッピングのエントリを一つ追加するだけです。既存のスペシャリストは変更されません。
各スペシャリストは型付き出力スキーマでその契約を強制します:
from pydantic import BaseModel, Field
class SpecialistResult(BaseModel):
"""Contract between any specialist and the orchestrator."""
result: str = Field(..., description="The specialist's output")
confidence: float = Field(default=0.0, ge=0.0, le=1.0)このパターンにより、オーケストレーターが生テキストを解析する必要がなくなります。型付きオブジェクトを消費するため、静的解析、検証、明確な責任境界が可能になります。
ダムコア、スマートエッジパターンは万能薬ではありません。ツールが一つだけでステップが少ない単純なエージェントの場合、スマートコアの方がシンプルで十分かもしれません。しかし、システムが複数のドメイン、ツール、独立して発展する必要のある責任を持つようになると、このパターンはテスト容易性、置き換え可能性、コスト効率をもたらし、追加される複雑さをはるかに上回ります。