<overview> Motifs middleware pour agents LangChain en production :
- HumanInTheLoopMiddleware / humanInTheLoopMiddleware : Mettre en pause avant les appels d'outils dangereux pour approbation humaine
- Middleware personnalisé : Intercepter les appels d'outils pour gestion d'erreurs, logging, logique de retry
- Reprise de commande : Continuer l'exécution après décisions humaines (approuver, éditer, rejeter)
Prérequis : Configuration Checkpointer + thread_id pour tous les workflows HITL. </overview>
Human-in-the-Loop
<ex-basic-hitl-setup> <python> Configurer un agent avec middleware HITL qui met en pause avant l'envoi d'emails pour approbation.
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import MemorySaver
from langchain.tools import tool
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email."""
return f"Email sent to {to}"
agent = create_agent(
model="gpt-4.1",
tools=[send_email],
checkpointer=MemorySaver(), # Required for HITL
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"send_email": {"allowed_decisions": ["approve", "edit", "reject"]},
}
)
],
)
</python> <typescript> Configurer un agent avec HITL qui met en pause avant l'envoi d'emails pour approbation humaine.
import { createAgent, humanInTheLoopMiddleware } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const sendEmail = tool(
async ({ to, subject, body }) => `Email sent to ${to}`,
{
name: "send_email",
description: "Send an email",
schema: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
}
);
const agent = createAgent({
model: "anthropic:claude-sonnet-4-5",
tools: [sendEmail],
checkpointer: new MemorySaver(),
middleware: [
humanInTheLoopMiddleware({
interruptOn: { send_email: { allowedDecisions: ["approve", "edit", "reject"] } },
}),
],
});
</typescript> </ex-basic-hitl-setup>
<ex-running-with-interrupts> <python> Exécuter l'agent, détecter une interruption, puis reprendre l'exécution après approbation humaine.
from langgraph.types import Command
config = {"configurable": {"thread_id": "session-1"}}
# Step 1: Agent runs until it needs to call tool
result1 = agent.invoke({
"messages": [{"role": "user", "content": "Send email to john@example.com"}]
}, config=config)
# Check for interrupt
if "__interrupt__" in result1:
print(f"Waiting for approval: {result1['__interrupt__']}")
# Step 2: Human approves
result2 = agent.invoke(
Command(resume={"decisions": [{"type": "approve"}]}),
config=config
)
</python> <typescript> Exécuter l'agent, détecter une interruption, puis reprendre l'exécution après approbation humaine.
import { Command } from "@langchain/langgraph";
const config = { configurable: { thread_id: "session-1" } };
// Step 1: Agent runs until it needs to call tool
const result1 = await agent.invoke({
messages: [{ role: "user", content: "Send email to john@example.com" }]
}, config);
// Check for interrupt
if (result1.__interrupt__) {
console.log(`Waiting for approval: ${result1.__interrupt__}`);
}
// Step 2: Human approves
const result2 = await agent.invoke(
new Command({ resume: { decisions: [{ type: "approve" }] } }),
config
);
</typescript> </ex-running-with-interrupts>
<ex-editing-tool-arguments> <python> Éditer les arguments de l'outil avant approbation quand les valeurs originales nécessitent une correction.
# Human edits the arguments — edited_action must include name + args
result2 = agent.invoke(
Command(resume={
"decisions": [{
"type": "edit",
"edited_action": {
"name": "send_email",
"args": {
"to": "alice@company.com", # Fixed email
"subject": "Project Meeting - Updated",
"body": "...",
},
},
}]
}),
config=config
)
</python> <typescript> Éditer les arguments de l'outil avant approbation quand les valeurs originales nécessitent une correction.
// Human edits the arguments — editedAction must include name + args
const result2 = await agent.invoke(
new Command({
resume: {
decisions: [{
type: "edit",
editedAction: {
name: "send_email",
args: {
to: "alice@company.com", // Fixed email
subject: "Project Meeting - Updated",
body: "...",
},
},
}]
}
}),
config
);
</typescript> </ex-editing-tool-arguments>
<ex-rejecting-with-feedback> <python> Rejeter un appel d'outil et fournir un retour expliquant pourquoi il a été rejeté.
# Human rejects
result2 = agent.invoke(
Command(resume={
"decisions": [{
"type": "reject",
"feedback": "Cannot delete customer data without manager approval",
}]
}),
config=config
)
</python> </ex-rejecting-with-feedback>
<ex-multiple-tools-different-policies> <python> Configurer différentes politiques HITL pour chaque outil en fonction du niveau de risque.
agent = create_agent(
model="gpt-4.1",
tools=[send_email, read_email, delete_email],
checkpointer=MemorySaver(),
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"send_email": {"allowed_decisions": ["approve", "edit", "reject"]},
"delete_email": {"allowed_decisions": ["approve", "reject"]}, # No edit
"read_email": False, # No HITL for reading
}
)
],
)
</python> </ex-multiple-tools-different-policies>
<boundaries>
Ce que vous POUVEZ configurer
- Quels outils nécessitent une approbation (politiques par outil)
- Décisions autorisées par outil (approuver, éditer, rejeter)
- Hooks middleware personnalisés :
before_model,after_model,wrap_tool_call,before_agent,after_agent - Middleware spécifique à l'outil (s'applique uniquement à certains outils) </boundaries>
Custom Middleware Hooks
Six hooks décorateurs sont disponibles. Deux motifs :
- Hooks wrap (
wrap_tool_call,wrap_model_call) :(request, handler)— appelerhandler(request)pour procéder, ou revenir tôt pour court-circuiter. - Hooks before/after (
before_model,after_model,before_agent,after_agent) :(state, runtime)— inspecter ou modifier l'état. RetournerNoneou un dictionnaire de mises à jour d'état.
<ex-wrap-tool-call>
<python>
@wrap_tool_call intercepte l'exécution de l'outil. Ne pas utiliser yield — cela crée un générateur et provoque une NotImplementedError.
from langchain.agents.middleware import wrap_tool_call
@wrap_tool_call
def retry_middleware(request, handler):
for attempt in range(3):
try:
return handler(request)
except Exception:
if attempt == 2:
raise
@wrap_tool_call
def guard_middleware(request, handler):
if request.tool_call["name"] == "dangerous_tool":
return "This tool is disabled" # short-circuit
return handler(request)
</python>
<typescript>
createMiddleware({ wrapToolCall }) intercepte l'exécution de l'outil.
import { createMiddleware } from "langchain";
const retryMiddleware = createMiddleware({
wrapToolCall: async (request, handler) => {
for (let attempt = 0; attempt < 3; attempt++) {
try { return await handler(request); }
catch (e) { if (attempt === 2) throw e; }
}
},
});
</typescript> </ex-wrap-tool-call>
<ex-before-after-hooks>
<python>
before_model / after_model / before_agent / after_agent partagent tous la signature (state, runtime).
from langchain.agents.middleware import before_model, after_model
@before_model
def log_calls(state, runtime):
print(f"Calling model with {len(state['messages'])} messages")
@after_model
def check_output(state, runtime):
print(f"Model responded")
</python>
<typescript>
Tous les hooks before/after partagent la même signature (state, runtime) via createMiddleware.
import { createMiddleware } from "langchain";
const loggingMiddleware = createMiddleware({
beforeModel: (state, runtime) => {
console.log(`Calling model with ${state.messages.length} messages`);
},
afterModel: (state, runtime) => {
console.log("Model responded");
},
});
</typescript> </ex-before-after-hooks>
<boundaries>
Ce que vous NE POUVEZ PAS configurer
- Interruption après l'exécution de l'outil (doit être avant)
- Ignorer l'exigence du checkpointer pour HITL </boundaries>
<fix-missing-checkpointer> <python> Le middleware HITL nécessite un checkpointer pour persister l'état.
# WRONG
agent = create_agent(model="gpt-4.1", tools=[send_email], middleware=[HumanInTheLoopMiddleware({...})])
# CORRECT
agent = create_agent(
model="gpt-4.1", tools=[send_email],
checkpointer=MemorySaver(), # Required
middleware=[HumanInTheLoopMiddleware({...})]
)
</python> <typescript> HITL nécessite un checkpointer pour persister l'état.
// WRONG: No checkpointer
const agent = createAgent({
model: "anthropic:claude-sonnet-4-5", tools: [sendEmail],
middleware: [humanInTheLoopMiddleware({ interruptOn: { send_email: true } })],
});
// CORRECT: Add checkpointer
const agent = createAgent({
model: "anthropic:claude-sonnet-4-5", tools: [sendEmail],
checkpointer: new MemorySaver(),
middleware: [humanInTheLoopMiddleware({ interruptOn: { send_email: true } })],
});
</typescript> </fix-missing-checkpointer>
<fix-no-thread-id> <python> Toujours fournir thread_id lors de l'utilisation de HITL pour suivre l'état de la conversation.
# WRONG
agent.invoke(input) # No config!
# CORRECT
agent.invoke(input, config={"configurable": {"thread_id": "user-123"}})
</python> </fix-no-thread-id>
<fix-wrong-resume-syntax> <python> Utiliser la classe Command pour reprendre l'exécution après une interruption.
# WRONG
agent.invoke({"resume": {"decisions": [...]}})
# CORRECT
from langgraph.types import Command
agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config=config)
</python> <typescript> Utiliser la classe Command pour reprendre l'exécution après une interruption.
// WRONG
await agent.invoke({ resume: { decisions: [...] } });
// CORRECT
import { Command } from "@langchain/langgraph";
await agent.invoke(new Command({ resume: { decisions: [{ type: "approve" }] } }), config);
</typescript> </fix-wrong-resume-syntax>