AI
Panduan Middleware LangChain create_agent: PII, Limit, Fallback
Oktober 202613 menit baca

LangChain 1.0 merekomendasikan langchain.agents.create_agent sebagai pengganti langgraph.prebuilt.create_react_agent. Fungsi ini tetap berjalan di runtime LangGraph, tetapi argumen prompt berganti nama menjadi system_prompt, dan pre_model_hook, post_model_hook, serta penanganan error ToolNode digantikan middleware yang diberikan lewat daftar middleware.
Hook before_agent dan before_model berjalan dari middleware pertama di daftar sampai yang terakhir. Hook after_model dan after_agent berjalan terbalik, dari terakhir ke pertama. wrap_model_call dan wrap_tool_call bersarang seperti pemanggilan fungsi, jadi middleware yang ditulis pertama menjadi pembungkus terluar.
Tidak. PIIMiddleware secara default hanya memeriksa input pengguna, sedangkan apply_to_output dan apply_to_tool_results sama-sama bernilai False. Bila tool mengembalikan data pelanggan, set apply_to_tool_results=True untuk setiap tipe PII yang bisa muncul di data itu, atau nilainya sampai ke model tanpa diubah.
ModelRetryMiddleware secara default memakai on_failure='continue', yang mengubah error terakhir menjadi AIMessage alih-alih melemparnya. Middleware fallback hanya bereaksi terhadap exception, jadi ia tidak pernah melihat kegagalannya. Set on_failure='error' pada retry dan tulis fallback sebelum retry agar fallback menjadi pembungkus terluar.
Gunakan HumanInTheLoopMiddleware dengan entri interrupt_on untuk tool tersebut dan tambahkan predikat when yang mengembalikan True hanya bila argumennya melewati ambang Anda. Opsi when membutuhkan langchain 1.3.3 atau lebih baru. Agent juga butuh checkpointer dan thread_id agar run yang dijeda bisa dilanjutkan dengan Command yang membawa keputusannya.

Ringkasan Utama
Di LangChain 1.x, create_agent menggantikan create_react_agent, sementara pre_model_hook, post_model_hook, dan penanganan error ToolNode yang lama kini menjadi middleware. Kelas bawaan mencakup redaksi PII, batas panggilan model dan tool, retry, model fallback, dan persetujuan manusia. Urutan penting: hook before berjalan dari pertama ke terakhir, hook after sebaliknya, dan hook wrap bersarang.
Cari cara menambahkan guardrail ke agent LangChain, dan banyak jawaban teratas masih mengimpor create_react_agent dari langgraph.prebuilt lalu memberinya pre_model_hook. Kode itu menggambarkan API sebelum LangChain 1.0. Rilis 1.0.0 yang terbit di PyPI pada 17 Oktober 2025 memindahkan agent ke langchain.agents.create_agent dan menyatukan argumen hook yang tersebar menjadi satu daftar middleware. Menyalin jawaban lama ke proyek sekarang langsung gagal di argumen ToolNode pertama.
Panduan ini membahas middleware langchain create_agent sesuai dokumentasi langchain 1.4.3, rilis PyPI tanggal 28 September 2026, yang membutuhkan Python 3.10 ke atas. Kita memigrasikan agent accounts-payable kecil dari API lama, menjelaskan enam hook beserta urutan eksekusinya, lalu membangun guardrail yang benar-benar dibutuhkan agent ERP: redaksi PII yang juga mencakup hasil tool, batas panggilan, guard periode tutup buku buatan sendiri, retry yang menyerahkan ke model fallback, dan persetujuan manusia di atas ambang nominal. Semua nama kelas dan parameter diambil dari dokumentasi dan source code LangChain.
Pergantian nama adalah bagian terkecil dari migrasi. create_agent tetap berjalan di runtime LangGraph, jadi persistence, checkpointing, dan interrupt bekerja seperti sebelumnya, tetapi sebagian besar argumen kustomisasi berpindah tempat. Panduan migrasi mencantumkan semua perubahannya; berikut yang merusak kode lama.
| Sebelum 1.0 | LangChain 1.x | Yang perlu diwaspadai |
|---|---|---|
| from langgraph.prebuilt import create_react_agent | from langchain.agents import create_agent | Runtime LangGraph yang sama di bawahnya, jadi checkpointer tetap terpakai |
| prompt= | system_prompt= | Berupa string; prompt dinamis pindah ke middleware @dynamic_prompt |
| pre_model_hook= | Middleware dengan before_model | Bisa ditumpuk; ringkasan percakapan tersedia sebagai SummarizationMiddleware |
| post_model_hook= | Middleware dengan after_model | Persetujuan tool tersedia sebagai HumanInTheLoopMiddleware |
| model= berupa callable yang memilih model | wrap_model_call dengan request.override(model=...) | Model yang sudah di-bind dengan bind_tools ditolak |
| tools=ToolNode(..., handle_tool_errors=...) | tools=[...] ditambah middleware wrap_tool_call | Instance ToolNode tidak lagi diterima |
| State kustom berupa model Pydantic atau dataclass | TypedDict turunan AgentState | Diatur lewat state_schema di agent atau di middleware |
| Node stream bernama agent | Node stream bernama model | Filter yang mencocokkan nama node diam-diam berhenti cocok |
| Dependensi di config configurable | context= saat invoke, context_schema= di create_agent | Bertipe, dan bisa diakses dari middleware lewat runtime.context |
Berikut agent khas sebelum 1.0 beserta padanannya di 1.x. Hook pemangkas riwayat berubah menjadi middleware ringkasan bawaan, dan error handler ToolNode menjadi fungsi wrap_tool_call empat baris.
# Before: LangGraph prebuilt agent (pre-1.0 style)
from langgraph.prebuilt import create_react_agent, ToolNode
def trim_history(state): # pre_model_hook
...
agent = create_react_agent(
model="openai:gpt-5.4-mini",
tools=ToolNode(
[lookup_invoice, post_journal_entry],
handle_tool_errors=lambda e: f"Tool error: {e}",
),
prompt="You are an accounts-payable assistant.",
pre_model_hook=trim_history,
)
# After: langchain 1.x
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware, wrap_tool_call
from langchain.messages import ToolMessage
@wrap_tool_call
def tool_errors_to_model(request, handler):
try:
return handler(request)
except ValueError as exc:
# Only bad-but-schema-valid input belongs here. Network failures go to
# ToolRetryMiddleware; bugs in the tool should still bubble up.
return ToolMessage(
content=f"Tool error: {exc}",
tool_call_id=request.tool_call["id"],
)
agent = create_agent(
model="openai:gpt-5.4-mini",
tools=[lookup_invoice, post_journal_entry], # plain tools, no ToolNode
system_prompt="You are an accounts-payable assistant.",
middleware=[
SummarizationMiddleware(model="openai:gpt-5.4-mini", trigger={"tokens": 4000}),
tool_errors_to_model,
],
)
# Streaming filters change too: the node is now called "model", not "agent".Dua perubahan mudah terlewat saat review. Chain lama, retriever, API indexing, dan modul hub pindah ke paket terpisah langchain-classic, jadi impor dari langchain.chains membutuhkan paket itu terpasang. Lalu pergantian nama node streaming terjadi tanpa peringatan: kode yang memfilter event dengan node sama dengan agent tetap berjalan, hanya saja tidak menerima apa pun.
Middleware punya dua jenis hook. Hook bergaya node, yaitu before_agent, before_model, after_model, dan after_agent, berjalan di titik tetap dan mengembalikan dict yang digabung ke state. Hook bergaya wrap, yaitu wrap_model_call dan wrap_tool_call, menerima request dan handler, lalu memutuskan apakah handler dipanggil nol kali, sekali, atau berkali-kali. Dengan tiga middleware dalam daftar, satu giliran model berjalan seperti ini:
Hook bergaya node juga bisa mengakhiri run lebih awal dengan mengembalikan jump_to bernilai end, tools, atau model, asalkan hook tersebut mendeklarasikan targetnya lewat can_jump_to. Begitulah middleware limit bawaan menghentikan loop tanpa melempar exception. Middleware kustom juga bisa mendeklarasikan state_schema untuk menambah field sendiri ke state agent, misalnya counter yang dibaca before_model dan dinaikkan after_model.
Pakai decorator seperti @before_model dan @wrap_tool_call untuk satu hook tanpa konfigurasi, dan buat subclass AgentMiddleware ketika satu concern butuh beberapa hook, pengaturan saat init, atau versi sync dan async sekaligus. Setiap hook kelas punya kembaran async berawalan a, misalnya abefore_model, dan agent yang dipanggil dengan ainvoke membutuhkannya.
PIIMiddleware menerima tipe PII, strategi, dan tiga flag cakupan. Tipe bawaannya adalah email, credit_card, ip, mac_address, dan url, dan nama lain pun bisa dipakai bila Anda menyediakan detector berupa string regex, pola yang sudah dikompilasi, atau fungsi. Strateginya adalah block yang melempar PIIDetectionError, redact yang mengganti nilai dengan placeholder REDACTED sesuai nama tipenya, mask yang menyisakan karakter terakhir, dan hash yang mengganti nilai dengan hash deterministik.
from langchain.agents.middleware import PIIMiddleware, PIIDetectionError
pii = [
# Built-in detector. Also scrub tool results: a vendor lookup returns emails.
PIIMiddleware(
"email",
strategy="redact",
apply_to_input=True,
apply_to_tool_results=True,
),
# Custom type: NIK, the 16-digit Indonesian national ID. The regex string
# is the detector; mask keeps the last digits so a human can still match it.
PIIMiddleware(
"nik",
detector=r"\b\d{16}\b",
strategy="mask",
apply_to_input=True,
apply_to_output=True,
apply_to_tool_results=True,
),
# Fail closed: a card number in a chat about invoices is never legitimate.
PIIMiddleware("credit_card", strategy="block", apply_to_input=True),
]
try:
result = agent.invoke({"messages": [{"role": "user", "content": text}]})
except PIIDetectionError:
# Raised by strategy="block". Tell the user, do not echo the input back.
reply = "Please remove the card number and send the request again."Bayangkan agent helpdesk ERP untuk sebuah distributor di Indonesia. Pelanggan menempelkan NIK, nomor induk kependudukan 16 digit, ke chat, dan tool pencarian vendor mengembalikan email kontak langsung dari master data. Tipe kustom nik dengan strategi mask menangani kasus pertama; apply_to_tool_results menangani kasus kedua, yang paling sering terlewat. Hash adalah pilihan yang lebih baik bila model tetap perlu membedakan dua orang tanpa melihat nomor keduanya.
Secara default hanya input pengguna yang dipindai. apply_to_output dan apply_to_tool_results sama-sama False, jadi tanpa diubah, tool yang mengembalikan data pelanggan mengirim setiap identitas di dalamnya ke penyedia model apa adanya. Aktifkan flag tersebut untuk setiap tipe yang bisa muncul di data tool Anda, dan ingat bahwa detector regex hanyalah filter, bukan program kepatuhan.
Dua kelas bawaan menghentikan agent yang berputar-putar. ModelCallLimitMiddleware membatasi jumlah round-trip ke model, dan ToolCallLimitMiddleware membatasi tool call secara global atau untuk satu tool tertentu. Keduanya menerima run_limit, yang direset setiap pesan pengguna, dan thread_limit, yang menghitung sepanjang percakapan sehingga butuh checkpointer untuk menyimpan hitungannya.
from langchain.agents.middleware import (
ModelCallLimitMiddleware,
ToolCallLimitMiddleware,
)
from langchain.agents.middleware.tool_call_limit import ToolCallLimitExceededError
from langgraph.checkpoint.memory import InMemorySaver
budgets = [
# Hard ceiling on model round-trips in one user turn: stops a tool loop.
ModelCallLimitMiddleware(run_limit=12, exit_behavior="end"),
# Soft global cap: over-limit calls get an error ToolMessage and the
# model decides how to wrap up (exit_behavior="continue" is the default).
ToolCallLimitMiddleware(run_limit=20),
# A chatty lookup the model likes to call in a loop.
ToolCallLimitMiddleware(tool_name="search_vendor", run_limit=5),
# The side-effecting tool: one posting per turn, three per conversation,
# and exceeding either is an exception, not a polite message.
ToolCallLimitMiddleware(
tool_name="post_journal_entry",
run_limit=1,
thread_limit=3,
exit_behavior="error",
),
]
agent = create_agent(
model="openai:gpt-5.4-mini",
tools=[lookup_invoice, search_vendor, post_journal_entry],
middleware=budgets,
checkpointer=InMemorySaver(), # required for any thread_limit
)
try:
agent.invoke(payload, config={"configurable": {"thread_id": "ap-0042"}})
except ToolCallLimitExceededError:
alert_finance_team("journal posting budget exceeded", thread="ap-0042")Setiap batas berdiri sendiri, jadi menumpuk batas global dengan batas per tool adalah hal biasa. ModelCallLimitMiddleware hanya punya end dan error sebagai exit behavior, dengan default end yang mengakhiri giliran secara wajar tanpa melempar exception.
Kelas bawaan menangani risiko umum; aturan bisnis butuh middleware sendiri. Fungsi wrap_tool_call melihat nama tool dan argumennya sebelum eksekusi dan bisa mengembalikan ToolMessage tanpa memanggil handler, artinya tool tidak pernah berjalan. Di sini agent ditolak saat mencoba memposting jurnal bertanggal di periode fiskal yang sudah ditutup, dengan tanggal tutup buku dikirim lewat runtime context yang bertipe.
from dataclasses import dataclass
from datetime import date
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call
from langchain.messages import ToolMessage
@dataclass
class ErpContext:
user_id: str
company: str
closed_until: date # end of the last closed fiscal period
@wrap_tool_call
def closed_period_guard(request, handler):
call = request.tool_call
if call["name"] != "post_journal_entry":
return handler(request)
ctx: ErpContext = request.runtime.context
posting_date = date.fromisoformat(call["args"]["posting_date"])
if posting_date <= ctx.closed_until:
# Short-circuit: handler() is never called, so nothing reaches the ERP.
# status="error" tells the model this was a refusal, not a result.
return ToolMessage(
content=(
f"Posting date {posting_date} falls in a closed period "
f"(closed until {ctx.closed_until}). Ask the user for a date "
"in an open period. Do not retry with the same date."
),
tool_call_id=call["id"],
status="error",
)
return handler(request)
agent = create_agent(
model="openai:gpt-5.4-mini",
tools=[lookup_invoice, post_journal_entry],
context_schema=ErpContext,
middleware=[closed_period_guard],
)
agent.invoke(
{"messages": [{"role": "user", "content": "Post the September freight accrual"}]},
context=ErpContext(user_id="u-17", company="PT Contoh", closed_until=date(2026, 8, 31)),
)Tulis pesan penolakan untuk model, bukan untuk log. Pesan yang menyebut aturannya dan langkah berikutnya, yaitu minta tanggal di periode yang masih terbuka, membuat model bisa pulih di giliran yang sama, sedangkan string error polos cenderung memicu panggilan yang sama lagi. Karena pengecekan ini berjalan di Python tanpa panggilan model, ia tidak menambah latensi dan tidak bisa dibujuk untuk mengubah keputusannya.
ModelRetryMiddleware mengulang panggilan model yang gagal dengan exponential backoff; secara default ia melakukan dua retry setelah percobaan pertama, mulai dari satu detik lalu berlipat dua, dengan jitter. ModelFallbackMiddleware menangkap exception dari model utama lalu memanggil rantai di dalamnya lagi dengan setiap model fallback secara bergiliran, dan melempar ulang error terakhir bila semuanya gagal. ToolRetryMiddleware melakukan hal yang sama untuk tool dan bisa dibatasi ke tool dan tipe exception tertentu.
from langchain.agents.middleware import (
ModelFallbackMiddleware,
ModelRetryMiddleware,
ToolRetryMiddleware,
)
# Wrong: ModelRetryMiddleware defaults to on_failure="continue", which turns
# the final error into an AIMessage. The outer fallback only acts on an
# exception, so it never sees one and the user gets an error message instead.
middleware = [
ModelFallbackMiddleware("anthropic:claude-sonnet-4-6"),
ModelRetryMiddleware(max_retries=2),
]
# Right: the exhausted retry raises, the fallback catches it and calls the
# inner chain again with the fallback model, which is retried the same way.
middleware = [
ModelFallbackMiddleware("anthropic:claude-sonnet-4-6"), # outermost
ModelRetryMiddleware(
max_retries=2, # 3 attempts in total
initial_delay=1.0,
backoff_factor=2.0, # 1 s, then 2 s, with jitter
on_failure="error",
),
# Tools get their own retry, scoped to the one that calls a flaky API.
ToolRetryMiddleware(
tools=["lookup_exchange_rate"],
max_retries=3,
retry_on=(ConnectionError, TimeoutError),
on_failure="continue", # the model reads the error and can carry on
),
]Karena hook wrap bersarang, middleware yang ditulis pertama adalah lapisan terluar. Dengan fallback ditulis sebelum retry, setiap model, baik utama maupun fallback, mendapat jatah retry penuh sebelum fallback berpindah, dan biasanya itulah perilaku yang diinginkan untuk error rate limit yang sementara.
ModelRetryMiddleware secara default memakai on_failure continue, yang mengubah error terakhir menjadi AIMessage alih-alih melemparnya. ModelFallbackMiddleware hanya bereaksi terhadap exception, jadi dengan pengaturan default fallback tidak pernah aktif dan pengguna malah membaca pesan error. Set on_failure ke error untuk setiap retry yang berada di dalam fallback.
HumanInTheLoopMiddleware menjeda run setelah model mengusulkan tool call dan sebelum tool dieksekusi. Setiap entri di interrupt_on memetakan nama tool ke True, False, atau konfigurasi berisi allowed_decisions yang dipilih dari approve, edit, reject, dan respond. Sejak langchain 1.3.3, konfigurasi juga bisa menerima predikat when, sehingga hanya panggilan yang memenuhi syarat yang dijeda. Persetujuan membutuhkan checkpointer dan thread_id, karena state yang dijeda harus bertahan sampai ada yang menjawab.
from langchain.agents.middleware import HumanInTheLoopMiddleware, ToolCallRequest
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
APPROVAL_THRESHOLD_IDR = 50_000_000
def needs_approval(request: ToolCallRequest) -> bool:
lines = request.tool_call["args"].get("lines", [])
return sum(line["debit"] for line in lines) >= APPROVAL_THRESHOLD_IDR
hitl = HumanInTheLoopMiddleware(
interrupt_on={
"post_journal_entry": {
"allowed_decisions": ["approve", "edit", "reject"],
"when": needs_approval, # below the threshold: no pause
},
"lookup_invoice": False, # read-only, never pauses
},
description_prefix="Journal entry awaiting finance approval",
)
agent = create_agent(
model="openai:gpt-5.4-mini",
tools=[lookup_invoice, post_journal_entry],
context_schema=ErpContext,
middleware=[hitl],
checkpointer=InMemorySaver(), # use a persistent saver, e.g. AsyncPostgresSaver
)
config = {"configurable": {"thread_id": "ap-2026-10-0042"}}
result = agent.invoke(payload, config=config, context=ctx, version="v2")
for interrupt in result.interrupts:
for action in interrupt.value["action_requests"]:
queue_for_review(action["name"], action["arguments"], thread="ap-2026-10-0042")
# Later, when the reviewer clicks reject on the approval screen.
# One decision per paused action, in the same order as action_requests.
agent.invoke(
Command(resume={"decisions": [{
"type": "reject",
"message": (
"Rejected by finance: wrong cost centre. Ask the user which cost "
"centre to use. Do not post this entry again unchanged."
),
}]}),
config=config,
context=ctx,
version="v2",
)Keputusan reject menerima message yang dikembalikan ke model sebagai hasil tool. Buat pesannya tegas tentang langkah berikutnya, yaitu batalkan, tanya pengguna, atau coba jalur lain, karena teks default hanya menyebut bahwa tool tidak dijalankan. Jangan pakai respond untuk menolak tool yang punya efek samping: dokumentasi memperingatkan bahwa teksnya diperlakukan sebagai hasil tool yang sukses.
Keputusan berupa list dengan satu entri per aksi yang dijeda, dengan urutan yang sama seperti action_requests. Tiga usulan posting sekaligus berarti butuh tiga keputusan, dan jumlah yang tidak cocok akan melempar ValueError alih-alih menebak.
Saat semuanya digabung, daftar middleware bukanlah himpunan; urutannya menentukan siapa melihat apa lebih dulu. Tabel berikut merangkum posisi setiap bagian di contoh di bawah dan alasannya.
| Middleware | Hook | Alasan posisinya |
|---|---|---|
| Entri PIIMiddleware | before_model, after_model | Paling awal di daftar, jadi input sudah dibersihkan sebelum hook lain atau model melihatnya |
| ModelFallbackMiddleware | wrap_model_call | Ditulis sebelum retry, jadi menjadi pembungkus terluar dan baru bertindak setelah retry habis |
| ModelRetryMiddleware | wrap_model_call | Di dalam fallback, dengan on_failure error agar fallback bisa melihat kegagalannya |
| closed_period_guard | wrap_tool_call | Berjalan saat eksekusi, setelah persetujuan, sebagai pengecekan deterministik terakhir sebelum ERP |
| ToolCallLimitMiddleware untuk posting | after_model | Ditulis setelah middleware persetujuan, jadi after_model-nya berjalan lebih dulu dan run yang melewati batas berhenti sebelum ada yang diminta menyetujui |
agent = create_agent(
model="openai:gpt-5.4-mini",
tools=[lookup_invoice, search_vendor, lookup_exchange_rate, post_journal_entry],
system_prompt="You are an accounts-payable assistant for PT Contoh.",
context_schema=ErpContext,
checkpointer=InMemorySaver(),
middleware=[
*pii, # before_model, first
ModelCallLimitMiddleware(run_limit=12),
ModelFallbackMiddleware("anthropic:claude-sonnet-4-6"), # outermost wrap
ModelRetryMiddleware(max_retries=2, on_failure="error"), # inside the fallback
ToolRetryMiddleware(tools=["lookup_exchange_rate"], retry_on=(ConnectionError, TimeoutError)),
closed_period_guard, # at execution time
hitl, # after_model
ToolCallLimitMiddleware( # after_model, runs
tool_name="post_journal_entry", # before hitl because
run_limit=1, # after hooks run
exit_behavior="error", # last to first
),
],
)Satu trade-off masih terlihat di sini. Guard periode tutup buku baru berjalan saat tool dieksekusi, yaitu setelah reviewer menyetujui panggilan, sehingga reviewer bisa menyetujui entri yang kemudian ditolak guard. Untuk kasus yang jarang hal ini masih wajar, dan penolakannya tetap sampai ke model; bila mulai sering terjadi, jalankan pengecekan tanggal yang sama di predikat when juga, agar posting ke periode tertutup tidak pernah masuk antrean persetujuan.
Aturan yang perlu dibawa dari LangChain 1.x: agent kini terdiri dari model, daftar tool, dan daftar middleware yang berurutan, dan urutan itu adalah bagian dari desain. Gunakan kelas bawaan untuk PII, batas, retry, fallback, dan persetujuan, tulis guard wrap_tool_call untuk aturan bisnis, dan periksa setiap default on_failure dan exit_behavior sebelum memercayai stack Anda.