Lab 87 — Agente com ferramenta: quem executa é o seu código
O problema, e a empresa que o tem
O agente de atendimento da Cadência, evolução direta do RAG do L83, parou de só responder e passou a agir. O time notou que metade das perguntas do golden set não pedia informação — pedia uma AÇÃO: "cadê meu pedido?", "quero o reembolso do 4821", "meu endereço mudou, atualiza aí". Responder com um trecho citado não resolve nenhuma das três. Alguém deu ao agente ferramentas — consultar status de pedido, emitir reembolso, atualizar cadastro — e, para "ele conseguir fazer o que precisar" sem passar o dia ajustando policy, anexou uma única IAM role ampla à função que executa qualquer ferramenta que o modelo decidir chamar.
Três semanas depois, o pedido #4821 recebeu reembolso de R$ 189,90 três vezes — R$ 569,70 debitados no gateway de pagamento da Cadência, R$ 379,80 a mais que o valor correto. Ninguém percebeu na hora: o agente respondeu ao cliente com uma frase educada de confirmação depois de CADA chamada, e as três soaram igualmente corretas. Só a conciliação bancária do fim do mês acusou a diferença, quando o time de finanças cruzou o extrato do gateway com os pedidos de reembolso abertos no sistema.
A causa não foi um bug de digitação nem uma falha do modelo em "entender português". O cliente reformulou a pergunta duas vezes ("o reembolso saiu?", "e aí, já caiu?"), e o agente, sem memória clara de que já tinha chamado a ferramenta com sucesso, interpretou cada reformulação como um pedido novo e chamou
emitir-reembolso de novo — e de novo. Nada no código impedia a repetição: não havia teto de quantas vezes o agente podia chamar a mesma ferramenta na mesma conversa, e não havia nenhum registro dizendo "esta ação já rodou para este pedido". A role ampla garantiu que a terceira chamada tivesse exatamente a mesma permissão que a primeira — e o dinheiro saiu três vezes.
R$ 379,80 de reembolso duplicado, e nenhum alarme disparou
O modelo não decidiu "enganar" ninguém, e não houve falha de segurança no sentido clássico — ninguém invadiu nada. O agente fez exatamente o que a arquitetura permitia: qualquer ferramenta, quantas vezes o raciocínio do modelo decidisse chamar, com uma credencial ampla o bastante para que a terceira chamada custasse tão pouco quanto a primeira. É esse o defeito que este laboratório resolve — não "o modelo errou", mas "nada no código impedia o erro de virar dinheiro real, duas vezes".
O que este laboratório NÃO é
Não é sobre desenhar o laço de raciocínio do agente do zero — Bedrock AgentCore já orquestra decidir-chamar-observar, e este laboratório usa esse orquestrador em vez de reescrevê-lo. Não é sobre detectar prompt injection ou vazamento entre clientes — isso é o L90. Não é sobre um agente puramente diagnóstico, sem poder de ação — isso é o L96, que leva o princípio de blast radius mínimo ao extremo de nunca escrever nada. Este laboratório prova a forma mínima de um agente que AGE com segurança: cada ferramenta com seu próprio código e sua própria IAM role, e um teto de voltas que o código aplica, não que o prompt pede.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando ou uma medição na seção de implantação, não com a sensação de ter entendido agentes.
- Explicar por que o LLM nunca deve segurar uma credencial de execução — e o que muda quando quem executa é uma função sua, determinística.
- Criar uma Lambda e uma IAM role por ferramenta, cada uma restrita à ação e ao recurso que aquela ferramenta específica precisa — não uma role ampla compartilhada.
- Aplicar um teto de voltas no código do orquestrador do agente, não como instrução de prompt.
- Implementar uma chave de idempotência em DynamoDB que impede uma ação irreversível (reembolso) de ser executada duas vezes para o mesmo pedido.
- Reproduzir um laço descontrolado de propósito, medir quantas voltas ele faria sem teto, e provar que o teto interrompe no número exato configurado.
- Ler um log de execução e distinguir a permissão de cada ferramenta sendo checada individualmente, em vez de uma única checagem para tudo.
- Diagnosticar, a partir do log, se uma interrupção veio do teto de voltas ou da checagem de idempotência — são duas defesas diferentes, para duas falhas diferentes.
- Distinguir IAM auth de banco (quem conecta) de GRANT do próprio banco (o que o conectado pode fazer) — as duas camadas de menor privilégio deste laboratório.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Laço de agente (agentic loop) | AIF-C01 | Bedrock AgentCore decide → chama ferramenta → observa resultado → decide de novo | o laço não tem limite embutido por padrão — quem define o teto é quem constrói |
| Teto de iterações / voltas | AIF-C01 | aplicado no código do orquestrador, contando tentativas por (conversa, ferramenta) no DynamoDB | instrução de prompt ("não repita") não é controle — é sugestão que o próprio raciocínio malformado pode ignorar |
| Permissão por ferramenta (least privilege por action group) | AIF-C01, SAA-C03 | uma Lambda e uma IAM role por ferramenta, nunca uma role para o agente inteiro | o mesmo princípio do L41 (derivar do uso real), aplicado por FERRAMENTA em vez de por role única |
| Quem executa a ação (agente vs. código) | AIF-C01 | o LLM nunca tem credencial de execução; ele pede, e a Lambda decide e assina a ação | confundir "o agente decidiu" com "o agente executou" é o erro central deste laboratório |
| Idempotência de ação | AIF-C01, DVA-C02 | chave de idempotência em DynamoDB bloqueia reembolso repetido antes de chamar o gateway | teto de voltas e idempotência resolvem falhas diferentes: uma limita QUANTAS tentativas, a outra limita QUANTAS execuções reais |
| Blast radius de um agente | AIF-C01, SAP-C02 | cada ferramenta só alcança o recurso e a ação que ela precisa; um raciocínio malformado numa ferramenta não vaza para as outras | reduzir o alcance de CADA ferramenta reduz o dano de qualquer decisão errada, mesmo dentro do teto permitido |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve um agente que executou uma ação real (cobrança, reembolso, envio) mais de uma vez, e pede a causa raiz. A resposta esperada não é "o modelo alucinou" — é que nada no desenho impedia a repetição: sem teto de voltas no código e sem idempotência na ação, o número de execuções reais depende só de quantas vezes o raciocínio do modelo decidiu chamar a ferramenta, e isso não tem limite superior garantido por padrão.
Requisitos, e como cada um muda o desenho
Requisito não funcional que não vira uma linha de código ou de policy é intenção. A coluna da direita é onde cada um deixou marca no Terraform, na Lambda ou no orquestrador.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Reembolso não pode ser executado duas vezes para o mesmo pedido | obrigatório | chave de idempotência em DynamoDB, PutItem condicional (attribute_not_exists) ANTES de tocar o gateway de pagamento |
| O laço do agente não pode rodar indefinidamente | teto de 4 tentativas por (conversa, ferramenta) | orquestrador consulta a contagem no DynamoDB antes de cada nova chamada; na 5ª tentativa, interrompe sem invocar a Lambda |
| Cada ferramenta só alcança o que ela precisa, nada além | obrigatório | uma IAM role por ferramenta, com Resource específico — nunca uma role para o agente inteiro |
| O LLM nunca segura credencial de execução | obrigatório | a Lambda é o único lugar que assume a IAM role e chama a API real; o modelo só recebe de volta um resultado estruturado |
| Consulta de status não pode escrever nada | obrigatório | usuário de banco (via RDS IAM auth) com GRANT só de SELECT, aplicado além da permissão de IAM de conectar |
| Interrupção por teto tem de virar atendimento humano, não silêncio | obrigatório | orquestrador responde com mensagem explícita e abre um ticket, em vez de só parar de responder |
| Toda tentativa, permitida ou bloqueada, é auditável depois | obrigatório | cada chamada grava um item no DynamoDB com timestamp, ferramenta, resultado e se foi bloqueada por teto ou por idempotência |
| Latência de cada ferramenta cabe no orçamento de um atendimento ao vivo | até 3 s por chamada de ferramenta | a checagem de idempotência e a contagem de teto rodam antes da chamada real, somando poucos milissegundos ao orçamento |
Arquitetura mínima: uma role, um laço, sem teto
Este é o desenho real da Cadência hoje, e ele é implantável de verdade — resolveu o problema de lançar rápido, com poucas linhas. O laboratório começa medindo exatamente o que ele permite, porque "a role é ampla demais" não é discutível; "esta policy concede rds-db:connect irrestrito e dynamodb:* que nunca é usado" é.
- → pergunta ou pedido em português, ex.: "e aí, já caiu o reembolso?"
- → encaminha a chamada HTTP ao laço do agente
- → decide chamar uma ferramenta — nada impede repetir a mesma na mesma conversa
- → assume a única role configurada, seja qual for a ferramenta
- → SELECT ou UPDATE, com o mesmo usuário de banco para as três ferramentas
- → lê o segredo do gateway sempre que a ferramenta chamada é reembolso
- → devolve o novo status; nada aqui registra que já houve uma chamada igual
- → resultado da ferramenta volta ao laço, sem contagem de tentativa
- → resposta educada, idêntica em confiança na 1ª e na 3ª chamada
- Fora da AWS
- Rede e entrega
- IA e machine learning
- Compute
- Segurança e identidade
- Banco de dados
É o que a Cadência tem hoje, adaptado às pressas do RAG do L83: o agente responde E age, mas quem age é uma única Lambda com uma role ampla, chamada quantas vezes o raciocínio do modelo decidir. Percorra os passos e repare que não existe nenhum nó contando tentativas — é essa ausência, não uma configuração errada, que deixou o reembolso repetir três vezes.
- O cliente reformula a mesma pergunta. "cadê meu reembolso?" e "já caiu?" chegam como duas mensagens distintas, sem nenhum sinal explícito de que tratam do mesmo pedido de reembolso.
- A API só repassa, como sempre. Não há etapa de deduplicação nem de checagem de histórico entre receber a mensagem e acordar o laço do agente.
- O agente decide chamar a ferramenta — de novo. Sem memória estruturada de que a ferramenta já rodou com sucesso, o raciocínio do modelo trata a reformulação como pedido novo e decide chamar emitir-reembolso outra vez.
- Uma única credencial executa qualquer ferramenta. A mesma role que consulta status também emite reembolso e também lê o segredo do gateway — a 3ª chamada de reembolso custa ao sistema exatamente o mesmo esforço de permissão que a 1ª.
- O ERP registra a mudança, sem saber que é repetida. O RDS aplica o UPDATE que a Lambda mandar; ele não tem como saber que o mesmo reembolso já foi aplicado dois minutos antes.
- O resultado volta sem nenhuma contagem. Nenhum nó deste desenho grava "esta foi a 3ª chamada desta ferramenta nesta conversa" — a informação que faltaria para interromper existe só na cabeça de quem olhar o log depois do incidente.
- A resposta ao cliente soa igualmente confiável nas três vezes. Sem sinal de erro nem de repetição, a mensagem de confirmação é a mesma — só a conciliação bancária do fim do mês revelou a diferença de R$ 379,80.
// Policy real anexada à role "cadencia-agente-execucao" — a mesma que gerou o
// reembolso triplicado. Uma única statement cobre as tres ferramentas, porque
// ninguem parou para separar o que cada uma precisa.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AmploParaConseguirAgir",
"Effect": "Allow",
"Action": [
"dynamodb:*",
"rds-db:connect",
"secretsmanager:GetSecretValue"
],
"Resource": "*"
// dynamodb:* sequer e usado hoje — foi deixado "por garantia". rds-db:connect
// sem Resource especifico deixa a role logar como QUALQUER usuario do banco,
// inclusive um usuario com permissao de escrita que a ferramenta de consulta
// nunca deveria alcançar. secretsmanager:GetSecretValue com "*" deixa a
// ferramenta de consulta ler o mesmo segredo do gateway de pagamento que só
// a ferramenta de reembolso deveria tocar.
}
]
}
O raio de repetição de uma role sem teto
Não é o tamanho da permissão que é o problema central — é que a MESMA permissão vale para a 1ª chamada de uma ferramenta e para a 30ª. Uma role ampla sem teto de voltas não é "insegura" no sentido de vazamento de dado: é insegura no sentido de que o número de vezes que uma ação irreversível pode rodar depende só de quanto o raciocínio de um modelo de linguagem decidir insistir — e isso não é um número que se garanta por instrução.
Arquitetura para produção: uma Lambda, uma role, um ledger por ferramenta
A diferença em relação à Figura 1 não é "adicionar um teto" — é que cada ferramenta ganha seu próprio código e sua própria credencial, e uma peça nova existe só para responder "já tentei isso demais?" e "já executei isso antes?" antes de qualquer ação real. Cada peça rastreia a uma linha da tabela de requisitos da seção anterior.
- → mesma pergunta ou pedido em português, mesma interface da Figura 1
- → encaminha a chamada ao orquestrador do agente
- → consulta a contagem de tentativas ANTES de decidir a próxima chamada
- → decide chamar consultar-status-pedido, dentro do teto
- → decide chamar emitir-reembolso, dentro do teto
- → decide chamar atualizar-cadastro, dentro do teto
- → assume role só-leitura, restrita ao usuário de banco da ferramenta
- → assume role restrita ao usuário de banco e ao segredo do gateway
- → assume role restrita ao usuário de banco de cadastro
- → SELECT com usuário cadencia_agente_consulta
- → UPDATE em colunas de cadastro, usuário cadencia_agente_cadastro
- → UPDATE em status de reembolso, usuário cadencia_agente_reembolso
- → lê o segredo do gateway — única ferramenta que alcança este nó
- → grava tentativa no ledger, mesmo sendo leitura
- → PutItem condicional: bloqueia a chamada ao gateway se o pedido já foi reembolsado
- → grava tentativa no ledger
- → publica métrica de voltas por conversa, base do alarme de teto
- → resposta final, ou aviso de teto atingido com encaminhamento humano
- Fora da AWS
- Rede e entrega
- IA e machine learning
- Compute
- Segurança e identidade
- Banco de dados
- Gestão e governança
A troca não é "adicionar um teto" — é que cada ferramenta ganha seu próprio código e sua própria credencial, e uma peça nova (o ledger em DynamoDB) passa a existir só para responder duas perguntas antes de qualquer ação real: "já tentei isso demais?" e "já executei isso antes?". Cada peça nova rastreia a uma linha da tabela de requisitos — nenhuma está aqui por hábito.
- O cliente pergunta, e reformula, como na Figura 1. Nada muda do lado de quem pergunta — a diferença inteira está atrás da API.
- O orquestrador nasce com teto de voltas no código, não na instrução. Bedrock AgentCore chama uma consulta ao ledger antes de decidir a próxima ferramenta — o teto é uma comparação numérica no orquestrador, não uma frase que o modelo pode ignorar.
- Cada ferramenta tem sua própria Lambda e sua própria role. consultar-status, emitir-reembolso e atualizar-cadastro rodam em três funções distintas, cada uma assumindo uma role que só alcança o que aquela ferramenta específica precisa.
- O banco vê três usuários diferentes, com GRANT diferente. IAM restringe QUEM conecta (por usuário de banco); o GRANT do Postgres restringe o QUE aquele usuário pode fazer depois de conectado — as duas camadas, não uma só.
- Só uma ferramenta alcança o segredo do gateway. consultar-status e atualizar-cadastro nunca precisam do segredo, e suas roles não têm a permissão — a negação vem do IAM, antes de qualquer linha de código rodar.
- A idempotência bloqueia o repetido, e o ledger alimenta o próximo turno. O PutItem condicional falha se já existe um item para (conversa, pedido, reembolso) — a 2ª e a 3ª tentativa nunca chegam a debitar o gateway. Toda tentativa, permitida ou bloqueada, vira um item no DynamoDB, que alimenta tanto o alarme do CloudWatch quanto a próxima checagem de teto.
- Na 5ª tentativa da mesma ferramenta, o orquestrador nem chama a Lambda. O teto de 4 tentativas é aplicado ANTES da chamada, não depois — a interrupção vira uma resposta explícita ao atendente, com encaminhamento humano, em vez de silêncio ou de uma 5ª execução.
Blast radius por ferramenta, não por agente
O ganho que mais importa não é ter três Lambdas em vez de uma — é que uma decisão errada do raciocínio do modelo dentro da ferramenta de consulta (por exemplo, tentar ler um campo que não existe) fica contida na role "agente-consulta", que não alcança o gateway de pagamento nem a tabela de cadastro. O blast radius de qualquer erro do agente agora é do tamanho da ferramenta que ele chamou, não do tamanho de tudo que o agente PODERIA fazer.
Como funciona, ponta a ponta
O log abaixo é o que a Lambda emitir-reembolso escreve no DynamoDB a cada tentativa — note que a 2ª tentativa é registrada como "bloqueado_por_idempotencia", não como um segundo reembolso executado.
{
"evento": "TENTATIVA_DE_FERRAMENTA_REGISTRADA",
"timestamp": "2026-08-08T09:14:02.118Z",
"conversaId": "conv-77f1",
"pedidoId": "4821",
"ferramenta": "emitir-reembolso",
"tentativaNumero": 1,
"resultado": "executado",
"chaveIdempotencia": "conv-77f1#4821#reembolso",
"_comentario": "1a chamada real: o PutItem condicional teve sucesso, o gateway foi debitado."
}
{
"evento": "TENTATIVA_DE_FERRAMENTA_REGISTRADA",
"timestamp": "2026-08-08T09:16:41.902Z",
"conversaId": "conv-77f1",
"pedidoId": "4821",
"ferramenta": "emitir-reembolso",
"tentativaNumero": 2,
"resultado": "bloqueado_por_idempotencia",
"chaveIdempotencia": "conv-77f1#4821#reembolso",
"_comentario": "cliente perguntou 'ja caiu?' de novo; PutItem condicional falhou porque a\nchave ja existia -- o gateway NUNCA foi chamado nesta tentativa."
}
O teto sozinho não teria evitado o incidente — a idempotência sim
Se o teto de voltas tivesse sido a única defesa, as 4 primeiras tentativas de reembolso teriam sido AUTORIZADAS pelo orquestrador — o teto só bloqueia a 5ª. É a chave de idempotência, e não o teto, que impede a 2ª e a 3ª de debitarem o gateway. As duas defesas resolvem problemas diferentes: uma limita quantas VEZES o agente pode insistir; a outra limita quantas EXECUÇÕES reais aquela insistência produz.
As decisões, e o que se perde em cada uma
📋 O agente de atendimento da Cadência já tem três ferramentas (consulta, reembolso, cadastro) rodando com uma role ampla e sem teto de voltas. Um reembolso já saiu triplicado, uma auditoria interna de segurança tem data marcada, e a equipe precisa manter o agente respondendo em produção enquanto corrige.
Separar o código por ferramenta é o que torna "blast radius por ferramenta" possível — sem isso, restringir a role do agente inteiro sempre esbarra na ferramenta mais permissiva que ele precisa ter. O teto no código, e não no prompt, garante o limite mesmo quando o raciocínio do modelo está malformado — é exatamente a situação em que uma instrução de prompt tem mais chance de ser ignorada. E a idempotência resolve o que o teto sozinho não resolve: mesmo dentro do limite permitido, uma ação real não pode rodar duas vezes.
Alt: Manter uma role só, e só adicionar uma instrução de prompt "não repita a mesma ação" — instrução de prompt é sugestão, não controle — é precisamente o tipo de regra que um raciocínio malformado (a causa real do incidente) pode não seguir; o modelo não estava tentando burlar nada, só reinterpretou uma reformulação como pedido novo
Alt: Confiar no timeout padrão da execução do agente para limitar o laço — timeout limita TEMPO, não NÚMERO de ações reais — um laço rápido pode executar várias chamadas de reembolso bem dentro de qualquer timeout razoável antes de ser cortado
Alt: Confiar só na proteção de duplicidade do próprio gateway de pagamento — depender de um sistema externo terceirizar sua própria idempotência é abrir mão de controle sobre a decisão; nem todo gateway garante isso, e quando garante, o comportamento em caso de bloqueio raramente é o que a Cadência quer mostrar ao cliente
Alt: Colocar um humano aprovando toda chamada de ferramenta, sempre — resolve o risco, mas destrói a proposta de valor do agente — volta ao mesmo tempo de resposta do atendimento manual que o agente existia para reduzir; aprovação humana cabe como reforço em ações de valor muito alto, não como regra geral
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Quem executa a ação real | código determinístico (Lambda), nunca o LLM diretamente | dar ao modelo uma credencial de execução direta | o código valida, decide e é auditável linha a linha; o raciocínio de um LLM não é | nada — é estritamente mais seguro sem custo de capacidade, porque o LLM continua decidindo O QUE chamar, só não executa |
| Escopo da IAM role | uma role por ferramenta, restrita a ação e recurso específicos | uma role para o agente inteiro | contém o dano de qualquer chamada malformada ao tamanho da ferramenta chamada | mais peças de infraestrutura para manter — três roles e três Lambdas em vez de uma |
| Onde aplicar o teto de voltas | código do orquestrador, contando no DynamoDB | instrução no prompt do sistema; limite de tempo (timeout) | é o único lugar que garante o número, independente do que o modelo "decidir" | exige manter um ledger — mais uma tabela, mais uma escrita por tentativa |
| Como evitar ação duplicada | chave de idempotência condicional no DynamoDB | confiar no teto de voltas sozinho; confiar no gateway externo | bloqueia a EXECUÇÃO repetida mesmo dentro do teto permitido de tentativas | exige desenhar a chave certa (conversa + pedido + ferramenta) — errar a chave deixa passar ou bloqueia ação legítima |
| Menor privilégio no banco | usuário de Postgres por ferramenta, com GRANT restrito, além do IAM restringir quem conecta | um único usuário de banco para as três ferramentas | IAM controla QUEM conecta; GRANT controla O QUE o conectado pode fazer — as duas juntas, não uma no lugar da outra | mais usuários de banco para provisionar e rotacionar |
Construir: uma Lambda e uma IAM role por ferramenta
Três funções, três roles, um ledger. O Terraform abaixo é o oposto direto da policy ampla da Figura 1 — cada Statement existe porque uma ferramenta específica precisa dela, e nenhuma role vê o que as outras duas alcançam.
# ferramentas.tf -- uma role e uma funcao por ferramenta. Nenhuma das tres
# roles concede dynamodb:* nem secretsmanager:* de forma ampla -- cada uma so
# alcança exatamente o que aquela ferramenta usa.
locals {
ledger_arn = aws_dynamodb_table.execucoes.arn
}
# -- Ferramenta 1: consultar-status-pedido (so leitura) ----------------------
resource "aws_iam_role" "agente_consulta" {
name = "cadencia-agente-consulta-role"
assume_role_policy = data.aws_iam_policy_document.confianca_lambda.json
tags = { squad = "atendimento", lab = "L87", ferramenta = "consulta" }
}
resource "aws_iam_role_policy" "agente_consulta_policy" {
role = aws_iam_role.agente_consulta.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["rds-db:connect"]
# Resource inclui o NOME DO USUARIO DE BANCO -- a mesma role nao
# consegue logar como cadencia_agente_reembolso nem cadencia_agente_cadastro,
# mesmo que alguem tente forcar isso no codigo da Lambda.
Resource = ["${data.aws_rds_cluster.erp.arn}/cadencia_agente_consulta"]
},
{
Effect = "Allow"
Action = ["dynamodb:PutItem", "dynamodb:GetItem"]
Resource = [local.ledger_arn]
},
]
})
}
resource "aws_lambda_function" "consultar_status" {
function_name = "cadencia-consultar-status-pedido"
role = aws_iam_role.agente_consulta.arn
runtime = "dotnet8"
handler = "ConsultarStatus::Handler::Executar"
timeout = 5
}
# -- Ferramenta 2: emitir-reembolso (irreversivel -- a mais restrita) --------
resource "aws_iam_role" "agente_reembolso" {
name = "cadencia-agente-reembolso-role"
assume_role_policy = data.aws_iam_policy_document.confianca_lambda.json
tags = { squad = "atendimento", lab = "L87", ferramenta = "reembolso" }
}
resource "aws_iam_role_policy" "agente_reembolso_policy" {
role = aws_iam_role.agente_reembolso.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["rds-db:connect"]
Resource = ["${data.aws_rds_cluster.erp.arn}/cadencia_agente_reembolso"]
},
{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
# Unica das tres roles com este Statement -- consulta e cadastro nao tem.
Resource = [aws_secretsmanager_secret.gateway_pagamento.arn]
},
{
Effect = "Allow"
Action = ["dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:UpdateItem"]
Resource = [local.ledger_arn]
},
]
})
}
resource "aws_lambda_function" "emitir_reembolso" {
function_name = "cadencia-emitir-reembolso"
role = aws_iam_role.agente_reembolso.arn
runtime = "dotnet8"
handler = "EmitirReembolso::Handler::Executar"
timeout = 8
}
# -- Ferramenta 3: atualizar-cadastro -----------------------------------------
resource "aws_iam_role" "agente_cadastro" {
name = "cadencia-agente-cadastro-role"
assume_role_policy = data.aws_iam_policy_document.confianca_lambda.json
tags = { squad = "atendimento", lab = "L87", ferramenta = "cadastro" }
}
resource "aws_iam_role_policy" "agente_cadastro_policy" {
role = aws_iam_role.agente_cadastro.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["rds-db:connect"]
Resource = ["${data.aws_rds_cluster.erp.arn}/cadencia_agente_cadastro"]
},
{
Effect = "Allow"
Action = ["dynamodb:PutItem", "dynamodb:GetItem"]
Resource = [local.ledger_arn]
},
]
})
}
resource "aws_lambda_function" "atualizar_cadastro" {
function_name = "cadencia-atualizar-cadastro"
role = aws_iam_role.agente_cadastro.arn
runtime = "dotnet8"
handler = "AtualizarCadastro::Handler::Executar"
timeout = 5
}
resource "aws_dynamodb_table" "execucoes" {
name = "cadencia-agente-execucoes"
billing_mode = "PAY_PER_REQUEST"
hash_key = "chaveIdempotencia"
attribute {
name = "chaveIdempotencia"
type = "S"
}
# chaveIdempotencia = "{conversaId}#{pedidoId}#{ferramenta}" -- e o PutItem
# condicional sobre esta chave (attribute_not_exists) que bloqueia repeticao.
tags = { squad = "atendimento", lab = "L87" }
}
A restrição de IAM sozinha não é suficiente quando o recurso final é um banco relacional: `rds-db:connect` controla quem consegue ABRIR a conexão como qual usuário, mas não controla quais comandos SQL esse usuário pode rodar depois de conectado. Essa segunda camada é GRANT do próprio Postgres — e as duas juntas, não uma no lugar da outra, são o que entrega menor privilégio de verdade aqui.
# grants-postgres.sh -- a segunda camada de menor privilegio, que o IAM
# sozinho nao cobre. rds-db:connect restringe QUEM loga como qual usuario;
# o GRANT do proprio Postgres restringe O QUE aquele usuario pode fazer
# depois de logado -- sem isso, um usuario com IAM auth restrito ainda
# poderia rodar qualquer SQL se tivesse sido criado com privilegio amplo.
psql -h $ERP_ENDPOINT -U admin -d cadencia <<'SQL'
-- Usuario da ferramenta de consulta: SELECT, nada mais.
CREATE USER cadencia_agente_consulta WITH LOGIN;
GRANT rds_iam TO cadencia_agente_consulta;
GRANT SELECT ON pedidos, clientes TO cadencia_agente_consulta;
-- Usuario da ferramenta de reembolso: so a coluna de status de reembolso.
CREATE USER cadencia_agente_reembolso WITH LOGIN;
GRANT rds_iam TO cadencia_agente_reembolso;
GRANT SELECT (id, valor_pedido) ON pedidos TO cadencia_agente_reembolso;
GRANT UPDATE (status_reembolso, valor_reembolsado) ON pedidos TO cadencia_agente_reembolso;
-- Usuario da ferramenta de cadastro: so colunas de endereco e contato.
CREATE USER cadencia_agente_cadastro WITH LOGIN;
GRANT rds_iam TO cadencia_agente_cadastro;
GRANT UPDATE (endereco, telefone, email) ON clientes TO cadencia_agente_cadastro;
SQL
# Confira depois: nenhum dos tres usuarios tem GRANT em nenhuma tabela alem
# das listadas acima -- \dp pedidos e \dp clientes no psql mostram exatamente
# isso, e e o que a auditoria vai conferir.
IAM restringe quem conecta; GRANT restringe o que o conectado faz
Um erro comum: criar as três roles de IAM certinho e deixar os três usuários de banco com o mesmo GRANT amplo "porque é mais rápido". Nesse caso, a role de consulta ainda consegue UPDATE em produção — a restrição de IAM barrou a conexão com o nome errado, mas não o comando errado com o nome certo. As duas camadas têm de ser conferidas separadamente; uma auditoria que só lê a policy de IAM não vê esse buraco.
Construir: o teto de voltas, aplicado antes de qualquer chamada
O orquestrador é o único lugar que decide se uma nova chamada de ferramenta é permitida. Repare que a checagem acontece ANTES de invocar a Lambda — a 5ª tentativa nem chega perto da credencial da ferramenta de reembolso.
// OrquestradorDoLaco.cs -- o teto de voltas vive AQUI, no codigo que decide
// se uma ferramenta pode ser chamada, nunca numa frase do prompt do sistema.
// E chamado pelo AgentCore a cada passo do laço, antes de invocar a Lambda
// da ferramenta escolhida pelo modelo.
public class OrquestradorDoLaco
{
private const int TetoDeVoltasPorFerramenta = 4;
private readonly AmazonDynamoDBClient _dynamo = new();
private const string TabelaLedger = "cadencia-agente-execucoes";
public async Task<DecisaoDeChamada> AvaliarProximaChamadaAsync(
string conversaId, string ferramenta, string pedidoId)
{
// Conta quantas tentativas ja existem para esta combinacao especifica --
// nao para a conversa inteira, para nao punir um cliente que faz tres
// perguntas legitimas sobre tres pedidos diferentes.
var tentativas = await ContarTentativasAsync(conversaId, ferramenta, pedidoId);
if (tentativas >= TetoDeVoltasPorFerramenta)
{
// A interrupcao acontece ANTES de invocar a Lambda -- a 5a tentativa
// nunca chega perto da credencial da ferramenta.
await RegistrarInterrupcaoPorTetoAsync(conversaId, ferramenta, pedidoId);
return DecisaoDeChamada.Bloquear(
motivo: "teto_de_voltas",
mensagemAoAtendente: "Não consigo confirmar isso automaticamente depois de " +
"várias tentativas — vou encaminhar para um atendente humano revisar " +
$"o pedido {pedidoId}.");
}
return DecisaoDeChamada.Permitir(tentativas + 1);
}
private async Task<int> ContarTentativasAsync(string conversaId, string ferramenta, string pedidoId)
{
var prefixo = $"{conversaId}#{pedidoId}#{ferramenta}";
var resposta = await _dynamo.QueryAsync(new QueryRequest
{
TableName = TabelaLedger,
KeyConditionExpression = "begins_with(chaveIdempotencia, :prefixo)",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
[":prefixo"] = new AttributeValue { S = prefixo },
},
});
return resposta.Count;
}
private Task RegistrarInterrupcaoPorTetoAsync(string conversaId, string ferramenta, string pedidoId)
{
// Publica a metrica que alimenta o alarme do CloudWatch -- e o sinal de
// que o LACO estava se comportando mal, nao so de que uma acao foi negada.
return _cloudwatch.PutMetricDataAsync(new PutMetricDataRequest
{
Namespace = "Cadencia/AgenteAtendimento",
MetricData = { new MetricDatum {
MetricName = "InterrupcoesPorTeto",
Value = 1,
Dimensions = { new Dimension { Name = "Ferramenta", Value = ferramenta } },
}},
});
}
}
Do outro lado, a Lambda em si não confia em ter sido chamada só uma vez — ela grava a própria chave de idempotência antes de tocar o gateway, como segunda camada independente do teto do orquestrador.
// EmitirReembolso.cs -- a Lambda so executa depois que o orquestrador ja
// autorizou a chamada; e ela quem grava a chave de idempotencia ANTES de
// tocar o gateway, porque e a UNICA garantia de que a acao real nao repete.
public class EmitirReembolsoHandler
{
private readonly AmazonDynamoDBClient _dynamo = new();
private readonly SecretsManagerClient _segredos = new();
private const string TabelaLedger = "cadencia-agente-execucoes";
public async Task<ResultadoFerramenta> ExecutarAsync(PedidoDeReembolso pedido)
{
var chave = $"{pedido.ConversaId}#{pedido.PedidoId}#reembolso";
// PutItem condicional: so grava se a chave NAO existir ainda. Se falhar,
// outra chamada (desta ou de uma tentativa anterior) ja processou este
// reembolso -- o gateway nunca e chamado nesta execucao.
try
{
await _dynamo.PutItemAsync(new PutItemRequest
{
TableName = TabelaLedger,
Item = new Dictionary<string, AttributeValue>
{
["chaveIdempotencia"] = new AttributeValue { S = chave },
["status"] = new AttributeValue { S = "tentando" },
["timestamp"] = new AttributeValue { S = DateTime.UtcNow.ToString("o") },
},
ConditionExpression = "attribute_not_exists(chaveIdempotencia)",
});
}
catch (ConditionalCheckFailedException)
{
return ResultadoFerramenta.JaProcessado(
$"O reembolso do pedido {pedido.PedidoId} já foi processado nesta conversa.");
}
// So a partir daqui o segredo do gateway e lido -- e so a role desta
// Lambda tem permissao de IAM para faze-lo.
var credencial = await _segredos.GetSecretValueAsync(
new GetSecretValueRequest { SecretId = "cadencia/gateway-pagamento" });
var resultadoGateway = await ChamarGatewayAsync(pedido.PedidoId, pedido.Valor, credencial);
await AtualizarLedgerAsync(chave, resultadoGateway);
return ResultadoFerramenta.Executado(resultadoGateway);
}
}
Defesa em profundidade: uma falha na primeira camada não derruba a segunda
As duas defesas são independentes de propósito. Se o orquestrador tivesse um bug e deixasse passar uma 5ª chamada, a chave de idempotência ainda bloquearia o débito repetido no gateway. Se a chave de idempotência tivesse um bug de formação, o teto de voltas ainda limitaria o número total de chamadas a 4. Nenhuma das duas sozinha cobre o que a outra cobre.
Implantar, e provar com número
A prova não é "parece que bloqueou" — é rodar o mesmo cenário de reformulação ambígua 20 vezes contra os dois desenhos, e contar quantas execuções reais cada um produziu.
# prova.sh -- reproduz de proposito um raciocinio malformado que insiste na
# mesma ferramenta, contra os DOIS desenhos, e conta quantas execucoes REAIS
# aconteceram em cada um. Nenhuma conclusao aqui vem de "parece que bloqueou".
CENARIO=cenario-reformulacao-ambigua.jsonl # 20 execucoes do mesmo pedido de
# reembolso, cada uma com o cliente
# reformulando a pergunta 2 a 3 vezes
# -- Rodada 1: arquitetura minima (Figura 1), sem teto e sem idempotencia -----
python3 provocar_laco.py --arquitetura minima --cenario "$CENARIO" \
--saida resultado-minima.json
# Resultado medido: media de 6,4 chamadas de emitir-reembolso por conversa;
# pior caso, 19 chamadas consecutivas antes do timeout interno do proprio
# Bedrock cortar a execucao -- nao um limite da aplicacao.
# Execucoes REAIS no gateway (nao simulado): 6,4 em media -- toda chamada
# aceita vira debito, porque nao ha idempotencia.
# -- Rodada 2: arquitetura de producao (Figura 2), teto=4 + idempotencia -----
python3 provocar_laco.py --arquitetura producao --cenario "$CENARIO" \
--saida resultado-producao.json
# Resultado medido: 20 de 20 conversas interrompidas EXATAMENTE na 4a tentativa
# de emitir-reembolso quando o raciocinio insiste; 0 execucoes reais duplicadas
# no gateway -- so a 1a tentativa de cada conversa debita.
jq '.[] | select(.tentativaNumero > 1) | .resultado' resultado-producao.json | sort | uniq -c
# 19 "bloqueado_por_idempotencia"
# 1 "bloqueado_por_teto"
# Nas 20 conversas, 19 tiveram 2a tentativa bloqueada pela chave de
# idempotencia; em 1 conversa o raciocinio insistiu ate a 5a tentativa, e o
# teto (nao a idempotencia) foi quem interrompeu.
| Prova | Antes — role ampla, sem teto (Figura 1) | Depois — Lambda+role por ferramenta, teto=4 (Figura 2) | O que mudou |
|---|---|---|---|
| Chamadas de emitir-reembolso por conversa (média, 20 execuções) | 6,4 | 1,0 | o teto e a idempotência juntos convergem para exatamente uma execução real por pedido |
| Pior caso de chamadas consecutivas numa única conversa | 19 (interrompido só pelo timeout interno do Bedrock, não pela aplicação) | 4 (interrompido pelo código do orquestrador, no teto configurado) | o limite deixou de depender de um timeout genérico e passou a ser um número que a Cadência escolheu e testou |
| Execuções reais no gateway de pagamento por pedido | até 3 no incidente medido (R$ 569,70 debitados por um reembolso de R$ 189,90) | sempre 1, mesmo quando o agente tenta de novo | a chave de idempotência bloqueia a execução repetida antes do gateway, independente de quantas vezes o agente decida chamar |
| Conversas interrompidas pelo teto de voltas (das 20) | 0 (nenhum teto existia) | 1 | o teto é raro por desenho — a maioria das reformulações é bloqueada mais cedo, pela idempotência, e só o caso mais insistente chega ao teto |
| Permissão de cada ferramenta, checada individualmente | não — uma única role respondia por qualquer ferramenta | sim — o log mostra qual role e qual usuário de banco autorizaram CADA chamada específica | auditoria consegue apontar exatamente qual permissão autorizou qual ação, ferramenta por ferramenta |
O teto de 4 não é mágico — é calibrado pela hipótese declarada
O teto de 4 tentativas é calibrado pela hipótese declarada na abertura — cliente reformula em média 1,8 vezes por conversa. Um teto de 2 interromperia conversas legítimas com alguma frequência; um teto de 10 deixaria o incidente original se repetir quase do mesmo jeito antes de interromper. O número certo depende da hipótese medida, não de uma constante universal — recalibre se o padrão de reformulação da Cadência mudar.
Segurança: o que muda quando o modelo passa a acionar código
Nos laboratórios anteriores desta banda, o pior resultado de uma manipulação era uma resposta errada. Aqui o pior resultado é uma AÇÃO errada, feita com as credenciais da sua conta, contra os dados reais de um cliente. A tabela de risco muda de natureza junto.
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Uma role para todas as ferramentas | Alta | Crítico | Uma função e uma role por ferramenta — a decisão central deste laboratório | Analisador de acesso sobre uso real da role | Separar a role e reduzir ao que foi efetivamente usado em 30 dias |
| Ação de escrita repetida pelo laço | Alta | Alto | Chave de idempotência por intenção, registrada antes de executar | Contagem de execuções idempotentes que encontraram registro anterior | Estornar a duplicata e conferir se a chave cobre a intenção, não a chamada |
| Conteúdo recuperado induzindo chamada de ferramenta | Média | Crítico | Ferramenta de escrita nunca é acionável a partir de texto do acervo sem confirmação; escopo de recurso amarrado ao cliente da sessão | Chamadas de ferramenta cujo argumento não aparece na conversa do usuário | É o L90 acontecendo dentro de um agente — tratar como incidente de segurança, não como defeito de qualidade |
| Ferramenta com escopo de recurso amplo | Alta | Crítico | A role da ferramenta de reembolso alcança o pedido da SESSÃO, não a tabela inteira | Negações de IAM por condição de recurso | Amarrar o escopo à identidade do cliente autenticado; sem isso, convencer o agente a citar outro número de pedido é suficiente |
| Laço sem teto consumindo cota e orçamento | Alta | Médio | Teto de voltas verificado ANTES de qualquer chamada, não depois | Distribuição de voltas por conversa; alarme na cauda | Encerrar a conversa com desfecho declarado ao usuário, nunca em silêncio |
| Argumento de ferramenta com dado pessoal em log | Alta | Alto | Registrar o NOME da ferramenta e o identificador, nunca o corpo do argumento | Varredura periódica por padrão de documento nos logs | Expurgar o fluxo e corrigir o serializador — o agente registra mais que uma API tradicional porque registra o raciocínio |
Quebrar de propósito: quatro falhas, e a que já aconteceu de verdade
A primeira injeção abaixo não é hipótese: é o incidente que originou este laboratório, reproduzido de propósito para provar que o desenho novo o contém.
| Falha injetada | Como injetar | Sintoma enganoso | Diagnóstico correto |
|---|---|---|---|
| Registro de idempotência desligado | Remover a checagem do registro e pedir o reembolso do pedido 4821 numa conversa em que o cliente reformula a pergunta três vezes | A conversa transcorre normalmente, o cliente é atendido, e o agente parece eficiente | Três reembolsos emitidos. O laço do agente não distingue "o cliente pediu de novo" de "eu ainda não fiz" — para ele, cada volta é uma decisão nova sobre o mesmo estado aparente. Idempotência num agente não é sobre repetição de requisição de rede, é sobre repetição de INTENÇÃO ao longo do raciocínio, e a chave precisa refletir a intenção do usuário, não o identificador da chamada |
| Teto de voltas verificado depois da chamada | Mover a checagem para depois da execução da ferramenta e fazer uma pergunta que o agente não consiga resolver | O teto funciona: a conversa termina no número certo de voltas | Terminou depois de executar a volta que deveria ter sido impedida. Para ferramenta de leitura isso custa tokens; para ferramenta de escrita, custa um reembolso a mais. O ponto de verificação não é detalhe de implementação — é a diferença entre um teto que limita gasto e um que limita dano |
| Role compartilhada entre duas ferramentas | Dar à função de consulta de pedido a mesma role da de reembolso | Tudo continua funcionando, e o código fica mais simples | O raio de alcance de um defeito na ferramenta de consulta passou a incluir emitir reembolso. Nada precisa ser explorado por um atacante para isso doer: basta o modelo chamar a ferramenta errada, o que ele faz ocasionalmente por construção. Uma role por ferramenta é o que torna o erro do modelo inofensivo |
| Modelo inventando uma ferramenta que não existe | Perguntar algo que sugira uma capacidade não implementada — "cancela e já reemite o pedido" | O agente responde que executou. Nenhum erro aparece | O despacho precisa rejeitar nome desconhecido explicitamente e devolver isso ao modelo como observação, para ele corrigir o rumo. Sem essa rejeição explícita, um `switch` sem caso padrão simplesmente não faz nada, e o modelo segue raciocinando como se tivesse feito — e diz isso ao cliente |
Um colega sugere: "vamos dar ao agente uma IAM role com permissão para as três ações — consultar, reembolsar, atualizar cadastro — assim ele consegue agir sem travar em erro de permissão". Qual é o defeito estrutural dessa proposta?
Observabilidade: as perguntas que o painel tem de responder
Uma API tradicional tem uma requisição por ação. Um agente tem um número variável de ações por conversa, decidido em tempo de execução por um modelo — e é essa variabilidade que o painel precisa tornar visível.
- Qual a distribuição de voltas por conversa, e não a média? A média fica em duas ou três; o que interessa é a cauda que encosta no teto, porque é ela que consome orçamento e sinaliza raciocínio em círculo.
- Quantas conversas terminaram por atingir o teto em vez de por conclusão? Cada uma é um cliente que não foi atendido.
- Qual a frequência de cada ferramenta, e qual a taxa de erro de cada uma separadamente?
- Quantas execuções idempotentes encontraram registro anterior? É a prova, em número, de que a primeira injeção de falha está sendo contida em produção.
- Quantas chamadas foram negadas por IAM, por ferramenta? Negação aqui é a barreira funcionando — e também sinal de que algo antes dela falhou.
- Qual o custo em tokens por conversa CONCLUÍDA, separado do custo por conversa abandonada?
| Alarme | Limiar inicial | O que ele pega |
|---|---|---|
| ConversasNoTeto | acima de 5% das conversas da hora | agente entrando em círculo — ferramenta com erro, ou tarefa fora do que ele consegue resolver |
| IdempotenciaAcionada | acima da linha de base medida na primeira semana | aumento de repetição de intenção: pode ser cliente ansioso, pode ser laço mal contido |
| NegacaoIamPorFerramenta | qualquer valor acima de zero | o modelo tentou uma ação fora do escopo — sempre investigar individualmente |
| FerramentaDesconhecida | qualquer ocorrência | a quarta injeção — modelo inventando capacidade, que costuma indicar descrição de ferramenta ambígua |
| CustoPorConversaP95 | acima do dobro da linha de base | crescimento silencioso do gasto por conversa, que a média mensal esconderia |
Escala: 10 conversas, 10 mil, 1 milhão, e a perda de uma zona
| Ordem de grandeza | O que muda no desenho | O que NÃO muda |
|---|---|---|
| 10 conversas por dia, piloto | Nada. O laço roda, as funções respondem, e o custo é irrelevante | O teto de voltas e a idempotência já são obrigatórios — o incidente do reembolso triplicado aconteceu em volume baixo |
| 10 mil conversas por dia | A concorrência das funções de ferramenta passa a importar, e o limite de taxa do modelo é sentido no pico. O custo por conversa vira a métrica de produto | A separação de role por ferramenta: ela não custa desempenho nenhum e continua sendo a peça que contém o dano |
| 1 milhão de conversas por dia | O laço síncrono deixa de caber: conversas longas passam a exigir estado externo e retomada, em vez de manter tudo na memória de uma execução. O teto de voltas passa a ser também um mecanismo de proteção de capacidade | A natureza do problema: mais escala não muda o fato de que o modelo decide quantas voltas dar, e que só o seu código pode limitar isso |
| Perda de uma zona de disponibilidade | Funções e modelo são regionais; o que precisa de duas zonas é o armazenamento do registro de idempotência | Se o registro de idempotência ficar indisponível, a decisão correta é RECUSAR a ação de escrita, não executá-la sem registro |
| Perda da região | O agente para. O registro de idempotência é o dado que não pode divergir entre regiões, e replicá-lo é o problema difícil de qualquer plano multi-região aqui | Vale a discussão do L99: uma ação executada em duas regiões porque o registro não replicou a tempo é o mesmo incidente do reembolso triplicado, em escala maior |
Custo: por que um agente custa mais que a soma das suas chamadas
A estimativa natural multiplica o custo de uma chamada pelo número de voltas. Ela subestima, e o motivo é estrutural: cada volta reenvia tudo que veio antes.
| Cenário | O que domina | O que ninguém nota |
|---|---|---|
| Conversa típica — 2 a 3 voltas | O contexto base, reenviado poucas vezes. Custo próximo da estimativa ingênua | É a conversa que todo mundo usa para estimar, e ela não representa a conta |
| Conversa difícil — 8 voltas até o teto | O histórico acumulado, com folga sobre todo o resto | Poucas conversas longas dominam a fatura de um agente. O custo médio por conversa é uma métrica enganosa aqui; a distribuição é a informação real, e é por isso que o painel mede a cauda |
| Ferramentas devolvendo objeto completo da API | O resultado de ferramenta, multiplicado pelo efeito quadrático | Recortar o retorno da ferramenta para os campos usados reduz o termo que mais cresce. É uma mudança de poucas linhas com efeito superlinear na conta — o oposto de micro-otimização |
O teto de voltas é decisão de arquitetura, não de segurança apenas
Preço por 1.000 tokens, por invocação e por requisição muda por região e por modelo. Trate os valores como ordem de grandeza; confirme o vigente na página de preços antes de usar em proposta. Ele limita o dano, limita a espera do cliente e limita um custo que cresce com o quadrado. Escolher o número é um trade-off explícito entre quantas tarefas difíceis o agente conclui e quanto a cauda custa — e o dado que sustenta a escolha é a distribuição de voltas por conversa concluída, que este laboratório instrumenta.
Well-Architected nos seis pilares
| Pilar | Situação hoje | Risco | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | Uma função e uma role por ferramenta, versionadas | Cada ferramenta nova acrescenta função, role e descrição — e a descrição é prompt, sem revisão de código equivalente | Tratar descrição de ferramenta como código: revisão, versão e teste de que o modelo a escolhe nos casos certos | Alta |
| Segurança | Escopo mínimo por ferramenta, teto antes da execução, registro de idempotência | O escopo de recurso ainda pode ser amplo demais dentro de uma ferramenta específica | Amarrar o recurso à identidade do cliente da sessão, não só ao tipo de ação | Alta |
| Confiabilidade | Ferramentas isoladas; falha de uma não derruba as outras | Registro de idempotência indisponível deixa a ação de escrita sem rede | Recusar escrita quando o registro estiver indisponível, de forma explícita e declarada | Alta |
| Eficiência de desempenho | Laço síncrono, com resultado de ferramenta integral no histórico | Latência e custo crescem com o quadrado das voltas | Recortar retorno de ferramenta; avaliar modelo menor para a decisão de qual ferramenta chamar | Média |
| Otimização de custo | Teto de voltas definido, retorno de ferramenta ainda verboso | A cauda de conversas longas domina a fatura sem aparecer na média | Instrumentar custo por conversa na cauda e recortar os retornos mais pesados | Alta |
| Sustentabilidade | Toda ferramenta é uma função sob demanda, sem capacidade ociosa | Voltas desperdiçadas em conversas que atingem o teto sem concluir | Reduzir conversas improdutivas melhorando a descrição das ferramentas — é economia de recurso e de experiência ao mesmo tempo | Baixa |
Onde mais IA entra neste agente, e onde ela não deve decidir
| Ideia | Vale a pena? | Por quê |
|---|---|---|
| Um modelo menor decidindo qual ferramenta chamar | Sim, e costuma se pagar rápido | A escolha de ferramenta é uma classificação sobre um conjunto pequeno e conhecido — tarefa em que modelo menor acerta bem e custa uma fração. Como o custo cresce com o quadrado das voltas, baratear a volta é a alavanca de maior efeito. A redação final da resposta pode continuar no modelo maior |
| O modelo definindo o VALOR do reembolso | Não, e esta é a linha que não se cruza | Valor de reembolso é regra de negócio determinística: sai do pedido, da política e da data. Um modelo pode buscar esses dados e explicar a conta ao cliente; não pode ser a autoridade sobre o número. A pergunta que decide é "se isto sair errado, dá para explicar a decisão a um auditor?" — e "o modelo achou" não é resposta |
| Detectar conversa que vai atingir o teto antes de ela atingir | Talvez, e provavelmente cedo demais | É previsão sobre um sinal que a Cadência ainda não tem em volume. O passo anterior, barato e útil, é ANALISAR as conversas que bateram no teto e descobrir se há poucas causas repetidas — quase sempre há, e a correção é uma ferramenta nova ou uma descrição melhor, não um previsor |
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Sintoma em produção | Forma correta |
|---|---|---|---|
| Uma IAM role ampla para o agente inteiro "para ele conseguir agir" | parece mais rápido não configurar policy por ferramenta — até o primeiro incidente; escrever três roles com Resource específico dá trabalho que uma role com "*" evita | uma decisão errada em QUALQUER ferramenta alcança tudo que qualquer outra ferramenta poderia alcançar — o raio de dano é o do agente inteiro | uma Lambda e uma IAM role por ferramenta, cada uma restrita ao que aquela ferramenta específica precisa |
| Confiar em instrução de prompt para limitar repetição ("não chame a mesma ferramenta duas vezes") | é a forma mais rápida de "resolver" o problema — uma linha no system prompt, sem precisar desenhar um ledger nem mexer no orquestrador | funciona na maioria das conversas e falha silenciosamente exatamente quando o raciocínio já está malformado — a situação em que a instrução mais precisava valer | teto de voltas aplicado no código do orquestrador, contando tentativas de forma que não dependa da obediência do modelo |
| Deixar o LLM montar e assinar a chamada à API de destino diretamente | parece eliminar uma camada de indireção — "por que ter uma Lambda no meio, se o modelo já sabe chamar a API?" | a credencial de execução fica exposta ao que o modelo decidir gerar, sem validação determinística entre a decisão e a ação | o LLM só pede; uma função sua valida a entrada, decide e é o único código que segura a credencial de execução |
| Tratar teto de voltas e idempotência como a mesma proteção | os dois soam como "controle de repetição", e implementar só um parece cobrir o risco inteiro | com só o teto, uma ação irreversível ainda executa múltiplas vezes DENTRO do limite permitido; com só a idempotência, o laço pode rodar indefinidamente em ferramentas sem efeito colateral, gastando token sem fim | as duas juntas: teto limita QUANTAS tentativas, idempotência limita QUANTAS execuções reais uma ação irreversível específica produz |
| Um único usuário de banco para todas as ferramentas do agente, com GRANT amplo | criar três usuários de Postgres com GRANT diferente é mais trabalho de setup do que reaproveitar a credencial que já existe | a role de IAM da ferramenta de consulta está corretamente restrita, mas o usuário de banco por trás dela ainda pode escrever — a auditoria de IAM não pega isso | um usuário de banco por ferramenta, com GRANT restrito à ação e às colunas que aquela ferramenta específica usa |
Evolução em níveis: de responder a agir com segurança
A terceira arquitetura não é um desenho novo — é a resposta a QUANDO trocar de desenho. Cada nível resolve um risco medido neste laboratório e compra outro no lugar.
O agente responde perguntas com RAG e citação, mas nunca AGE — toda ação real (reembolso, atualização de cadastro) continua manual, feita por um humano depois de ler a resposta.Agente com ferramenta: uma Lambda e uma IAM role por ferramenta, teto de voltas no código do orquestrador, chave de idempotência antes de qualquer ação irreversível.Reembolso acima de um limiar (ex.: R$ 500) exige um segundo fator antes da Lambda tocar o gateway — uma aprovação determinística (regra de valor) ou humana, não mais raciocínio do próprio agente decidindo sozinho.Um agente de diagnóstico, com acesso só de leitura, separado por desenho — não por regra — do agente que executa ações. É o padrão que o L96 leva ao extremo: um agente de operação que só produz hipótese com evidência, sem NUNCA poder agir.Um golden set de cenários de ferramenta (no molde do L88) testa, no CI, se o agente continua respeitando o teto e não repetindo ação a cada mudança de prompt ou de modelo.Um catálogo de ferramentas compartilhado entre agentes de vários times, cada uma com contrato e IAM próprios — a mesma disciplina de menor privilégio por ferramenta, agora em escala de plataforma. É também o padrão medido na solução S3 do catálogo de arquiteturas de IA na AWS: agente com ferramenta que consulta o estado real do pedido reduz até 40% do escalonamento para humano — o ganho vem de ter ferramenta, não de prompt melhor.Por que o L96 depende deste laboratório mesmo sendo só leitura
A ordem importa: separar ferramentas por Lambda e role (nível 2) é pré-requisito para isolar leitura de escrita por desenho (nível 4) — sem a separação já existir por ferramenta, não há "agente só de leitura" para destacar, só uma role ampla que também lê. É por isso que o L96, mesmo focado em diagnóstico sem ação nenhuma, declara este laboratório como dependência.
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Log ou métrica | Correção |
|---|---|---|---|---|
| A mesma ação foi executada mais de uma vez na conversa | Chave de idempotência derivada da chamada, não da intenção | Reconstruir a conversa e comparar as chaves geradas em cada volta | Execuções idempotentes que encontraram registro anterior — se for zero num caso repetido, a chave não está agrupando | Derivar a chave da intenção — cliente, pedido e tipo de ação — e não do identificador da requisição |
| O agente diz que executou algo que não aconteceu | Nome de ferramenta desconhecido caindo num despacho sem caso padrão | Procurar na conversa a chamada cujo nome não corresponde a nenhuma ferramenta registrada | Contagem de ferramenta desconhecida | Rejeitar explicitamente e devolver a rejeição ao modelo como observação, para ele corrigir o rumo em vez de seguir iludido |
| Conversas travando no teto de voltas | Ferramenta com erro persistente, ou descrição ambígua fazendo o modelo insistir na escolha errada | Agrupar as conversas que bateram no teto pela sequência de ferramentas chamadas | Proporção de conversas encerradas por teto; taxa de erro por ferramenta | Quase sempre há poucas causas repetidas. Corrigir a ferramenta ou a descrição resolve mais que aumentar o teto |
| Negações de IAM aparecendo no painel | O modelo tentou uma ação fora do escopo da role daquela ferramenta | Ler o evento negado e o argumento que o motivou | Negações por ferramenta, no rastro de auditoria | A barreira funcionou — investigar por que a tentativa surgiu. Se for uso legítimo, o escopo está apertado demais; se não for, é o L90 |
| O custo por conversa dobrou sem aumento de tráfego | Retorno de ferramenta ficou maior, e o efeito é quadrático nas voltas | Comparar o tamanho médio do resultado de ferramenta antes e depois | Tokens de entrada por conversa, na cauda | Recortar o retorno para os campos usados. Uma mudança de API a montante pode ter engordado o objeto sem ninguém notar |
Limpeza: o que o destroy não leva
# limpeza.sh -- ordem importa: as tres Lambdas referenciam as tres roles e o
# ledger; remover a tabela do DynamoDB antes de remover as funcoes deixa
# invocacao de teste falhando sem necessidade.
# 1) Remover as tres funcoes e suas roles (Terraform cuida disso na ordem certa)
terraform destroy -target=aws_lambda_function.consultar_status \
-target=aws_lambda_function.emitir_reembolso \
-target=aws_lambda_function.atualizar_cadastro -auto-approve
# 2) Revogar os tres usuarios de banco -- terraform destroy NAO conhece usuario
# de Postgres criado via psql fora do provider; isso fica de fora do state.
psql -h $ERP_ENDPOINT -U admin -d cadencia <<'SQL'
DROP USER IF EXISTS cadencia_agente_consulta;
DROP USER IF EXISTS cadencia_agente_reembolso;
DROP USER IF EXISTS cadencia_agente_cadastro;
SQL
# 3) terraform destroy cuida do resto: roles de IAM, tabela do ledger, segredo
# do gateway, grupo de logs de cada Lambda
terraform destroy -auto-approve
# 4) Conferir que nao sobrou item nenhum no ledger cobrando leitura/escrita
aws dynamodb scan --table-name cadencia-agente-execucoes \
--query 'Count' --output text
# Esperado: tabela ja removida -- o comando acima deve falhar com
# ResourceNotFoundException, nao retornar 0.
Usuário de banco criado fora do Terraform não morre com o destroy
Os três usuários de Postgres (`cadencia_agente_consulta`, `cadencia_agente_reembolso`, `cadencia_agente_cadastro`) foram criados via `psql`, fora do state do Terraform — `terraform destroy` não sabe que eles existem e não os remove. Um usuário de banco esquecido com `rds_iam` concedido continua sendo uma porta de entrada válida por IAM auth, mesmo depois de a Lambda que o usava ter sido destruída.
Resumo: problema, peça e motivo
| Problema | Peça | Motivo |
|---|---|---|
| LLM com credencial de execução direta | Lambda por ferramenta, sempre entre o modelo e a API real | código determinístico valida e executa; o raciocínio do modelo só decide O QUE chamar |
| Role ampla "para conseguir agir" | uma IAM role por ferramenta, Resource específico | contém o dano de uma decisão errada ao tamanho da ferramenta chamada, não do agente inteiro |
| Laço sem limite de tentativas | teto de voltas no código do orquestrador, contado no DynamoDB | garante o número independente de instrução de prompt ou de raciocínio malformado |
| Ação irreversível executada mais de uma vez | chave de idempotência condicional antes de chamar o gateway | bloqueia a execução real repetida mesmo dentro do teto de tentativas permitido |
| IAM restrito, mas SQL aberto por trás | usuário de Postgres por ferramenta, com GRANT restrito | segunda camada de menor privilégio que o IAM sozinho não cobre num banco relacional |
| Interrupção sem aviso | resposta explícita ao atendente mais encaminhamento humano quando o teto é atingido | teto sem esse retorno vira silêncio, e silêncio não é o que o requisito de atendimento pede |
Desafio — sem roteiro
O requisito
Adicione uma ferramenta nova ao agente que executa uma ação DESTRUTIVA (cancelar um pedido, deletar um registro) e prove que ela passa pelo MESMO portão de confirmação que as outras ações destrutivas do laboratório já exigem.
Critério de aceite — executável, não "verifique se funciona"
Uma chamada do agente à ferramenta nova é interceptada ANTES de executar — o sistema pede confirmação explícita (humana ou de uma segunda camada de política) — e só executa de fato depois da aprovação; sem aprovação, a ação NÃO acontece, mesmo que o modelo "decida" chamá-la.
- Dica 1: O portão de confirmação vive fora do modelo — é código determinístico que intercepta a chamada de ferramenta antes de executá-la, nunca uma instrução de prompt pedindo pro modelo "sempre confirmar antes" (isso é sugestão, não controle).
- Dica 2: Reuse o MESMO mecanismo (a mesma função/middleware) que já protege as outras ações destrutivas do laboratório — escrever um portão paralelo só para a ferramenta nova é o tipo de duplicação que diverge com o tempo.
- Dica 3: Teste o caminho de REJEIÇÃO explicitamente: simule a confirmação sendo negada e confirme que a ferramenta não roda e que o agente recebe de volta uma mensagem de que a ação foi negada — não apenas o caminho feliz de aprovação.
Lembrete de limpeza
Todo recurso que este desafio criar entra no mesmo `terraform destroy` do laboratório — nada fica para trás cobrando sozinho.
Perguntas frequentes
❓ Por que o LLM nunca deve ter uma credencial de IAM própria para agir?
❓ O teto de voltas não deveria ficar no prompt do sistema, já que é mais simples de ajustar?
❓ Se o teto de voltas já limita as tentativas, por que também precisa de idempotência?
❓ Uma role um pouco mais ampla, compartilhada entre ferramentas, não economiza trabalho?
❓ O que acontece com o cliente quando o agente atinge o teto de voltas?
❓ Bedrock AgentCore já aplica algum limite de iterações, sem eu escrever o orquestrador?
❓ Por que a ferramenta de consulta também grava uma tentativa no ledger?
Fixando
O time decide colocar a instrução "nunca chame a mesma ferramenta duas vezes na mesma conversa" no system prompt do agente, em vez de aplicar um teto no código do orquestrador. Por que essa escolha não garante o comportamento esperado?
Na arquitetura de produção, o teto de voltas está configurado em 4 tentativas por ferramenta, mas o incidente medido na seção de prova mostra que a 2ª e a 3ª tentativa de reembolso foram bloqueadas por IDEMPOTÊNCIA, não pelo teto. O que isso prova sobre a relação entre as duas defesas?
Próximo laboratório
Próximo passo: L96 (agente só-leitura) e L88 (avaliação contínua do agente)
Este laboratório separou ferramentas por Lambda e IAM role, e aplicou teto de voltas e idempotência para uma ação de ESCRITA (reembolso). O L96 leva o mesmo princípio de blast radius mínimo a um agente inteiramente de LEITURA — um diagnóstico de incidente operacional que produz hipótese com evidência, mas nunca tem permissão para agir — e mostra por que "somente leitura" ainda precisa de role própria por ferramenta, não de uma role ampla "só de consulta". O L88, em paralelo, ataca a pergunta que este laboratório não responde: como saber, de forma contínua e não só no dia do incidente, se o agente continua respeitando o teto e a idempotência a cada mudança de prompt.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…