No post anterior, o watchdog de quota do Azure OpenAI era um script com if pct_of_tpm > 0.8: alert. Funciona, mas carrega um problema que qualquer pessoa que já configurou alerta de monitoramento conhece de cor: threshold fixo não entende contexto. Um batch job que sempre consome 90% de TPM por 10 minutos no fechamento do mês e depois volta ao normal é, para o script, o mesmo evento que algum agent solto no ambiente entrando em loop e queimando tokens sem parar. Os dois cruzam o mesmo threshold; só um deles merece acordar alguém.
Este post é sobre fechar essa lacuna: dar ao watchdog uma camada de raciocínio, sem abrir mão de nenhum dos guardrails que já definimos.
tl;dr: O poller continua determinístico. O modelo só entra quando o threshold é cruzado e decide apenas a prioridade do alerta usando histórico, sem permissão para agir no recurso.
O que muda (e o que não muda)
O que não muda: o servidor continua apenas lendo telemetria e apenas escrevendo em canais de notificação. Nenhuma nova tool para agir sobre o recurso. Essa fronteira estava certa no post 2 e continua certa agora. Dar autonomia sobre como alertar é uma coisa; dar autonomia para agir no recurso de produção é outra completamente diferente, e eu não recomendaria a segunda sem um nível de maturidade operacional bem maior do que dois posts de blog conseguem garantir.
O que muda é o que acontece entre detectar o threshold e decidir a resposta. Duas novas tools entram no servidor:
from datetime import timedelta
from typing import Literal
import httpx
@mcp.tool()
def get_token_usage_history(deployment_name: str, days_back: int = 30) -> dict[str, object]:
"""Returns the deployment's consumption pattern over the last N days,
aggregated by day of week and hour of day, for comparison against
the current spike."""
response = _get_metrics_client().query_resource(
resource_uri=OPENAI_RESOURCE_ID,
metric_names=["TokenTransaction"],
timespan=timedelta(days=days_back),
granularity=timedelta(hours=1),
filter=f"ModelDeploymentName eq '{deployment_name}'",
)
# aggregate by weekday-hour and return avg/max per bucket
...
@mcp.tool()
def send_priority_alert(message: str, priority: Literal["info", "warning", "urgent"]) -> str:
"""Posts an alert to the right notification path for its priority."""
webhook_url = {
"info": SLACK_INFO_WEBHOOK_URL,
"warning": SLACK_INFO_WEBHOOK_URL,
"urgent": SLACK_URGENT_WEBHOOK_URL,
}[priority]
prefix = {"info": "ℹ️", "warning": "⚠️", "urgent": "🚨 @oncall-ai"}[priority]
response = httpx.post(webhook_url, json={"text": f"{prefix} {message}"}, timeout=10.0)
response.raise_for_status()
return "sent"
(mcp aqui é a mesma instância de FastMCP criada no post 2: essas duas tools entram no mesmo servidor watchdog429, não em um servidor separado.)
A primeira dá ao agent uma linha de base para comparação: “isso já aconteceu neste mesmo horário antes?” A segunda separa o ato de notificar do nível de urgência: a camada de raciocínio decide a prioridade, e a tool só publica no destino correspondente.
Onde o modelo entra (e onde não entra)
Um detalhe operacional que faz toda a diferença neste desenho: o modelo não roda a cada minuto. O script determinístico do post 2 continua sendo o poller. Ele roda no cron, uma vez por minuto, a custo zero de LLM, e só dispara uma chamada de modelo quando o threshold é cruzado. Colocar um LLM no loop em toda iteração de monitoramento é gastar dinheiro em um caminho que, na esmagadora maioria do tempo, não precisa de raciocínio algum. Só vale pagar o custo da chamada de modelo nos minutos em que o threshold realmente foi cruzado.
cron (1x/min, zero LLM cost)
│
▼
get_token_usage_trend > 0.8 ? ──no──▶ loop continues
│ yes
▼
invoke the agent (1 model call)
│
▼
get_token_usage_history + reasoning
│
▼
send_priority_alert(priority = info | warning | urgent)
O system prompt do agent continua pequeno e direto: ele não precisa de muito mais do que isto:
You are an Azure OpenAI quota watchdog. You were triggered because TPM
consumption passed 80%. Your only decision is the alert's priority
level: info, warning, or urgent.
Use get_token_usage_history to compare the current spike against the
pattern from the last 30 days at the same day of week and time.
- If the current pattern is consistent with past spikes at this same time: info.
- If it's an unprecedented spike but the curve is leveling off: warning.
- If it's an unprecedented spike AND the curve keeps climbing fast: urgent.
You have no tool that can act on the deployment. Your only possible
output is calling send_priority_alert exactly once.
Repare na última linha: ela existe para reforçar, no próprio prompt, o limite que já está embutido na arquitetura. É redundância intencional: o guardrail real é a ausência da tool; o prompt é só a segunda camada.
Onde isso roda de verdade: o cron do post 2 e a etapa condicional de invocar o modelo cabem tranquilamente em um Azure Container Apps Job com trigger agendado para uma vez por minuto: ele sobe um container Python, executa get_token_usage_trend, decide se vai invocar o modelo e encerra. Nada de servidor permanentemente ligado, nada de VM para manter. A managed identity do job (azurerm_user_assigned_identity + um azurerm_role_assignment com a role Monitoring Reader na Cognitive Services account) substitui qualquer API key fixa; o Terraform completo está no repositório companion da série.
Dois cenários lado a lado
Cenário A: último dia útil do mês, 23h, TPM em 87%. O agent chama get_token_usage_history, vê que esse mesmo deployment bate 80-90% todo fechamento de mês nesse horário há seis meses seguidos e sempre volta ao normal em menos de 15 minutos. Decisão: info. Uma mensagem no canal, sem menção, sem page.
Cenário B: terça-feira aleatória, 14h, TPM em 84% e subindo rápido nos últimos três minutos. O agent consulta o histórico, não encontra nenhum padrão parecido para esse dia da semana ou horário, e a curva não mostra sinal de estabilização. Decisão: urgent. Mesma tool, prioridade diferente, e desta vez alguém realmente recebe page.
A diferença entre os dois cenários não estava em nenhum threshold fixo. Estava em comparar o presente com o histórico, que é exatamente o tipo de julgamento que um if simples não lida bem e que um agent lida razoavelmente bem, desde que as tools certas estejam disponíveis.
Os guardrails extras que esta etapa exige
Dar autonomia de decisão, ainda que só sobre o nível do alerta, abre uma categoria nova de risco que o script puro não tinha: alert fatigue ao contrário. Um agent mal calibrado pode alertar demais (tudo vira urgent e o time aprende a ignorar) ou alertar de menos (um incidente real é classificado como info porque o histórico coincidiu por acaso). Três coisas resolvem a maior parte disso.
Primeiro, um rate limit no próprio alerta: no máximo N chamadas para send_priority_alert por hora, independentemente do que o agent decidir, para impedir que uma reavaliação a cada minuto vire uma enxurrada. Segundo, registrar cada decisão com os sinais usados na classificação e uma justificativa curta para auditoria, não o texto bruto do raciocínio interno. É isso que permite, num retro depois de um incidente, responder “por que isso foi classificado como info” sem chute. Terceiro, revisão humana periódica das decisões classificadas como info: não para aprovar uma a uma em tempo real, o que destruiria o benefício da automação, mas para auditar em lote, semanalmente, se o padrão de classificação continua fazendo sentido.
Vale notar uma diferença em relação ao risco do post 1: lá, os dados que alimentavam o raciocínio do agent vinham de fora (logs, que podem ser adulterados). Aqui, a entrada são métricas numéricas do próprio Azure Monitor. A superfície de prompt injection é praticamente inexistente, porque não há texto arbitrário de terceiros entrando no contexto. Nem todo agent tem o mesmo perfil de risco, e vale mapear isso caso a caso em vez de aplicar o mesmo checklist para tudo.
Próximo da série
- ✅ MCP e agents: o 101
- ✅ O watchdog 429 determinístico
- ✅ Este post: dando autonomia de decisão com guardrails
- Times de agents na prática: um orquestrador combinando o diagnóstico de AKS do post 1 com este watchdog, para correlacionar automaticamente “o consumo de tokens disparou” com “houve deploy recente no cluster”
- Governança base para agents no Microsoft Foundry
O próximo passo natural é parar de tratar esses dois agents como projetos isolados e ver o que acontece quando um orquestrador enxerga os dois ao mesmo tempo. É exatamente o ponto em que “time de agents” deixa de ser conceito de slide e vira uma ferramenta real de debugging.
Este é o post 3 da série “MCP, Agentes e Times de Agentes para Engenheiros de Infraestrutura”:
- MCP e Agentes 101
- O Watchdog 429 Determinístico
- De Script a Agente
- Orquestração Multi-Agentes
- Governança no Microsoft Foundry
Repositório companion: agentic-infra-handbook
Read this post in English.