コンテンツにスキップ

LangChain v1 create_agent + Middleware 用語集 — 写経で詰まったところ

LangChain 1.0 で入った create_agent と Middleware 機構を、ex01〜06 を写経しながら学ぶための学習メモ+用語集。 「フックが何種類もあって違いが分からない」「@before_model@wrap_model_call の使い分けが曖昧」「create_react_agent と何が違う」で迷う人向けに、混同しやすい同系語を表で整理した。

  • 対応連載: 第29回(create_agent + Middleware 入門 / ../../software-design/29/STUDY_NOTES.md)、第31回(PII / レジリエンス / ツール選択の実戦 / ../../software-design/31/STUDY_NOTES.md
  • サンプルの並び順: README.md

0. このフォルダで「何を学ぶ」のか(1行ずつ)

# ファイル 一言で
01 ex01_create_agent_minimal.py create_agent で ReAct エージェントを 1 行で組む。model は文字列でよい
02 ex02_response_format.py response_format=ToolStrategy(Schema) で最終出力を Pydantic にする
03 ex03_middleware_hooks.py @before_model / @after_model で「監視」フックを差す
04 ex04_wrap_model_call.py @wrap_model_call で「制御」する(リクエスト改変・モデル切替・リトライ)
05 ex05_class_middleware.py AgentMiddleware 継承で状態(カウンタ)を持つ Middleware
06 ex06_builtin_middleware.py 標準 Middleware(ModelCallLimitMiddleware 等)で暴走を止める

補足: フォルダには ex0N.py(サフィックスなし)と ex0N_xxx.py(サフィックス付き)が両方ある。_xxx.py が手本、ex0N.py は写経した自分の版という構成。後述「§7 写経で踏んだ罠」で両者の差(特に ex04 の request.override())を扱う。ex03.py だけは別フォルダ(functional_api)の @task サンプルが紛れ込んでいるので無視してよい。


1. 一番混乱する話:create_react_agentcreate_agent は何が違うのか

第20回までずっと使ってきた langgraph.prebuilt.create_react_agent後継が、v1 の langchain.agents.create_agent。やることは同じ「ツールを使う ReAct ループを 1 関数で組む」だが、API と拡張点が違う。

観点 旧: create_react_agent(langgraph.prebuilt) 新: create_agent(langchain.agents, v1)
モデル指定 ChatOpenAI(...) インスタンスを渡す "openai:gpt-4o-mini" の文字列でよい(内部で init_chat_model が解決)
前後処理の差し込み StateGraph を自分で組み直す or ラップする Middleware で宣言的に差せる(ex03〜06)
構造化出力 response_format はあるが書き味が固い response_format=ToolStrategy(Schema) で簡潔(ex02)
戻り値 CompiledStateGraph CompiledStateGraph(同じ。invoke({"messages": [...]}) で実行)

さらに歴史をさかのぼると、もっと古い世代がある。混同しやすいので並べる:

世代 代表 API 立ち位置
v0 初期(〜2023) LLMChain / initialize_agent / AgentExecutor 「チェーン」と「エージェント実行器」が別物。AgentExecutor が while ループを回していた
LangGraph 移行期 create_react_agent(langgraph.prebuilt) ループ=グラフ。状態は MessagesState。本連載の主役だった
v1(2025〜) create_agent(langchain.agents) create_react_agent を包み直して Middleware を生やした版。このフォルダの主役

判定基準: コードに from langchain.agents import create_agent があれば v1 系。from langgraph.prebuilt import create_react_agent なら移行期。AgentExecutor / LLMChain が出てきたら v0 系の古い記事なので、現代版は create_agent に読み替える。

よくある誤解: 「create_agentcreate_react_agent の単なる別名」→ 違う。Middleware フックを生やせるのが本質的な追加価値。ループそのものは同じ ReAct。


2. 最重要:Middleware フック 6 種の違い(ここで全員迷う)

Middleware は「エージェントのライフサイクルにフックを生やす機構」。いつ呼ばれるかhandler を握れるかの2軸で6種類ある。

flowchart TD
    start([invoke 開始]) --> ba["before_agent<br/>(エージェント全体の最初・1回)"]
    ba --> loop{{ReAct ループ}}
    loop --> bm["before_model<br/>(モデル呼び出し直前・毎回)"]
    bm --> wmc["wrap_model_call<br/>(モデル呼び出しを包む)"]
    wmc -->|"handler(request)"| model["🤖 LLM 呼び出し"]
    model --> wmc
    wmc --> am["after_model<br/>(モデル呼び出し直後・毎回)"]
    am --> hastool{tool_calls?}
    hastool -->|あり| wtc["wrap_tool_call<br/>(ツール実行を包む)"]
    wtc --> tool["🔧 ツール実行"]
    tool --> loop
    hastool -->|なし| aa["after_agent<br/>(エージェント全体の最後・1回)"]
    aa --> done([invoke 終了])
フック いつ 回数 handler を握る? 主な用途 このフォルダの例
before_agent 全体の最初 1回 × 入力フィルタ、jump_to で早期終了 (なし)
before_model モデル呼び出し直前 毎ターン × ログ、プロンプト改変 ex03 log_before
wrap_model_call モデル呼び出しを包む 毎ターン モデル切替・リトライ・リクエスト改変 ex04 / ex05
after_model モデル呼び出し直後 毎ターン × レスポンス検査、ログ ex03 log_after
wrap_tool_call ツール実行を包む ツール毎 PII マスキング、ツールのリトライ (第31回で登場)
after_agent 全体の最後 1回 × 最終出力の後処理 (なし)

before_modelwrap_model_call の決定的な違い(このフォルダの肝)

両方「モデル呼び出しの周辺」に効くが、握れるものが違う。

@before_model(ex03) @wrap_model_call(ex04)
シグネチャ (state, runtime) (request: ModelRequest, handler) -> ModelResponse
モデル呼び出しを動かせる 動かせない(呼ばれて終わり) handler(request) を自分で呼ぶ。呼ばないと LLM が走らない
できること 監視・観測(ログ、state へのマージ) 制御(リクエスト改変、モデル差替、handler を2回呼んでリトライ)
抽象度 低い(イベント通知) 高い(呼び出しを包む=デコレータ的)

1文の判定基準: 「ログを出したい・state を覗きたいだけ」なら before_model / after_model。「リクエストを書き換えたい・モデルを変えたい・条件で2回呼びたい」なら wrap_model_call

比喩: before_model会議の前に資料を眺める傍聴者wrap_model_call発言の前後に割り込んで内容を差し替えられる司会。司会だけが「もう一度言って(リトライ)」を命じられる。

よくある誤解: 「wrap_model_call の中で handler を呼び忘れても before/after みたいに勝手にモデルが走る」→ 走らない。handler(request)唯一の実行トリガー。呼ばなければ LLM は呼ばれず、戻り値の ModelResponse も自分で用意する必要がある。


3. 関数(デコレータ)Middleware と クラス Middleware の違い

同じフックを「デコレータで書く」か「クラスのメソッドで書く」かの2スタイルがある。

デコレータ版(ex03/ex04) クラス版(ex05)
書き方 @before_model def f(state, runtime): ... class MW(AgentMiddleware): def before_model(self, state, runtime): ...
状態を持てるか closure を使えば一応可能だが汚い __init__self.count 等を素直に持てる
複数フックの束ね フックごとに別関数(バラバラ) 1クラスに before_modelwrap_model_call もまとめられる(ex05 がまさにそれ)
引数付き生成 不可(モジュールロード時に確定) MW(threshold=3) のようにパラメータ化できる
向いてる用途 副作用のない単発フック カウンタ・レート計測・複数フック連携・設定可変

判定基準: 「state を持たない1個のフック」ならデコレータ。「カウンタなど内部状態を持つ」or「複数フックを1つの関心事としてまとめたい」or「MW(threshold=2) のように引数を取りたい」ならクラス(AgentMiddleware 継承)。

ex05 の実コードで「状態を持つ」とは何かを追う(ex05_class_middleware.py:32-54):

class CallCounterMiddleware(AgentMiddleware):
    def __init__(self, threshold: int = 3):
        super().__init__()
        self.threshold = threshold
        self.count = 0                       # ① インスタンス状態(呼び出しを跨いで残る)

    def before_model(self, state, runtime):  # ② 毎ターン呼ばれる
        self.count += 1                      #    → ここでインクリメントできるのがクラスの旨味
        if self.count > self.threshold:
            print(f"  ⚠️ 閾値 {self.threshold} を超えました")
        return None                          # ③ None なので state は変更しない

    def wrap_model_call(self, request, handler):  # ④ 同じクラスに別フックを同居
        return handler(request)                   #    今回は素通し(demo)
やってること なぜそうする
self.count をインスタンスに保持 デコレータ版だと global か closure が必要。クラスなら自然に状態を持てる
before_model を override 必要なフックのメソッドだけ書けばよい(書かないフックは何もしない)
return None dict を返すと state にマージされる。監視だけなら None
wrap_model_call も同居 カウンタ(before)と制御(wrap)を1つの関心事として束ねられる

4. response_formatToolStrategy まわり

ex02 で「最終出力を Pydantic にする」仕組み。同系の語が混ざりやすい。

用語 何者か 役割
response_format= create_agent の引数 「最終出力をこの形に整える」指定口
ToolStrategy(Schema) response_format に渡す戦略オブジェクト 「スキーマを tool として LLM に呼ばせる」方式。LLM が Schema という名の関数を呼ぶ形で構造化を実現
result["structured_response"] invoke の戻り dict のキー ここに Pydantic インスタンスが入る(result["messages"] とは別物)

仕組み(内部メカニズム): ToolStrategy は、与えた Pydantic スキーマをツール定義に変換して LLM に渡す。LLM はそのツールを「呼ぶ」形で各フィールドを埋め、その引数を Pydantic でパースして structured_response に格納する。OpenAI / Anthropic 双方の tool calling 機構に乗るので、プロバイダ非依存で動く。

ex02 の取り出し(ex02_response_format.py:44-52):

result = agent.invoke({"messages": [("user", raw)]})
structured = result["structured_response"]   # ← Pydantic インスタンス。.name / .email で属性アクセス

判定基準: 「自然言語の文章から決まった項目を抜きたい(抽出・分類)」なら response_format=ToolStrategy(Schema)result["messages"][-1].content(生テキスト)を正規表現でパースする旧来のやり方は不要になる。

混同注意: structured_response(構造化結果)と messages(会話履歴)は別キー。前者が目的の Pydantic、後者は tool 呼び出しの軌跡。

旧 API との違い: v0 では構造化出力に PydanticOutputParser + プロンプトに format 指示を埋め込む、という手作業が必要だった。v1 + tool calling では「スキーマを渡すだけ」で済む。


5. 標準 Middleware(production 安全策)の用語

ex06 の「暴走を止める」系。thread_limit という引数名が分かりにくいので表に。

Middleware 何を制限 主な引数 上限到達時
ModelCallLimitMiddleware モデル呼び出し(=ループ)回数 thread_limit=4 例外ではなく安全に停止して最後のメッセージを返す
ToolCallLimitMiddleware 特定ツールの呼び出し回数 thread_limit=3, tool_name="flaky_search" 同上

thread_limit の意味: 1つの thread_id(=1会話セッション)内での累積上限。ex06 が config={"configurable": {"thread_id": "safety-1"}} を渡しているのは、この上限がスレッド単位で効くため。複数 invoke を跨いで数えたいときに効く設計。

なぜ必要か: ツールが「結果なし」を返し続けると、エージェントは「別の語でもう一度」と無限にループしうる(ex06 の flaky_search がわざとそういう設計)。ModelCallLimitMiddleware がなければ API コストが青天井になる。production では最低でも ModelCallLimitMiddleware を入れるのが安全側。

よくある誤解: 「上限に達したら例外が飛ぶ」→ 飛ばない。正常終了として最後のメッセージを返す。だから呼び出し側は try/except ではなく、返ってきた messages を見て「打ち切られたか」を判断する。


6. request.override() と「request を直接書き換える」の違い(写経で踏む罠)

ex04 には2つの版があり、ここが一番のハマりどころ。

コード 問題
手本(ex04_wrap_model_call.py:41 request.messages = new_messages の後 handler(request) request を直接ミューテートしている。動くが immutable 原則に反し、モデル差替(request.model = "...")は壊れる
写経の修正版(ex04.py:31, 53 handler(request.override(messages=new_messages)) / handler(request.override(model=model)) override() が新しい request を返す。元を壊さない。これが正しい流儀

request.override(...) の役割: ModelRequest直接書き換えず、指定フィールドだけ差し替えた新しい request を返すメソッド(immutable update)。messages=model= をキーワードで渡す。

さらに ex04.py が直している、もう1つの罠(ex04.py:13-14):

MODEL_MINI = init_chat_model("openai:gpt-4o-mini")
MODEL_FULL = init_chat_model("openai:gpt-4o")
# request.model は「解決済みモデルオブジェクト」を期待する。
# request.override(model="openai:gpt-4o") のように文字列を入れると壊れる。
用語 何者か 注意
init_chat_model("openai:gpt-4o") 文字列 → 解決済みモデルオブジェクトに変換する関数 create_agent(model=...) には文字列を渡せるが、request.override(model=...) には解決済みオブジェクトを渡す必要がある
ModelRequest wrap_model_call が受け取る「これからのモデル呼び出し」を表すオブジェクト .messages / .model 等を持つ。書き換えは .override() 経由が安全
ModelResponse handler(request) の戻り値 wrap_model_call はこれを返す(=モデルの応答)

1文の判定基準: wrap_model_call の中で request をいじるなら、代入(request.x = ...)ではなく request.override(x=...)。model を差し替えるなら文字列ではなく init_chat_model(...) で解決したオブジェクトを渡す。


7. 写経で踏んだ罠まとめ(ファイル構成の事情)

  • ex0N.py(サフィックスなし)= 自分が手で写した版、ex0N_xxx.py = 元の手本。両方残してあるのは「手本と自分の差分を見比べる」ため。ex04.py は手本の素直なミューテート版を override() ベースに直した、改善された写経になっている。
  • ex03.py は無関係。中身は functional_api の @task / slow_double 並列サンプルで、Middleware とは関係ない(コピー時の取り違えと思われる)。Middleware の ex03 を読むなら ex03_middleware_hooks.py を見る。
  • .env の作り方: README は cp ../functional_api/.env.sample .envOPENAI_API_KEY を直書き、と書いてあるが、リポジトリ方針では .env.op + op run --env-file=.env.op -- uv run python ex01.py を使う(生キーを env に置かない)。フォルダに .env.op が既にある。

8. 学んだこと(要点)

  • create_agentcreate_react_agent の Middleware 拡張版。ループ(ReAct)自体は同じで、model を文字列で渡せること・前後フックを差せることが新しい。
  • Middleware は「いつ呼ばれるか × handler を握るか」で6種類。監視系(before/after)と制御系(wrap)を分けて覚えると混乱しない。wrap_* だけが handler を握り、呼び出しを動かす/差し替える/繰り返せる。
  • デコレータ版 vs クラス版は「状態を持つか」「複数フックを束ねたいか」「引数を取りたいか」で選ぶ。AgentMiddleware 継承なら全部できる。
  • 構造化出力は ToolStrategy(Schema) で tool calling に乗せるのが v1 流。v0 の PydanticOutputParser + プロンプト指示は不要。結果は structured_response キー。
  • production には ModelCallLimitMiddleware。上限到達は例外でなく安全停止。thread_limit はスレッド(会話)単位の累積。
  • wrap_model_call で request をいじるなら override()(immutable update)。model 差替は init_chat_model() の解決済みオブジェクトで。

9. 拡張アイデア(最低3案)

  1. wrap_tool_call で PII マスキング(第31回シナリオ1): ツールに渡る/から返る引数を正規表現でマスクするフックを書き、echo ツールにメールアドレスを通して伏字化を観察する。wrap_model_call のツール版として握り方を比較する。
  2. wrap_model_call の handler 2回呼びでリトライ: 1回目の ModelResponse が空 or エラーなら handler(request) をもう一度呼ぶレジリエンス Middleware を書く。ex06 の上限 Middleware と組み合わせ「リトライしつつ上限で止まる」を作る(第31回シナリオ2の最小版)。
  3. before_model で動的システムプロンプト切替(ツール選択誘導 / 第31回シナリオ3): state の会話長や直近メッセージを見て system プロンプトを差し替え、使うツールを誘導する Middleware を書く。wrap_model_calloverride(messages=...) する版とどちらが綺麗か比較する。
  4. クラス Middleware でコスト計測: after_modelresponse の token usage を読み、self.total_tokens に積算して invoke 後に合計コストを出す。ex05 のカウンタを「回数」から「トークン/円」に拡張する。
  5. response_format のネスト/リスト化: ex02 の ContactInfolist[ContactInfo] を含む親スキーマに拡張し、複数連絡先が混じった文章から全件抽出できるか確認する。ToolStrategy がネスト Pydantic をどう扱うかを観察。

10. クイック早見表(迷ったらここ)

困りごと 見る/使うもの
ReAct エージェントを1行で組みたい create_agent(model="openai:...", tools=[...])(ex01)
create_react_agent と何が違う? §1 の比較表(Middleware が生やせる点が本質)
ログだけ出したい・state を覗きたい @before_model / @after_model(ex03)
リクエストを書き換えたい・モデルを変えたい・リトライしたい @wrap_model_call + handler / request.override()(ex04)
カウンタなど状態を持ちたい AgentMiddleware 継承(クラス版, ex05)
複数フックを1つにまとめたい クラス版に before_modelwrap_model_call を同居(ex05)
暴走・無限ループを止めたい ModelCallLimitMiddleware / ToolCallLimitMiddleware(ex06)
自然文から決まった項目を抽出したい response_format=ToolStrategy(Schema)result["structured_response"](ex02)
wrap_model_call で request を壊さず書き換えたい request.override(messages=..., model=...)(ex04.py)
model を差し替えたら壊れる 文字列でなく init_chat_model("openai:...") の解決済みオブジェクトを渡す
thread_limit が効かない config={"configurable": {"thread_id": ...}} を invoke に渡しているか確認

記事参照


作成: 2026-06-12 / 最終更新: 2026-06-12