Dev.to · 16 min read

Criando um agente de IA 24/7 para aplicações .NET

Criando um agente de IA 24/7 para aplicações .NET

São 3h12 da manhã. A API de pedidos começa a devolver erro 500, o health check fica vermelho e ninguém está olhando. Quando o time chega às 8h, o problema já virou reclamação de cliente, e a primeira hora do dia é gasta só para entender o que aconteceu. Esse é o cenário em que um agente de IA de plantão faz sentido. Ele não substitui o time nem sai reiniciando servidor por conta própria. Ele faz a triagem: percebe o problema, investiga com as ferramentas que você liberou, junta as evidências, abre o incidente com um diagnóstico e, se for o caso, pede autorização para agir. Neste artigo eu mostro como construir esse agente em .NET 10, rodando 24 horas por dia, usando o gpt-5.4-mini no Microsoft Foundry. E falo de um assunto que quase ninguém aborda até tomar o primeiro erro 429 em produção: as cotas do Foundry. Agente 24/7 não é chat Num chat, tem uma pessoa do outro lado. Se a resposta demorar, falhar ou sair estranha, ela percebe. Num agente que roda sozinho de madrugada, ninguém percebe nada. Isso muda as regras do jogo: Custo: um loop que chama o modelo a cada minuto, 24 horas por dia, gera uma conta que ninguém aprovou. Cota: o modelo tem limite de tokens e requisições por minuto. Quando estoura, o agente fica cego justamente quando mais precisa enxergar. Segurança: um agente com permissão para agir, sem ninguém supervisionando, é um risco operacional. Travamento: se o próprio agente travar, quem avisa? O código que eu mostro aqui foi desenhado em cima desses quatro problemas. A arquitetura O código completo está no meu GitHub: hgmauri/sample-agente-24x7. São duas aplicações: Loja.Api: a aplicação vigiada. Tem health check, um endpoint de pedidos e um jeito de simular a queda do banco. Vigia.Agente: o agente. Um BackgroundService em .NET 10 que acorda a cada ciclo, coleta os sinais e só chama a IA quando existe algo para investigar. O ciclo do agente é sempre o mesmo: Coleta os sinais: health check e erros 5xx dos últimos 15 minutos. Aplica as regras: está tudo bem? Esse problema já foi reportado? Ainda tem orçamento de tokens? Se precisar, chama o modelo com as ferramentas liberadas. O modelo investiga, abre o incidente e, se fizer sentido, pede um reinício, que fica aguardando aprovação humana. O worker: um ciclo por vez, com tempo máximo protected override async Task ExecuteAsync(CancellationToken stoppingToken) { logger.LogInformation("Agente iniciado. Ciclo a cada {Intervalo}", vigia.Value.IntervaloDoCiclo); // PeriodicTimer não acumula ticks: se um ciclo atrasar, o próximo não roda em paralelo using var timer = new PeriodicTimer(vigia.Value.IntervaloDoCiclo); do { await ExecutarCicloAsync(stoppingToken); } while (await timer.WaitForNextTickAsync(stoppingToken)); } private async Task ExecutarCicloAsync(CancellationToken stoppingToken) { using var limite = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); limite.CancelAfter(vigia.Value.TempoMaximoPorCiclo); try { var resultado = await AnalisarAsync(limite.Token); estado.Registrar(resultado.Descricao, usouIa: resultado.UsouIa); } catch (OperationCanceledException) when (!stoppingToken.IsCancellationRequested) { logger.LogWarning("Ciclo passou de {Limite} e foi cancelado", vigia.Value.TempoMaximoPorCiclo); estado.Registrar("Ciclo cancelado por tempo", falhou: true); } catch (Exception ex) when (ex is not OperationCanceledException) { // Um ciclo com erro nunca derruba o agente. Registra e segue para o próximo. logger.LogError(ex, "Falha no ciclo do agente"); estado.Registrar($"Falha: {ex.GetType().Name}", falhou: true); } } Três decisões que mantêm o agente de pé: PeriodicTimer em vez de Task.Delay solto. Ele não acumula ticks. Se um ciclo atrasar, o próximo não roda em paralelo. Tempo máximo por ciclo. Uma chamada pendurada no modelo ou na aplicação vigiada não pode travar o agente para sempre. Um ciclo com erro nunca derruba o agente. Registra, conta a falha e segue para o próximo. Regra primeiro, IA depois Esse é o trecho que mais economiza dinheiro e cota. A IA só entra depois que as regras dizem que vale a pena: private async Task AnalisarAsync(CancellationToken ct) { var opcoes = vigia.Value; var sinais = await fonte.ColetarAsync(opcoes.JanelaDeErrosEmMinutos, ct); // 1. Regra primeiro: com tudo saudável, nenhum token é gasto if (!sinais.PrecisaDeAtencao(opcoes.LimiteDeErros)) return ("Aplicação saudável", false); // 2. Mesmo problema de 10 minutos atrás? Não abre outro incidente nem chama a IA de novo var assinatura = sinais.Assinatura(); if (incidentes.HouveIncidenteRecente(assinatura, opcoes.IntervaloEntreIncidentes)) return ("Problema já reportado, aguardando o time", false); // 3. Orçamento do dia esgotado: incidente só com os dados das regras if (!orcamento.TemSaldo) { await AbrirSemIaAsync(sinais, assinatura, "Orçamento diário de tokens esgotado", ct); return ("Incidente aberto sem IA (orçamento)", false); } // 4. Agora sim, a IA investiga usando as ferramentas var ferramentas = new FerramentasDoAgente(fonte, incidentes, aprovacoes, assinatura); List mensagens = [ new(ChatRole.System, PromptDoAgente.Sistema), new(ChatRole.User, $"Sinais coletados agora ({DateTimeOffset.UtcNow:O}):\n{JsonSerializer.Serialize(sinais)}") ]; try { var resposta = await chat.GetResponseAsync(mensagens, new ChatOptions { Tools = ferramentas.Todas(), MaxOutputTokens = foundry.Value.MaxTokensResposta }, ct); orcamento.Consumir(resposta.Usage); logger.LogInformation("Análise da IA: {Resumo} ({Tokens} tokens)", resposta.Text, resposta.Usage?.TotalTokenCount); // Garantia: se o modelo não abriu incidente, o agente abre if (!incidentes.HouveIncidenteRecente(assinatura, opcoes.IntervaloEntreIncidentes)) await AbrirSemIaAsync(sinais, assinatura, "O modelo não abriu incidente", ct); return ("Incidente analisado pela IA", true); } // catch (CotaEsgotadaException): veja a seção de cotas } Na prática, com a aplicação saudável, o agente roda o dia inteiro gastando zero tokens. Quando um problema aparece, a IA é chamada uma vez. Nos ciclos seguintes, a deduplicação segura novas chamadas até o intervalo configurado passar. E se o orçamento do dia acabar, o agente continua de plantão, abrindo incidente só com os dados das regras. As ferramentas: o que o agente pode fazer Aqui mora a segurança do agente. Eu separo as ferramentas em três níveis: leitura livre, escrita que só registra e ação com efeito colateral, que nunca é executada pela IA. public IList Todas() => [ AIFunctionFactory.Create(VerificarSaudeAsync, "verificar_saude"), AIFunctionFactory.Create(ConsultarErrosRecentesAsync, "consultar_erros_recentes"), AIFunctionFactory.Create(AbrirIncidenteAsync, "abrir_incidente"), AIFunctionFactory.Create(SolicitarReinicio, "solicitar_reinicio"), ]; [Description("Abre um incidente para o time de plantão. Use uma única vez por análise.")] public async Task AbrirIncidenteAsync( [Description("Título curto e objetivo.")] string titulo, [Description("Severidade: SEV1 (fora do ar), SEV2 (degradado) ou SEV3 (atenção).")] string severidade, [Description("Diagnóstico baseado apenas nos dados consultados.")] string diagnostico, [Description("Próximo passo recomendado para o time.")] string acaoRecomendada, CancellationToken ct) { var incidente = await incidentes.AbrirAsync(titulo, severidade, diagnostico, acaoRecomendada, assinaturaDoCiclo, geradoPorIa: true, ct); return $"Incidente {incidente.Id} aberto."; } [Description("Solicita o reinício da aplicação. Não reinicia: cria um pedido que precisa de aprovação humana.")] public string SolicitarReinicio([Description("Motivo do reinício, com a evidência.")] string motivo) { var solicitacao = aprovacoes.Solicitar("reiniciar-aplicacao", motivo); return $"Solicitação {solicitacao.Id} registrada. Aguardando aprovação humana."; } Repare no solicitar_reinicio. A descrição diz claramente para o modelo que a ferramenta não reinicia nada. Ela cria um pedido que aparece em /aprovacoes, e só uma pessoa aprova. Em produção, eu levo essa aprovação para um card no Teams. O registro do IChatClient monta o pipeline do Microsoft.Extensions.AI. O UseFunctionInvocation executa as ferramentas que o modelo pede, e o limite de voltas evita que o agente entre em loop gastando tokens: services.AddChatClient(sp => { var opcoes = sp.GetRequiredService().Value; if (string.IsNullOrWhiteSpace(opcoes.Endpoint)) return new ChatClientSimulado(); var reserva = string.IsNullOrWhiteSpace(opcoes.EndpointReserva) ? null : FoundryChatClientFactory.Criar(opcoes.EndpointReserva, opcoes.DeploymentReserva); return new ChatClientResiliente( FoundryChatClientFactory.Criar(opcoes.Endpoint, opcoes.Deployment), reserva, FoundryChatClientFactory.ClassificarCota, sp.GetRequiredService(), sp.GetRequiredService()); }) // Executa as ferramentas pedidas pelo modelo, com limite de voltas para não entrar em loop .UseFunctionInvocation(configure: f => f.MaximumIterationsPerRequest = 6) .UseLogging(); O prompt do agente public const string Sistema = """ Você é o agente de plantão da aplicação Loja.Api, uma API .NET em produção. Você é acionado quando os sinais de saúde ou de erros passam do limite. Como trabalhar: 1. Investigue com as ferramentas antes de concluir. Não suponha. 2. Abra exatamente um incidente com diagnóstico objetivo e uma ação recomendada. 3. Só peça reinício (solicitar_reinicio) se houver evidência de que ele resolve. Quem aprova é uma pessoa. 4. Nunca invente dados, causas ou números. Se a evidência for insuficiente, diga isso no diagnóstico. 5. Responda ao final com um resumo de no máximo três linhas. """; Prompt curto, com regras objetivas. Para agente autônomo, eu prefiro instruções que caibam em uma tela. Quanto maior o prompt, mais tokens por chamada e mais cota consumida. Quem vigia o vigia? Um agente 24/7 precisa de batimento cardíaco. O health check abaixo fica vermelho se o agente ficar três ciclos sem rodar, e a plataforma reinicia o container: public sealed class AgenteVivoHealthCheck(EstadoDoAgente estado, IOptions opcoes, TimeProvider relogio) : IHealthCheck { public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { // Três ciclos sem batimento: o agente travou e a plataforma deve reiniciar o container var limite = opcoes.Value.IntervaloDoCiclo * 3 + opcoes.Value.TempoMaximoPorCiclo; if (estado.UltimoCiclo is { } ultimo && relogio.GetUtcNow() - ultimo > limite) return Task.FromResult(HealthCheckResult.Unhealthy($"Sem ciclo desde {ultimo:O}")); return Task.FromResult(HealthCheckResult.Healthy(estado.UltimoResultado)); } } E, por padrão, uma exceção não tratada em um BackgroundService derruba o processo inteiro. Para o agente, eu prefiro que o processo continue e o batimento decida: // Um erro em um serviço em segundo plano não deve derrubar o processo inteiro builder.Services.Configure(o => { o.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore; o.ShutdownTimeout = TimeSpan.FromSeconds(30); }); Cotas do Microsoft Foundry: o que ninguém conta Agora a parte que mais gera dor de cabeça. Todo deployment no Foundry tem cota, e quando ela estoura o modelo responde HTTP 429 (Too Many Requests). Entender como essa cota funciona evita metade dos problemas. Como a cota funciona A cota é medida em TPM (tokens por minuto) e definida por assinatura, por região, por modelo e por tipo de deployment. Você distribui esse total entre os seus deployments. O RPM vem junto. Cada unidade de cota carrega uma proporção fixa de requisições por minuto, e o limite de requisições é avaliado em janelas curtas, de 1 ou 10 segundos. Dez chamadas no mesmo segundo podem tomar 429 mesmo com o minuto longe do limite. A estimativa usa o max_tokens. Quando a requisição chega, o Foundry estima o consumo pelo tamanho do prompt e pelo max_tokens pedido, não pelo que vai ser gerado de fato. Um max_tokens exagerado consome cota que você não usa. Requisição que falhou também conta. Retry sem espera piora o throttling. Os valores padrão variam por modelo e por nível da assinatura. Na data deste artigo, a documentação lista para o gpt-5.4-mini em Global Standard, no Tier 1, 1.000.000 TPM e 1.000 RPM. Confira sempre o valor atual da sua assinatura no portal. Os dois tipos de 429 "Rate limit is exceeded": você passou da sua cota. A solução é reduzir consumo, redistribuir ou aumentar cota. "System is experiencing high demand": a capacidade da região está sob pressão, mesmo com a sua cota sobrando. Esperar o retry-after-ms e ter um plano B em outra região resolve. A resposta traz cabeçalhos que ajudam: retry-after-ms (quanto esperar), x-ratelimit-remaining-tokens e x-ratelimit-remaining-requests (quanto ainda sobra). Dá para usar esses dois últimos para desacelerar antes de tomar o erro. O SDK já tenta de novo, mas não resolve tudo O SDK da OpenAI para .NET já faz retry com backoff exponencial e respeita o retry-after. O padrão são 3 tentativas, e dá para ajustar: options: new OpenAIClientOptions { Endpoint = new Uri(new Uri(endpoint), "openai/v1/"), // O SDK já tenta de novo com backoff e respeita o retry-after. 3 é o padrão. RetryPolicy = new ClientRetryPolicy(maxRetries: 3) }); Só que retry não cria cota. Se o deployment está esgotado por alguns minutos, o agente precisa de um plano B. No exemplo, o ChatClientResiliente faz isso: public async Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { var agora = relogio.GetUtcNow(); Exception? ultimaFalha = null; TimeSpan espera = TimeSpan.FromMinutes(1); if (agora >= _principalBloqueadoAte) { try { return await principal.GetResponseAsync(messages, options, cancellationToken); } catch (Exception ex) when (classificarCota(ex) is { } tempo) { _principalBloqueadoAte = agora + tempo; (ultimaFalha, espera) = (ex, tempo); logger.LogWarning("Deployment principal com cota esgotada. Bloqueado por {Tempo}", tempo); } } if (reserva is not null && agora >= _reservaBloqueadaAte) { try { logger.LogInformation("Usando deployment reserva"); return await reserva.GetResponseAsync(messages, options, cancellationToken); } catch (Exception ex) when (classificarCota(ex) is { } tempo) { _reservaBloqueadaAte = agora + tempo; (ultimaFalha, espera) = (ex, TimeSpan.FromTicks(Math.Min(tempo.Ticks, espera.Ticks))); } } throw new CotaEsgotadaException(espera, ultimaFalha!); } E é ele que lê o tempo de espera que o Foundry mandou: public static TimeSpan? ClassificarCota(Exception ex) { if (ex is not ClientResultException { Status: 429 } erro) return null; var cabecalhos = erro.GetRawResponse()?.Headers; if (cabecalhos?.TryGetValue("retry-after-ms", out var ms) == true && double.TryParse(ms, out var milissegundos)) return TimeSpan.FromMilliseconds(milissegundos); if (cabecalhos?.TryGetValue("retry-after", out var s) == true && double.TryParse(s, out var segundos)) return TimeSpan.FromSeconds(segundos); return TimeSpan.FromMinutes(1); } Se principal e reserva estiverem esgotados, o agente não fica cego. Ele cai em modo degradado e continua de plantão sem IA: catch (CotaEsgotadaException ex) { // Cota do Foundry estourou em todos os deployments: o plantão continua sem IA logger.LogWarning("Cota do Foundry esgotada. Nova tentativa após {Espera}", ex.TentarApos); await AbrirSemIaAsync(sinais, assinatura, $"Cota do Foundry esgotada (429). IA indisponível por {ex.TentarApos}", ct); return ("Incidente aberto sem IA (cota)", false); } Dicas para não sofrer com cota Chame a IA menos vezes. No agente 24/7, a regra antes da IA é a principal economia. O melhor token é o que você não gasta. Ajuste o MaxOutputTokens ao que você precisa. Se a resposta tem 300 tokens, não reserve 4.000. Em modelos de raciocínio, deixe margem para o raciocínio, mas sem exagero. Enxugue o prompt. Mande para o modelo só os sinais agregados, não o log inteiro. Prompt menor é menos TPM. Evite picos. Vários agentes acordando no mesmo segundo estouram o RPM. Espalhe os horários dos ciclos. Prefira Global Standard. Ele roteia a requisição para onde há capacidade e costuma ter cota padrão maior. Se houver exigência de residência de dados, use Data Zone. Tenha um deployment reserva em outra região. Como a cota é por região, um segundo deployment em outra região dobra o fôlego e cobre o 429 de alta demanda. Redistribua antes de pedir mais. No portal do Foundry, em Gerenciar > Cota, dá para tirar TPM de um deployment ocioso e passar para o que precisa. Para ver a cota da assinatura, o usuário precisa da role Cognitive Services Usages Reader no escopo da assinatura. Peça aumento com histórico. O pedido é feito no mesmo lugar, em Solicitar cota. A Microsoft prioriza quem já usa bem a cota atual, então peça com o consumo na mão. Para produção crítica, avalie PTU. Provisioned Throughput reserva capacidade dedicada, com latência previsível. E o recurso de spillover manda o excedente do PTU para um deployment Standard. O que não é tempo real vai para batch. Relatórios e análises noturnas não precisam disputar cota com o agente de plantão. Monitore. Acompanhe consumo e 429 no Azure Monitor e coloque alerta. Cota é capacidade de produção, trate como trata CPU e memória. Cuidado ao apagar recurso pela API. Recurso excluído por API ou CLI segura a cota por 48 horas até ser expurgado. Se for recriar, faça o purge. Rodando localmente Sem o Foundry configurado, o agente usa um cliente simulado que chama as ferramentas de verdade. Dá para ver o ciclo completo sem Azure e sem custo: # terminal 1: a aplicação vigiada dotnet run --project src/Loja.Api # terminal 2: o agente, com ciclo de 10 segundos para a demo dotnet run --project src/Vigia.Agente -- --Vigia:IntervaloDoCiclo=00:00:10 # terminal 3: derruba o banco e gera erros curl -X POST http://localhost:5100/simular/falha for i in $(seq 1 8); do curl -s http://localhost:5100/pedidos/$i > /dev/null; done # o que o agente fez curl http://localhost:5200/incidentes curl http://localhost:5200/status Para usar o modelo de verdade, configure Foundry:Endpoint (e, se quiser, Foundry:EndpointReserva) com user secrets e faça az login. O agente autentica com Entra ID, sem chave. Publicando para rodar 24/7 Para esse tipo de carga, eu uso o Azure Container Apps. O repositório traz um Dockerfile e o script infra/deploy-containerapps.sh. Os pontos que importam: --min-replicas 1 --max-replicas 1. Mínimo 1 para nunca escalar para zero. Máximo 1 para nunca rodar duplicado: dois agentes abririam incidentes em dobro. Se precisar de alta disponibilidade, use eleição de líder (por exemplo, lease em um blob). Managed Identity. A identidade do container recebe a role Cognitive Services OpenAI User no recurso do Foundry. Nenhum segredo no container. Liveness probe no /health. É o batimento do agente. Se ele travar, o Container Apps reinicia. probes: - type: Liveness httpGet: path: /health port: 8080 periodSeconds: 30 failureThreshold: 3 Evite hospedar o agente em plano que hiberna (Functions no plano de consumo ou App Service sem Always On). Agente de plantão que dorme não está de plantão. Antes de colocar em produção Sinais reais: troque a FonteDeSinais por consultas ao Application Insights ou Log Analytics. Incidentes onde o time já trabalha: Azure DevOps, Jira ou ServiceNow, com notificação no Teams. Aprovação com identidade: o endpoint de aprovação precisa de autenticação, ou a aprovação vai para um card no Teams. Telemetria: adicione UseOpenTelemetry() no pipeline do IChatClient para ver tokens, latência e chamadas de ferramenta por ciclo. Persistência: incidentes, aprovações e consumo de tokens em banco, para sobreviver a reinício. Conclusão O que faz um agente 24/7 dar certo não é o modelo. É a engenharia em volta dele: regra antes da IA, ferramentas com permissão bem definida, humano aprovando o que é irreversível, teto de gasto e um plano para quando a cota acabar. E tem um pré-requisito que eu repito sempre: o agente só é tão bom quanto os sinais que ele lê. Sem health check decente e sem log estruturado, não existe IA que faça diagnóstico. Primeiro os dados e as APIs, depois a IA. Se as suas regras de negócio já estão expostas como servidor MCP, elas viram ferramentas desse agente sem esforço, e o mesmo Foundry que atende o seu chat com React e .NET atende o plantão. O código completo está em github.com/hgmauri/sample-agente-24x7. Roda no modo simulado, derruba o banco e vê o agente trabalhar.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News