HumanInTheLoopMiddleware: een goedkeurknop voor je LangChain-agent
Een agent die zelfstandig mailtjes verstuurt of SQL uitvoert, is precies zo nuttig als hij eng is. Je wilt hem laten werken, maar niet blind. LangChain heeft daar een kant-en-klaar antwoord voor: HumanInTheLoopMiddleware pauzeert de agent vlak voordat een risicovolle tool draait en wacht op jouw besluit.
Nog niet bekend met het framework? Begin dan bij Wat is LangChain?.
Vier stappen, en je hebt een goedkeurknop.
Stap 1: hang de middleware aan je agent
Middleware geef je mee aan create_agent. Je bepaalt per tool wat er moet gebeuren:
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model="gpt-5.5",
tools=[write_file, execute_sql, read_data],
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"write_file": True,
"execute_sql": {"allowed_decisions": ["approve", "reject"]},
"read_data": False,
},
description_prefix="Tool execution pending approval",
),
],
checkpointer=InMemorySaver(),
)
Drie configuratievormen: True staat alle besluittypes toe, False laat de tool zonder tussenkomst draaien, en een dictionary met allowed_decisions beperkt de keuzes. Bij execute_sql hierboven mag je goedkeuren of weigeren, maar niet de query aanpassen (Bron: LangChain Docs).
De middleware matcht op de naam van de tool. Een functie met de @tool-decorator ontleent die naam aan de functienaam, dus def send_email(...) levert de sleutel "send_email" op.
Stap 2: regel de checkpointer en de thread_id
Dit vergeten mensen, en dan werkt het niet. Pauzeren betekent dat de agent moet stoppen en later precies daar verdergaan — dus moet de tussenstand ergens staan. Zonder checkpointer geen interrupt.
InMemorySaver is prima om te proberen. In productie neem je iets duurzaams als AsyncPostgresSaver of MongoDBSaver, anders verdampt elke openstaande goedkeuring bij een herstart.
Bij het aanroepen geef je een thread_id mee, zodat de pauze aan een gesprek hangt:
config = {"configurable": {"thread_id": "some_id"}}
result = agent.invoke(
{"messages": [{"role": "user", "content": "Verwijder oude records uit de database"}]},
config=config,
version="v2",
)
print(result.interrupts)
Je krijgt een GraphOutput terug met een interrupts-attribuut, waarin de acties staan die om een besluit vragen — inclusief de tool-naam, de argumenten en welke besluiten zijn toegestaan.
Stap 3: kies je besluit
Vier smaken, en het verschil tussen de laatste twee is belangrijker dan het lijkt:
| Besluit | Wat er gebeurt | Wanneer |
|---|---|---|
approve | Tool draait met de originele argumenten | De actie klopt |
edit | Jij past de argumenten aan, dán draait hij | Verkeerde ontvanger, te ruime query |
reject | Tool draait niet, agent krijgt feedback | De actie mag niet |
respond | Jouw bericht komt terug als tool-resultaat | Alleen bij ask-user-tools |
Hervatten doe je met een Command:
from langgraph.types import Command
agent.invoke(
Command(resume={"decisions": [{"type": "approve"}]}),
config=config,
version="v2",
)
Staan er meerdere acties in de wacht, dan geef je één besluit per actie, in dezelfde volgorde als ze in het interrupt-verzoek staan.
Twee dingen om te onthouden. Bij reject kun je een message meegeven die uitlegt waarom, en of de agent iets anders moet proberen — laat je die weg, dan krijgt het model een standaardbericht dat het niet opnieuw moet proberen. En gebruik respond nooit om een actie met bijwerkingen te weigeren: dat bericht komt binnen als een geslaagd resultaat, en dan denkt je model dat de mail verstuurd is.
Nog een subtiliteit bij edit: pas argumenten terughoudend aan. Grote wijzigingen kunnen het model doen besluiten zijn hele aanpak te herzien, met dubbele tool-aanroepen of onverwachte stappen tot gevolg.
Stap 4: pauzeer alleen wanneer het spannend wordt
Standaard onderbreekt elke aanroep in interrupt_on. Dat wordt vermoeiend als 90% van je queries onschuldige SELECT’s zijn. Met een when-predicaat toets je op de argumenten:
from langchain.agents.middleware import HumanInTheLoopMiddleware, ToolCallRequest
def is_write_query(request: ToolCallRequest) -> bool:
query = request.tool_call["args"].get("query", "")
return not query.lstrip().upper().startswith("SELECT")
HumanInTheLoopMiddleware(
interrupt_on={
"execute_sql": {
"allowed_decisions": ["approve", "reject"],
"when": is_write_query,
},
},
)
Geeft het predicaat False terug, dan draait de aanroep gewoon door en belandt hij nooit in de interrupt-batch. Je reviewer ziet dus alleen wat er echt toe doet. Deze optie vereist langchain>=1.3.3.
Waarom dit meer is dan een veiligheidsmaatregel
Een goedkeurknop wordt vaak gezien als rem. In de praktijk is het vooral wat je nodig hebt om een agent überhaupt in productie te durven zetten: je zet het aan voor de drie tools die iets onomkeerbaars doen, en de rest draait vrij. Het scheelt ook tokens: een agent die pas na twintig stappen wordt teruggefloten, heeft die twintig stappen wel betaald. Wat je verder aan je agent kunt meegeven zonder hem te overladen, staat in Context engineering: geef je LangChain-agent alleen wat nodig is.
Waar de discussie over zelfstandig opererende agents nu staat, lees je in de achtergrond AI-agents in 2026: wat zijn ze en wat kun je er echt mee? op hetlaatsteainieuws.nl.
