コンテンツにスキップ

STUDY NOTES

第29回: LangChain v1.0 の create_agent とミドルウェア — エージェント開発の標準 API が確立した

2025 年 10 月にリリースされた LangChain v1.0 で、create_agent と Middleware が正式 API として導入された。第29回は Jupyter Notebook 形式 (create_agent.ipynb) で全14セクションのハンズオンを通じてこの新 API を体系的に学ぶ回。

位置付け: 第11回 (ReAct Agent / LCEL), 第19回〜第28回 (LangGraph + DSPy) と来て、ようやく LangChain 側にもプロダクション grade のエージェント APIが整った。第20回までの「LCEL でちまちま組む」時代と、第29回以降の「create_agent で 1 行」時代の分水嶺

create_agent は LangGraph の create_react_agent置き換える後継 API。LangGraph の表現力を保ちつつ、「ミドルウェア」というレイヤを足して横断的関心事(ロギング、認証、リトライ、要約、レート制限)を分離した。

違い
langgraph.prebuilt.create_react_agent langchain.agents.create_agent 同じ MessagesState 互換 + middleware パラメータ追加
自前 StateGraph + before_node / after_node @before_model / @after_model / wrap_model_call デコレータ LangGraph 内部にフック点が標準化
自前 system_prompt 切替 @dynamic_prompt ミドルウェア プロンプト動的生成が公式パターン
自前 SQLite checkpointer + thread_id 同じく checkpointer=InMemorySaver() API は変わらず
自前 store 管理 store=InMemoryStore() + runtime.store 公式の「長期記憶」ファサード

全体像

29/
├── README.md
├── pyproject.toml                ← langchain 1.1.3, langchain-anthropic 1.2.0, langgraph 1.0.4
└── create_agent.ipynb            ← 全14セクションのハンズオン Notebook

Notebook の構造:

flowchart TD
    Setup[Section 1<br/>環境セットアップ] --> Basic

    subgraph Basic ["基本編 Sec 2-6"]
        S2[Section 2<br/>create_agent の最小例] --> S3
        S3[Section 3<br/>tools 引数 + @tool デコレータ] --> S4
        S4[Section 4<br/>model 引数<br/>文字列 vs ChatAnthropic インスタンス] --> S5
        S5[Section 5<br/>system_prompt<br/>str vs SystemMessage + cache_control] --> S6
        S6[Section 6<br/>response_format<br/>ToolStrategy で構造化出力]
    end

    Basic --> Advanced

    subgraph Advanced ["応用編 Sec 7-10"]
        S7[Section 7<br/>checkpointer<br/>InMemorySaver + thread_id] --> S8
        S8[Section 8<br/>ToolRuntime<br/>runtime.state / context / store / stream_writer] --> S9
        S9[Section 9<br/>store=InMemoryStore<br/>セッション横断の長期記憶] --> S10
        S10[Section 10<br/>state_schema + context_schema<br/>カスタム State/Context]
    end

    Advanced --> Middleware

    subgraph Middleware ["ミドルウェア編 Sec 11-14"]
        S11[Section 11<br/>デコレータ Middleware<br/>@before_model / @after_model / @dynamic_prompt + jump_to] --> S12
        S12[Section 12<br/>AgentMiddleware クラス継承<br/>状態を持つ Middleware] --> S13
        S13[Section 13<br/>wrap_model_call<br/>ModelRequest.override で<br/>メッセージ・モデル動的差し替え] --> S14
        S14[Section 14<br/>標準ミドルウェア 14 種紹介<br/>Summarization, ModelCallLimit, ToolRetry など]
    end

ミドルウェアの実行順序(典型例):

sequenceDiagram
    participant User
    participant Agent as create_agent
    participant MW1 as before_agent MW
    participant MW2 as before_model MW
    participant Model
    participant MW3 as after_model MW
    participant Tool
    participant MW4 as after_agent MW

    User->>Agent: invoke({messages: [...]})
    Agent->>MW1: before_agent(state, runtime)
    MW1-->>Agent: state を変更可能、jump_to=end で即終了も可
    loop ReAct ループ
        Agent->>MW2: before_model(state, runtime)
        MW2-->>Agent: state 変更
        Agent->>Model: 推論 (wrap_model_call MW でリクエスト/レスポンス差し替え可)
        Model-->>Agent: AIMessage
        Agent->>MW3: after_model(state, runtime)
        MW3-->>Agent: state 変更
        Agent->>Tool: tool_call 実行
        Tool-->>Agent: ToolMessage
    end
    Agent->>MW4: after_agent(state, runtime)
    MW4-->>User: 最終 state

使用ライブラリ・原理

langchain.agents.create_agent — 新エージェント API
from langchain.agents import create_agent
from langchain.tools import tool

agent = create_agent(
    model="anthropic:claude-sonnet-4-5-20250929",
    tools=[some_tool],
    system_prompt="...",
    response_format=ToolStrategy(Schema),     # 構造化出力
    checkpointer=InMemorySaver(),             # 短期記憶
    store=InMemoryStore(),                    # 長期記憶
    state_schema=CustomState,                 # カスタム state
    context_schema=AppContext,                # カスタム context
    middleware=[mw1, mw2, ...],               # ★ 新概念
)

主要パラメータ:

パラメータ 役割 旧 LangGraph での代替
model LLM。"anthropic:claude-..." 文字列 or ChatAnthropic(...) インスタンス create_react_agent(model=...) 同じ
tools @tool デコレートされた関数のリスト 同じ
system_prompt str か SystemMessagecache_control 等の高度機能用) 文字列のみだった
response_format ToolStrategy(PydanticModel) で構造化出力強制 with_structured_output 相当
checkpointer 会話履歴の永続化 同じ
store セッション横断の長期 KV ストア 同じ
state_schema カスタム state(AgentState 継承) 同じ
context_schema カスタム context(TypedDict、state とは別 新規
middleware 本回の主役。横断的関心事を分離 新規
ToolRuntime — ツール内から runtime にアクセス
from langchain.tools import ToolRuntime

@tool
def get_user_info(runtime: ToolRuntime) -> str:
    """現在のユーザー情報を取得する"""
    user_name = runtime.context.get("user_name", "不明")     # context_schema で渡した値
    user_id = runtime.context.get("user_id", "不明")
    return f"ユーザー名: {user_name}, ID: {user_id}"

@tool
def count_messages(runtime: ToolRuntime) -> str:
    messages = runtime.state.get("messages", [])              # state からアクセス
    return f"現在のメッセージ数: {len(messages)}件"

@tool
def search_database(query: str, runtime: ToolRuntime) -> str:
    writer = runtime.stream_writer                            # stream イベント送信
    writer("検索中...")
    return "結果"

runtime 引数は LLM には見えず、LangChain が自動注入する特殊引数。型ヒントを ToolRuntime にすることでフレームワークが認識する。

runtime から触れるもの:

属性 役割
runtime.state 現在の AgentState (messages を含む)
runtime.context context_schema で渡した不変データ
runtime.store 長期記憶 (InMemoryStore 等)
runtime.stream_writer 中間イベントを stream に push(第22回の StreamWriter と同じ)
state_schema vs context_schema — 「変わる」と「変わらない」を分ける
from langchain.agents import AgentState
from typing import TypedDict

# state: 実行中に変化する
class CustomState(AgentState):                # AgentState 継承(messages を含む)
    user_preferences: dict
    session_count: int

# context: 実行開始時に渡され、変化しない
class AppContext(TypedDict):                  # TypedDict(軽量)
    user_id: str
    environment: str

agent = create_agent(
    model="...",
    tools=[...],
    state_schema=CustomState,
    context_schema=AppContext,
)

# 呼び出し時
result = agent.invoke(
    {"messages": [...], "user_preferences": {...}, "session_count": 5},  # state
    context={"user_id": "user_456", "environment": "production"},        # context
)

設計意図:

  • state: ReAct ループの各ステップで update され、checkpointer に保存される。messages がその典型
  • context: 1 リクエスト内で不変。LLM 呼び出しのたびに変わらない設定(user_id, environment, request_id)
Middleware — 4 種類のフック点
フック タイミング 戻り値
@before_agent エージェント実行開始直前 dict で state 更新 or {"jump_to": "end"} で即終了
@before_model LLM 呼び出し直前 同上
@after_model LLM 応答受信直後 同上
@after_agent エージェント実行完了直後 同上
@wrap_model_call LLM 呼び出しの前後を包む(最強) ModelResponse
@dynamic_prompt system_prompt を動的に生成 str
wrap_model_call の override パターン
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse

@wrap_model_call
def add_context_to_request(request: ModelRequest, handler) -> ModelResponse:
    """リクエストにコンテキスト情報を追加"""
    current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    new_messages = [{"role": "system", "content": f"現在時刻: {current_time}"}] + list(request.messages)

    modified_request = request.override(messages=new_messages)
    return handler(modified_request)

request.override(...)任意のフィールドを差し替えた新 request を作り、handler(modified_request) で実行。model 自体も差し替えできるので、「短いメッセージは Haiku、長いメッセージは Sonnet」のような動的ルーティングが書ける。

標準ミドルウェア 14 種(Section 14)
ミドルウェア 役割
SummarizationMiddleware トークン上限近づいたら自動要約
HumanInTheLoopMiddleware ツール呼び出し前に承認要求
ModelCallLimitMiddleware LLM 呼び出し回数制限
ToolCallLimitMiddleware ツール呼び出し回数制限
ModelFallbackMiddleware メインモデル失敗時に別モデルに切替
PIIMiddleware 個人情報検出・マスキング
TodoListMiddleware Claude Code 風 TODO 管理
LLMToolSelectorMiddleware tools が多いとき関連ツールだけ LLM に渡す
ToolRetryMiddleware ツール失敗時の指数バックオフ自動リトライ
ModelRetryMiddleware モデル失敗時の指数バックオフ
LLMToolEmulatorMiddleware テスト用にツールを LLM でエミュレート
ContextEditingMiddleware 古い tool 結果を削除して context 圧縮
ShellToolMiddleware シェル実行ツールをワンライナーで追加
FilesystemFileSearchMiddleware Glob/Grep ツールをワンライナーで追加

第24回の Claude Code 風エージェントを自作したコードが、第29回では TodoListMiddleware + FilesystemFileSearchMiddleware + ShellToolMiddleware の数行で書ける時代になった。ライブラリの進化を体感する好例


ファイル別の役割

ファイル 役割
README.md セクション一覧と起動方法
pyproject.toml langchain >= 1.1.3, langchain-anthropic >= 1.2.0, langgraph >= 1.0.4
create_agent.ipynb 全14セクションのハンズオン。最小例 → ミドルウェアまで段階的に積み上げ

行レベルの工夫(中核ロジックの抜粋)

① 最小エージェント (create_agent.ipynb Section 2)
from langchain.agents import create_agent

simple_agent = create_agent(
    model="anthropic:claude-sonnet-4-5-20250929",                            # ①
    tools=[],                                                                 # ②
    system_prompt="あなたは親切なアシスタントです。"
)

result = simple_agent.invoke({                                                # ③
    "messages": [{"role": "user", "content": "こんにちは!簡単に自己紹介してください。"}]
})

last_message = result["messages"][-1]                                         # ④
print(last_message.content)
やってること なぜそうする
model="anthropic:claude-..." 文字列で provider:model_id 形式 LiteLLM 経由で provider を切替可能。"openai:gpt-4.1""google:gemini-2.5-pro" もこの形式
tools=[] でツールなしも許可 単純な「LLM ラッパ」としても使える。ChatAnthropic(...).invoke(...) の代替
{"messages": [{"role": "user", "content": "..."}]} 形式 LangGraph MessagesState の標準形式
result["messages"][-1] で最新応答取得 同じく LangGraph 流
② Tool 内から runtime にアクセス (Section 8)
@tool
def get_user_info(runtime: ToolRuntime) -> str:                              # ①
    """現在のユーザー情報を取得する"""
    user_name = runtime.context.get("user_name", "不明")                     # ②
    return f"ユーザー名: {user_name}"

context_agent = create_agent(
    model="anthropic:claude-sonnet-4-5-20250929",
    tools=[get_user_info],
    context_schema=UserContext,                                              # ③
)

result = context_agent.invoke(
    {"messages": [{"role": "user", "content": "私のユーザー情報を教えてください"}]},
    context={"user_name": "田中太郎", "user_id": "user_123"},                # ④
)
やってること なぜそうする
runtime: ToolRuntime の型ヒント フレームワークがこの引数をLLM のスキーマから除外し、実行時に自動注入。LLM 側は query: str だけ見える
runtime.context.get(...) で context にアクセス tool 側で「呼び出し元のユーザは誰か」「環境は本番か」を直接知れる
context_schema で型を宣言 invoke 時に context として渡せる値を型で制約
invoke(input, context={...}) で context を渡す state とは別の引数で渡す(混同注意)
③ デコレータ Middleware + jump_to で実行制御 (Section 11)
from langchain.agents.middleware import before_agent

@before_agent(can_jump_to=["end"])                                            # ①
def content_filter(state, runtime) -> dict[str, Any] | None:
    first_message = state["messages"][0].content.lower()
    if "不適切なキーワード" in first_message:
        return {                                                              # ②
            "messages": [{"role": "assistant", "content": "申し訳ございません..."}],
            "jump_to": "end",
        }
    return None

filtered_agent = create_agent(
    model="anthropic:claude-sonnet-4-5-20250929",
    tools=[],
    middleware=[content_filter],
)
やってること なぜそうする
@before_agent(can_jump_to=["end"]) でジャンプ可能先を宣言 フレームワークが「このフックは end へジャンプし得る」と認識し、グラフを正しく組む
{"messages": [...], "jump_to": "end"} で即時終了 LLM 呼び出しすら走らない。コンテンツフィルタリング、PII 検出、レート制限の典型パターン
wrap_model_call でモデル動的ルーティング (Section 13)
@wrap_model_call
def select_model_by_complexity(request: ModelRequest, handler) -> ModelResponse:
    user_messages = [m for m in request.messages if getattr(m, 'type', None) == 'human']
    last_user_msg = user_messages[-1] if user_messages else None
    msg_length = len(last_user_msg.content) if last_user_msg else 0

    if msg_length > 100:
        modified = request.override(model=smart_model)                       # ①
    else:
        modified = request.override(model=fast_model)

    return handler(modified)                                                  # ②

dynamic_model_agent = create_agent(
    model=fast_model,
    tools=[],
    middleware=[select_model_by_complexity],
)
やってること なぜそうする
request.override(model=...)LLM 自体を差し替え コスト最適化(短文 → Haiku, 長文 → Sonnet)の常套パターン
handler(modified) で実行 handler は「元の処理を実行する関数」。ミドルウェアパターン(Express / Koa / Rack と同じ)
⑤ クラスベース Middleware で状態を持つ (Section 12)
class LoggingMiddleware(AgentMiddleware):
    def __init__(self, log_level: str = "INFO"):
        super().__init__()
        self.log_level = log_level
        self.call_count = 0                                                   # ①

    def before_agent(self, state, runtime) -> dict[str, Any] | None:
        print(f"[{self.log_level}] AIエージェント開始")
        return None

    def wrap_model_call(self, request, handler) -> ModelResponse:
        self.call_count += 1                                                  # ②
        print(f"[{self.log_level}] モデル呼び出し #{self.call_count}")
        response = handler(request)
        return response

    def after_agent(self, state, runtime) -> dict[str, Any] | None:
        print(f"[{self.log_level}] AIエージェント完了 (合計 {self.call_count} 回)")
        return None

class_middleware_agent = create_agent(
    model="anthropic:claude-sonnet-4-5-20250929",
    tools=[get_weather],
    middleware=[LoggingMiddleware(log_level="DEBUG")],
)
やってること なぜそうする
self.call_count = 0インスタンス状態を保持 デコレータ Middleware は関数なので state を持てない。クラスベースなら state-ful な処理が書ける(例: コスト集計、APM トレース)
self.call_count += 1 で呼び出しごとに増やす 複数フックをまたいで同じ state を共有

学んだこと(要点)

  • create_agent は LangChain v1.0 で固まったエージェント API の決定版。第11回 (AgentExecutor 旧 API) や第20回 (create_react_agent) は移行候補
  • Middleware が新概念の中心。Express/Rack/Django Middleware と同じ「実行フローに介入する横断的関心事の分離」を、LLM エージェントに持ち込んだ
  • @dynamic_prompt でプロンプトをコンテキスト依存に生成できる。第24回でプロンプトを format で組み立てていたコードが、こちらでは Middleware 1 個で完結
  • request.override(model=...) が強力。LLM の動的ルーティング(コスト最適化、Fallback、A/B)がフレームワークレベルでサポート
  • state_schema vs context_schema の分離は綺麗な設計。「変わる」と「変わらない」を型レベルで分離 → checkpointer の正しさにも寄与
  • 標準ミドルウェア 14 種は要チェック。TodoListMiddleware だけで第24回相当の「TODO 駆動エージェント」が組める。PIIMiddleware で個人情報マスキング、HumanInTheLoopMiddleware で第22回の人間介入が標準化
  • ToolRuntime の自動注入は型ヒント駆動。LLM 視点には見えない引数を増やせる
  • cache_controlSystemMessage の content list で指定すると、Anthropic prompt cache が使える(大量 context の繰返し利用でコスト 90% カット)
  • Notebook 形式の README は実行ハードルが低い。写経用としても最適

拡張アイデア

  1. 既存サンプルを create_agent で書き直す — 第20回 (MCP × LangGraph) や第24回 (TODO Agent) を create_agent + middleware で再実装。コード行数の縮小を比較
  2. PIIMiddleware の動作確認 — 個人情報を含む input を投げて、どう masking されるかを観察。production の入り口で必須レベル
  3. HumanInTheLoopMiddleware で承認ループ — 第22回の interrupt() パターンを公式 Middleware で書き直す
  4. 複数 Middleware の組合せTodoListMiddleware + FilesystemFileSearchMiddleware + ShellToolMiddleware + ToolRetryMiddleware で Claude Code クローンを最小コードで実装
  5. response_format=ToolStrategy(...) の使い分け — 構造化出力を with_structured_output ではなく ToolStrategy で書く理由を理解(中間 ToolMessage が残る vs 残らない、validation 再試行等)
  6. @dynamic_prompt で多言語対応 — context の locale を見て言語別 system_prompt を返す Middleware
  7. ModelFallbackMiddleware — メイン Sonnet 失敗時に Haiku に fallback。SLA を保ちつつコスト管理
  8. コスト集計 Middleware の自作 — クラスベース Middleware で wrap_model_call を override し、各リクエストのトークン数を集計して runtime.store に保存

現代版に移植するなら

1. API キー設定は .env.op + op run に切り替える(CLAUDE.md ルール 8)
# 29/.env.op
ANTHROPIC_API_KEY=op://Personal/anthropic-api-key/credential

Notebook 起動:

op run --env-file=.env.op -- uv run jupyter notebook create_agent.ipynb

load_dotenv()os.environ を見るだけなので、op run 経由で注入された環境変数を Notebook 内から普通に読める。

2. モデル ID の更新

サンプルは claude-sonnet-4-5-20250929 を使っている。2026 年現在は claude-sonnet-4-6 / claude-opus-4-7 等が利用可能。Notebook 全セクションで模型 ID をハードコードしているので、先頭で MODEL_ID = "claude-sonnet-4-6" と変数化して全置換するのが楽。

3. Notebook の出力を git にコミットしない

.ipynb は実行結果が JSON に埋まる。API レスポンスや個人情報が漏れる可能性があるので、.gitignore に Notebook の出力だけ除外する設定:

# .gitattributes
*.ipynb diff=jupyter

# pre-commit hook で nbstripout を実行
4. Notebook を Python script に変換

写経で動作確認するなら .ipynb のままで良いが、CI で動かしたいなら:

uv run jupyter nbconvert --to script create_agent.ipynb

.py 化してテスト統合可能に。

5. LLMToolSelectorMiddleware で MCP ツール大量時の対策

第20回・第24回で MCP ツールを大量にロードすると context overflow になる問題があった。LLMToolSelectorMiddleware を挟むと、毎回のリクエスト前に「関連するツールだけ」を smart LM で選別して main LM に渡す → context 圧迫が解消する。


既知の不具合・注意点

  • README タイポ: ; このサンプルはuvを使用しています。; は意図不明( の typo?)
  • Notebook の実行費用に注意: 全14セクション x 複数回 invoke で API 呼び出しは数十回。Sonnet 4.5 だと $1-3 / 全体実行
  • Section 9 の store.put((...), key, value) の tuple namespace: ("users",) のようにタプルで namespace を渡すのは LangGraph の流儀。直感的でない
  • Section 10 の AgentState 継承: AgentState は TypedDict 派生で、自作 State は TypedDict のキーが衝突しないよう注意messages を上書きすると挙動が壊れる
  • Section 13 の getattr(m, 'type', None) == 'human': LangChain のメッセージ型判定が isinstance(m, HumanMessage) ではなく m.type 文字列なのは LangChain 流。明示的でない
  • Section 12 の LoggingMiddleware.__init__super().__init__(): 親 AgentMiddleware のコンストラクタを呼ぶ必要があるかは version 依存。v1.0 では明記ないが呼んでおくのが安全
  • stream_writer の挙動が docs に薄い: Section 8 で for chunk in stream_agent.stream(..., stream_mode="custom"): で chunk を受けるが、これが第22回の StreamWriter と互換かは要確認
  • @before_agent(can_jump_to=["end"]) の指定漏れ: can_jump_to を宣言しないと jump_to が無視される。フォーラムで「動かない」と報告されがちな罠
  • prompt cache の例がコメントアウト: Section 5 で cache_control の例がコメントアウトされている。実際に試したい人は手動で uncomment 必要

記事参照

  • Software Design 2026 年 2 月号(推定)連載第29回「LangChain v1.0 create_agent 入門」
  • 関連: 第20回 STUDY_NOTEScreate_react_agent での MCP 統合。create_agent で書き直し候補
  • 関連: 第22回 STUDY_NOTESinterrupt() / StreamWriter。Middleware で書き直し候補
  • 関連: 第24回 STUDY_NOTES — TODO 駆動エージェント。TodoListMiddleware で大幅短縮可能
  • 公式 create_agent: https://docs.langchain.com/oss/python/langchain/agents
  • 公式 Middleware: https://docs.langchain.com/oss/python/langchain/middleware
  • 公式 Migration guide (v0 → v1): https://docs.langchain.com/oss/python/migration

作成: 2026-05-25 / 最終更新: 2026-06-10