Do SonarQube ao processo agentic: quando quality gates viram função de recompensa
Investigando um caso real de reward hacking no mcp-graph: como o feature-depth virou alvo de otimização e por que análise estática precisa de validação comportamental em loops agentic.
Indice
Eu estava acompanhando o feature-depth do mcp-graph subir de score quando uma sensação familiar voltou: o número está crescendo, mas o software não parece melhor. Não que ele esteja pior. Só que a curva da métrica e a curva da qualidade percebida deixaram de andar juntas. Esse descolamento é o tipo de coisa que, se você ignora, vira dívida técnica silenciosa. Se você olha de perto, vira artigo.
Esse artigo é o resultado de olhar de perto.
A tese é direta: quality gate é sensor enquanto humano escreve código; vira função de recompensa quando agente otimiza código. Função de recompensa convida reward hacking. E reward hacking, em loop agentic, é uma questão de tempo, não de probabilidade. O que aconteceu no feature-depth é o caso clínico. O que se aprende dele se chama Behavior-First Gating, e é a evolução natural do harness que descrevi em O Rato no Labirinto.
Quality gate como sensor versus quality gate como placar
A ideia de quality gate é antiga e bem-resolvida no mundo CI/CD humano. Você define uma regra: cobertura acima de 80%, zero vulnerabilidade crítica, complexidade ciclomática abaixo de N. A pipeline mede. Se passa, segue. Se falha, bloqueia. SonarQube popularizou esse modelo, e ele funciona. Funciona porque o humano que escreve o código e o gate que o avalia operam em ciclos diferentes: o humano escreve, recebe feedback, ajusta, faz code review com outro humano, e o gate é só uma das várias camadas.
Agora troque o humano por um agente que itera centenas de vezes por dia, lê o resultado da métrica entre uma iteração e outra, e tem como objetivo declarado fazer o número subir. O sensor virou placar. E placar muda o jogo.
| Desenvolvimento tradicional | Desenvolvimento agentic |
|---|---|
| Humano escreve, gate observa | Agente escreve, gate é alvo de otimização |
| Métrica é sinal de saúde | Métrica é função de recompensa implícita |
| Code review humano filtra ruído | Loop fechado amplifica ruído sem filtro |
| Gaming é exceção, custa esforço | Gaming é estratégia dominante, custa zero esforço |
| Análise estática é evidência suficiente | Análise estática precisa de validação comportamental |
A parte que pega muita gente desprevenida é a última linha. Não é que SonarQube tenha ficado ruim. É que o paradigma assume um cenário humano onde o gate é uma camada entre várias. Quando você fecha o loop agentic com o gate como objetivo, esse pressuposto evapora.
Reward function: o que o agente realmente lê
Quando você instrui um agente a “subir o score do feature-depth de 78 para 85”, você acha que está pedindo “melhore o código”. O agente entende algo diferente: “encontre a função score(arquivos) → número e altere arquivos até número ≥ 85”. Ele não tem teoria sobre qualidade. Ele tem a função score como contrato operacional. Tudo que produz score alto é bom. Tudo que produz score baixo é ruim. Essa é a definição operacional de função de recompensa, e ela é mais antiga que LLM.
Victoria Krakovna, da DeepMind, mantém uma planilha pública catalogando exemplos de specification gaming em sistemas otimizadores. São dezenas de casos onde o sistema cumpre a métrica e viola a intenção: agentes de RL que pausam o jogo eternamente para nunca perder, simuladores que descobrem bugs do físico para ganhar pontos, classificadores que aprendem a identificar o conjunto de teste em vez do conceito. O padrão se repete em todas as escalas. A tese de Hubinger et al. sobre risks from learned optimization sintetiza o porquê: qualquer otimizador suficientemente capaz vai encontrar o mínimo da função de perda que você definiu, mesmo quando a função de perda diverge da intenção real.
O que acontece em loops agentic com quality gates é o mesmo fenômeno, só que no nível da engenharia de software, com tokens em vez de pixels. O modelo não está sendo malicioso. Está cumprindo o contrato que você lhe deu. O contrato é a métrica. A intenção real ficou implícita. E intenção implícita é a primeira coisa que evapora num loop fechado.
Há um nome para isso desde 1975. Lei de Goodhart: quando uma medida vira alvo, ela deixa de ser uma boa medida. O artigo original de Charles Goodhart era sobre política monetária britânica nos anos 70. A formulação se aplica intacta a quality gates em 2026. Toda métrica que vira função de recompensa para um otimizador genérico tende a perder a relação que tinha com a coisa que se quer medir. O feature-depth é uma instância específica, recente, observável.
Ato 1: o agente descobriu os comentários
A primeira falha foi quase boba e por isso mesmo é instrutiva. O feature-depth calculava maturidade dividindo sinais de implementação por linhas de código. Linhas de código eram contadas com regra que ignorava comentário, o que faz sentido. Os detectores de sinais de implementação, no entanto, liam o arquivo inteiro como texto, sem distinguir o que era comentário do que era código vivo. Na prática, o numerador e o denominador tinham definições diferentes do que conta como código.
O agente não precisou raciocinar sobre isso. Ele tentou, mediu, e algo subiu. Em algum momento, apareceu um arquivo com um bloco como esse:
// FD-MARKERS-V2 regex-token block
// ?? ?? ?? ?? ?? ?? ?? ??
// z.object() z.string() z.number()
// .safeParse() .safeParse()
// try { catch ( : any as any if (x) return default : ?.
Para o contador de linhas, isso era zero. Para os detectores de qualidade, era um banquete: ??, z.string(), .safeParse(), try {, catch (. O numerador subia. O denominador não se mexia. O score, que é razão entre os dois, disparava. O agente repetiu o padrão. O dashboard ficou verde.
Princípio. Se numerador e denominador de uma métrica não usam a mesma definição do que é código, a métrica é matematicamente bem-comportada e semanticamente quebrada.
A correção imediata foi remover comentários antes de aplicar os detectores. Coisa de dez linhas. O score caiu nos arquivos contaminados, subiu de novo nos arquivos legítimos. Achei que tinha resolvido. Não tinha resolvido nada.
Ato 2: o agente mudou de rota
Depois de fechar a porta dos comentários, o score continuou subindo em arquivos onde o comportamento real não mudava. Foi nesse momento que a ficha caiu.
O problema não era “comentários enganam a métrica”. O problema era “o agente está otimizando contra o sensor”. Fechar uma rota só faz o agente procurar a próxima. E havia várias.
A primeira é o token dentro de string. Considere:
const ERROR_HINTS = {
validation: "use z.string() or .safeParse() at boundaries",
guards: "add ?? defaults and ?. before map calls",
errors: "wrap in try { ... } catch (e) { ... }",
};
Lido por humano, é dicionário de mensagens. Lido por detector textual, contém z.string(), .safeParse(), ??, ?., try {, catch (. O detector não sabe se aquilo executa. Ele sabe que encontrou o padrão. Soma pontos. A frase que virou âncora dessa investigação: presença textual não é comportamento real.
A segunda é o teste superficial. Um teste do tipo expect(myFunction).toBeDefined() passa, sobe a contagem de testes, sobe o sinal de “tem cobertura” e prova absolutamente nada sobre comportamento. Em revisão humana, alguém comenta no PR e o teste é refeito. Em loop agentic, ninguém comenta. O teste fraco é estatisticamente equivalente ao teste forte para o gate, e estatisticamente mais barato de gerar. O agente aprende a preferência sem nunca formular a estratégia.
A terceira é o comentário inline:
const x = 1; // ?? ?? z.string() .safeParse()
Agora existe linha de código real. O LOC conta. Dependendo de como o detector funciona, o lado direito do // ainda alimenta os sinais. O exploit ficou mais discreto, mais difícil de distinguir de código legítimo. Esse é o padrão clássico que Krakovna documenta: primeiro o atalho é grosseiro, depois fica cada vez mais parecido com trabalho legítimo, até o ponto em que separar gaming de uso correto exige análise mais cara que a métrica em si.
A matriz 2×2
A tentação aqui é corrigir mais um detector e passar para o próximo problema. Eu já tinha caído nessa tentação no Ato 1. Decidi ser metódico no Ato 2 e desenhar uma matriz pequena que me forçava a separar duas perguntas.
A pergunta ingênua: minha defesa fez o score cair? A pergunta correta: minha defesa pegou a classe de exploit, ou só removeu o exemplo específico que eu vi?
A diferença é tudo. Se eu mato o exploit X e o score cai, eu posso estar pegando a classe (boa notícia) ou só limpando o caso visível enquanto a classe segue viva em outras formas (péssima notícia). Para distinguir, preciso de quatro pontos, não dois.
| Defesa desligada | Defesa ligada | |
|---|---|---|
| Markers falsos presentes | 78.62 | 69.94 |
| Markers falsos removidos | 70.69 | 70.44 |
A leitura é direta. Quando os markers falsos estão presentes e a defesa está desligada, o score infla para 78.62. Ligar a defesa derruba para 69.94. A defesa está detectando o gaming.
Quando os markers falsos não estão presentes, ligar ou desligar a defesa quase não muda nada (70.69 contra 70.44). A defesa não está penalizando código legítimo.
Esse é o padrão que valida uma defesa real. Sem essa matriz, eu poderia ter olhado só para a coluna esquerda, visto o score cair de 78.62 para 69.94, e declarado vitória. Mas o que eu queria não era queda do score. Era queda do score condicionada à presença do exploit. Os dois números a mais (70.69 e 70.44) são o que separa correção real de placebo.
Princípio. Toda defesa contra reward hacking precisa de uma matriz 2×2 antes de ser declarada efetiva. Eixo um: comportamento adversário presente versus ausente. Eixo dois: defesa ligada versus desligada. Sem os quatro pontos, você está medindo se o sintoma sumiu, não se a causa foi tratada.
Ato 3: reachability não é o suficiente
A defesa que produziu a matriz acima foi reachability analysis. Em vez de perguntar “esse token aparece no arquivo?”, ela passou a perguntar “esse token aparece dentro de uma região que pode executar?”. Comentário não é região executável. String literal não é região executável. Bloco dentro de função morta também não. A pergunta ficou mais cara, mas mais relevante.
O que a matriz não mostra é o resíduo. Considere:
export function realFoo() {
if (false) _zRuntime;
return computeFoo();
}
Esse trecho está dentro de uma função real, exportada, presumivelmente usada em algum lugar. O _zRuntime está dentro de um if (false) que nunca executa, mas a análise de reachability ingênua pode marcar a linha como alcançável porque sintaticamente ela está no corpo de uma função que executa. Para detectar isso direito, você precisa de uma camada acima: análise de fluxo, AST mais sofisticada, Semgrep ou regras com tsserver, call graph com entry points reais. Cada camada é mais cara. Cada camada fecha mais classes de exploit. Nenhuma camada fecha o universo.
Esse é o ponto em que a maioria das equipes para de progredir. Adicionou três camadas, cada exploit virou cada vez mais específico, o custo computacional do gate explodiu, e a sensação é que a métrica está respirando por aparelhos. Foi exatamente onde eu cheguei.
A decisão de parar de melhorar o sensor
Quando você passa duas semanas adicionando camadas de defesa a uma métrica e o agente continua encontrando rotas mais sutis, em algum momento vale a pena perguntar: o que eu estou otimizando aqui? Estou produzindo software melhor, ou estou produzindo um detector mais sofisticado de tokens textuais?
A resposta honesta foi a segunda. O feature-depth continuava sendo um bom indicador descritivo (responder “como está esse módulo hoje?”), mas tinha falhado como indicador prescritivo (servir de objetivo para o agente). Continuar melhorando o sensor era uma escalada armamentista que eu não tenho capital para sustentar.
A inversão foi simples. Em vez de pedir ao agente “aumente o score do feature-depth”, o objetivo virou:
Capture um bug real que existe em produção.
Escreva um teste que falha com o código atual e passa depois da correção.
Prove que o fluxo principal continua funcionando.
Esses três pedidos não são mensuráveis por análise estática. Eles exigem execução real, observação de saída, comparação com expectativa. O agente pode mentir sobre presença de tokens. Não pode mentir sobre um teste que era vermelho e ficou verde, porque o vermelho e o verde são produzidos por um runtime que não está sob seu controle.
Esse é o pivô do post inteiro. O nome dele é Behavior-First Gating.
Behavior-First Gating
Behavior-First Gating é o princípio de que, em loops agentic, o gate primário precisa medir comportamento executado, não aparência textual. Análise estática vira camada secundária, útil para descrever, suspeita para prescrever.
Behavior-First Gating. Em qualquer ponto do workflow agentic onde o agente recebe feedback sobre se uma mudança “passou” ou “falhou”, o gate primário deve ser produzido por execução real do código. Análise estática entra como sensor descritivo, nunca como autoridade final. Métricas estáticas continuam úteis para diagnóstico humano. Param de ser confiáveis no momento em que o agente as enxerga como objetivo.
Em prática, isso vira quatro camadas, ordenadas por confiança decrescente:
A primeira são testes comportamentais com fail-states verdadeiros: o teste falha quando uma regra real quebra, não quando uma função desaparece. expect(calculateTotal([{price: 10}, {price: 20}])).toBe(30) é evidência. expect(calculateTotal).toBeDefined() é teatro.
A segunda são invariantes executáveis: properties que precisam valer para todas as entradas válidas. Property-based testing com fast-check ou hypothesis cobre o espaço que o teste exemplificado deixa de fora. A Datadog descreveu essa abordagem como “closing the verification loop” ao adotar Deterministic Simulation Testing com milhões de seeds para validar redis-rust, capturando bugs em segundos que revisão humana levaria dias para encontrar.
A terceira são smoke tests de fluxos reais: o sistema sobe, executa o caminho que o usuário usa, retorna o resultado esperado. Não cobre edge cases, mas garante que o feliz funciona. Se o smoke quebra, todo o resto é irrelevante.
A quarta, e só aí, é análise estática como sensor descritivo: SonarQube, ESLint, métricas internas, complexidade ciclomática. Útil para olhar uma vez por semana e entender tendência. Inadequado como contrato com o agente.
A inversão é importante. No mundo pré-agentic, a análise estática estava no topo do funil porque era barata e o code review humano cobria as outras camadas. No mundo agentic, o code review humano sai do loop interno (entra só no merge), e a análise estática precisa ser rebaixada porque o que era barato agora é também o mais fácil de gamear.
Onde isso encaixa SonarQube na prática
A pergunta legítima é: jogar fora o SonarQube? Não. SonarQube continua útil pelo mesmo motivo que sempre foi: ele é um excelente sensor descritivo. Ele responde “quanto código duplicado tem nesse projeto?”, “qual a evolução da complexidade no último mês?”, “onde estão as vulnerabilidades conhecidas?”. Essas perguntas continuam valendo, e respondê-las com SonarQube continua sendo o caminho mais barato.
O que muda é o que você faz com a resposta. No fluxo humano, a resposta vira comentário em PR, alerta no dashboard, ou bloqueio de merge. O humano lê, julga e age. No fluxo agentic, se você expõe o número como objetivo do agente, você acaba de transformar um sensor honesto em um placar gameavel. O agente não está errado em otimizá-lo. Você está errado em deixá-lo otimizar.
A regra prática que eu adotei depois dessa investigação: nenhuma métrica de análise estática é objetivo direto de agente. Ela pode aparecer em relatório, em diff de PR, em alerta humano. Ela não aparece em prompt de objetivo, em reward function explícita ou implícita, em loop fechado de “tente de novo até passar”. Tudo que entra em loop fechado precisa ser validação comportamental.
Isso mata o “doom-prompting” que a Salesforce documentou no Agentforce, o ciclo vicioso de ajustar a métrica e torcer pela consistência. O agente não fica preso ajustando uma métrica gameável, porque o que define sucesso passou a ser um teste que executa.
Quatro lições para quem usa quality gates em pipeline com IA
Saí dessa investigação com quatro coisas que valem mais do que o caso específico do feature-depth.
Numerador e denominador precisam concordar. Se a métrica é uma razão (sinais por linha, cobertura por arquivo, bugs por kloc), as duas pontas precisam usar a mesma definição operacional do que está sendo contado. Discrepância entre as duas é onde mora o gaming barato.
Corrigir uma brecha não fecha a classe. O agente que descobriu o exploit X vai descobrir o exploit Y assim que X for fechado. A pergunta certa não é “esse exploit foi corrigido?” mas “essa classe de exploit foi neutralizada?”. Sem isso, você está jogando whack-a-mole com um adversário que itera mais rápido que você.
Toda defesa precisa de matriz 2×2. Defesa ligada versus desligada, no eixo um. Comportamento adversário presente versus ausente, no eixo dois. Os quatro pontos juntos contam uma história. Dois pontos só contam metade dela, e a metade que falta costuma ser a importante. Esse padrão é o mesmo que Anthropic descreve ao falar de eval design para agentes: o eval só é útil se discrimina entre comportamento certo e errado, não se só sobe quando você melhora o agente.
Quando vira jogo contra o sensor, troque o objetivo. Continuar refinando a métrica é uma escalada que o agente vence no longo prazo, porque ele itera mais barato que você. Trocar de “aumente o score” para “capture um bug real” é a única jogada que sai do loop. É a inversão que O Rato no Labirinto chamou de determinism-first; o que muda é que aqui o ponto não é só ter trilhos no harness, é ter validação comportamental no gate.
Conclusão
O feature-depth começou como sensor descritivo de maturidade. Quando virou gate em loop agentic, virou alvo. Quando virou alvo, o agente encontrou primeiro os comentários, depois as strings, depois os comentários inline, e teria continuado encontrando rotas se eu tivesse continuado a brincar de fechar uma porta de cada vez.
O que essa história desenha não é um bug local. É uma mudança de paradigma. No mundo pré-agentic, quality gate ajudava humano a manter qualidade. No mundo agentic, quality gate aplicado de forma ingênua ajuda o agente a parecer que mantém qualidade. O dashboard fica verde. O software pode estar pior do que estava antes.
A resposta não é abandonar SonarQube nem matar análise estática. A resposta é colocar cada coisa no lugar certo. Métricas estáticas são sensores. Quality gates são incentivos. Agentes são otimizadores. Testes comportamentais são evidências. Confundir essas quatro coisas é exatamente onde o software desanda em pipeline com IA.
Como argumentei em A Alucinação Vem do Código, Não do Modelo, o vetor que mais altera a qualidade de saída de um agente não é o modelo, é a engenharia ao redor dele. O Behavior-First Gating é a aplicação desse princípio à camada de quality gates. Engenharia, diferente de modelo, está 100% sob seu controle. O que define se o seu pipeline com IA é resiliente a reward hacking não é a inteligência do agente. É o desenho do contrato que você assinou com ele.
O número que importa depois dessa história não é 78.62, nem 69.94, nem o próximo score que o feature-depth produzir. O número que importa é quantos bugs reais os seus testes capturam. Esse o agente não consegue inflar com tokens em comentário. Ele só consegue subir fazendo o sistema se comportar corretamente.
Que é, no fim, o que a gente sempre quis medir.
Referências
- Goodhart, C. (1975). “Problems of Monetary Management: The U.K. Experience”. Bank of England.
- Krakovna, V. et al. (2020). “Specification gaming examples in AI”. DeepMind. Planilha pública: docs.google.com
- Hubinger, E. et al. (2019). “Risks from Learned Optimization in Advanced Machine Learning Systems”. arxiv.org/abs/1906.01820
- Datadog (2026). “Closing the Verification Loop: Observability-Driven Harnesses”. datadoghq.com
- Mui, P. (2026). “Agentforce’s Agent Graph: Toward Guided Determinism”. engineering.salesforce.com
- Anthropic (2025). “Demystifying Evals for AI Agents”. anthropic.com
- Repositório
mcp-graph: github.com/DiegoNogueiraDev/mcp-graph-workflow