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 か SystemMessage(cache_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_schemavscontext_schemaの分離は綺麗な設計。「変わる」と「変わらない」を型レベルで分離 → checkpointer の正しさにも寄与- 標準ミドルウェア 14 種は要チェック。
TodoListMiddlewareだけで第24回相当の「TODO 駆動エージェント」が組める。PIIMiddlewareで個人情報マスキング、HumanInTheLoopMiddlewareで第22回の人間介入が標準化 ToolRuntimeの自動注入は型ヒント駆動。LLM 視点には見えない引数を増やせるcache_controlをSystemMessageの content list で指定すると、Anthropic prompt cache が使える(大量 context の繰返し利用でコスト 90% カット)- Notebook 形式の README は実行ハードルが低い。写経用としても最適
拡張アイデア¶
- 既存サンプルを
create_agentで書き直す — 第20回 (MCP × LangGraph) や第24回 (TODO Agent) をcreate_agent+ middleware で再実装。コード行数の縮小を比較 - PIIMiddleware の動作確認 — 個人情報を含む input を投げて、どう masking されるかを観察。production の入り口で必須レベル
- HumanInTheLoopMiddleware で承認ループ — 第22回の
interrupt()パターンを公式 Middleware で書き直す - 複数 Middleware の組合せ —
TodoListMiddleware + FilesystemFileSearchMiddleware + ShellToolMiddleware + ToolRetryMiddlewareで Claude Code クローンを最小コードで実装 response_format=ToolStrategy(...)の使い分け — 構造化出力をwith_structured_outputではなく ToolStrategy で書く理由を理解(中間 ToolMessage が残る vs 残らない、validation 再試行等)@dynamic_promptで多言語対応 — context のlocaleを見て言語別 system_prompt を返す MiddlewareModelFallbackMiddleware— メイン Sonnet 失敗時に Haiku に fallback。SLA を保ちつつコスト管理- コスト集計 Middleware の自作 — クラスベース Middleware で
wrap_model_callを override し、各リクエストのトークン数を集計して runtime.store に保存
現代版に移植するなら¶
1. API キー設定は .env.op + op run に切り替える(CLAUDE.md ルール 8)¶
Notebook 起動:
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 の出力だけ除外する設定:
4. Notebook を Python script に変換¶
写経で動作確認するなら .ipynb のままで良いが、CI で動かしたいなら:
.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_NOTES —
create_react_agentでの MCP 統合。create_agentで書き直し候補 - 関連: 第22回 STUDY_NOTES —
interrupt()/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