Flujos de Aprobación
Intermedio⏱35 min📖3 min de lecturaLección 6 de 10

Flujos de Aprobación

Implementa flujos de aprobación estructurados con interrupciones condicionales, interrupciones antes/después de nodos y enrutamiento dinámico humano-en-el-bucle.

Los flujos de aprobación combinan interrupts con enrutamiento condicional para crear procesos estructurados con humano en el bucle. Esta lección cubre interrupts antes del nodo, interrupts después del nodo y patrones dinámicos de aprobación.


Visión General del Patrón de Aprobación

Agent proposes action → Human reviews → Approve? → Yes → Execute ↓ No Reject → Provide feedback → Revise or Stop

El agente presenta una propuesta, la persona decide y la ejecución sigue en consecuencia.


Interrupción Antes del Nodo

Interrumpe antes de que se ejecute un nodo: la persona decide si el nodo debe ejecutarse:

python
from langgraph.graph import StateGraph, START, END from langgraph.checkpoint.memory import MemorySaver from langgraph.types import interrupt, Command from typing_extensions import TypedDict class ApprovalState(TypedDict): query: str search_plan: str approved: bool feedback: str result: str def plan_node(state: ApprovalState) -> dict: plan = f"Search for: {state['query']}" return {"search_plan": plan} def approve_search(state: ApprovalState) -> dict: # BEFORE interrupt — human decides if search should happen response = interrupt({ "type": "approval", "agent_plan": state["search_plan"], "prompt": "Approve this search operation?" }) return {"approved": response.get("approved", False), "feedback": response.get("feedback")} def execute_search(state: ApprovalState) -> dict: if not state["approved"]: return {"result": f"Rejected. Feedback: {state.get('feedback')}"} return {"result": f"Executed: {state['search_plan']}"} def after_search_review(state: ApprovalState) -> dict: # AFTER interrupt — human reviews results response = interrupt({ "type": "review", "result": state["result"], "prompt": "Are these results acceptable?" }) return {"approved": response.get("approved", False)} builder = StateGraph(ApprovalState) builder.add_node("plan", plan_node) builder.add_node("approve_search", approve_search) builder.add_node("execute", execute_search) builder.add_edge(START, "plan") builder.add_edge("plan", "approve_search") builder.add_edge("approve_search", "execute") builder.add_edge("execute", END) app = builder.compile(checkpointer=MemorySaver())
ℹ️Nota

Un interrupt antes del nodo impide que una acción se produzca hasta que se apruebe. Es el patrón de aprobación más común: evita acciones no autorizadas o arriesgadas.


Interrupción Después del Nodo

Interrumpe después de que se ejecute un nodo: la persona revisa el resultado:

python
def draft_email(state: EmailState) -> dict: email = llm.invoke(f"Draft email about: {state['topic']}") return {"draft": email.content} def review_draft(state: EmailState) -> dict: # AFTER interrupt — human reviews the draft response = interrupt({ "type": "review", "draft": state["draft"], "prompt": "Edit or approve this email draft?" }) return { "approved": response.get("approved", False), "revision": response.get("revision", "") } def finalize_or_revise(state: EmailState) -> dict: if state["approved"]: return {"final": state["draft"]} return {"draft": state["revision"], "approved": False}
💡Consejo

Los interrupts después del nodo son útiles para la revisión de contenido, la validación de resultados y cualquier flujo de trabajo en que la persona deba verificar la salida antes de usarla.


Enrutamiento Condicional de Aprobación

Enruta según lo que la persona haya aprobado o rechazado:

python
def approval_router(state: ApprovalState) -> str: if state["approved"]: return "approved" return "rejected" builder.add_conditional_edges( "human_review", approval_router, { "approved": "execute", "rejected": "revise_plan" # Send back for revision } )

Patrón de Bucle de Revisión

python
class RevisionState(TypedDict): draft: str approved: bool revision_count: int def generate_draft(state: RevisionState) -> dict: draft = llm.invoke(f"Write content about: {state['topic']}") return {"draft": draft.content} def human_review(state: RevisionState) -> dict: response = interrupt({ "draft": state["draft"], "revision_count": state["revision_count"], "prompt": "Approve, or provide revision instructions." }) return { "approved": response.get("approved", False), "feedback": response.get("feedback", ""), "revision_count": state["revision_count"] + 1 } def revise_draft(state: RevisionState) -> dict: revised = llm.invoke( f"Revise this draft based on feedback:\n" f"Draft: {state['draft']}\nFeedback: {state['feedback']}" ) return {"draft": revised.content} def revision_router(state: RevisionState) -> str: if state["approved"]: return "finalize" if state["revision_count"] >= 5: return "max_revisions" return "revise" builder.add_conditional_edges( "human_review", revision_router, { "finalize": "finalize", "revise": "revise_draft", "max_revisions": "error_handler" } ) builder.add_edge("revise_draft", "human_review") # Loop back for another review
⚠️Advertencia

Limita siempre los bucles de revisión. Sin un máximo, un revisor insatisfecho podría quedar en bucle para siempre.


Flujo de Escalada

Enruta a un revisor sénior si el junior rechaza o si la solicitud es de alto riesgo:

python
class EscalationState(TypedDict): request: str risk_level: str junior_approved: bool senior_approved: bool def junior_review(state: EscalationState) -> dict: response = interrupt({"request": state["request"], "risk": state["risk_level"]}) return {"junior_approved": response.get("approved", False)} def escalate(state: EscalationState) -> dict: response = interrupt({"request": state["request"], "risk": state["risk_level"], "note": "Escalated from junior review"}) return {"senior_approved": response.get("approved", False)} def escalation_router(state: EscalationState) -> str: if state["junior_approved"]: return "approved" if state["risk_level"] == "high": return "escalate" return "rejected" builder.add_conditional_edges( "junior_review", escalation_router, { "approved": "execute", "escalate": "senior_review", "rejected": "reject_node" } )

Aprobación con Datos Dinámicos

Incluye los datos relevantes en la solicitud de aprobación:

python
def approval_with_context(state: State) -> dict: # Gather context for the human reviewer context = { "request_id": state["request_id"], "requester": state["user_name"], "action": state["proposed_action"], "cost_estimate": state.get("estimated_cost", "Unknown"), "similar_approved_count": state.get("similar_count", 0), "policy_reference": "Policy #42: External API calls require approval", "timestamp": datetime.now().isoformat() } response = interrupt(context) return {"approval_response": response}

Timeout para Aprobaciones

Maneja el caso en que la persona nunca responde:

python
def approval_with_timeout(state: State) -> dict: # Set a timeout via configuration config = state.get("config", {}) timeout = config.get("approval_timeout_minutes", 60) response = interrupt({ "prompt": "Approve within {timeout} minutes or it will be auto-rejected.", "request": state["request"] }) # If human doesn't respond within timeout, # calling invoke again with a timeout command handles it return {"human_response": response}
ℹ️Nota

LangGraph no tiene un timeout integrado para los interrupts. Implementa la lógica de timeout en la capa de la aplicación comprobando cuánto tiempo lleva pausado un hilo.


Ejemplo Completo de Flujo de Aprobación

python
from langgraph.graph import StateGraph, START, END from langgraph.checkpoint.memory import MemorySaver from langgraph.types import interrupt, Command from typing_extensions import TypedDict from datetime import datetime class WorkflowState(TypedDict): request: str risk_score: float draft_response: str approved: bool feedback: str result: str step: str def analyze_request(state: WorkflowState) -> dict: risk = len(state["request"]) * 0.01 # Simplified risk calc return {"risk_score": min(risk, 1.0), "step": "analyzed"} def generate_draft(state: WorkflowState) -> dict: draft = f"Response to: {state['request']}" return {"draft_response": draft, "step": "drafted"} def human_approval(state: WorkflowState) -> dict: response = interrupt({ "request": state["request"], "draft": state["draft_response"], "risk": state["risk_score"], "timestamp": datetime.now().isoformat() }) return {"approved": response.get("approved", False), "feedback": response.get("feedback", ""), "step": "reviewed"} def execute(state: WorkflowState) -> dict: if state["approved"]: return {"result": state["draft_response"], "step": "completed"} return {"result": f"Rejected: {state.get('feedback')}", "step": "rejected"} builder = StateGraph(WorkflowState) builder.add_node("analyze", analyze_request) builder.add_node("draft", generate_draft) builder.add_node("approve", human_approval) builder.add_node("execute", execute) builder.add_edge(START, "analyze") builder.add_edge("analyze", "draft") builder.add_edge("draft", "approve") builder.add_edge("approve", "execute") builder.add_edge("execute", END) app = builder.compile(checkpointer=MemorySaver())

Preguntas de Práctica (Practice Questions)

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

What is a before-node interrupt?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

What is an after-node interrupt useful for?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

How do you create a revision loop in an approval workflow?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

Why should revision loops have a maximum count?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

What is an escalation workflow?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

How do you include context for the human reviewer in an interrupt?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

What determines which path the graph takes after an approval interrupt?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

What happens when a human rejects a proposal in an approval workflow?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

Can an approval workflow have multiple sequential approval steps?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

How does the approval workflow pattern enhance LangGraph agents?


✅Éxito

Conclusiones Clave

  • Los interrupts antes del nodo impiden acciones hasta la aprobación humana
  • Los interrupts después del nodo permiten que las personas revisen los resultados antes de continuar
  • Las aristas condicionales enrutan según las decisiones de aprobación/rechazo
  • Los bucles de revisión se repiten hasta la aprobación o hasta alcanzar el máximo de revisiones
  • Los flujos de escalamiento enrutan a revisores sénior cuando es necesario
  • Pasa un contexto rico en interrupt() para ayudar a las personas a tomar decisiones informadas
  • Limita siempre los bucles de revisión y escalamiento con recuentos máximos
  • Se pueden encadenar múltiples aprobaciones secuenciales en un solo grafo
Progreso60%