Design agents where human judgment is a core engineering requirement, not an afterthought. Learn to build escalation gates using the Anthropic tool-use API — grounded in a real clinical-AI deployment and the support triage running example.
Quando eu estava implantando o sistema de IA clínica (de-identificado), a etapa de revisão humana não era opcional — era o produto. Uma recomendação de IA sobre cuidado ao paciente tinha que passar pelos olhos de um clínico antes de chegar a qualquer registro. Sem exceções, sem substituições, sem bypass por "alta confiança".
Essa restrição mudou como eu pensava sobre o design de sistemas. A maioria dos tutoriais de agentes enquadra o humano no loop como um fallback — o que o sistema faz quando a IA está incerta. Fizemos o oposto: o portão humano foi o primeiro componente que projetamos. Todo o resto existia para levar o trabalho a esse portão corretamente e obter uma decisão de volta de forma limpa.
O resultado não foi um sistema mais lento. Foi um confiável. Os clínicos que o usaram disseram que se sentiam no controle, não ignorados. Essa distinção importa enormemente — é a diferença entre uma ferramenta que é usada e uma que é contornada. Quando um portão de segurança é invisível ou não confiável, os praticantes constroem soluções alternativas informais que derrotam seu propósito.
Pense em um portão humano como um parar de emergência em uma linha de fábrica. Você não remove o parar de emergência para tornar a linha mais rápida. Você projeta a linha para que o parar seja acessível, confiável e rápido de acionar — e então otimiza tudo ao redor. O objetivo não é eliminar o parar; é fazer com que, em cada segundo que a linha esteja funcionando, o parar esteja ao alcance e o operador confie nele.
Princípio central: Um portão humano é um requisito de engenharia, não uma limitação. Projete-o primeiro; otimize ao redor dele depois.
Esse princípio se generaliza muito além da IA clínica. Na plataforma de automação de suporte, o mesmo padrão se aplicava: tickets que o agente não conseguia classificar com confiança — disputas de cobrança ambíguas, escalações com implicações legais, qualquer coisa relacionada a cancelamento de conta — iam para um humano antes de qualquer resposta ser enviada. O líder de suporte que revisava as escalações nos disse que o portão os tornava mais dispostos a deixar o agente lidar com os casos fáceis. Eles confiavam no sistema precisamente porque sabiam que os casos difíceis ainda chegavam a eles.
Isso se baseia em outra dinâmica que vale nomear: sistemas que ignoram revisores — mesmo com boas intenções, mesmo com alta precisão — corroem a confiança rapidamente quando um erro surge. Em domínios de alto risco, um erro sempre surge eventualmente. Um portão que está consistentemente em vigor é um portão que pode ser melhorado. Um portão que foi ignorado, mesmo uma vez, é um portão que não pode ser confiável.
Este é o Fator 8 da metodologia dos agentes 12 fatores: Contate Humanos como Ferramentas.
O caminho de escalação não é um modo de falha; é uma chamada de ferramenta — assim como busca
ou uma leitura de banco de dados. Assim como você define uma ferramenta search_knowledge_base
para que o agente possa recuperar artigos relevantes, você define uma ferramenta
escalate_to_human para que o agente possa invocar julgamento humano. O modelo a chama quando
reconhece que não consegue resolver uma situação com confiança. Esse enquadramento importa:
tratar a escalação como uma capacidade em vez de um fallback molda como você constrói, testa e
mede o portão.
E é o Fator 9: Encontre os Usuários Onde Eles Estão — a superfície de revisão deve estar onde seus revisores já trabalham, não onde um fornecedor de framework acha que eles deveriam trabalhar. Na plataforma de automação de suporte, isso significava Slack. O líder de suporte não queria um novo portal; queria uma mensagem no canal que já monitorava.
Os dois fatores trabalham juntos. O Fator 8 torna o julgamento humano uma operação de primeira classe no conjunto de ferramentas do agente. O Fator 9 torna a superfície de revisão sem fricção o suficiente para que os revisores realmente a usem. Um portão que dispara corretamente mas envia notificações para um portal que ninguém verifica não é um portão funcionando — é um portão que foi socialmente ignorado.
Ao final deste módulo, você será capaz de:
escalate_to_human ao agente de triagem de suporte usando a API de uso de ferramentas da AnthropicNo SDK da Anthropic, a escalação humana é implementada como uma chamada de ferramenta — o
mesmo mecanismo usado para busca, leituras de banco de dados e execução de código. O agente vê
escalate_to_human em sua lista de ferramentas e a chama quando lhe falta confiança. Isso
mantém a interface consistente: o modelo decide quando escalar, e o código decide o que fazer
com essa decisão.
O modelo mental para isso: pense em escalate_to_human da mesma forma que pensa em
send_email ou query_database. Cada uma é uma ferramenta que o agente usa para interagir
com o mundo. A diferença é que send_email aciona um sistema automatizado e escalate_to_human
aciona um humano. Da perspectiva do modelo, a interface é a mesma — chame a ferramenta, forneça
os inputs necessários e aguarde o resultado. Da perspectiva da engenharia, a diferença é que a
resposta do humano pode levar minutos ou horas, então o agente deve suspender entre chamar a
ferramenta e receber o resultado. Esse padrão de suspensão é o desafio central de design que
este módulo aborda.
Defina a ferramenta de escalação como um schema JSON. Este schema também é a política de
escalação do agente: quando um novo engenheiro pergunta "que tipos de tickets vão para revisão
humana?", o campo description é a resposta autoritativa — não uma thread do Slack ou uma
página de wiki desatualizada.
Observe que o campo description serve também como documentação. Como a política é código,
as mudanças nela passam por revisão de código — adicionar uma nova condição de escalação é um
pull request, não uma mensagem do Slack, e carrega um revisor, um diff e um timestamp.
O schema também define as informações que o modelo deve fornecer ao escalar. O campo reason
garante que o revisor entenda por que o ticket foi escalado, não apenas que foi. O enum
priority — normal ou urgent — roteia a notificação do Slack: escalações urgentes
alertam o líder de plantão imediatamente; as normais vão para a fila de revisão. Essa lógica
de roteamento vive no notificador, não no agente. A definição da ferramenta é o contrato;
a implementação é em outro lugar.
Agora use a ferramenta em uma única chamada client.messages.create. O modelo classifica o
ticket e ou rascunha uma resposta ou chama escalate_to_human — em uma única viagem de ida
e volta:
Copie este bloco, defina ANTHROPIC_API_KEY e execute. A função faz uma coisa: pede ao Claude
para lidar com um ticket, depois verifica se ele quer escalar. Se qualquer bloco de conteúdo for
uma chamada tool_use para escalate_to_human, o modelo decidiu que não consegue resolver o
ticket com confiança. Caso contrário, retorna uma resposta rascunhada diretamente.
Usar uso de ferramentas na mesma chamada que a etapa de rascunho evita dois erros comuns. Primeiro, passes de classificação separados criam duas viagens de ida e volta e duas chances para o raciocínio do modelo divergir: o classificador pode dizer "escalar", mas se você então pedir ao modelo para rascunhar um motivo de escalação em uma segunda chamada, a segunda chamada pode discordar sobre a sensibilidade do ticket. Manter em uma chamada significa que o raciocínio do modelo é auto-consistente. Segundo, listas de regras codificadas — "se o ticket contém 'cancelar' OU 'legal' então escalar" — ficam desatualizadas no momento em que o vocabulário dos tickets evolui. O julgamento do modelo se adapta a novas fraseologias; sua lista de regras não se adapta.
Erro comum: Usar uma chamada de IA separada para decidir se deve escalar. Mantenha a decisão de escalação dentro da mesma chamada de mensagem que rascunha a resposta. Dividi-la cria duas viagens de ida e volta e duas chances para o raciocínio do modelo divergir.
Agora defina o portão que recebe uma escalação e a roteia para um revisor humano. Comece com a estrutura de dados que o portão precisa:
O request_id é um UUID para que, quando múltiplos tickets forem escalados simultaneamente,
a resposta de aprovação do revisor referencie o correto. O campo reviewer_note carrega o
rascunho editado do revisor ou o motivo de rejeição de volta para o agente. Um revisor que
aprova o rascunho como está deixa reviewer_note vazio; um que quer mudar a formulação o
preenche antes de clicar em Aprovar. O agente prefere a versão do revisor quando ela existe.
Agora a classe do portão em si. Suas três responsabilidades: serializar a escalação, notificar o revisor e sinalizar que a execução deve pausar:
A chamada state_store.save precede o notificador por uma razão sutil mas importante: se um
revisor responder no instante em que a notificação chegar e o estado ainda não tiver sido
salvo, o runner falhará ao carregar a solicitação pendente. Sempre persista o estado antes de
acionar efeitos colaterais externos.
O notifier e o state_store são injetados como dependências em vez de codificados. O portão
não precisa saber se o notificador envia Slack, email ou um alerta de pager — injetar
dependências mantém o portão testável e substituível sem modificar a lógica.
Antes de continuar, considere: o que quebraria se state_store.save e self.notifier.send
acontecessem na ordem inversa? Que modo de falha específico a ordenação atual previne e sob
quais condições de tempo você realmente o veria?
Considere também: a classe HumanApprovalGate recebe um notifier e um state_store como
argumentos do construtor. O que você precisaria mudar se quisesse mudar do Slack para email?
E se quisesse usar um banco de dados diferente para o armazenamento de estado? Como esse design
se compara a um que codifica uma implementação específica de notificador diretamente na classe?
Na plataforma de automação de suporte, nenhum ticket escalado poderia ficar sem resposta indefinidamente. Se o líder de suporte não respondesse em 30 minutos, o sistema escalava para o gerente. Se isso também expirasse, o ticket era bloqueado — nunca aprovado automaticamente e enviado ao cliente sem aprovação humana.
Codificar esta política em código significa que ela passa por revisão de código, tem um timestamp de merge e pode ser pesquisada por qualquer pessoa na equipe. Equipes que pulam esta conversa descobrem a política de escalação às 2h da manhã durante um incidente. Escrever o código força a conversa antes do sistema entrar em produção.
A linha mais importante é return 2. O Nível 2 é bloqueado, não aprovado automaticamente.
Muitas equipes automatizam o fallback porque se preocupam com o throughput. Em um contexto de
suporte onde uma resposta errada a uma disputa legal ou de cobrança pode criar obrigações que a
empresa não pode reverter, esse trade-off está errado. Um ticket bloqueado expõe uma falha de
processo que pode ser investigada e corrigida.
Anti-padrão: Aprovar automaticamente no timeout. Se nenhum humano responder a tempo, a ação correta é bloquear ou escalar — nunca prosseguir como se a aprovação tivesse sido concedida. Uma política de escalação que aprova automaticamente não é um portão de segurança; é um atraso com etapas extras.
As constantes no topo são colocadas lá deliberadamente. Quando um auditor de conformidade pergunta "quanto tempo um ticket pode ficar antes de ser escalado?", a resposta é uma busca simples no código. Código que codifica política operacional deve tornar essa política legível para não-engenheiros, não enterrada em arquivos de configuração.
O portão se encaixa no agente de triagem como uma etapa em uma cadeia linear. Dois caminhos
existem: o caminho feliz (agente resolve o ticket automaticamente) e o caminho de escalação
(agente chama escalate_to_human e o portão roteia para um humano). Cada etapa recebe um
estado e retorna um estado.
Esta é uma escolha arquitetural importante. Muitos tutoriais introduzem o humano no loop como um modo especial que requer infraestrutura separada — um motor de workflow, um processo de longa duração, uma fila de mensagens. Essas ferramentas são úteis em escala, mas não são necessárias para o padrão funcionar. Um armazenamento de estado e um runner agendado são suficientes para começar.
O dataclass TriageState torna cada campo que o agente carrega explícito. Não há estado oculto.
Quando o sistema é depurado após um incidente de produção, o estado em qualquer ponto da execução
é inspecionável.
O retorno booleano de step_classify_and_draft é o ponto de ramificação: True roteia para o
portão de aprovação; False significa que o modelo rascunhou uma resposta que o runner pode
enviar diretamente.
A segunda etapa solicita aprovação e suspende. Observe que ela não espera em um loop:
step_request_approval registra o request_id e retorna — não bloqueia. O chamador, o runner
do agente, verifica o armazenamento de estado do portão em um cronograma e retoma a etapa
final quando uma decisão chega. Isso mantém o agente sem estado entre as etapas: nenhuma
thread fica aberta esperando por um humano. O agente pode ser reiniciado, implantado em um
processo diferente ou travar completamente — a solicitação pendente vive no armazenamento de
estado, não na memória.
Este é o mesmo princípio de "etapas sem estado com estado explícito" do módulo de fluxo de controle, agora aplicado ao desafio específico de decisões humanas assíncronas.
Quando o humano age, o runner carrega o ApprovalRequest, verifica seu status e chama o
caminho apropriado. Existem exatamente dois caminhos: aprovar e rejeitar. Tornar esses dois
caminhos funções explícitas os mantém independentemente testáveis e claramente nomeados.
Antes de ver o código, note a assimetria: o caminho de aprovação executa uma ação; o caminho de rejeição apenas registra dados e para. Essa assimetria é intencional. Aprovação é uma permissão para agir; rejeição é um sinal para parar e investigar.
Bom padrão: Torne o caminho de rejeição tão visível quanto o caminho de aprovação. Uma rejeição é dado — registre o motivo, exponha-o às pessoas certas e pare de forma limpa. Rejeições repetidas com a mesma nota são um sinal para melhorar o prompt do agente ou expandir a description da ferramenta para ser mais específica.
O caminho de rejeição não tenta novamente por padrão. Na plataforma de automação de suporte, uma resposta rejeitada acionou um fluxo de trabalho manual de acompanhamento — um processo humano, não automatizado. O agente havia feito sua parte ao expor o rascunho; a rejeição era um sinal para a equipe de suporte, não um código de erro a ser tratado programaticamente.
O campo reviewer_note em rejection_reason também é o loop de feedback para engenharia de
prompt. Se um tipo específico de ticket for rejeitado repetidamente com o mesmo motivo — "tom
muito formal para uma disputa de cobrança" ou "contexto de conta ausente" — esse padrão é
visível no log de estado e pode ser traduzido diretamente em uma melhoria de prompt.
O runner une todas as etapas em uma sequência e lida com ambos os caminhos. Observe que o caminho feliz retorna cedo — a maioria dos tickets nunca deve chegar ao portão de escalação:
Cada etapa é uma função; o runner é uma sequência de chamadas de função. As etapas podem ser
testadas unitariamente de forma independente, reordenadas sem reescrever uma hierarquia de
classes e entendidas de uma vez. O runner recebe state_store como parâmetro — a mesma função
run_triage_agent pode ser chamada em testes com um armazenamento na memória e em produção
com um armazenamento com suporte de Postgres.
O padrão de runner fino também significa que o runner pode ser substituído por um motor de workflow adequado mais tarde. As etapas estão já desacopladas do runner, então a migração seria uma mudança apenas no runner, não em nenhuma função de etapa. Projete as etapas para serem portáteis; o runner é uma preocupação operacional.
Quando adicionar aprovação automatizada baseada em confiança — onde o sistema envia a resposta se o raciocínio do modelo parece completo — introduziria mais risco do que manter o portão humano? Que condições precisariam ser verdadeiras para que a automação fosse a escolha genuinamente mais segura? O que você mediria ao longo do tempo para saber que cruzou esse limiar?
Application-level questions on human-agent collaboration patterns.
1. After `step_request_approval` returns, what does the agent runner do next?
Correct Answer: B
The step stores the request_id and returns without blocking. The runner checks the state store on a schedule and resumes finalization when a decision is recorded. This keeps the agent stateless between steps and does not hold a thread open.
2. In `request_approval`, `state_store.save` is called before `notifier.send`. What failure would occur if the order were reversed?
Correct Answer: B
If the notifier fires first and a reviewer responds immediately, the runner tries to load the request from the state store before save has been called. The state would be missing and the runner would error. Always persist state before triggering external side effects.
3. In the escalation policy, Tier 2 returns 'blocked' rather than auto-approving. What is the primary reason?
Correct Answer: B
An escalation policy encodes the system's risk tolerance. In high-stakes contexts, the cost of an unchecked error vastly exceeds the cost of a blocked workflow. Tier 2 being 'blocked' communicates that risk tolerance to everyone who reads the code.
4. What is the main benefit of storing all agent state in an explicit TriageState dataclass rather than local variables?
Correct Answer: C
When state is explicit, you can inspect it at any point — before the gate, after the gate, or after a crash. Hidden state is a leading cause of bugs in agent systems because failures become invisible until they compound into larger problems.
5. When would adding an automated confidence-threshold approval introduce MORE risk than a human gate?
Correct Answer: C
Automation is appropriate when errors are cheap to reverse and latency costs more than an error. When errors are expensive — irreversible actions, legal implications, customer trust — human gates reduce total risk even if they increase latency.
6. After a rejection, the code records the reviewer's note and stops — it does not retry. What is the main reason to avoid automatic retry on rejection?
Correct Answer: B
A rejection communicates that the draft was not acceptable. Retrying immediately treats the reviewer as an obstacle rather than a collaborator. Surface the rejection to the right people and let a human decide whether and how to proceed.
7. Why does defining `escalate_to_human` as an Anthropic tool (rather than a separate AI call to classify) improve the design?
Correct Answer: B
Using tool-use in the same message call as the drafting step means the model evaluates the ticket once and either returns a draft or calls escalate_to_human. A separate classification call creates two round trips, two chances for the model's reasoning to diverge, and higher latency with no benefit.