QA e DevOps AI-Native: Evals em Produção, Testes Probabilísticos e IaC Gerada por IA

English summary (PT-BR article): Deterministic tests do not scale to LLM outputs. This article shows how to build an ai-native QA and DevOps pipeline with offline and online evals, probabilistic CI gates, AI-generated IaC, token-level observability and production guardrails. Includes TypeScript eval runners, YAML pipelines, Terraform snippets and Mermaid diagrams for the CI/CD loop.
O artigo anterior desta série, sobre codar ai-native no front-end e no back-end, terminou em um lugar desconfortável: quando a IA gera código em escala, a forma tradicional de garantir qualidade e operar o sistema passa a ficar para trás.
Apanhei disso na prática. Em um copiloto financeiro, um teste E2E verde durante semanas não detectou que 3% das respostas do modelo vinham com um caractere de escape inválido no JSON e quebravam o gráfico em produção. O teste estava certo. O que estava errado era a definição de "passou".
A tese deste artigo é direta: QA e DevOps para sistemas ai-native não são uma versão maior do QA tradicional. São uma camada nova que mede comportamento probabilístico, custa tokens, regrediu sem diff de código e precisa de observabilidade para outputs não determinísticos.
A boa notícia é que a maior parte do que precisamos já existe como peças separadas: evals, traces, canary, IaC, guardrails. O que muda é como elas se conectam.
Cobertura de testes mede quais linhas de código foram executadas durante a suíte. Para um sistema determinístico, isso correlaciona com confiança. Para um sistema que chama um LLM, não.
A razão é estrutural. O bug não está no código que você escreveu. Está na resposta que o modelo devolveu. E essa resposta muda mesmo sem um único commit, porque o provedor atualizou o modelo, porque a temperatura escolheu um token diferente, ou porque o contexto do usuário mudou.
A tabela abaixo resume o limite do teste tradicional diante de outputs de LLM:
| Dimensão | Teste determinístico | Avaliação (eval) |
|---|---|---|
| O que mede | Estrutura, fluxo, contrato | Qualidade semântica do output |
| Tipo de saída esperada | Valor exato ou schema | Faixa de aceitação, score |
| Critério de falha | AssertionError | Taxa de sucesso abaixo do limiar |
| Custo por execução | Milissegundos | Tokens, latência, dólares |
| Quando falha sem diff | Bug raro (flakiness) | Comum (modelo mudou no servidor) |
| Provê sinal de regressão | Sim, imediato | Sim, se houver baseline |
Conclusão: você precisa dos dois. Teste determinístico garante que a UI renderiza, que o schema da API está válido, que o roteamento funciona. Eval garante que a resposta do modelo ainda atende ao padrão.
A literatura recente da Anthropic sobre demystifying evals para agentes e da própria OpenAI sobre evals separa duas famílias:
Evals offline rodam antes do deploy, sobre um conjunto fixo de casos. São o equivalente ai-native da suíte de testes.
Evals online rodam depois do deploy, sobre tráfego real.
A regra prática: offline protege contra regressão conhecida. Online protege contra regressão que você não previu. Os dois cobrem lacunas diferentes.
Quando falamos "qualidade", estamos falando de métricas concretas. As mais usadas em produção hoje, em grande parte derivadas de frameworks como RAGAS e das práticas descritas pela Langfuse sobre evals, são:
| Métrica | O que mede | Pergunta típica |
|---|---|---|
| Faithfulness | O output é fiel às fontes? | Há claim sem apoio no contexto? |
| Groundedness | Está ancorado no contexto fornecido? | Inventou fato? |
| Relevance | Responde ao que foi perguntado? | Esta fora do topico? |
| Answer correctness | Está factualmente correto? | Concorda com referência? |
| Toxicity | Contém conteúdo tóxico ou ofensivo? | Custo de reputação? |
| Tool selection | A tool certa foi escolhida? | Caminho correto? |
| Cost per task | Quanto custou chegar ao resultado? | Economia viável? |
"Está bom" não é métrica. "Faithfulness maior que 0.9 no dataset de suporte ao cliente" é métrica.
O padrão determinístico é binário: rodou uma vez, passou ou falhou. Para LLM, isso é frágil. Uma única execução pode passar por sorte.
A mudança é rodar N amostras e aceitar se pelo menos X% passarem. Em vez de:
test('responde saldo corretamente', () => {
expect(modelo.perguntar('Qual meu saldo?')).toBe('R$ 1.500');
});
Você escreve:
test('responde saldo com fidelidade >= 0.9 em 95% das amostras', async () => {
const amostras = await rodarNAmostres({
prompt: 'Qual meu saldo?',
n: 20,
temperatura: 0.2,
});
const fiaveis = amostras.filter((s) => faithfulness(s) >= 0.9);
const taxa = fiaveis.length / amostras.length;
expect(taxa).toBeGreaterThanOrEqual(0.95);
});
Esse padrão é aprofundado em detalhe no artigo sobre testes probabilísticos vs. E2E determinístico com SpecLoop, que cobre cold/hot context, findings e orquestração em Rust. Aqui, o ponto essencial é: a unidade de teste passa a ser uma distribuição, não um evento.
Abaixo, um esqueleto de eval runner que combina golden dataset com LLM-as-judge. Não é framework; é a arquitetura visível.
type CasoGolden = {
id: string;
entrada: string;
contexto?: string;
esperado?: string;
};
type Score = {
casoId: string;
faithfulness: number;
relevance: number;
groundedness: number;
custoTokens: number;
latenciaMs: number;
};
type ResultadoSuite = {
total: number;
taxaFaithfulness: number; // proporção de casos >= 0.9
taxaRelevance: number;
custoTotal: number;
p95LatenciaMs: number;
passou: boolean;
};
const LIMIAR_FAITHFULNESS = 0.9;
const LIMIAR_RELEVANCE = 0.85;
const TAXA_MINIMA = 0.9;
async function rodarEvalSuite(
casos: CasoGolden[],
modelo: (entrada: string, contexto?: string) => Promise<string>,
juiz: (entrada: string, output: string, contexto?: string) => Promise<Omit<Score, 'casoId' | 'custoTokens' | 'latenciaMs'>>,
): Promise<ResultadoSuite> {
const scores: Score[] = [];
for (const caso of casos) {
const inicio = Date.now();
const output = await modelo(caso.entrada, caso.contexto);
const latenciaMs = Date.now() - inicio;
const medido = await juiz(caso.entrada, output, caso.contexto);
scores.push({
casoId: caso.id,
...medido,
custoTokens: estimarTokens(caso.entrada, output),
latenciaMs,
});
}
const taxaF = scores.filter((s) => s.faithfulness >= LIMIAR_FAITHFULNESS).length / scores.length;
const taxaR = scores.filter((s) => s.relevance >= LIMIAR_RELEVANCE).length / scores.length;
return {
total: scores.length,
taxaFaithfulness: taxaF,
taxaRelevance: taxaR,
custoTotal: scores.reduce((acc, s) => acc + s.custoTokens, 0),
p95LatenciaMs: percentile(scores.map((s) => s.latenciaMs), 95),
passou: taxaF >= TAXA_MINIMA && taxaR >= TAXA_MINIMA,
};
}
function percentile(valores: number[], p: number): number {
const ordenado = [...valores].sort((a, b) => a - b);
const idx = Math.ceil((p / 100) * ordenado.length) - 1;
return ordenado[Math.max(0, idx)];
}
function estimarTokens(entrada: string, saida: string): number {
return Math.ceil((entrada.length + saida.length) / 4);
}
O ponto não é o código em si. É o contrato: cada execução da suíte devolve uma distribuição, um custo e uma latência. Esses três números viram sinais de CI.
DevOps ai-native não é "deixar a IA subir em produção". É mudar quem escreve o boilerplate.
O padrão que funciona na prática é:
terraform plan), avalia o diff e aprova.Exemplo de prompt que gera HCL útil:
Gere um módulo Terraform para um bucket S3 com:
- nome prefixado por projeto
- versionamento habilitado
- criptografia SSE-S3
- lifecycle policy movendo para Glacier após 90 dias
- bloqueio de acesso público
Use convenções da AWS, sem providers deprecated.
Saída em HCL válido, sem comentários explicativos.
A IA devolve algo como:
resource "aws_s3_bucket" "dados" {
bucket_prefix = "${var.projeto}-dados-"
force_destroy = false
}
resource "aws_s3_bucket_versioning" "dados" {
bucket = aws_s3_bucket.dados.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "dados" {
bucket = aws_s3_bucket.dados.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "dados" {
bucket = aws_s3_bucket.dados.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_lifecycle_configuration" "dados" {
bucket = aws_s3_bucket.dados.id
rule {
id = "move-to-glacier"
status = "Enabled"
transition {
days = 90
storage_class = "GLACIER"
}
}
}
O que muda aqui não é o HCL. É o fluxo. O guia de Terraform sobre modules segue sendo a referência canônica para estrutura. O que a IA agrega é velocidade na escrita do recurso que você já sabe que precisa.
O anti-pattern é o inverso: humano descreve intenção vaga, IA gera 400 linhas de HCL com providers deprecated, ninguém revisa, apply vira jogo de azar. A documentação oficial do Terraform e o relatório DORA State of DevOps são claros em um ponto: automação sem revisão humana piora confiabilidade, não melhora.
O pipeline de CI para um sistema ai-native tem um step a mais que o pipeline tradicional: o eval gate.
A lógica é simples: depois dos testes determinísticos e antes do deploy, roda a suíte de evals. Se a taxa de sucesso regrediu em relação ao baseline, o deploy é bloqueado.
O exemplo abaixo é um step de GitHub Actions que implementa esse gate. A suíte devolve um JSON com as taxas; o step decide se bloqueia.
name: ci-ai-native
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm lint
- run: pnpm test -- --coverage
eval-gate:
needs: build-and-test
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Rodar eval suite
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: pnpm eval:run -- --report=json > eval-report.json
- name: Comparar com baseline
run: |
node scripts/eval-gate.mjs \
--report=eval-report.json \
--baseline=.evals/baseline.json \
--min-taxa-faithfulness=0.9 \
--min-taxa-relevance=0.9 \
--regressao-maxima=0.02
- name: Upload do relatório
if: always()
uses: actions/upload-artifact@v4
with:
name: eval-report
path: eval-report.json
deploy:
needs: eval-gate
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy canary
run: ./scripts/deploy-canary.sh
- name: Promover se estável
run: ./scripts/promote-if-stable.sh
O script eval-gate.mjs faz o trabalho real de ler o JSON, comparar com o baseline e falhar o step se a regressão passar do tolerado. A regra de ouro é: regressão de 2% na taxa de faithfulness já é suficiente para bloquear. Não espere regressões grandes.
Em produção, você não sabe o que aconteceu se não registrou. Para sistemas ai-native, OpenTelemetry e ferramentas especializadas como Langfuse são o padrão.
Cada request para um LLM deve gerar um trace contendo:
Exemplo de hook de observabilidade em TypeScript, usando a convenção OpenTelemetry:
import { trace, context, SpanKind, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('llm-runner');
type LLMRequest = {
modelo: string;
prompt: string;
contexto?: string;
maxTokens: number;
};
type LLMResponse = {
output: string;
tokensEntrada: number;
tokensSaida: number;
latenciaMs: number;
custoEstimado: number;
};
export async function chamarLLMComTrace(req: LLMRequest): Promise<LLMResponse> {
return tracer.startActiveSpan(
`llm.call ${req.modelo}`,
{ kind: SpanKind.CLIENT, attributes: { 'llm.model': req.modelo } },
async (span) => {
const inicio = Date.now();
try {
const promptRedigido = redigirPII(req.prompt);
span.setAttribute('llm.prompt.length', promptRedigido.length);
const resultado = await providerLLM(req.modelo, promptRedigido, req.maxTokens);
const latenciaMs = Date.now() - inicio;
const custoEstimado = calcularCusto(req.modelo, resultado.tokensEntrada, resultado.tokensSaida);
span.setAttributes({
'llm.tokens.input': resultado.tokensEntrada,
'llm.tokens.output': resultado.tokensSaida,
'llm.latency_ms': latenciaMs,
'llm.cost_usd': custoEstimado,
'llm.ttft_ms': resultado.ttftMs,
});
return {
output: resultado.output,
tokensEntrada: resultado.tokensEntrada,
tokensSaida: resultado.tokensSaida,
latenciaMs,
custoEstimado,
};
} catch (erro) {
span.recordException(erro as Error);
span.setStatus({ code: SpanStatusCode.ERROR, message: (erro as Error).message });
throw erro;
} finally {
span.end();
}
},
);
}
Custo não é detalhe. Em sistemas ai-native, custo por request é métrica de SLO tanto quanto latência. Sem ele, você descobre o problema só quando a fatura chega.
Trocar de modelo é uma das operações mais arriscadas em sistemas ai-native. O novo pode estar mais barato e pior em faithfulnes, e o bug só aparece semanas depois.
Duas técnicas reduzem esse risco:
Shadow traffic envia a mesma requisição para o modelo novo, mas não devolve o output ao usuário. Compara-se offline as duas saídas em um conjunto de métricas (similaridade, custo, latência). Se após N dias o novo está alinhado ou melhor, promove.
Canary direciona uma fração pequena (1%, depois 5%, depois 25%) do tráfego real para o novo modelo, com rollback automático se qualquer SLO deteriorar: aumento de latência p95, crescimento de custo por request, queda de score nos evals online, aumento de reclamação de usuário.
A diferença com deploy canary tradicional é o conjunto de sinais. Em DevOps clássico, observa-se error rate e latência. Em ai-native, adiciona-se score de faithfulness calculado assincronamente pelo judge e custo por request.
Produção ai-native precisa de guardrails que a aplicação clássica não precisa:
| Guardrail | Por quê | Implementação típica |
|---|---|---|
| Rate limit por usuário | Evita abuso e estouro de custo | Redis + token bucket |
| Detecção de prompt injection | Usuário tenta extrair instruções do sistema | Classificador + blocklist de padrões |
| Filtragem de PII no log | LGPD/GDPR | Regex + NER antes de gravar trace |
| Limite de custo por sessão | Evita usuário único queimar orçamento | Contador por user_id |
| Timeout agressivo | Latência de LLM varia muito | Abortar após Xs, devolver fallback |
| Output validation | Modelo pode devolver JSON inválido | Schema validation, retry com repair |
| Blocklist de tópicos | Complpliance, segurança | Filtro semântico no output |
O detalhe operacional é: guardrail falha também. Todo guardrail precisa ser tratado como sistema, com observabilidade própria. Se o detector de prompt injection tem falso positivo alto, ele virou ataque de disponibilidade contra seus próprios usuários.
Cobertura mede linhas executadas. Não mede qualidade do output do modelo. Um arquivo com 100% de cobertura e faithfulness 0.6 em produção é um incidente esperando acontecer.
Deployar prompt novo ou modelo novo sem gate de eval é a forma mais comum de introduzir regressão silenciosa. O CI passa, a fatura diminui, e três semanas depois descobre-se que faithfulness caiu de 0.92 para 0.78.
Se o trace não registra tokens de entrada, tokens de saída e custo por request, você não consegue nem debugar nem otimizar. A fatura vira caixa-preta.
LLM-as-judge é rápido, barato e tendencioso. Todo judge precisa ser calibrado contra um conjunto rotulado por humanos. Sem calibração, o score 0.9 do judge pode significar 0.6 na realidade.
Guardrails são código. Precisam de testes. Se o detector de prompt injection nunca recebeu um corpus de ataques conhecidos, ele é esperança, não defesa.
A IA gera 400 linhas de Terraform, ninguém lê, o apply roda em prod e abre acesso público a um bucket. O terraform plan foi feito exatamente para esse momento. Use.
Comece pequeno. Em uma semana:
Em um mês:
plan e revisão humana antes do apply.QA e DevOps ai-native não substituem o que já existe. Eles adicionam uma camada que mede comportamento probabilístico onde testes determinísticos não enxergam.
A diferença entre um sistema ai-native que escala e um que vira incidente recorrente está em três perguntas que o time consegue responder com dados:
Se a resposta para alguma dessas for "não sei", o trabalho começa ali. Eval primeiro. Trace sempre. Gate no CI. Canary no deploy. Guardrails em produção. O resto é DevOps que já conhecemos, agora com uma variável nova: o modelo que muda sozinho.
A próxima parada desta série é Métricas, Cultura e Segurança AI-Native: como medir o time ai-native, quais KPIs importam quando código é gerado por IA, e como cultura e segurança acompanham essa mudança.
Produtos gratuitos e pagos para transformar ideias em uma base que você consegue executar.
13 produtos disponíveisContinue explorando tópicos similares

English summary: AI-native organizations need a measurement operating system, not an AI-usage leaderboard. This article starts with a minimal outcome event and dashboard, then conn

--- title: "Papéis e Novas Skills no Time AI-Native: Do Senior Engineer ao AI Systems Designer" subtitle: "Como a engenharia de software se reorganiza em torno de loops de agentes,

English summary: AI-native teams redesign how work crosses boundaries: context, decision rights, evaluation, and human accountability become explicit so autonomy can grow without h
Checklist de 47 pontos para encontrar bugs, riscos de segurança e problemas de performance antes do lançamento.
Templates testados em produção, usados por desenvolvedores. Economize semanas de setup no seu próximo projeto.