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_agent と create_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_agent は create_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_model と wrap_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_model も wrap_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_format と ToolStrategy まわり¶
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 .env→OPENAI_API_KEYを直書き、と書いてあるが、リポジトリ方針では.env.op+op run --env-file=.env.op -- uv run python ex01.pyを使う(生キーを env に置かない)。フォルダに.env.opが既にある。
8. 学んだこと(要点)¶
create_agentはcreate_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案)¶
wrap_tool_callで PII マスキング(第31回シナリオ1): ツールに渡る/から返る引数を正規表現でマスクするフックを書き、echoツールにメールアドレスを通して伏字化を観察する。wrap_model_callのツール版として握り方を比較する。wrap_model_callの handler 2回呼びでリトライ: 1回目のModelResponseが空 or エラーならhandler(request)をもう一度呼ぶレジリエンス Middleware を書く。ex06 の上限 Middleware と組み合わせ「リトライしつつ上限で止まる」を作る(第31回シナリオ2の最小版)。before_modelで動的システムプロンプト切替(ツール選択誘導 / 第31回シナリオ3): state の会話長や直近メッセージを見て system プロンプトを差し替え、使うツールを誘導する Middleware を書く。wrap_model_callでoverride(messages=...)する版とどちらが綺麗か比較する。- クラス Middleware でコスト計測:
after_modelでresponseの token usage を読み、self.total_tokensに積算して invoke 後に合計コストを出す。ex05 のカウンタを「回数」から「トークン/円」に拡張する。 response_formatのネスト/リスト化: ex02 のContactInfoをlist[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_model と wrap_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 に渡しているか確認 |
記事参照¶
- Software Design 連載 第29回(
create_agent+ Middleware 入門)/ 第31回(PII・レジリエンス・ツール選択の3シナリオ) - LangChain v1 公式: https://docs.langchain.com/oss/python/langchain/overview
create_agentAPI: https://docs.langchain.com/oss/python/langchain/create-agent- Middleware ガイド: https://docs.langchain.com/oss/python/langchain/middleware
- 連載メモ: 第29回
../../software-design/29/STUDY_NOTES.md/ 第31回../../software-design/31/STUDY_NOTES.md
作成: 2026-06-12 / 最終更新: 2026-06-12