Human-in-the-loop is not a UI feature you bolt on later — it's a control structure you decide on before the agent gets write access to anything. The question isn't "should a human review this," it's "which actions can the agent take unattended, which need a human to confirm before execution, and which need a human to act because the agent shouldn't be trusted with them at all." Get that classification wrong and you either drown reviewers in rubber-stamp approvals they stop reading, or you let an agent silently take an action that needed a second pair of eyes.
Three tiers, not a binary switch
Treating human review as on/off is where most implementations fail. In practice you want at least three tiers. Tier 1 — autonomous: low-blast-radius, reversible actions (searching a knowledge base, drafting a reply, querying a read-only API) execute without approval. Tier 2 — approve-before-execute: actions with real consequences (sending an email to a customer, issuing a refund under $500, merging a PR) generate a proposed action that a human confirms or rejects before the tool actually runs. Tier 3 — human-only: the agent is not allowed to call the tool at all — it can only draft a recommendation and hand it to a person, because the cost of a wrong call (terminating an account, wiring money above a threshold) is asymmetric enough that you don't want an LLM anywhere near the execute path.
The mistake teams make is picking one tier for the whole agent. A support agent that's autonomous for FAQ answers and locked to Tier 3 for refunds needs the classification to live at the tool level, not the agent level — each tool declares its own tier, and the orchestration layer enforces it regardless of what the model decides to do.
Confidence thresholds are a trap if that's all you have
It's tempting to route to a human whenever the model's self-reported confidence is low. Don't rely on this alone — LLMs are frequently confidently wrong, and asking a model to grade its own certainty produces a number that correlates poorly with actual correctness. A better signal is structural: does the action match a pattern you've seen before and validated, is the dollar amount inside a known range, does the tool call touch a resource the agent has touched successfully many times. Combine a cheap structural check with an optional confidence score rather than trusting the score in isolation.
Asking "on a scale of 1-10, how confident are you" and gating on the number gives you a false sense of safety. Models trained with RLHF tend to hedge toward the middle regardless of actual correctness. If you need a real confidence signal, use log-probabilities on the decision token where the API exposes them, or run the same query twice and check for agreement — disagreement is a far more honest escalation trigger than a self-graded score.
Designing the approval gate
An approval gate is a pause in the tool-calling loop, not a separate system. When the agent emits a tool call for a Tier 2 action, you intercept it before execution, store the proposed call (name, arguments, and the reasoning that led to it) with a pending status, and surface it to a reviewer with enough context to decide in seconds — not "review this JSON blob," but "Agent wants to refund $340 to order #88213 because the customer reported a damaged item; here's the support thread." The agent's chain of reasoning, not just the final action, is what makes the review fast. Strip that context out and every approval becomes a coin flip.
On approval, replay the exact tool call against the real API — don't let the reviewer's approval trigger a fresh, possibly-different generation. On rejection, feed the rejection reason back into the conversation as a tool result so the agent can propose an alternative, rather than silently dropping the turn.
TOOL_TIERS = {
"search_kb": "auto",
"draft_reply": "auto",
"issue_refund": "approve",
"close_account": "human_only",
}
def handle_tool_call(call, context):
tier = TOOL_TIERS.get(call.name, "approve") # unknown tools default to approve
if tier == "human_only":
raise ToolBlocked(f"{call.name} requires a human operator")
if tier == "auto":
return execute(call)
pending = save_pending_action(
tool=call.name,
args=call.arguments,
reasoning=context.last_assistant_reasoning,
status="pending",
)
notify_reviewer(pending)
return {"status": "awaiting_approval", "pending_id": pending.id}
def on_reviewer_decision(pending_id, approved, reason=""):
pending = load_pending_action(pending_id)
if approved:
return execute_exact(pending.tool, pending.args)
return {"status": "rejected", "reason": reason} # fed back as a tool result
Escalation paths need an owner and a timeout
A queue of pending approvals that nobody is paged for is worse than no human-in-the-loop at all — it just adds latency with no safety benefit. Route Tier 2 actions to a specific queue with an on-call owner and a timeout policy: does the action expire and get rejected automatically, or does it escalate to a second reviewer after 15 minutes? For customer-facing agents, a stale pending approval usually means "tell the customer we're looking into it" rather than leaving them stuck mid-conversation. Decide this explicitly instead of discovering the gap when a customer complains that the bot went silent.
Watch the approval rate, not just latency
The metric that tells you whether human-in-the-loop is working isn't how fast reviewers approve things — it's the ratio of approvals to rejections to edits, tracked per tool and per agent version. A tool sitting at 98%+ approval for months is a strong signal it's safe to demote from Tier 2 to Tier 1. A tool with a rising edit rate (reviewers approving but changing the arguments first) means the agent's reasoning has drifted from what reviewers actually want, and that's worth investigating before it becomes a rejection spike.
Human-in-the-loop should shrink over time for well-behaved tools and grow for tools that start misfiring after a model or prompt change. Treat the tier assignment as a config value you revisit with real approval-rate data, not a one-time design decision.
Wrapping up
Human-in-the-loop works when the tiering lives at the tool level, the approval gate gives reviewers the reasoning behind the call instead of raw arguments, and escalation has an owner and a timeout instead of an open-ended queue. Confidence scores are a weak standalone signal — use structural checks and agreement-across-samples instead, and let real approval-rate data tell you when a tool has earned its way down to a lower tier.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.