Lab 81 — Primeira chamada ao Bedrock do .NET 8
O problema, e a empresa que o tem
A Cadência (L01, L03) cresceu o cadastro de lojistas o suficiente para que o time de produto percebesse um padrão: o lojista digita o produto de forma crua — "caneca 300ml azul ceramica tema pet" — e a descrição que aparece na vitrine para o comprador final é literalmente esse texto, sem revisão. Alguém do time de produto reescrevia manualmente as descrições dos itens mais vendidos, uma vez por semana, num ritmo que não escala com o número de lojistas.
A ideia de usar um LLM para transformar o atributo cru em descrição de vitrine surgiu rápido. O que não surgiu rápido foi a resposta para uma pergunta simples: onde essa chamada mora? Um dev, sob pressão do primeiro piloto com 20 lojistas, colocou a chamada ao Bedrock direto no componente React da tela de cadastro — no navegador do lojista, com uma chave de API temporária embutida no bundle JavaScript para autenticar. Foi bloqueado em code review na mesma tarde: qualquer pessoa que abrisse o DevTools do navegador tinha a chave completa, com o mesmo alcance de permissão que a aplicação inteira.
A segunda tentativa moveu a chamada para dentro do CatalogoApi.cs, o serviço .NET 8 no Fargate que já existe desde o L01 e que, depois do L42, roda com uma task role sem nenhuma chave estática. Certo até ali. Mas a chamada em si ficou do jeito mais simples que compila: síncrona, bloqueante, sem streaming — o lojista olha uma tela parada por 4 a 6 segundos enquanto o modelo gera o texto inteiro antes de devolver qualquer coisa —, sem retentativa para quando o Bedrock sinaliza limite de taxa, sem nenhum registro de quantos tokens a chamada consumiu, e com uma policy IAM que dá `bedrock:*` sobre `Resource: "*"` porque foi copiada do primeiro exemplo que apareceu numa busca.
Nenhuma dessas cinco decisões é dramática sozinha. Juntas, formam exatamente o que este laboratório existe para consertar: uma primeira chamada ao Bedrock que funciona no piloto com 20 lojistas e não tem nenhuma das propriedades que o time vai precisar quando o piloto virar produto.
O que este laboratório NÃO é
Não é sobre RAG — respostas com trecho citável e taxa de acerto medida é o L83, e ele parte exatamente do cliente que este módulo constrói. Não é sobre versionar ou testar prompt em produção — isso é o L82. Não é sobre escolher QUAL modelo usar para cada tarefa (o mais barato vs. o mais capaz) — essa decisão de custo e latência em escala é o L89. Este laboratório resolve uma coisa: onde a chamada ao Bedrock mora dentro da arquitetura da Cadência, e com quais propriedades mínimas ela precisa nascer para não ser reescrita da próxima vez que alguém mexer nela.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando, uma medição ou uma chamada real na seção de implantação — não com a sensação de ter entendido.
- Explicar por que o ID de um modelo no Bedrock carrega o prefixo do provedor (`anthropic.`, `amazon.`), e o que muda quando o provedor troca.
- Chamar o Bedrock com streaming (`InvokeModelWithResponseStream`) e medir a diferença de tempo até o primeiro token contra uma chamada bloqueante.
- Configurar retry seletivo que trata `ThrottlingException` como retentável e `ValidationException`/`AccessDeniedException` como erro de programador, não de rede.
- Escrever uma policy IAM que escopa `bedrock:InvokeModel` e `bedrock:InvokeModelWithResponseStream` ao ARN de um modelo específico, sem `Resource: "*"`.
- Registrar, a cada chamada, os tokens de entrada e de saída retornados pelo Bedrock e transformar isso num log que um metric filter consegue contar.
- Reproduzir, de propósito, as três falhas mais comuns de uma primeira integração (corpo sem `anthropic_version`, IAM sem o ARN certo, throttling sem retry) e diagnosticar cada uma pelo log.
- Explicar por que a decisão de ONDE a chamada mora (frontend, microsserviço novo, ou o serviço existente) pesa tanto quanto o código da chamada em si.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| ID de modelo com prefixo de provedor | AIF-C01 | `anthropic.claude-3-5-haiku-20241022-v1:0` — o prefixo antes do primeiro ponto é sempre o provedor do modelo, não a AWS | que o Bedrock hospeda modelos de VÁRIOS provedores atrás da mesma API, e o ID é como você sabe qual formato de request/response usar |
| On-demand vs. throughput provisionado | AIF-C01, DVA-C02 | este laboratório usa on-demand (paga por token, sem reserva) — o modo certo para um piloto com volume incerto | que throughput provisionado troca custo por token por uma capacidade reservada, e só compensa em volume alto e prevísivel |
| API de streaming de resposta | AIF-C01, DVA-C02 | `InvokeModelWithResponseStream` entrega a resposta em eventos incrementais, não como um único payload | que streaming reduz o tempo até o PRIMEIRO token, não o tempo total de geração — as duas métricas respondem perguntas diferentes |
| Least privilege em InvokeModel | DVA-C02, SAP-C02 | IAM policy restrita ao ARN do(s) modelo(s) específico(s) que o CatalogoApi usa, não a `bedrock:*` | que o ARN de um foundation model do Bedrock é público e nomeável — wildcard nele é tão adorno quanto seria em qualquer outro recurso nomeável |
| Modelo de cobrança por token | AIF-C01, MLA-C01 | log estruturado com tokens de entrada e de saída de cada chamada, sem nenhum valor em dólar fixado no texto | que o Bedrock cobra por token de entrada e por token de saída SEPARADAMENTE, com taxas diferentes — a conta que importa soma as duas |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve uma equipe que troca de modelo (por exemplo, de `anthropic.claude-3-5-haiku` para `amazon.titan-text-express`) e a integração quebra, e pede a causa. A resposta esperada não é "o Bedrock está com bug" — é que cada provedor define seu PRÓPRIO formato de corpo de requisição e de resposta por trás da mesma API `InvokeModel`. O prefixo do ID não é decoração: é o que diz qual contrato de payload usar, e trocar de modelo sem trocar o código que monta o corpo é o erro mais comum de quem testou só com um provedor.
Requisitos, e como cada um muda o desenho
Requisito não funcional que não aparece numa linha de configuração é intenção. A coluna da direita é onde cada um deixou marca.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Lojista vê o texto sendo gerado, não uma tela parada | obrigatório, produto | `InvokeModelWithResponseStream` em vez de `InvokeModel`; a API repassa os eventos ao navegador conforme chegam, em vez de esperar o modelo terminar |
| Toda chamada tem custo registrado | obrigatório, FinOps | log estruturado no fim de cada stream com tokens de entrada e de saída, somados do evento de metadata que o próprio Bedrock devolve |
| IAM não alcança nenhum modelo além dos dois em uso | obrigatório, segurança | policy com `Resource` igual ao ARN de cada foundation model específico, nunca `bedrock:*` sobre `"*"` |
| Throttling do Bedrock não pode derrubar o cadastro do lojista | obrigatório, medido | retry restrito a `ThrottlingException`, com backoff e jitter, herdando o padrão `ShouldHandle` seletivo do L79 |
| Erro de corpo malformado não deve ser tratado como falha transitória | obrigatório | `ValidationException` e `AccessDeniedException` ficam FORA do `ShouldHandle` — são erro de programador, e retentar não os resolve |
| Prompt não pode carregar dado do lojista além do necessário | privacidade | o corpo da requisição ao Bedrock leva só nome do produto e atributos de catálogo — nunca CPF/CNPJ do lojista nem dado de outro pedido |
| Custo por chamada fora do esperado precisa ser detectável | requisito operacional | métrica de tokens publicada no CloudWatch a cada chamada, com alarme via SNS quando o total do dia sai da faixa do piloto |
Arquitetura mínima: a chamada que funciona e não conta o que custou
Este desenho é exatamente o estado da segunda tentativa da Cadência — não uma versão simplificada de propósito para este laboratório. Ele é legítimo como ponto de partida: gera a descrição corretamente, todo dia, para os 20 lojistas do piloto. O laboratório começa medindo o que falta para ele aguentar o próximo passo.
- → envia nome e atributos crus do produto cadastrado
- → encaminha a requisição HTTP de geração de descrição
- → InvokeModel síncrono, corpo Messages API, IAM com `bedrock:*` sobre `"*"`
- → resposta completa de uma vez, ou `ThrottlingException` sem nenhum retry
- → devolve a descrição pronta, ou erro 500 sem detalhe
- → tela parada por 4 a 6 s, depois o texto aparece de uma vez
- Fora da AWS
- Rede e entrega
- Compute
- IA e machine learning
Este é o estado real da segunda tentativa: a chamada já saiu do frontend e já usa a task role do L42, sem chave estática. O defeito não é ONDE ela mora — é COMO ela chama: síncrona, sem streaming, sem retry seletivo, sem log de custo, com uma IAM que alcança qualquer modelo. Percorra os passos e repare onde cada ausência aparece.
- Lojista termina o cadastro e pede a descrição gerada. É o fluxo comum: o formulário de atributos crus é enviado, e a tela mostra um spinner genérico enquanto espera.
- O ALB encaminha para o CatalogoApi, o mesmo serviço desde o L01. Nenhuma infraestrutura nova aqui — é o mesmo Fargate service e a mesma task role que o L42 deixou sem chave estática.
- InvokeModel corre síncrono, sem timeout explícito nem retry. O SDK usa o timeout padrão do `HttpClient` interno, que não foi calibrado para esta chamada. Não há `Polly` nem `ShouldHandle` — a chamada simplesmente espera.
- A policy IAM alcança qualquer modelo, porque `Resource` é `"*"`. Copiada do primeiro exemplo de documentação encontrado — funciona, e por isso ninguém revisou o escopo antes de ir ao ar.
- Em pico de cadastro simultâneo, o Bedrock sinaliza limite de taxa. `ThrottlingException` sobe da mesma forma que qualquer outra exceção — não há distinção entre "tente de novo" e "o corpo da requisição está errado".
- O lojista vê a tela parada e, no sucesso, o texto inteiro de uma vez. Sem streaming, os 4 a 6 segundos de geração são tempo morto na tela — e nenhum log registra quantos tokens aquela chamada específica consumiu.
`bedrock:*` sobre `"*"` não é atalho — é a policy que a próxima integração vai copiar
Uma IAM policy ampla demais num serviço que só chama dois modelos específicos não é um risco abstrato: é o exemplo que o PRÓXIMO desenvolvedor vai copiar quando integrar outro modelo, porque "já existe e funciona" é sempre o caminho de menor resistência. Cada chamada nova herda o alcance da mais ampla já aprovada — é assim que uma policy de piloto vira a policy de produção sem ninguém decidir isso conscientemente.
Arquitetura para produção
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. O CatalogoApi e o Bedrock são os MESMOS da Figura 1 — nenhum compute novo. O que muda é inteiramente o comportamento da chamada, e é isso que a comparação com a Figura 1 mede.
- → pede a descrição, mesma ação da Figura 1
- → encaminha a requisição, agora com 2 AZs atrás do ALB
- → InvokeModelWithResponseStream — só corre se o `ShouldHandle` permitir a tentativa
- → eventos de streaming (texto incremental), ou `ThrottlingException` retentável
- → repassa cada chunk de texto assim que chega, via streaming HTTP
- → publica tokens de entrada/saída e latência ao FIM de cada stream
- → alarme quando o total de tokens do dia sai da faixa do piloto
- Fora da AWS
- Rede e entrega
- Compute
- IA e machine learning
- Gestão e governança
- Integração de apps
O CatalogoApi e o Bedrock são os MESMOS da Figura 1 — nenhuma infraestrutura de compute nova. O que muda é inteiramente o comportamento da chamada: ela decide ANTES de discar se vale retentar, entrega o texto em pedaços conforme o modelo gera, e nunca termina sem publicar quanto custou. Percorra os passos: cada peça nova rastreia a uma linha da tabela de requisitos.
- Lojista pede a descrição; a chamada atravessa o mesmo ALB da Figura 1. Nada muda do lado do navegador: a URL e a ação são idênticas. A diferença inteira está no que a API faz atrás do ALB, agora com 2 AZs.
- O pipeline Polly decide: a exceção anterior era retentável?. `ShouldHandle` trata só `ThrottlingException` como retentável, com backoff e jitter — `ValidationException` e `AccessDeniedException` vão direto ao erro, sem retentativa nenhuma.
- A chamada usa `InvokeModelWithResponseStream`, com a IAM escopada ao ARN do modelo. A policy não alcança nenhum modelo além dos dois específicos que o CatalogoApi usa — trocada da `bedrock:*` sobre `"*"` da Figura 1.
- O Bedrock devolve eventos de streaming: `message_start`, deltas de texto, `message_stop`. Cada evento chega assim que o modelo gera aquele pedaço — não é a API esperando o texto inteiro para só então responder.
- A API repassa cada chunk ao navegador conforme chega, sem armazenar o texto inteiro antes. É o que faz o lojista ver a descrição sendo escrita progressivamente, em vez de uma tela parada seguida de um texto completo.
- Ao fim do stream, a API soma os tokens do evento final de metadata e publica no CloudWatch. O próprio Bedrock devolve `input_tokens` e `output_tokens` no último evento do stream — a API só precisa somar e logar, sem chamada extra nenhuma.
- O alarme cobre o caso em que o custo diário sai da faixa do piloto. Um pico isolado de cadastro simultâneo é esperado. Custo sustentado acima da faixa é sinal de algo diferente — um loop chamando o modelo sem necessidade, por exemplo — e essa distinção exige alguém olhar.
Repare no que NÃO foi adicionado: nenhum segredo novo. A autenticação contra o Bedrock continua sendo a mesma task role do L42, assinando a requisição via SigV4 — não existe API key de terceiro para guardar, porque o Bedrock é um serviço da própria AWS. É um dos poucos casos desta série em que "produção" não introduz `secretsmanager`, e a ausência é uma decisão, não um esquecimento.
Por que streaming não muda o custo, só a percepção de espera
`InvokeModelWithResponseStream` cobra pelo MESMO número de tokens que `InvokeModel` seria — streaming muda quando o texto chega, não quantos tokens o modelo gerou. O ganho aqui é inteiramente de experiência: o lojista vê a primeira palavra em frações de segundo em vez de esperar a geração inteira, mesmo que o tempo TOTAL de geração seja idêntico nos dois casos.
O caminho de uma descrição, do clique do lojista ao primeiro token
O payload abaixo é o corpo real que a Cadência envia ao Bedrock para gerar a descrição — o formato Messages API que o Anthropic Claude usa, com o campo `anthropic_version` que o `ValidationException` mais comum reclama quando falta.
{
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 220,
"system": "Voce escreve descricoes de vitrine curtas e objetivas a partir de atributos crus de produto. Nunca invente atributo que nao foi informado.",
"messages": [
{
"role": "user",
"content": "Produto: caneca 300ml azul ceramica tema pet. Gere uma descricao de vitrine em ate 3 frases."
}
]
}
// Evento final do stream, de onde a API le os tokens consumidos:
// { "type": "message_delta", "usage": { "output_tokens": 47 } }
// Os tokens de ENTRADA vem no evento message_start.usage.input_tokens
As decisões, e o que se perde em cada uma
📋 A Cadência precisa decidir onde a chamada ao Bedrock mora. A primeira tentativa (frontend, com chave embutida no bundle) foi bloqueada em code review por expor credencial ao navegador. A segunda (dentro do CatalogoApi existente, síncrona e sem observabilidade) funciona, mas não tem nenhuma das propriedades que o time vai precisar em produção.
O CatalogoApi já tem identidade estabelecida (L42), já está em produção, e adicionar um endpoint novo é mudança de código, não de infraestrutura. Um microsserviço novo isolaria melhor o raio de explosão de uma falha do Bedrock, mas duplicaria deploy, observabilidade e identidade para um piloto que ainda não provou volume que justifique isso — e todo problema real identificado (streaming, retry, custo, IAM) se resolve sem separar o serviço.
Alt: Chamar o Bedrock direto do frontend — Expõe qualquer credencial ao navegador do lojista — bloqueado em code review na primeira tentativa, e continua errado mesmo com uma chave "de uso limitado": o limite vale para o QUE a chave alcança, não para QUEM pode vê-la.
Alt: Criar um microsserviço novo só para chamadas ao Bedrock — Isola melhor o raio de explosão de uma falha do modelo, mas soma deploy, observabilidade e uma segunda identidade IAM para gerenciar — custo de operação desproporcional a um piloto com 20 lojistas.
Alt: Uma função Lambda por trás do API Gateway — Reduz operação em volume baixo, mas streaming de resposta via Lambda tem semântica mais complexa que num Fargate de vida longa, e o CatalogoApi já tem a task role certa — trocar de compute agora resolve um problema que não existe.
Alt: Esperar o volume crescer antes de resolver streaming e custo — Adia exatamente as duas propriedades que ficam mais caras de adicionar depois: streaming muda o contrato HTTP do endpoint, e custo sem log desde o primeiro dia deixa sem baseline para saber se um aumento de fatura é normal ou é bug.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Onde a chamada mora | dentro do CatalogoApi existente | frontend (rejeitado); microsserviço novo; Lambda | reaproveita identidade (L42) e infraestrutura já em produção | acopla o ciclo de deploy da geração de descrição ao do resto do catálogo — um bug na geração pode exigir deploy do serviço inteiro |
| Modo de chamada | `InvokeModelWithResponseStream` | `InvokeModel` síncrono (Figura 1) | reduz o tempo até o PRIMEIRO token, que é o que o lojista percebe como "travou" | código mais complexo: a API precisa processar eventos incrementais em vez de um único payload |
| O que o retry trata | só `ThrottlingException` | toda exceção; nenhum retry | retentar `ValidationException` custaria tempo sem NENHUMA chance de sucesso — o corpo malformado continua malformado na segunda tentativa | uma falha de rede momentânea que apareça sob outro tipo de exceção não seria retentada |
| Escopo da IAM | ARN de cada modelo específico em uso | `bedrock:*` sobre `"*"` (Figura 1) | um foundation model do Bedrock é recurso nomeável — igual a qualquer outro ARN, wildcard nele é adorno de rigor | cada modelo novo exige uma linha nova de policy — mais atrito do que copiar o wildcard, e é exatamente esse atrito que a segurança compra |
Construir: o cliente .NET com streaming e custo por chamada
O cliente abaixo entra no CatalogoApi existente — não é um serviço novo. Ele retorna um `IAsyncEnumerable<string>`, que o endpoint minimal API do ASP.NET Core repassa direto ao navegador como streaming HTTP.
// ClienteBedrock.cs — cliente do CatalogoApi para o Bedrock Runtime.
// Pacote: AWSSDK.BedrockRuntime. Autenticacao: task role do L42 (SigV4,
// sem chave estatica). Retry: Microsoft.Extensions.Http.Resilience (Polly).
using Amazon.BedrockRuntime;
using Amazon.BedrockRuntime.Model;
using Polly;
using Polly.Retry;
public sealed class ClienteBedrock
{
// O prefixo do provedor ("anthropic.") nao e cosmetico: decide qual
// formato de corpo montar. Trocar de provedor sem trocar o corpo produz
// ValidationException, nao um erro de rede.
private const string ModeloId = "anthropic.claude-3-5-haiku-20241022-v1:0";
private readonly AmazonBedrockRuntimeClient _cliente;
private readonly ResiliencePipeline _pipeline;
public ClienteBedrock(AmazonBedrockRuntimeClient cliente)
{
_cliente = cliente;
// ShouldHandle so trata ThrottlingException como retentavel — o
// mesmo raciocinio seletivo do L79, aplicado a um tipo de exceção
// diferente: aqui a falha previsivel é limite de taxa, nao timeout.
_pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder()
.Handle<ThrottlingException>(),
MaxRetryAttempts = 2,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
Delay = TimeSpan.FromMilliseconds(150),
})
.Build();
}
public async IAsyncEnumerable<string> GerarDescricaoAsync(
string nomeProduto, string atributosCrus, ILogger log)
{
var corpo = new
{
anthropic_version = "bedrock-2023-05-31",
max_tokens = 220,
system = "Voce escreve descricoes de vitrine curtas e objetivas " +
"a partir de atributos crus de produto. Nunca invente " +
"atributo que nao foi informado.",
messages = new[]
{
new { role = "user", content = $"Produto: {nomeProduto} {atributosCrus}. " +
$"Gere uma descricao de vitrine em ate 3 frases." },
},
};
var requisicao = new InvokeModelWithResponseStreamRequest
{
ModelId = ModeloId,
ContentType = "application/json",
Body = JsonSerializer.SerializeToUtf8Bytes(corpo).ToMemoryStream(),
};
var inicio = Stopwatch.StartNew();
var primeiroTokenMs = -1L;
var tokensEntrada = 0;
var tokensSaida = 0;
// Retry so envolve a ABERTURA do stream. Uma vez que o primeiro
// chunk chegou, a chamada ja teve sucesso — retentar no meio de um
// stream em andamento duplicaria texto para o lojista.
var resposta = await _pipeline.ExecuteAsync(
async ct => await _cliente.InvokeModelWithResponseStreamAsync(requisicao, ct),
CancellationToken.None);
await foreach (var evento in resposta.Body)
{
var payload = ExtrairPayload(evento);
if (payload.Tipo == "message_start")
tokensEntrada = payload.UsageEntrada;
if (payload.Tipo == "content_block_delta")
{
if (primeiroTokenMs < 0)
primeiroTokenMs = inicio.ElapsedMilliseconds;
yield return payload.TextoDelta;
}
if (payload.Tipo == "message_delta")
tokensSaida = payload.UsageSaida;
}
// Log estruturado UMA vez, ao fim do stream — nao a cada chunk. O
// metric filter do Terraform (abaixo) casa neste evento.
log.LogInformation(
"{Evento} {@Dados}", "GERACAO_DESCRICAO_CONCLUIDA",
new
{
tokensEntrada,
tokensSaida,
latenciaPrimeiroTokenMs = primeiroTokenMs,
latenciaTotalMs = inicio.ElapsedMilliseconds,
modelo = ModeloId,
});
}
}
Por que o retry só envolve a abertura do stream, não o stream inteiro
Uma vez que o primeiro chunk de texto chegou, a chamada teve sucesso — retentar a partir do meio de um stream em andamento duplicaria texto que o lojista já está vendo na tela. O `ResiliencePipeline` envolve só a chamada que ABRE a conexão (`InvokeModelWithResponseStreamAsync`); depois disso, uma falha no meio do stream vira erro para o chamador decidir, não uma retentativa automática.
Construir: IAM escopada e observabilidade do custo
O Terraform abaixo não cria nenhuma infraestrutura de compute nova — anexa uma policy à task role que o L42 já criou, e adiciona o log group, o metric filter e o alarme que dão visibilidade ao custo por chamada.
# iam-bedrock.tf — policy escopada aos DOIS modelos que o CatalogoApi usa.
# Anexada a task role do L42 — nao cria role nova, so adiciona uma policy.
data "aws_iam_policy_document" "bedrock_invoke" {
statement {
sid = "InvokeModelosDaGeracaoDeDescricao"
effect = "Allow"
actions = [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
]
# ARN de foundation model do Bedrock nao carrega account id — e recurso
# publico da AWS, nomeavel pelo model id. "Resource": "*" aqui e o
# antipadrao que a tabela de anti-padroes desta pagina nomeia.
resources = [
"arn:aws:bedrock:${var.regiao}::foundation-model/anthropic.claude-3-5-haiku-20241022-v1:0",
"arn:aws:bedrock:${var.regiao}::foundation-model/amazon.titan-text-express-v1",
]
}
}
resource "aws_iam_policy" "bedrock_invoke" {
name = "cadencia-catalogo-api-bedrock-invoke"
policy = data.aws_iam_policy_document.bedrock_invoke.json
}
resource "aws_iam_role_policy_attachment" "catalogo_api_bedrock" {
# task_role_name vem do L42 — nenhuma role nova criada aqui.
role = var.catalogo_api_task_role_name
policy_arn = aws_iam_policy.bedrock_invoke.arn
}
# ── Observabilidade: log group + metric filter de custo ────────────────────
resource "aws_cloudwatch_log_group" "geracao_descricao" {
name = "/cadencia/catalogo-api/geracao-descricao"
retention_in_days = 30
}
resource "aws_cloudwatch_log_metric_filter" "tokens_saida" {
name = "cadencia-tokens-saida-por-chamada"
log_group_name = aws_cloudwatch_log_group.geracao_descricao.name
pattern = "{ $.Evento = \"GERACAO_DESCRICAO_CONCLUIDA\" }"
metric_transformation {
name = "TokensSaidaPorChamada"
namespace = "Cadencia/CatalogoApi"
value = "$.tokensSaida"
default_value = "0"
}
}
resource "aws_sns_topic" "custo_bedrock_fora_do_esperado" {
name = "cadencia-custo-bedrock-fora-do-esperado"
}
resource "aws_cloudwatch_metric_alarm" "custo_diario_fora_da_faixa" {
alarm_name = "cadencia-custo-bedrock-fora-da-faixa-do-piloto"
namespace = "Cadencia/CatalogoApi"
metric_name = "TokensSaidaPorChamada"
statistic = "Sum"
period = 86400 # 1 dia
evaluation_periods = 1
comparison_operator = "GreaterThanThreshold"
# Calibrado para o volume do piloto (~150 chamadas/dia) — recalibre antes
# de levar para um volume maior. Ver Regra 6: cota se diz onde ver, nao o
# valor que envelhece.
threshold = var.limite_tokens_saida_diario
alarm_actions = [aws_sns_topic.custo_bedrock_fora_do_esperado.arn]
}
O ARN do foundation model muda por região — confira antes de aplicar
O formato `arn:aws:bedrock:${var.regiao}::foundation-model/<model-id>` referencia um recurso público da AWS, disponível só nas regiões onde aquele modelo específico foi lançado. Aplicar este Terraform numa região sem o modelo disponível falha na chamada, não na aplicação da policy — confira a disponibilidade do modelo na região antes de depurar a IAM.
Implantar, e provar que o streaming chega antes da resposta pronta
"Funciona" não é prova. A medição abaixo compara os dois clientes contra o mesmo cadastro de produto — "caneca 300ml azul cerâmica tema pet" — dez vezes cada, e registra o tempo até o primeiro caractere aparecer na tela.
| Métrica | Chamada bloqueante (Figura 1) | Streaming (Figura 2) |
|---|---|---|
| Tempo até o 1º token visível | ≈4.100 ms (igual ao tempo total — nada aparece antes) | ≈420 ms, medido em 10 chamadas consecutivas |
| Tempo total até o texto completo | ≈4.100 ms | ≈4.050 ms — praticamente igual, porque o modelo gera na mesma velocidade nos dois casos |
| Tokens de entrada (prompt + atributos) | ≈95 tokens, medidos no evento `message_start` | mesmo valor — o streaming não muda o que é enviado |
| Tokens de saída (descrição gerada) | ≈47 tokens para uma descrição de 3 frases | mesmo valor — streaming não muda QUANTO o modelo gera |
O número que importa não é o tempo TOTAL — é o tempo até o primeiro token: streaming reduz de ≈4.100 ms para ≈420 ms o momento em que o lojista vê algo acontecendo, uma redução de aproximadamente 10×, mesmo que o texto completo leve praticamente o mesmo tempo para terminar nos dois casos.
O que não foi medido em produção real
Os números acima vêm de 10 chamadas de teste no ambiente do piloto, com um único formato de atributo. Latência de rede até a região do Bedrock, variação por tamanho de prompt e comportamento sob os ≈150 cadastros/dia reais ainda precisam ser medidos com o volume verdadeiro — os números aqui orientam a ordem de grandeza, não substituem a medição em produção.
Quebrar de propósito: três falhas e o diagnóstico
Reproduza cada uma antes de seguir — o diagnóstico pelo log é a parte que mais ensina, porque as três produzem sintomas fáceis de confundir entre si.
| Sintoma | Causa provável | Como investigar | Log/métrica | Correção |
|---|---|---|---|---|
| `ValidationException` na primeira chamada, sempre | corpo sem o campo `anthropic_version`, obrigatório no formato Messages API do Bedrock para modelos Anthropic | inspecione o corpo serializado ANTES de enviar — o campo é fácil de esquecer porque não existe na API nativa do Anthropic, só na do Bedrock | CloudWatch Logs do CatalogoApi, mensagem da exceção do SDK | adicionar `anthropic_version: "bedrock-2023-05-31"` no corpo |
| `AccessDeniedException` mesmo com a task role anexada | policy IAM escopada a um ARN de modelo diferente do `ModelId` usado no código, ou região do ARN diferente da região do cliente | compare o `ModelId` da chamada com o `Resource` da policy, campo por campo — inclusive a região | CloudTrail, evento `InvokeModelWithResponseStream` com `errorCode` | corrigir o ARN na policy Terraform e reaplicar |
| Streaming "trava" e só aparece tudo no fim | cliente HTTP do navegador ou proxy intermediário fazendo buffer da resposta em vez de repassar cada chunk | curl direto no endpoint com `--no-buffer` e compare com o comportamento no navegador | não aparece em log nenhum — é comportamento de rede, não de aplicação | desabilitar buffering no proxy/CDN para esta rota, ou confirmar que o `Content-Type` da resposta é compatível com streaming |
| `ThrottlingException` sustentado, retry não resolve | volume real ultrapassou a cota da conta para o modelo — retry não aumenta cota, só espalha as tentativas no tempo | confira a cota atual em Service Quotas para o modelo e a região | métrica `TokensSaidaPorChamada` do CloudWatch caindo enquanto o volume de chamadas sobe | solicitar aumento de cota, ou considerar throughput provisionado se o volume for previsível |
Fallback que devolve um texto genérico é pior que mostrar o erro
É tentador, ao ver `ThrottlingException` repetido, fazer a API devolver uma descrição genérica fixa ("Produto de qualidade, confira os detalhes") em vez de propagar o erro. Isso esconde o problema do lojista sem resolvê-lo: a vitrine parece funcionando, mas está publicando texto que não reflete o produto real — pior para conversão do que mostrar "não foi possível gerar agora, tente novamente" e deixar o lojista decidir.
Segurança: o que o cliente do Bedrock expõe
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Chave de API exposta no bundle do frontend | alta, se a chamada mover para o navegador | alto — qualquer visitante alcança o Bedrock com o alcance da aplicação | chamada ao Bedrock só a partir do backend, nunca do navegador | revisão de código bloqueando qualquer SDK da AWS em pacote de frontend | revogar a chave, mover a chamada para o backend |
| IAM com `bedrock:*` sobre `"*"` | alta na primeira integração — é o exemplo mais fácil de copiar | médio — qualquer modelo na conta fica alcançável, inclusive os mais caros | policy com `Resource` no ARN de cada modelo específico | AWS Config regra customizada checando `Resource` amplo em policy com ação `bedrock:*` | restringir a policy ao(s) ARN(s) em uso e reaplicar |
| Dado sensível do lojista dentro do prompt | baixa neste laboratório — o corpo só leva nome e atributos de catálogo | alto se acontecer — dado sensível vira parte do log de prompt/resposta do modelo | corpo do prompt monta só de campos de catálogo público, nunca de CPF/CNPJ ou endereço | revisão do payload antes de habilitar log de conteúdo completo do prompt (hoje só tokens são logados, não o texto) | remover o campo do prompt e girar qualquer log que o tenha capturado |
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvokeModelosDaGeracaoDeDescricao",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": [
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-haiku-20241022-v1:0",
"arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-text-express-v1"
]
}
]
}
Um dev troca o `ModelId` de `anthropic.claude-3-5-haiku-20241022-v1:0` para `amazon.titan-text-express-v1`, mantendo o MESMO corpo de requisição — inclusive o campo `anthropic_version: "bedrock-2023-05-31"`. O que acontece na primeira chamada com o modelo novo?
Observabilidade: as perguntas que o painel tem de responder
- Quantos tokens de entrada e de saída o CatalogoApi consumiu hoje, e como isso se compara a ontem?
- Qual o tempo até o primeiro token, em p50 e p95, para as chamadas de streaming das últimas 24 h?
- Quantas chamadas retentaram por `ThrottlingException`, e isso está subindo?
- Quantas chamadas falharam com `ValidationException` ou `AccessDeniedException` — erros que retry nenhum resolve?
- O custo diário está dentro da faixa calibrada para o volume atual de lojistas, ou fora dela?
| Alarme | Limiar inicial | O que ele detecta |
|---|---|---|
| TokensSaidaPorChamada — soma diária | acima da faixa calibrada para o volume do piloto | custo saindo do esperado — loop chamando o modelo sem necessidade, ou crescimento real de volume que precisa de recalibração |
| Taxa de `ThrottlingException` na janela de 10 min | acima de 5% das chamadas | cota da conta ficando pequena para o volume real — sinal para pedir aumento antes que vire erro visível ao lojista |
| Taxa de `ValidationException`/`AccessDeniedException` | qualquer ocorrência > 0 | esses dois nunca deveriam acontecer em produção estável — qualquer ocorrência é regressão de código ou de IAM, não flutuação normal |
Escala: 20 lojistas, 1 mil, pico de campanha, e falha de região
| Ordem de grandeza | O que muda | O que quebra primeiro |
|---|---|---|
| 20 lojistas (piloto atual) | ≈150 chamadas/dia, IAM e retry cobrem o volume sem ajuste | nada — é o volume calibrado neste laboratório |
| 1.000 lojistas | ≈7.500 chamadas/dia — a cota padrão da conta para o modelo pode não cobrir mais | `ThrottlingException` sustentado; hora de checar Service Quotas antes de o volume chegar aqui, não depois |
| Pico de campanha (importação em lote de catálogo) | centenas de cadastros em minutos, não distribuídos ao longo do dia | o `MaxRetryAttempts=2` do pipeline não segura uma rajada — chamadas em lote pedem fila (SQS) para nivelar, não mais retry |
| 1 milhão de descrições/mês | volume previsível o suficiente para considerar throughput provisionado em vez de on-demand | custo por token on-demand deixa de ser o modo mais barato — é a decisão que o L89 formaliza |
| Falha da região onde o Bedrock está configurado | o modelo específico pode não estar disponível em toda região — diferente de um serviço regional genérico | sem um `ModelId` de fallback numa segunda região, a geração de descrição para inteiramente, mesmo com o CatalogoApi saudável |
Custo: o que este laboratório acrescenta à fatura
Nenhum valor em dólar aparece nesta seção por decisão — preço de modelo é o número mais volátil da série (Regra 6 da skill desta trilha). O que importa reter é o MODELO de cobrança: por token, com taxas diferentes para entrada e saída, sem custo de infraestrutura ociosa.
| Cenário | Volume | O que domina o custo |
|---|---|---|
| Protótipo (este laboratório) | ≈150 chamadas/dia, 20 lojistas | tokens de saída — descrições curtas (≈50 tokens) mantêm o custo unitário baixo; nenhum custo de infraestrutura ociosa, porque Bedrock on-demand não reserva capacidade |
| Produção pequena | ≈1.000 lojistas, ≈7.500 chamadas/dia | ainda tokens, mas a cota da conta passa a ser o fator limitante antes do custo — Service Quotas, não o preço, decide se o volume cabe |
| Alta escala | 1 milhão+ de chamadas/mês, volume previsível | a comparação entre on-demand e throughput provisionado deixa de ser óbvia — é a decisão que o L89 mede com números reais de produção |
O custo que ninguém mede: chamada gerada e descartada
Um lojista que clica "gerar descrição" três vezes seguidas, insatisfeito com o resultado, paga três chamadas completas — o Bedrock não sabe que as duas primeiras foram descartadas. Nenhum controle deste laboratório limita regeração; é um comportamento de produto a decidir (limitar tentativas? cachear por atributo idêntico?), registrado aqui como limitação conhecida, não como bug.
Well-Architected nos seis pilares
| Pilar | Situação | Risco | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | log estruturado por chamada, sem dashboard consolidado ainda | diagnóstico depende de alguém ler log bruto no CloudWatch | dashboard com os alarmes desta página já configurados | média |
| Segurança | IAM escopada ao ARN do modelo, sem chave estática | prompt não filtra explicitamente dado sensível, depende de disciplina de código | validação de payload antes de enviar, recusando campos fora da lista permitida | alta |
| Confiabilidade | retry seletivo para throttling, sem circuit breaker | rajada sustentada de throttling gera retentativas em cascata sem um teto de "pare de tentar" | circuit breaker calibrado pelo volume real, como o L79 fez para o SageMaker | média |
| Eficiência de performance | streaming reduz tempo até o primeiro token | nenhum, para o volume atual | cache de respostas para atributos idênticos, se a taxa de repetição justificar | baixa |
| Otimização de custo | on-demand, sem reserva de capacidade | custo escala linear com volume, sem desconto de escala | avaliar throughput provisionado quando o volume for previsível (L89) | baixa por agora |
| Sustentabilidade | sem infraestrutura ociosa — Bedrock on-demand não reserva GPU parada | nenhum identificado neste laboratório | n/a neste estágio | baixa |
Evolução em níveis: da chamada crua ao sistema de IA generativa
É o estado real da segunda tentativa da Cadência: funciona, mas com tela parada e IAM ampla demais.O cliente ganha as quatro propriedades que a tabela de requisitos pede, sem infraestrutura nova.O prompt de sistema sai do código-fonte solto e ganha versionamento com teste que reprova alteração ruim antes de ir ao ar.A geração deixa de depender só do que o lojista digitou e passa a recuperar dados reais do catálogo antes de responder.Um golden set mede regressão de qualidade no CI, e cache reduz o custo de prompts repetidos.A geração de descrição se junta a busca com IA (L94) e enriquecimento em lote do acervo (L95) — o mesmo modelo, aplicado a mais dados e mais pontos do produto.A ordem não é arbitrária: cada nível resolve um risco que o anterior deixou explícito, e nenhum salta direto ao nível 6 sem passar pelo controle de prompt (nível 3) e pela recuperação de dado real (nível 4) — é a mesma lição que o catálogo de laboratórios explica na abertura da banda 9: um sistema com IA sem essas fundações erra mais e ninguém percebe.
Onde IA entra nesta arquitetura, e onde não entra
O modelo já é a peça de IA aqui — o que este laboratório NÃO faz é decidir retry com IA
A decisão de retentar (`ThrottlingException`, sim; `ValidationException`, não) é determinística e cabe inteira num `ShouldHandle` — usar outro modelo para "decidir se vale tentar de novo" seria complexidade sem ganho, o exato hype que esta escola evita. Onde IA de fato resolve um problema que regra fixa não resolveria é na PRÓPRIA geração de texto a partir de atributo cru — e é por isso que o modelo está no centro do diagrama, não decidindo a infraestrutura ao redor dele. Quando a descrição errar (atributo mal interpretado, texto genérico demais), o fallback é humano: o lojista edita o texto gerado antes de publicar, e nenhuma publicação é automática sem essa revisão.
Anti-padrões deste laboratório
| Erro | Por que alguém faz | Sintoma em produção | Forma correta |
|---|---|---|---|
| Policy IAM com `bedrock:*` sobre `Resource: "*"` | por que alguém faz: é o exemplo copiado da documentação, que usa wildcard para simplificar o tutorial — e funciona de primeira, o que reforça a escolha | qualquer modelo da conta fica alcançável pelo CatalogoApi, inclusive modelos mais caros nunca usados de propósito | `Resource` com o ARN de cada modelo específico em uso |
| Chamar `InvokeModel` síncrono achando que streaming é otimização prematura | por que alguém faz: o código síncrono é mais simples de escrever e testar, e "funciona" no ambiente local sem carga real | tela parada por segundos a cada geração, sentida pelo usuário como lentidão mesmo quando o modelo responde no mesmo tempo total | `InvokeModelWithResponseStream` desde a primeira versão do endpoint |
| Retentar toda exceção igualmente | por que alguém faz: `catch (Exception)` com retry genérico parece mais "resiliente" à primeira vista, e é menos código que distinguir tipos | `ValidationException` retentada gasta tempo e chamadas sem NENHUMA chance de sucesso — o corpo malformado continua malformado | `ShouldHandle` restrito ao tipo de exceção genuinamente transitório |
| Não registrar tokens por chamada | por que alguém faz: o piloto tem poucos lojistas, e "depois a gente mede" parece razoável quando o volume é pequeno | a fatura sobe e ninguém sabe se foi aumento de volume, prompt maior, ou um bug chamando o modelo em loop | log estruturado com tokens de entrada/saída em CADA chamada, desde o primeiro dia |
| Chamar o Bedrock direto do frontend | por que alguém faz: elimina uma camada de rede e parece mais rápido de prototipar, sem precisar tocar no backend | qualquer credencial usada fica visível no DevTools do navegador de qualquer visitante | chamada sempre a partir do backend, autenticada pela task role |
Quando algo não funciona
Os seis sintomas abaixo cobrem quase todo chamado de suporte da primeira semana de uma integração com o Bedrock. Os dois últimos são específicos de streaming em .NET, e são os que mais consomem tempo, porque o código está certo e o problema está no caminho entre ele e o navegador.
| Sintoma | Causa provável | Como investigar | Log ou métrica | Correção |
|---|---|---|---|---|
| `AccessDeniedException` já na primeira chamada | Acesso ao modelo não habilitado nesta conta e região, ou a policy da task role não cobre o ARN daquele modelo específico | Listar os modelos disponíveis para a conta na região e conferir o ARN exato da policy contra o `modelId` que o código envia | Evento `InvokeModel` negado no CloudTrail, com o ARN que foi tentado | Habilitar o acesso ao modelo na região e escopar a policy ao ARN do modelo, não a `*`. Acesso a modelo é por região: funcionar em us-east-1 não diz nada sobre sa-east-1 |
| `ValidationException` com o corpo que funcionava em outro modelo | O formato do corpo da requisição é por família de modelo, e não é intercambiável | Comparar o corpo enviado com o exemplo da documentação daquela família específica | A mensagem da exceção nomeia o campo rejeitado | Usar a API Converse quando a intenção for trocar de modelo sem reescrever o corpo — ela normaliza o formato entre famílias, e é a razão de ela existir |
| `ThrottlingException` sob carga de campanha | Cota de invocação por modelo e região atingida | Comparar o pico de requisições por minuto com a cota vigente da conta | Contagem de `ThrottlingException` por minuto | Retry com backoff exponencial e jitter — o mesmo padrão do L36 — e pedido de aumento de cota antes da campanha, não durante. Cota é por região: distribuir entre regiões também é resposta, com a ressalva de residência do L99 |
| O custo por chamada não aparece no painel | O metric filter não casa com o formato da linha de log | Rodar o padrão do filtro contra uma linha real do log group | A métrica derivada fica sem nenhum dado, o que é diferente de ficar em zero | Corrigir o padrão contra a linha real. Métrica ausente e métrica zerada parecem iguais no gráfico e significam coisas opostas — só o alarme com `treat missing data` explícito separa as duas |
| O streaming funciona no `curl` mas a tela só atualiza no fim | Alguma camada entre a aplicação e o navegador está acumulando a resposta: middleware de compressão, buffer de proxy reverso, ou o próprio `response buffering` | Chamar o endpoint direto na task, sem passar pelo balanceador, e comparar | Tempo até o primeiro byte medido nos dois caminhos | Desabilitar compressão nessa rota e desligar o buffer do proxy. O tempo até o primeiro token é a métrica que este laboratório promete, e ela é a primeira a ser destruída por infraestrutura intermediária |
| O tempo até o primeiro token é igual ao tempo total | O `IAsyncEnumerable` está sendo materializado antes de retornar — um `ToListAsync`, uma serialização de coleção, ou um `await` sobre a sequência inteira dentro do endpoint | Registrar o instante de cada item produzido dentro do cliente e comparar com o instante em que o primeiro byte sai do endpoint | A diferença entre o timestamp do primeiro item gerado e o do primeiro byte enviado | Repassar a sequência sem materializar. O cliente pode estar perfeito e o endpoint desfazer todo o trabalho numa linha — foi por isso que a prova deste módulo mede tempo até o primeiro caractere na TELA, não dentro do serviço |
Checklist de produção
- IAM da task role escopada ao ARN de cada modelo em uso — nenhum `bedrock:*` sobre `"*"`.
- Chamada usa `InvokeModelWithResponseStream`, não `InvokeModel` síncrono.
- `ShouldHandle` do Polly trata só `ThrottlingException` como retentável.
- Log estruturado com tokens de entrada/saída publicado ao fim de CADA chamada.
- Metric filter e alarme de custo diário configurados e testados com um valor de teste.
- Corpo da requisição contém só campos de catálogo — nenhum dado sensível do lojista.
- Cota do modelo na região confirmada em Service Quotas para o volume esperado.
- Log group com retenção definida (30 dias) — não indefinida.
Limpeza: o que o destroy não leva
# Ordem: primeiro o alarme e o topico SNS (dependem do log group),
# depois o metric filter, depois o log group, por ultimo a policy IAM.
terraform destroy \
-target=aws_cloudwatch_metric_alarm.custo_diario_fora_da_faixa \
-target=aws_sns_topic.custo_bedrock_fora_do_esperado \
-target=aws_cloudwatch_log_metric_filter.tokens_saida \
-target=aws_cloudwatch_log_group.geracao_descricao \
-target=aws_iam_role_policy_attachment.catalogo_api_bedrock \
-target=aws_iam_policy.bedrock_invoke
# Confirme que o log group realmente sumiu — retencao de 30 dias significa
# que ele PARA de cobrar por armazenamento novo, mas o que ja foi gravado
# so sai daqui:
aws logs describe-log-groups \
--log-group-name-prefix /cadencia/catalogo-api/geracao-descricao
O que o destroy NÃO leva
O Bedrock em si não tem recurso para destruir — é serviço regional, sem instância nem endpoint provisionado, e o `terraform destroy` acima cobre tudo que este laboratório de fato criou: o log group (aws_cloudwatch_log_group), que continua cobrando por armazenamento até ser removido, o metric filter, o alarme e o tópico SNS. Chamadas já feitas ao Bedrock já foram cobradas e não há reembolso — o destroy previne cobrança FUTURA, não desfaz a passada.
Resumo: problema, peça e motivo
| Problema | Serviço/peça | Motivo |
|---|---|---|
| Ninguém sabe onde a chamada ao LLM deve morar | CatalogoApi existente (Fargate, L01) | reaproveita identidade (L42) e infraestrutura já em produção, sem duplicar deploy para um piloto |
| Tela parada durante a geração | Bedrock Runtime, `InvokeModelWithResponseStream` | entrega o texto em eventos incrementais — reduz o tempo até o primeiro token percebido pelo lojista |
| Throttling derruba a chamada sem tratamento | Polly, retry restrito a `ThrottlingException` | retenta só o que é genuinamente transitório, sem desperdiçar tempo em erro de programador |
| IAM ampla demais | policy escopada ao ARN de cada modelo específico | foundation model é recurso nomeável — wildcard nele é adorno de rigor, não simplificação legítima |
| Ninguém sabe quanto cada chamada custou | CloudWatch, log estruturado + metric filter + alarme SNS | tokens de entrada/saída vêm de graça no próprio evento de metadata do Bedrock — só falta somar e publicar |
Perguntas frequentes
❓ Por que o ID do modelo no Bedrock começa com o nome do provedor?
❓ Streaming no Bedrock reduz o custo da chamada?
❓ Por que a IAM não pode usar `bedrock:*` com `Resource: "*"`?
❓ O que a exceção `ThrottlingException` do Bedrock significa?
❓ Preciso de VPC endpoint para o CatalogoApi chamar o Bedrock?
❓ Por que este laboratório não faz RAG nem cita fonte na resposta?
❓ Um modelo mais barato sempre é a escolha certa para este caso de uso?
Fixando
A Cadência mede que o tempo até o primeiro token caiu de ≈4.100 ms para ≈420 ms depois de trocar `InvokeModel` por `InvokeModelWithResponseStream`, mantendo o mesmo prompt e o mesmo modelo. O que aconteceu com o CUSTO daquela chamada?
O pipeline Polly deste laboratório trata `ThrottlingException` como retentável, mas exclui `ValidationException` e `AccessDeniedException` do `ShouldHandle`. Um engenheiro sugere "simplificar" e retentar TODA exceção vinda do Bedrock, com os mesmos parâmetros de backoff. Qual é o efeito mais provável dessa mudança?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L42 (task role da CatalogoApi sem chave estática), L79 (retry seletivo por tipo de exceção, mesmo raciocínio Polly), .NET 8 |
| Conhecimentos adquiridos | por que o ID do modelo carrega prefixo de provedor; streaming reduz tempo até o primeiro token sem mudar custo; IAM escopada ao ARN do modelo; retry seletivo distinguindo falha transitória de erro de programador; custo por chamada a partir de tokens de entrada e saída |
| Limitação que fica | nenhum controle limita regeração repetida por um mesmo lojista insatisfeito com o resultado — cada clique é uma chamada completa cobrada de novo, sem cache nem limite, registrado como decisão de produto ainda em aberto |
| Próximos exemplos recomendados | L82 — versionar o prompt de sistema com teste de regressão; L83 — RAG mínimo respondendo com trecho citável, usando este mesmo cliente como base |
| Também habilitado por este módulo | o padrão de IAM escopada por ARN de modelo e o log estruturado de tokens se aplicam a qualquer outra chamada da Cadência ao Bedrock, não só à geração de descrição de produto |
| Data da última validação técnica | 8 de agosto de 2026 |
Documentação oficial consultada: AWS SDK for .NET — Amazon Bedrock Runtime (InvokeModelAsync, InvokeModelWithResponseStreamAsync); Anthropic Claude Messages API on Bedrock (formato do corpo, campo anthropic_version); Polly — pollydocs.org (estratégia Retry, PredicateBuilder, ShouldHandle), a mesma referência do L79 para o padrão de retry seletivo. Nenhum valor em dólar aparece neste módulo por decisão: preço por token muda por modelo, por região e por acordo de conta — confira o AWS Pricing Calculator ou o console do Bedrock para o valor vigente.
O que não foi verificado, e você deve conferir na sua conta
Os números de latência (≈4.100 ms bloqueante, ≈420 ms até o primeiro token em streaming) e de tokens (≈95 de entrada, ≈47 de saída) são medições de exemplo deste laboratório, com um único formato de atributo de produto e uma única região — não são constantes do Bedrock. A disponibilidade de cada modelo específico (`anthropic.claude-3-5-haiku-20241022-v1:0`, `amazon.titan-text-express-v1`) varia por região, e a cota padrão da conta para cada um também — confirme os dois antes de levar este código para outra conta ou outra região.
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…