LangGraph: Human-in-the-Loop (interrupt și Command) în LangChain & LangGraph

Oprește graful înaintea acțiunilor sensibile și așteaptă decizia unui om: interrupt() în nod, Command(resume=...) la reluare.

Sintaxă & invocare Python
from langgraph.types import interrupt, Command

def cere_aprobare(state: State):
    raspuns = interrupt({"intrebare": "Aprobi plata?", "suma": state["suma"]})
    return {"aprobat": raspuns}

rez = graph.invoke({"suma": 500, "aprobat": False}, config)   # se oprește la interrupt
graph.invoke(Command(resume=True), config)                   # reia cu răspunsul omului

interrupt() din langgraph.types oprește graful în interiorul unui nod, salvează starea prin checkpointer și așteaptă oricât timp. Valoarea dată (orice valoare serializabilă JSON) ajunge la apelant: cu invoke() în result["__interrupt__"], cu stream_events(..., version="v3") în stream.interrupts. Execuția se reia cu graph.invoke(Command(resume=valoare), config) pe același thread_id, iar valoare devine rezultatul apelului interrupt().

Sunt necesare un checkpointer (persistent în producție) și un thread_id. La reluare, nodul se execută din nou de la început, deci codul de dinaintea lui interrupt() rulează a doua oară; efectele secundare de acolo trebuie să fie idempotente. interrupt() funcționează aruncând o excepție specială, de aceea nu se pune într-un try/except generic. Din langgraph 1.2.12, parametrul response_schema validează răspunsul omului.

interrupt_before și interrupt_after (la compile() sau la invoke()) sunt breakpoint-uri statice, reluate cu invoke(None, config). Documentația le recomandă pentru depanare, nu pentru human-in-the-loop. La agenții creați cu create_agent, aprobarea uneltelor se face cu HumanInTheLoopMiddleware, cu deciziile approve, edit, reject și respond.

Concepte cheie și caracteristici

Concept / Parametru Descriere practică
interrupt(valoare) Oprește nodul și trimite valoare apelantului.
Command(resume=...) Reia execuția; valoarea devine rezultatul lui interrupt(). Este singura formă de Command destinată ca intrare pentru invoke.
result["__interrupt__"] Unde apar cererile de intervenție la invoke().
response_schema Model Pydantic, TypedDict sau dataclass care validează răspunsul (din langgraph 1.2.12).
interrupt_before / interrupt_after Breakpoint-uri statice înainte sau după noduri; pentru depanare.
invoke(None, config) Reia după un breakpoint static.
get_state(config).next Nodurile care urmează să ruleze.

Exemple practice și scenarii reale

Exemplul 1: Aprobarea unei plăți

from typing_extensions import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command

class State(TypedDict):
    suma: int
    aprobat: bool

def cere_aprobare(state: State):
    raspuns = interrupt({"intrebare": "Aprobi plata?", "suma": state["suma"]})
    return {"aprobat": raspuns}

def executa_plata(state: State):
    if state["aprobat"]:
        ...   # apelul real către sistemul de plăți
    return {}

builder = StateGraph(State)
builder.add_node("cere_aprobare", cere_aprobare)
builder.add_node("executa_plata", executa_plata)
builder.add_edge(START, "cere_aprobare")
builder.add_edge("cere_aprobare", "executa_plata")
builder.add_edge("executa_plata", END)
graph = builder.compile(checkpointer=InMemorySaver())

config = {"configurable": {"thread_id": "plata-1"}}
rez = graph.invoke({"suma": 500, "aprobat": False}, config)
print(rez["__interrupt__"])
graph.invoke(Command(resume=True), config)

Exemplul 2: Breakpoint static pentru depanare

graph = builder.compile(checkpointer=InMemorySaver(), interrupt_before=["executa_plata"])
graph.invoke({"suma": 500, "aprobat": True}, config)
print(graph.get_state(config).next)
graph.invoke(None, config)

Greșeli frecvente și capcane

  • Să folosești interrupt_before pentru aprobări în producție. Documentația spune că breakpoint-urile statice nu sunt recomandate pentru human-in-the-loop; folosește interrupt().
  • Să trimiți Command(update=...) ca intrare pentru a continua o conversație. Doar Command(resume=...) este destinat ca intrare; pentru un mesaj nou trimiți un dicționar obișnuit.
  • Efecte secundare înainte de interrupt() (scriere în bază, e-mail). La reluare nodul rulează din nou de la început și le repetă.

Noțiuni și module înrudite