langchain-middleware

Par langchain-ai · langchain-skills

Invoquez cette skill lorsque vous avez besoin d'une approbation humaine dans la boucle, d'un middleware personnalisé ou d'une sortie structurée. Couvre `HumanInTheLoopMiddleware` pour l'approbation humaine des appels d'outils dangereux, la création de middleware personnalisé avec des hooks, les patterns de reprise avec `Command`, ainsi que la sortie structurée avec Pydantic/Zod.

npx skills add https://github.com/langchain-ai/langchain-skills --skill langchain-middleware

<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) — appeler handler(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. Retourner None ou 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>

Skills similaires