O que é Humano no Circuito?
Humano no Circuito (Human-in-the-Loop) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Comportamento de agentes. Pausar em pontos definidos para um humano aprovar, editar ou recusar.
Human-in-the-Loop
Humano no Circuito (Human-in-the-Loop) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Comportamento de agentes. Pausar em pontos definidos para um humano aprovar, editar ou recusar.
Pausar a execução em pontos definidos para que uma pessoa aprove, edite ou recuse o que o sistema vai fazer. Serve para fluxos em que um erro do modelo custa mais do que alguns segundos ou horas de espera.
O copiloto que lê o checklist em voz alta e espera o “confere” antes de cada etapa crítica da decolagem.
Rafael é responsável pelo assistente jurídico de um escritório. O assistente revisa contratos de clientes, aponta cláusulas de risco e redige sugestões de alteração. No piloto, as sugestões iam direto para o cliente por email, porque “o modelo é muito bom”.
Na terceira semana, o assistente sugeriu remover uma cláusula de foro que o cliente precisava manter, com uma justificativa convincente e errada. O cliente aceitou. O problema só apareceu meses depois, num litígio.
Tirar o assistente não era opção: ele economizava horas de trabalho dos advogados. Pedir para os advogados “conferirem tudo” também não funcionava: sem um ponto claro de revisão, ninguém sabia o que já tinha sido conferido.
Esse tipo de erro já saiu do laboratório. Em Moffatt v. Air Canada (2024), o chatbot da companhia inventou uma política de tarifa por luto; o tribunal britânico-colombiano considerou a empresa responsável pelo que o bot afirmou. Em 2023, o chatbot de uma concessionária Chevrolet em Watsonville “aceitou” vender um Tahoe por 1 dólar depois de um prompt injection (injeção de instrução). Nos dois casos, uma afirmação ou um efeito saiu sem uma pessoa (nem um sistema de registro) no meio.
A pergunta que este padrão responde: onde e como colocar uma pessoa no fluxo para que o sistema seja útil e os erros caros não saiam?
Escolha os pontos do fluxo em que uma decisão humana muda o risco: antes de um efeito irreversível, antes de algo sair para fora, quando a confiança do modelo é baixa, ou quando a regra exige uma pessoa responsável. Em cada ponto, o sistema:
A pessoa não revisa tudo; revisa o que importa, num formato feito para revisão rápida. As decisões viram dado: aprovações, edições e recusas alimentam o eval e mostram onde o sistema erra.
A aprovação precisa vir de um canal que o modelo e o conteúdo externo não conseguem escrever. Um “sim” no mesmo chat que lê emails de terceiros não prova nada. A prévia mostra dados do sistema (nome do recebedor na base, texto da cláusula no contrato), não a prosa convincente do modelo.
O assistente de Rafael propõe um email ao cliente. Antes de send_to_client, o código monta a prévia com o contrato na base, não com o resumo do modelo:
{
"acao": "send_to_client",
"destinatario": "maria@cliente.com",
"clausula": {
"id": "12",
"titulo": "Foro",
"texto": "Fica eleito o foro da comarca da sede do Cliente.",
"efeito_proposto": "remover a cláusula 12 do anexo"
},
"motivo_do_modelo": "cláusula de foro é incomum em contratos deste porte"
}
A advogada vê o texto da cláusula 12, o efeito (remover) e o email de destino. Recusa com motivo: “cliente exige foro na sede”. O modelo recebe a recusa como observação e propõe outra coisa, ou para. Sem a prévia com o texto real, a justificativa “incomum” teria passado, como passou no piloto.
No Pix, a mesma ideia: a prévia mostra o nome que o banco devolve para a chave, o valor em reais e a data. Não mostra “pagar a conta de luz”, que o modelo inventou a partir de um PDF anexado.
flowchart TD
T(["1. Tarefa"]):::io --> A(["2. Agente trabalha"]):::llm
A --> C{"3. Ponto de revisão?"}
C -->|não| A
C -->|sim| S[("4. Estado salvo")]:::store
S --> P["5. Prévia com dados e motivo"]:::tool
P --> H((("6. Pessoa: aprovar, editar, recusar"))):::human
H --> R["7. Retomar com a decisão"]:::tool
R --> A
A --> OUT(["8. Resultado"]):::io
H --> L[("9. Registro da decisão")]:::store
schema Review { decision: "approve" | "edit" | "reject", edited_action: any?, reason: string?, reviewer: string }
REVIEW_POLICY = {
"send_to_client": always,
"change_contract": always,
"schedule_payment": when(amount > 500_00),
"draft_note": never,
}
function step(state):
reply = llm_with_tools(SYSTEM, TOOLS, state) # 2
for call in reply.tool_calls:
if REVIEW_POLICY[call.name](call.args): # 3
checkpoint = state.save() # 4
preview = build_preview(call) # 5. dados do sistema
review = human.approve(preview) -> Review # 6. pode levar dias
audit.log(call, preview, review) # 9
state = checkpoint.restore() # 7
if review.decision == "reject":
state.add(tool_result(call, "recusado: " + review.reason))
continue
if review.decision == "edit":
call = review.edited_action
state.add(execute(call))
return state
O smell que este padrão trata de frente é Efeito Sem Portão. A refatoração correspondente é Aprovar Antes do Efeito.
No atendimento de banco, a prévia de um Pix de R$ 1.890 precisa do nome que o PSP devolveu para a chave, o valor em reais e a data. “Pagar a conta de luz, como o cliente pediu no PDF” é a frase que o golpe quer que a pessoa carimbe. O nome do recebedor na tela é o que faz a advogada, o operador ou o próprio cliente hesitar.
| Ingênuo | Correto | |
|---|---|---|
| O que revisar | Tudo que o modelo produz | Só efeito irreversível, saída externa ou exceção |
| Prévia | Justificativa do modelo | Dado do sistema mais o efeito concreto |
| Canal do “sim” | A mesma conversa que lê o mundo | Botão, app ou fila com identidade |
| Espera | Request HTTP aberto por dois dias | Checkpoint e retomada durável |
| Aspecto | O que acontece | Como aparece no trace |
|---|---|---|
| Latência | A tarefa espera a pessoa: segundos a dias | span human_review com duração longa |
| Custo | Tempo de pessoas; o custo do modelo quase não muda | revisões por dia e tempo médio por revisão |
| Falha: fadiga | Aprovação sem leitura | tempo de revisão muito curto, taxa de aprovação perto de 100% |
| Falha: prévia ruim | A pessoa aprova algo diferente do que acha | recusas e reclamações depois da execução |
| Falha: espera sem fim | Ninguém responde e a tarefa fica parada | revisões pendentes acima do prazo |
| Falha: retomada quebrada | O estado salvo não basta para continuar | erros logo depois de resume |
{ "action": "send_to_client", "proposal": "remover cláusula 12 (foro)", "expected_review": "reject", "reason": "cliente exige foro na sede" }
Coloque no eval a prévia que a pessoa vê, não só a ação. Se a prévia omite o texto da cláusula, o caso “deveria recusar” não testa o produto real.
human_review com ação, prévia, decisão, revisor e duração.A aprovação precisa vir de um canal que o modelo e o conteúdo externo não conseguem escrever. Um “sim” digitado no mesmo chat que lê emails de terceiros não prova que o usuário aprovou. Mostre dados do sistema, não texto gerado, na prévia.
Isso também é defesa contra o caso Chevrolet: o modelo pode “aceitar” um preço absurdo no diálogo. O sistema de registro (tabela de preço, limite, pessoa) é quem vende, não o turno do chat.
| Prós | Contras |
|---|---|
| Erros caros não saem sem uma pessoa ver | Latência e custo de pessoas |
| Responsabilidade clara em domínios regulados | Fadiga leva a aprovação automática |
| Edições e recusas viram dados de melhoria | Exige estado persistido para esperas longas |
| Permite dar mais capacidade ao agente com segurança | Não escala para volume muito alto sem amostragem |
As abas aplicam a política de revisão dentro da execução, com as três respostas possíveis. Python e TypeScript pausam dentro da ferramenta, no mesmo processo. LangGraph pausa o grafo com interrupt e um checkpointer, para a espera sobreviver a um restart.
Python. Use quando o loop é seu e a aprovação é síncrona, como uma confirmação no app.
import json
from dataclasses import dataclass
from typing import Callable, Literal
import anthropic
client = anthropic.Anthropic()
@dataclass
class Review:
decision: Literal["approve", "edit", "reject"]
edited_input: dict | None = None
reason: str = ""
reviewer: str = ""
POLICY: dict[str, Callable[[dict], bool]] = { # 3. quando revisar
"send_to_client": lambda a: True,
"schedule_payment": lambda a: a["amount_cents"] > 50_000,
"draft_note": lambda a: False,
}
def run_tool(block, execute, ask_human) -> dict:
args = block.input
if POLICY.get(block.name, lambda a: True)(args): # ferramenta desconhecida: revisa
preview = build_preview(block.name, args) # 5. dados do sistema, não texto do modelo
review: Review = ask_human(preview) # 6. bloqueia até a decisão
audit.log(block.name, preview, review) # 9
if review.decision == "reject":
return {"type": "tool_result", "tool_use_id": block.id,
"content": f"Recusado pelo revisor: {review.reason}", "is_error": True}
if review.decision == "edit":
args = review.edited_input
result = execute(block.name, args) # 7
return {"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result)}O que você ganhou: a política fica num lugar só, e ferramentas novas são revisadas por padrão. O que você perdeu: a espera é síncrona; para esperas de horas ou dias, salve o estado e retome com um workflow durável.
TypeScript. Use quando o agente roda no runner do SDK e o portão fica dentro da ferramenta.
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
type Review =
| { decision: "approve" }
| { decision: "edit"; edited: { amount_cents: number; key: string; date: string } }
| { decision: "reject"; reason: string };
export function schedulePaymentTool(askHuman: (preview: object) => Promise<Review>) {
return betaZodTool({
name: "schedule_payment",
description: "Agenda um pagamento. Sempre passa por revisão do usuário acima de R$ 500.",
inputSchema: z.object({ key: z.string(), amount_cents: z.number().int().positive(), date: z.string() }),
run: async (args) => {
let input = args;
if (input.amount_cents > 50_000) {
// 5. prévia com dados do sistema
const preview = { recebedor: await bank.lookupKeyOwner(input.key), valor: input.amount_cents, data: input.date };
const review = await askHuman(preview); // 6
await audit.log("schedule_payment", preview, review); // 9
if (review.decision === "reject") return `Recusado pelo usuário: ${review.reason}`;
if (review.decision === "edit") input = review.edited;
}
const result = await bank.schedulePayment(input); // 7
return JSON.stringify(result);
},
});
}O que você ganhou: o portão vive na ferramenta, então nenhum caminho do runner o pula. O que você perdeu: a política fica espalhada por ferramenta; centralize num objeto quando houver muitas.
Python + LangGraph. Use quando a pessoa pode responder minutos ou dias depois, e o processo pode reiniciar no meio.
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt
from typing_extensions import TypedDict
class State(TypedDict):
action: dict
decision: str
result: str
def review(state: State) -> dict:
# 5 e 6. a prévia sai para um canal que o modelo não escreve; o grafo pausa aqui
decision = interrupt({"preview": state["action"], "reason": "pagamento acima do limite"})
return {"decision": decision}
def act(state: State) -> dict:
# 7. retomar com a decisão
if state["decision"] != "approve":
return {"result": f"recusado: {state['decision']}"}
return {"result": bank.schedule_payment(state["action"])}
graph = StateGraph(State)
graph.add_node("review", review)
graph.add_node("act", act)
graph.add_edge(START, "review")
graph.add_edge("review", "act")
graph.add_edge("act", END)
# 4. estado salvo. MemorySaver morre com o processo; em produção use o checkpointer do banco.
app = graph.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "pay-8831"}}
# 3. só entre neste grafo se a política marcou a ação como ponto de revisão
app.invoke({"action": payment, "decision": "", "result": ""}, config)
# mais tarde, no botão do app, não numa frase do chat:
app.invoke(Command(resume="approve"), config)bank e payment são seus. resume aceita "approve", "reject" ou o payload editado. Grave a decisão no mesmo lugar da aba Python (passo 9).
O que você ganhou: a espera sobrevive ao restart e o sim não viaja no texto que o modelo lê. O que você perdeu: um checkpointer para operar, e a regra de qual ação pede revisão, que continua no seu código.
Conferido na documentação de interrupts do LangGraph 1.x em outubro de 2026.
| Biblioteca | Neste padrão | Continua seu |
|---|---|---|
| Loop à mão | A aba Python, pausa síncrona | A política e o canal autenticado |
| SDK da Anthropic | O portão dentro da ferramenta, no runner | A mesma política |
| LangGraph | interrupt mais checkpointer |
Quais ações pausam, e o registro da decisão |
| Temporal | A espera longa como atividade humana, se o resto do fluxo já é um workflow durável | A prévia que a pessoa vê |
Humano no Circuito (Human-in-the-Loop) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Comportamento de agentes. Pausar em pontos definidos para um humano aprovar, editar ou recusar.
Nos cenários reais Assistente financeiro com aprovação, Bot de suporte com escalonamento humano, Assistente de saúde com guardrails e Assistente jurídico.