AI / LLM
Human-in-the-Loop Patterns for LLM Agents
An agent that can take production actions is an agent that can cause production incidents. Deployments, payments, message sending, data deletion, and any irreversible operation belong to a class of decisions where full autonomy is not a feature but a liability. The right answer is rarely "make the model better at deciding"; the right answer is to put a human in the loop at the specific points where human judgment outperforms the model, and to let the agent operate autonomously everywhere else.
Human-in-the-loop is the umbrella term for the patterns that make this tractable. A pause point in the agent's execution surfaces the proposed action to a human for review. The human approves, edits, rejects, or escalates. The agent resumes with the decision applied. Checkpointing, covered in the previous article, is the infrastructure that makes these pauses cheap: the agent's state is already saved, so pausing costs nothing. OpenAI's Agents SDK treats this as a first-class concept (OpenAI, 2024); LangGraph exposes it through interrupt_before and update_state. This article covers the five common patterns and when each applies.
Five patterns
flowchart TD
A[Agent proposes action] --> R{Human review}
R -->|approve| E[Execute action]
R -->|edit| M[Human modifies action] --> E
R -->|reject| RP[Agent replans] --> A
R -->|escalate| H[Human takes over]
E --> C[Continue]
The five patterns are variations on the same diagram, each applied to a different decision context.
Approval gate. The agent pauses before any action marked dangerous. The human sees the proposed action, the reasoning behind it, and the expected effect, then approves or rejects. This is the default for deployments, payments, outbound messages, and irreversible database changes.
State editing. The agent pauses at a checkpoint. Instead of approving an action, the human edits the state directly: correcting a misunderstood requirement, fixing a value in a plan, or adding missing context. The agent resumes with the modified state as if it had produced the edit itself.
Feedback injection. The agent pauses at a natural milestone and asks for free-form feedback. The human provides a short note; the agent folds it into subsequent steps. This is closer to a collaboration pattern than a safety one and works well for research and drafting tasks.
Escalation. The agent recognizes it is stuck or uncertain and hands the conversation to a human. No specific action is proposed; the human takes over the task. Detection is the hard part; the agent needs a reliable signal of "I do not know what to do."
Tiered autonomy. Different actions have different approval thresholds. Low-risk actions (read-only lookups) run automatically. Medium-risk actions (writes to non-production systems) are logged for asynchronous review. High-risk actions (production deployments, financial transactions) require synchronous approval. The agent operates efficiently on the low-risk path while maintaining human oversight where it matters.
Most production systems use several of these at once. Tiered autonomy is the policy framework; approval gates, state editing, feedback injection, and escalation are the mechanisms that implement specific tiers.
Two versions in code
The excerpt below sketches an approval gate without a framework. The agent emits a proposed action; a separate approval call (in reality, a UI surfacing the action to a human) returns a verdict; the agent acts only on approval.
from openai import OpenAI
from pydantic import BaseModel
from typing import Literal
client = OpenAI()
class Proposal(BaseModel):
action: str
reasoning: str
risk: Literal["low", "medium", "high"]
def propose(task: str) -> Proposal:
r = client.responses.parse(
model="gpt-4o-mini",
instructions=("Propose an action for the task. Explain reasoning. "
"Classify risk: low, medium, or high."),
input=task, text_format=Proposal)
return r.output[0].content[0].parsed
def human_review(proposal: Proposal) -> dict:
# In production: surface to a web UI, wait for response.
# Here: simulated user input.
print(f"Action: {proposal.action}\nRisk: {proposal.risk}\n"
f"Reasoning: {proposal.reasoning}")
verdict = input("approve / edit / reject / escalate: ").strip()
if verdict == "edit":
new_action = input("modified action: ")
return {"verdict": "approve", "action": new_action}
return {"verdict": verdict, "action": proposal.action}
def execute_or_halt(task: str) -> str:
proposal = propose(task)
if proposal.risk == "low":
return run(proposal.action)
decision = human_review(proposal)
if decision["verdict"] == "approve":
return run(decision["action"])
if decision["verdict"] == "reject":
return execute_or_halt(task) # simple replan: re-propose
if decision["verdict"] == "escalate":
return "escalated to human"
return "unknown verdict"
The LangGraph version uses interrupt_before to pause the graph at a designated node. The harness then surfaces the state to a human, receives an edit or an approval, and calls invoke(None, ...) to resume.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langchain.chat_models import init_chat_model
model = init_chat_model("gpt-4o-mini")
class State(TypedDict):
task: str
action: str
approved: bool
result: str
def propose_node(s: State) -> State:
action = model.invoke(f"Propose action for: {s['task']}").content
return {**s, "action": action, "approved": False}
def execute_node(s: State) -> State:
if not s["approved"]:
return {**s, "result": "skipped: not approved"}
return {**s, "result": f"executed: {s['action']}"}
graph = (StateGraph(State)
.add_node("propose", propose_node).add_node("execute", execute_node)
.add_edge(START, "propose").add_edge("propose", "execute")
.add_edge("execute", END)
.compile(checkpointer=InMemorySaver(),
interrupt_before=["execute"])) # pause before execute
cfg = {"configurable": {"thread_id": "t1"}}
# Run until interrupt
graph.invoke({"task": "deploy to production"}, config=cfg)
# Human reviews state, then either edits or approves
graph.update_state(cfg, {"approved": True}) # or edit "action" here too
graph.invoke(None, config=cfg) # resume from interrupt
Full runnable versions will live at github.com/subodhjena/agentic-patterns under examples/23_human_in_the_loop.py as that lesson lands.
Designing the interrupt points
Choosing where to interrupt is the load-bearing design decision. Too many interrupts and the agent is a slow assistant the human has to babysit. Too few interrupts and the agent ships production incidents that a quick review would have caught. A small set of guidelines keeps the decision tractable.
Interrupt on side effects, not on reasoning. Reasoning steps are cheap to redo if they go wrong. Side effects are not. Interrupt before the effect, not before the decision to consider it.
Interrupt on irreversibility. Actions that cannot be undone need approval. Actions that can be undone within a minute usually do not.
Interrupt on scope expansion. When an agent's plan grows beyond the scope the human expected, an interrupt lets the human catch the drift before it propagates.
Interrupt on low confidence. When the agent's structured output includes a confidence score and it is below a threshold, interrupt. The threshold is tuned empirically.
Interrupt on tool failures. A tool that returns an error the agent cannot self-correct is a signal to pause and ask. The human often sees a root cause the agent misses.
Where human-in-the-loop goes wrong
The patterns are not a substitute for good agent design; they can make things worse when applied carelessly.
Alarm fatigue. Too many approvals train the human to click through without reading. The approvals become ceremony, not gating. Fix: tier aggressively; only the highest-risk actions should require synchronous approval.
Human as bottleneck. An agent that must pause before every tool call waits more than it works. If the pause rate is high, the agent's utility drops below a plain interactive tool. Rework the tiering or rethink whether an agent is the right shape.
State modification without validation. A human edits the agent's state in ways the agent cannot handle. The next step produces incoherent output. Apply the same input validation to human edits as to any other state source.
Escalation without context. The agent hands off to a human with no summary of what happened. The human must read the full trace to catch up. Require the agent to produce a short status report at escalation time.
Silent overrides. A human edit that contradicts the agent's plan without updating the plan itself leaves the agent proceeding on a stale plan. Treat plan updates and state edits together; either change the plan or change the state, but keep them in sync.
No audit trail. An approval without a record of who approved what, when, and why, is a compliance failure waiting to happen. Log every pause, every review, every decision.
Trade against full autonomy and full oversight
Human-in-the-loop sits between fully autonomous agents and fully supervised workflows. The table describes the spectrum.
| Axis | Full autonomy | Tiered HITL | Full oversight |
|---|---|---|---|
| Latency | Lowest | Variable by tier | Highest |
| Throughput | Highest | Medium | Lowest |
| Safety on high-risk actions | Lowest | High | Highest |
| Cognitive load on humans | None | Bounded | Continuous |
| Fit | Low-risk, high-volume | Mixed workloads | Regulated, critical systems |
| Implementation complexity | Low | Medium | Low |
Tiered HITL is the right default for any agent with production side effects. Full autonomy is appropriate only when every possible action is safe by construction. Full oversight is what pre-agent workflow tools provide; choose it when the cost of a wrong action is catastrophic.
Neighbors in the series
Persistence and checkpointing, two articles ago, is the infrastructure that makes pause-and-resume tractable. The tool-calling agent loop, in the Agents stage, is where interrupt points usually fire; the loop's anatomy includes guardrails at the entry and the exit. Guardrails, the next article, is the complementary pattern: instead of asking a human, a deterministic or model-based check vets the action. Agent evaluation, two articles later, covers how to measure whether HITL thresholds are correctly calibrated.
References
- OpenAI. Practices for deploying LLM-based agents. 2024.
- Anthropic. Building effective agents. December 2024.
- LangChain. Human-in-the-loop with LangGraph interrupts. 2024.
- Ruan, Yangjun, et al. Identifying the Risks of LM Agents with an LM-Emulated Sandbox. ICLR 2024.
- Yuan, Tongxin, et al. R-Judge: Benchmarking Safety Risk Awareness for LLM Agents. 2024.
