Pattern: Human-in-the-Loop¶
Motivation¶
Apprentices learn under a master's guidance. Surgeons have assistants for critical steps. Editors review writers' work before publication. Humans naturally incorporate oversight, feedback, and collaboration into complex processes. The Human-in-the-Loop pattern brings this to agents: integrating human judgment, feedback, and decision-making into agent workflows for safety, quality, and trust, especially in high-stakes situations.
"AI is becoming a collaborator, not a component." — Andrej Karpathy
Pattern Overview¶
What it is: Human-in-the-Loop (HITL) is a pattern that strategically integrates human oversight, judgment, and intervention into AI agent workflows, creating a symbiotic partnership between human intelligence and AI capabilities.
When to use: Use this pattern when deploying AI in domains where errors have significant safety, ethical, or financial consequences, such as healthcare, finance, or autonomous systems. It is essential for tasks involving ambiguity and nuance that LLMs cannot reliably handle.
Why it matters: AI systems, including advanced LLMs, often struggle with tasks that require nuanced judgment, ethical reasoning, or a deep understanding of complex, ambiguous contexts. Deploying fully autonomous AI in high-stakes environments carries significant risks, as errors can lead to severe safety, financial, or ethical consequences. HITL ensures that AI operates within ethical boundaries, adheres to safety protocols, and achieves objectives with optimal effectiveness.
The Human-in-the-Loop pattern represents a pivotal strategy in the development and deployment of Agents. It deliberately interweaves the unique strengths of human cognition—such as judgment, creativity, and nuanced understanding—with the computational power and efficiency of AI. This strategic integration is not merely an option but often a necessity, especially as AI systems become increasingly embedded in critical decision-making processes.
HITL acknowledges that even with rapidly advancing AI technologies, human oversight, strategic input, and collaborative interactions remain indispensable. The approach fundamentally revolves around the idea of synergy between artificial and human intelligence. Rather than viewing AI as a replacement for human workers, HITL positions AI as a tool that augments and enhances human capabilities. This augmentation can take various forms, from automating routine tasks to providing data-driven insights that inform human decisions.
HITL encompasses several key aspects: Human Oversight (monitoring AI performance and output), Intervention and Correction (humans rectifying errors or guiding agents), Human Feedback for Learning (collecting feedback to refine models), Decision Augmentation (AI provides analysis, humans make final decisions), Human-Agent Collaboration (cooperative interaction leveraging respective strengths), and Escalation Policies (protocols for when agents should escalate to humans).
Key Concepts¶
- Interruptible Workflow: The agent workflow can be paused at specific nodes to request human input, approval, or guidance.
- Human Oversight: Monitoring AI agent performance and output to ensure adherence to guidelines and prevent undesirable outcomes.
- Intervention and Correction: Human operators rectifying errors, supplying missing data, or guiding agents when they encounter problems.
- Action Handlers: Different types of human interactions (confirmations, text input, selections) with corresponding handlers that process responses and route workflow accordingly.
- State Preservation: Agent state is preserved during workflow pauses, enabling seamless resumption after human response.
- Configurable Approval Points: Workflow can be configured to interrupt at specific nodes (after tool calls, before critical actions, etc.) for human approval.
- Human Feedback for Learning: Collecting and using human feedback to refine AI models, prominently in reinforcement learning with human feedback.
- Decision Augmentation: AI provides analyses and recommendations, humans make final decisions, enhancing decision-making through AI-generated insights.
- Escalation Policies: Established protocols dictating when and how agents should escalate tasks to human operators.
How It Works¶
HITL works through an interruptible workflow pattern that strategically pauses agent execution at configurable approval points:
1. Workflow Interruption
When an agent determines human input is needed, it creates a FollowUpAction specifying:
- Action Type: The type of interaction needed (confirmation, text input, selection, etc.)
- Action Description: What the human needs to do or decide
- Return Point: Which node to return to after human response
2. Suggest Human Actions
The SuggestHumanActions node:
- Receives the action request from the agent
- Adds it to the conversation history
- Routes to WaitForResponse node
3. Wait for Response
The WaitForResponse node:
- Uses graph interrupts to pause workflow execution
- Preserves complete agent state (variables, history, context)
- Waits for human response via the interrupt mechanism
- Captures the human's response in ActionResponse format
4. Resume with Response
After receiving human response:
- State is restored with the human's input
- Action handlers process the response based on action_id
- Workflow resumes at the appropriate node (typically the node that requested the interruption)
5. Action Handlers Different action types have corresponding handlers: - Confirmation Actions: Approve/reject decisions (e.g., tool execution, flow approval) - Text Input Actions: Natural language responses, additional information - Selection Actions: Choose from predefined options - Custom Actions: Domain-specific handlers for specialized workflows
Implementation Components:
- SuggestHumanActions Node: Initiates human interaction, routes to wait node
- WaitForResponse Node: Pauses workflow using graph interrupts, preserves state
- InterruptToolNode: Special interrupt point after tool calls for approval
- Action Handlers: Process human responses and route workflow accordingly
State Preservation: The complete agent state (messages, variables, execution history, context) is preserved during the pause, ensuring seamless resumption. The workflow can resume exactly where it left off, with the human's response incorporated into the state.
Configurable Approval Points:
Approval points can be configured at:
- After tool/API calls (via InterruptToolNode)
- Before critical actions (via agent decision to escalate)
- At specific workflow nodes (via conditional routing)
- Based on confidence thresholds or policy violations
Benefits¶
The interruptible workflow pattern provides several key benefits:
-
Safety and Compliance: Human approval at critical points ensures compliance with regulations, safety protocols, and ethical guidelines. Critical actions (e.g., financial transactions, medical decisions) can be reviewed before execution.
-
User Control: Users maintain control over agent behavior, approving or rejecting actions, providing guidance, and making final decisions. This builds trust and ensures alignment with user intent.
-
Error Recovery: When agents encounter errors or ambiguous situations, human intervention can provide correction, additional context, or alternative approaches, enabling graceful error recovery.
-
Policy Enforcement: Approval points can enforce organizational policies, ensuring agents don't violate constraints or exceed authority. Policy violations trigger human review automatically.
-
Quality Assurance: Human review of outputs ensures quality standards are met, especially for high-stakes or public-facing content. Review workflows can catch errors before they impact users.
-
Learning and Adaptation: Human feedback collected during HITL interactions can be used to improve agent behavior, refine policies, and enhance future autonomous performance.
When to Use This Pattern¶
✅ Use this pattern when:¶
- High-stakes decisions: Errors have significant safety, ethical, or financial consequences.
- Ambiguous scenarios: Tasks involve nuance and ambiguity that LLMs cannot reliably handle.
- Ethical considerations: Decisions require ethical reasoning or moral judgment.
- Quality requirements: Outputs must meet high quality standards requiring human validation.
- Learning from feedback: You want to continuously improve AI models with high-quality human-labeled data.
❌ Avoid this pattern when:¶
- High-volume, low-stakes tasks: The task requires scale that human oversight cannot provide.
- Real-time constraints: Human intervention adds unacceptable latency for time-sensitive applications.
- Simple, deterministic tasks: The task is straightforward enough that AI can handle it autonomously.
- Cost constraints: Human oversight is too expensive for the use case.
- Privacy concerns: Sensitive information cannot be exposed to human operators.
Decision Guidelines¶
Use HITL when the benefits of human judgment and oversight outweigh the costs of reduced scalability and increased latency. Consider: the criticality of decisions (more critical = more need for HITL), the ambiguity of tasks (more ambiguous = more need for HITL), and the availability of human expertise (expertise available = effective HITL). Be aware that HITL has significant caveats: lack of scalability, dependence on skilled operators, and privacy concerns requiring data anonymization. For production systems, implement hybrid approaches combining automation for scale with HITL for accuracy.
Practical Applications & Use Cases¶
The Human-in-the-Loop pattern is vital across a wide range of industries and applications, particularly where accuracy, safety, ethics, or nuanced understanding are paramount.
- Content Moderation: AI filters content rapidly, but ambiguous or borderline cases are escalated to human moderators for nuanced judgment.
- Autonomous Driving: Self-driving cars handle most tasks autonomously but hand over control to human drivers in complex or dangerous situations.
- Financial Fraud Detection: AI flags suspicious transactions, but high-risk or ambiguous alerts are sent to human analysts for investigation and final determination.
- Legal Document Review: AI scans and categorizes documents, but human legal professionals review findings for accuracy, context, and legal implications.
- Customer Support: Chatbots handle routine inquiries, but complex or emotionally charged issues are seamlessly handed over to human support agents.
- Data Labeling: Humans accurately label images, text, or audio to provide ground truth for AI training.
- Generative AI Refinement: Human editors review and refine AI-generated creative content to ensure it meets brand guidelines and quality standards.
Implementation¶
Core Components¶
Interruptible Workflow Pattern:
from typing import Literal, Optional, List, TypedDict, Any
from enum import Enum
from pydantic import BaseModel, Field
from langgraph.types import Command, interrupt
from langchain_core.messages import AIMessage
class ActionType(str, Enum):
"""Types of human interactions."""
CONFIRMATION = "confirmation"
NATURAL_LANGUAGE = "natural_language"
TEXT_INPUT = "text_input"
SELECT = "select"
MULTI_SELECT = "multi_select"
class SelectOption(BaseModel):
"""Option for select actions."""
value: str
label: str
class FollowUpAction(BaseModel):
"""Action request for human input."""
action_id: str
action_name: str
description: str
type: ActionType
options: Optional[List[SelectOption]] = None
placeholder: Optional[str] = None
button_text: Optional[str] = None
class ActionResponse(BaseModel):
"""Human response to action request."""
action_id: str
response_type: ActionType
text_response: Optional[str] = None
confirmed: Optional[bool] = None
selected_values: Optional[List[str]] = None
class AgentState(TypedDict, total=False):
"""Agent state with HITL support."""
messages: List[AIMessage]
hitl_action: Optional[FollowUpAction]
hitl_response: Optional[ActionResponse]
sender: str
tool_call: Optional[dict]
next_action: Optional[dict]
context: Dict[str, Any]
# Usage
action = FollowUpAction(
action_id="approve_tool",
action_name="Approve Tool",
description="Approve execution?",
type=ActionType.CONFIRMATION,
button_text="Approve"
)
print(f"Created action: {action.action_name}")
SuggestHumanActions Node:
from langgraph.types import Command
from langchain_core.messages import AIMessage
class SuggestHumanActions:
"""Node that initiates human interaction."""
@staticmethod
async def node_handler(state: AgentState) -> Command:
"""Handle suggest human actions node."""
if not state.get("hitl_action"):
raise ValueError("hitl_action must be set before SuggestHumanActions")
# Add action to conversation history
state["messages"].append(AIMessage(
content=state["hitl_action"].model_dump_json()
))
# Route to wait node
return Command(update=state, goto="WaitForResponse")
# Usage
state: AgentState = {
"messages": [],
"hitl_action": FollowUpAction(
action_id="approve",
action_name="Approve",
description="Approve this action?",
type=ActionType.CONFIRMATION
),
"hitl_response": None,
"sender": "PlannerAgent",
"tool_call": None
}
import asyncio
async def main():
command = await SuggestHumanActions.node_handler(state)
if __name__ == "__main__":
asyncio.run(main())
WaitForResponse Node:
from langgraph.types import Command, interrupt
class WaitForResponse:
"""Node that pauses workflow for human response."""
@staticmethod
async def node_handler(state: AgentState) -> Command:
"""Handle wait for response node."""
if not state.get("hitl_action"):
raise ValueError("hitl_action must be set before WaitForResponse")
# Interrupt workflow - preserves state automatically
# In real implementation, this pauses and waits for human input
response_data = interrupt(state["hitl_action"].model_dump())
# Parse human response
state["hitl_response"] = ActionResponse(**response_data)
# Clear action (response is now in hitl_response)
prev_sender = state.get("sender", "PlannerAgent")
state["sender"] = "WaitForResponse"
# Return to previous node that requested interruption
return Command(update=state, goto=prev_sender)
# Usage (in real workflow, interrupt() would pause and wait)
# state["hitl_action"] = action
# command = await WaitForResponse.node_handler(state)
InterruptToolNode:
from langchain_core.messages import AIMessage
class InterruptToolNode:
"""Special interrupt point after tool calls."""
@staticmethod
async def node_handler(state: AgentState) -> AgentState:
"""Handle interrupt tool node."""
# Tool call was interrupted, restore it
if state.get("tool_call"):
msg = AIMessage(content="", tool_calls=[state["tool_call"]])
state["messages"].append(msg)
state["tool_call"] = None
return state
# Usage
state: AgentState = {
"messages": [],
"hitl_action": None,
"hitl_response": None,
"sender": "ActionAgent",
"tool_call": {"name": "execute_task", "args": {"task": "process"}}
}
import asyncio
async def main():
state = await InterruptToolNode.node_handler(state)
if __name__ == "__main__":
asyncio.run(main())
Action Handlers:
from typing import Callable
from langgraph.types import Command
class ActionHandler:
"""Processes human responses and routes workflow."""
def __init__(self):
self.handlers: dict[str, Callable] = {
"approve_tool": self._handle_approval,
"consult_human": self._handle_consultation,
"save_reuse": self._handle_save_reuse,
}
def handle(self, state: AgentState, node_name: str) -> Command:
"""Route based on action_id."""
if not state.get("hitl_response"):
raise ValueError("hitl_response must be set")
action_id = state["hitl_response"].action_id
if action_id in self.handlers:
return self.handlers[action_id](state, node_name)
# Default: return to previous node
return Command(update=state, goto=state.get("sender", "PlannerAgent"))
def _handle_approval(self, state: AgentState, node_name: str) -> Command:
"""Handle tool execution approval."""
if state["hitl_response"].confirmed:
# Execute approved tool
return Command(update=state, goto="ExecuteTool")
else:
# Reject, return to planner
return Command(update=state, goto="PlannerAgent")
def _handle_consultation(self, state: AgentState, node_name: str) -> Command:
"""Handle human consultation response."""
# Use human's guidance
guidance = state["hitl_response"].text_response
# Store guidance in state for use by planner
return Command(update=state, goto="PlannerAgent")
def _handle_save_reuse(self, state: AgentState, node_name: str) -> Command:
"""Handle save/reuse action."""
return Command(update=state, goto="ReuseAgent")
# Usage
handler = ActionHandler()
state["hitl_response"] = ActionResponse(
action_id="approve_tool",
response_type=ActionType.CONFIRMATION,
confirmed=True
)
command = handler.handle(state, "PlannerAgent")
Basic Example: Interruptible Workflow¶
import asyncio
from typing import TypedDict, Optional, Dict, Any
from langgraph.types import Command
from langchain_core.messages import AIMessage
# Mock type definitions for demonstration
class AgentState(TypedDict):
messages: list
hitl_action: Optional[Any]
hitl_response: Optional[Any]
sender: str
tool_call: Optional[Dict]
next_action: Dict
class FollowUpAction:
def __init__(self, action_id: str, action_name: str, description: str, type: str, button_text: str):
self.action_id = action_id
self.action_name = action_name
self.description = description
self.type = type
self.button_text = button_text
def model_dump_json(self) -> str:
return f'{{"action_id": "{self.action_id}", "description": "{self.description}"}}'
def model_dump(self) -> Dict:
return {
"action_id": self.action_id,
"action_name": self.action_name,
"description": self.description,
"type": self.type
}
class ActionType:
CONFIRMATION = "confirmation"
class ActionResponse:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
class ActionHandler:
def handle(self, state: AgentState, sender: str) -> Command:
return Command(update=state, goto="ExecuteTool")
def interrupt(action_data: Dict) -> Dict:
"""Mock interrupt function - in real implementation, this pauses workflow."""
# Simulated human response
return {"action_id": action_data.get("action_id"), "response": "approved"}
def requires_approval(action: dict) -> bool:
"""Determine if action requires human approval."""
# Example: require approval for critical actions
critical_actions = ["delete", "transfer", "purchase"]
return any(critical in action.get("name", "").lower() for critical in critical_actions)
# Agent determines human approval needed
async def planner_node(state: AgentState) -> Command:
"""Planner node that may request human approval."""
next_action = state.get("next_action", {})
# Check if tool execution needs approval
if requires_approval(next_action):
# Create action request
state["hitl_action"] = FollowUpAction(
action_id="approve_tool",
action_name="Approve Tool Execution",
description="Approve execution of this tool?",
type=ActionType.CONFIRMATION,
button_text="Approve"
)
state["sender"] = "PlannerAgent"
return Command(update=state, goto="SuggestHumanActions")
# Continue autonomously
return Command(update=state, goto="ExecuteTool")
# SuggestHumanActions node
async def suggest_actions_node(state: AgentState) -> Command:
"""Node that suggests human actions."""
if not state.get("hitl_action"):
raise ValueError("hitl_action must be set")
# Add action to history
state["messages"].append(AIMessage(
content=state["hitl_action"].model_dump_json()
))
return Command(update=state, goto="WaitForResponse")
# WaitForResponse node - pauses workflow
async def wait_for_response_node(state: AgentState) -> Command:
"""Node that waits for human response."""
if not state.get("hitl_action"):
raise ValueError("hitl_action must be set")
# Interrupt preserves state automatically
# In real implementation, this pauses and waits
response_data = interrupt(state["hitl_action"].model_dump())
state["hitl_response"] = ActionResponse(**response_data)
# Return to node that requested interruption
prev_sender = state.get("sender", "PlannerAgent")
return Command(update=state, goto=prev_sender)
# Action handler processes response
async def handle_response(state: AgentState) -> Command:
"""Handle human response and route workflow."""
handler = ActionHandler()
return handler.handle(state, "PlannerAgent")
# Usage example
async def example_workflow():
state: AgentState = {
"messages": [],
"hitl_action": None,
"hitl_response": None,
"sender": "PlannerAgent",
"tool_call": None,
"next_action": {"name": "delete_file"} # Critical action
}
# Planner determines approval needed
command = await planner_node(state)
print(f"Planner routed to: {command.goto}")
# Suggest actions
if command.goto == "SuggestHumanActions":
command = await suggest_actions_node(state)
print(f"Suggested actions, routing to: {command.goto}")
async def main():
await example_workflow()
if __name__ == "__main__":
asyncio.run(main())
Explanation: This demonstrates the interruptible workflow pattern. The planner creates an action request, SuggestHumanActions routes to WaitForResponse, which pauses the workflow. After human response, the handler processes it and routes back to the appropriate node.
Advanced Example: Multiple Action Types¶
from typing import Dict, Any, Callable
from langgraph.types import Command
# Confirmation action (approve/reject)
async def request_approval(state: AgentState) -> Command:
"""Request approval for an action."""
state["hitl_action"] = FollowUpAction(
action_id="approve_tool",
action_name="Approve Tool",
description="Approve execution?",
type=ActionType.CONFIRMATION,
button_text="Approve"
)
state["sender"] = "PlannerAgent"
return Command(update=state, goto="SuggestHumanActions")
# Natural language consultation
async def consult_human(state: AgentState) -> Command:
"""Request human consultation."""
state["hitl_action"] = FollowUpAction(
action_id="consult_human",
action_name="Human Consultation",
description="How should I proceed?",
type=ActionType.NATURAL_LANGUAGE,
placeholder="Provide guidance..."
)
state["sender"] = "PlannerAgent"
return Command(update=state, goto="SuggestHumanActions")
# Selection action (choose from options)
async def request_selection(state: AgentState) -> Command:
"""Request selection from options."""
state["hitl_action"] = FollowUpAction(
action_id="select_option",
action_name="Choose Approach",
description="Which approach should I use?",
type=ActionType.SELECT,
options=[
SelectOption(value="approach_a", label="Approach A"),
SelectOption(value="approach_b", label="Approach B")
]
)
state["sender"] = "PlannerAgent"
return Command(update=state, goto="SuggestHumanActions")
# Action handler with multiple handlers
class AdvancedActionHandler:
"""Advanced handler supporting multiple action types."""
def __init__(self):
self.handlers: Dict[str, Callable[[AgentState], Command]] = {
"approve_tool": self._handle_approval,
"consult_human": self._handle_consultation,
"select_option": self._handle_selection,
}
def handle(self, state: AgentState) -> Command:
"""Route based on action_id."""
if not state.get("hitl_response"):
raise ValueError("hitl_response must be set")
action_id = state["hitl_response"].action_id
if action_id not in self.handlers:
# Default: return to sender
return Command(update=state, goto=state.get("sender", "PlannerAgent"))
return self.handlers[action_id](state)
def _handle_approval(self, state: AgentState) -> Command:
"""Handle approval response."""
if state["hitl_response"].confirmed:
return Command(update=state, goto="ExecuteTool")
return Command(update=state, goto="PlannerAgent")
def _handle_consultation(self, state: AgentState) -> Command:
"""Handle consultation response."""
# Use human's text response
guidance = state["hitl_response"].text_response
# Store guidance in state (would need to extend AgentState)
if "context" not in state:
state["context"] = {}
state["context"]["human_guidance"] = guidance
return Command(update=state, goto="PlannerAgent")
def _handle_selection(self, state: AgentState) -> Command:
"""Handle selection response."""
# Use selected option
if not state["hitl_response"].selected_values:
raise ValueError("No selection made")
selected = state["hitl_response"].selected_values[0]
# Store selection in state
if "context" not in state:
state["context"] = {}
state["context"]["selected_approach"] = selected
return Command(update=state, goto="PlannerAgent")
# Usage
handler = AdvancedActionHandler()
# Example: Handle approval response
state: AgentState = {
"messages": [],
"hitl_action": None,
"hitl_response": ActionResponse(
action_id="approve_tool",
response_type=ActionType.CONFIRMATION,
confirmed=True
),
"sender": "PlannerAgent",
"tool_call": None,
"context": {}
}
command = handler.handle(state)
print(f"Routed to: {command.goto}")
Example: Interrupt After Tool Call¶
from langgraph.graph import StateGraph
from langchain_core.messages import AIMessage
def needs_approval(state: AgentState) -> bool:
"""Check if current state needs approval."""
# Example: require approval for tool calls
return state.get("tool_call") is not None
# InterruptToolNode - special interrupt point
async def interrupt_tool_node(state: AgentState) -> AgentState:
"""Handle interrupt after tool call."""
# Tool call was interrupted, restore it
if state.get("tool_call"):
msg = AIMessage(content="", tool_calls=[state["tool_call"]])
state["messages"].append(msg)
state["tool_call"] = None
return state
async def action_node(state: AgentState) -> AgentState:
"""Example action node that may create tool calls."""
# Simulate creating a tool call
state["tool_call"] = {"name": "execute_task", "args": {"task": "process"}}
return state
# Configure graph to interrupt after tool calls
def create_graph_with_interrupts():
"""Create graph with interrupt points."""
graph = StateGraph(AgentState)
graph.add_node("action_agent", action_node)
graph.add_node("interrupt_tool", interrupt_tool_node)
# Interrupt after action agent
graph.add_edge("action_agent", "interrupt_tool")
graph.add_conditional_edges(
"interrupt_tool",
lambda s: "suggest_actions" if needs_approval(s) else "continue"
)
return graph
# Usage
# graph = create_graph_with_interrupts()
# compiled = graph.compile()
Workflow Integration¶
from langgraph.graph import StateGraph, END
from typing import Callable
def requires_approval(state: AgentState) -> bool:
"""Check if approval is required."""
return state.get("hitl_action") is not None
async def executor_node(state: AgentState) -> AgentState:
"""Example executor node."""
# Execute action
return state
async def handle_response_node(state: AgentState) -> AgentState:
"""Handle human response."""
handler = ActionHandler()
command = handler.handle(state, "PlannerAgent")
# In real implementation, would route based on command
return state
# Complete HITL workflow
def create_hitl_workflow():
"""Create complete HITL workflow graph."""
graph = StateGraph(AgentState)
# Add HITL nodes
graph.add_node("suggest_actions", suggest_actions_node)
graph.add_node("wait_response", wait_for_response_node)
graph.add_node("interrupt_tool", interrupt_tool_node)
graph.add_node("handle_response", handle_response_node)
# Regular workflow nodes
graph.add_node("planner", planner_node)
graph.add_node("executor", executor_node)
# Conditional routing with HITL
graph.add_conditional_edges(
"planner",
lambda s: "suggest_actions" if requires_approval(s) else "executor"
)
# HITL flow
graph.add_edge("suggest_actions", "wait_response")
graph.add_edge("wait_response", "handle_response")
graph.add_edge("handle_response", "planner") # Return to planner after handling
# Interrupt after tool calls
graph.add_edge("executor", "interrupt_tool")
graph.add_conditional_edges(
"interrupt_tool",
lambda s: "suggest_actions" if requires_approval(s) else "planner"
)
# Set entry point
graph.set_entry_point("planner")
return graph
# Usage
# graph = create_hitl_workflow()
# compiled = graph.compile()
# result = await compiled.ainvoke(initial_state)
Key Integration Points: - Configurable Interrupts: Set approval points at specific nodes - State Preservation: Complete state preserved during pause - Action Handlers: Route workflow based on human response type - Resume Logic: Return to appropriate node after human input
Key Takeaways¶
-
Interruptible Workflow Pattern: HITL uses an interruptible workflow that pauses execution at configurable approval points, preserving complete agent state during the pause for seamless resumption.
-
Core Components:
- SuggestHumanActions: Initiates human interaction, routes to wait node
- WaitForResponse: Pauses workflow using graph interrupts, preserves state
- InterruptToolNode: Special interrupt point after tool calls for approval
-
Action Handlers: Process human responses and route workflow based on action type
-
Action Types: Support multiple interaction types (confirmation, text input, selection) with corresponding handlers that process responses and route workflow accordingly.
-
State Preservation: Complete agent state (messages, variables, execution history, context) is preserved during workflow pauses, enabling seamless resumption after human response.
-
Configurable Approval Points: Approval points can be configured at specific nodes (after tool calls, before critical actions, based on confidence thresholds) to balance automation with human oversight.
-
Best Practice: Implement clear escalation policies, confidence thresholds, and action handlers for effective HITL. Use different action types based on the type of human input needed.
-
Common Pitfall: HITL lacks scalability and depends on skilled operators; use hybrid approaches combining automation with selective human oversight at critical decision points.
-
Performance Note: HITL adds latency and cost but is essential for high-stakes applications requiring human judgment, safety compliance, and error recovery. The interruptible pattern ensures minimal overhead when human input isn't needed.
Related Patterns¶
This pattern works well with: - Exception Handling - Critical errors can be escalated to human operators - Guardrails and Safety - HITL provides human oversight for safety-critical decisions - Learning and Adaptation - Human feedback is used to improve AI models
This pattern is often combined with: - Goal Setting and Monitoring - Human oversight ensures goals are met appropriately - Evaluation and Monitoring - Human review is part of evaluation processes
References
- A Survey of Human-in-the-loop for Machine Learning: https://arxiv.org/abs/2109.02840
- Google ADK Agents: https://google.github.io/adk-docs/agents/
- LangChain Human Approval: https://python.langchain.com/docs/modules/callbacks/human_approval/