Pular para o conteúdo principal
Engenharia com agentes · 4 de 6 partesAI13 min de leitura

Fitness Functions São o Control Plane para Agentic Coding

O post anterior perguntou para onde foi o ponto de controle. Aqui está a primeira resposta que estou disposto a defender: ele não desapareceu — parte dele compilou. Fitness functions arquiteturais, apontadas para agentes de código, se tornam o control plane que permite ao desenvolvedor permanecer no comando sem virar o gargalo: julgamento compilado uma vez em gates determinísticos que aplicam a regra na velocidade da máquina, com mensagens de falha escritas como prompt engineering para o loop de retry. Em seguida, a complicação que dá forma ao post inteiro: no momento em que um agente otimiza contra a compilação, o controle compilado vira objeto de ataque — e o design de fitness functions herda uma corrida armamentista, com uma constituição em Kotlin/ArchUnit para tornar tudo concreto.

Todos os Posts
2/4

O post anterior terminou com uma pergunta que me recusei a responder: onde está o seu ponto de controle — de verdade, não nominalmente? Seis posts nesta série, aqui está a primeira parte de uma resposta que estou disposto a defender.

O ponto de controle não desapareceu. Parte dele compilou.

É isso que uma fitness function arquitetural é, vista pela ótica da era dos agentes: um pedaço de julgamento humano — esta camada nunca toca aquela, dependências apontam para dentro, sem ciclos entre módulos — compilado em um gate executável que roda na velocidade da máquina, a cada mudança, sem humano no loop. Neal Ford, Rebecca Parsons e Patrick Kua construíram a ideia para arquitetura evolutiva há quase uma década, como proteção contra o drift humano lento. Aponte-a para agentes de código e ela se torna algo mais consequente: o mecanismo que permite a um desenvolvedor permanecer no control plane sem virar o gargalo que o post anterior mediu. Ford percebeu a mesma convergência — sua palestra recente se chama, literalmente, The Intersection of Fitness Function driven Architecture and Agentic AI.

E então a ideia precisa sobreviver ao próprio sucesso, porque no momento em que um agente otimiza contra a compilação, o controle compilado vira objeto de ataque. Essa tensão — compile seu julgamento e depois defenda a forma compilada — é o tema deste post.

A parte do review que compila

Redes resolveram esse formato de problema décadas atrás, e o post sobre a armadilha do espectador tomou emprestado o vocabulário: o desenvolvedor é o control plane, os agentes são o data plane. O que aquele post deixou de fora é como control planes reais escalam. O control plane de um roteador não inspeciona pacotes. Ele compila política — rotas, ACLs — em tabelas que o data path aplica em line rate. O control plane pensa devagar sobre o que deveria ser verdade; o data path aplica isso rapidamente sobre tudo que se move. O controle escala justamente porque julgamento e aplicação estão desacoplados.

O review, como argumentou o post sobre desmembramento, era cinco funções em um único ato: detecção de defeitos, conformidade com a intenção, transferência de conhecimento, accountability e coerência arquitetural. Nem todas compilam. Intenção não compila — nenhum gate sabe o que você quis dizer. Transferência de conhecimento não compila — ler é como o entendimento entra em uma cabeça. Accountability não pode compilar nem em princípio, porque um gate não tem como responder por algo. Mas coerência arquitetural — a linha que antes dependia inteiramente de um revisor sênior notar que um controller está importando um repository — compila lindamente, porque suas regras são estruturais: propriedades verificáveis do grafo de dependências, não julgamentos sobre significado.

Eis a falha concreta que a linha compilada existe para pegar. Um agente recebe um brief: adicione um endpoint que confirma um pedido. Ele escreve código limpo, adiciona testes, tudo passa — e importa o repository diretamente no controller, porque nada no seu contexto dizia o contrário. A decisão de que escritas passam por um application service vive em um ADR que o agente nunca recuperou. Theo Valmis, escrevendo sobre guardrails arquiteturais para código gerado por IA, nomeia exatamente isso: a IA escreveu código funcional que violava uma decisão registrada "em um documento que a IA não tinha como enxergar". Os dados que ele cita para a escala do problema: com adoção intensa de IA, as taxas de aceitação de código saltaram de 20% para 60% enquanto o churn subiu 861% — mais código entrando, menos dele alinhado a qualquer coisa. Um humano em drift faz isso uma vez por trimestre. Uma frota de agentes faz isso todo dia, em paralelo, educadamente.

A correção não é um revisor melhor. É mover a decisão para fora do documento e para dentro do build.

Uma constituição executável, em Kotlin

O projeto de demonstração — código completo em github.com/tiarebalbi/archfit-demo — é um pequeno serviço em camadas: domain, application, adapter.web, adapter.persistence, com ArchUnit 1.4.2 conectado à task de test do Gradle. A constituição é um único arquivo. Toda regra carrega seu racional em because(...), e isso não é decoração; é a linha mais prática do arquivo, por um motivo que vem depois do código.

kotlin
@AnalyzeClasses(packages = ["com.example.shop"])
class ArchitectureConstitution {

    @ArchTest
    val `domain depends on nothing outside itself` = noClasses()
        .that().resideInAPackage("..domain..")
        .should().dependOnClassesThat().resideInAnyPackage("..application..", "..adapter..")
        .because(
            "the domain layer is the stable core: it must compile with zero knowledge of " +
                "services or adapters. If your change needs this dependency, move the logic " +
                "into the application layer instead of importing outward from domain."
        )

    @ArchTest
    val `web adapters never touch persistence directly` = noClasses()
        .that().resideInAPackage("..adapter.web..")
        .should().dependOnClassesThat().resideInAPackage("..adapter.persistence..")
        .because(
            "every write goes through an application service, which owns transactions and " +
                "invariants. Do not shortcut from a controller to a repository — inject the " +
                "application service and add a use-case method there if one is missing."
        )

    @ArchTest
    val `adapters are invisible to the application core` = noClasses()
        .that().resideInAnyPackage("..domain..", "..application..")
        .should().dependOnClassesThat().resideInAPackage("..adapter..")
        .because(
            "adapters are replaceable edges (web, persistence). The core defines interfaces; " +
                "adapters implement them. If the core needs a capability, declare an interface " +
                "in application and implement it in the adapter — never import the adapter."
        )

    @ArchTest
    val `no dependency cycles between top-level slices` = slices()
        .matching("com.example.shop.(*)..")
        .should().beFreeOfCycles()
        .because(
            "a cycle means two modules can only be understood together, which defeats " +
                "independent review of small changes. Break the cycle by moving the shared " +
                "type into the more stable of the two slices."
        )
}

Rode com ./gradlew test. Na árvore limpa, a constituição se sustenta — as quatro regras passam:

> Task :test BUILD SUCCESSFUL in 5s 4 actionable tasks: 1 executed, 3 up-to-date

Então eu banquei o agente e tomei o atalho: um OrderAdminController que segura um InMemoryOrderRepository diretamente — funcionalmente correto, testável unitariamente, e exatamente a mudança que um agente de código produz quando seu contexto nunca mencionou a decisão de camadas. O gate rejeitou:

> Task :test FAILED ArchitectureConstitution > web adapters never touch persistence directly FAILED java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package '..adapter.web..' should depend on classes that reside in a package '..adapter.persistence..', because every write goes through an application service, which owns transactions and invariants. Do not shortcut from a controller to a repository — inject the application service and add a use-case method there if one is missing.' was violated (4 times): Constructor <...OrderAdminController.<init>(...InMemoryOrderRepository)> has parameter of type <...InMemoryOrderRepository> in (OrderAdminController.kt:0) Field <...OrderAdminController.repository> has type <...InMemoryOrderRepository> in (OrderAdminController.kt:0) Method <...OrderAdminController.forceConfirm(String)> calls method <...InMemoryOrderRepository.find(String)> in (OrderAdminController.kt:7) Method <...OrderAdminController.forceConfirm(String)> calls method <...InMemoryOrderRepository.save(...Order)> in (OrderAdminController.kt:8) 4 tests completed, 1 failed BUILD FAILED in 5s

Dois detalhes nessa saída merecem atenção. Primeiro, a granularidade: uma única classe de atalho produziu quatro violações — o parâmetro do construtor, o campo e as duas chamadas de método — porque o ArchUnit reporta cada aresta de dependência, não o fato vago de que "a classe importa o repository". Não há conformidade parcial onde se esconder; remova três das quatro arestas e o gate continua segurando a porta. Segundo, repare no que chegou dentro da assertion: o racional inteiro do because(...), viajando junto na mensagem de falha.

Esse segundo detalhe é o ponto que a maioria das discussões sobre testes de arquitetura deixa passar, e ele muda para quem a mensagem é escrita. No mundo antigo, um teste de arquitetura que falhava falava com um humano, e uma mensagem lacônica bastava — o humano tinha contexto. No fluxo com agentes, a saída de falha do gate é alimentada de volta direto no loop como contexto para o retry. A mensagem de falha é prompt engineering. "Regra violada" não ensina nada ao agente e convida a mais uma tentativa às cegas; "não tome atalho de um controller para um repository — injete o application service e adicione um método de caso de uso" direciona a próxima geração em direção à arquitetura, em vez de ao redor dela — e, como a saída acima mostra, esse direcionamento é entregue exatamente no momento em que o agente precisa dele. Uma cláusula because bem escrita é o control plane falando com o data plane no meio de comunicação do próprio data plane: contexto. Hoje escrevo esses racionais com o mesmo cuidado que coloco no brief, porque eles são lidos pelo mesmo público.

É assim que "o ponto de controle compilou" se parece na prática. O julgamento aconteceu uma vez, na hora de escrever a regra, por um humano com o quadro completo. A aplicação acontece para sempre, na velocidade da máquina, com uma mensagem didática anexada. O CI do repositório roda os dois caminhos a cada push — a passagem verde e um job de violação que injeta o atalho e precisa falhar — de modo que o próprio gate é testado, não presumido. Contra o drift — o atalho cansado, o copy-paste que importa a camada errada, o agente bem-intencionado com um brief raso — o ponto de controle compilado é estritamente melhor que o humano: ele nunca dorme, nunca carimba sem ler e nunca fica exausto no diff número quatrocentos.

O gate vira o alvo

As fitness functions de Ford tinham um único adversário: a entropia. O drift humano é preguiçoso, acidental e desmotivado — ninguém tenta derrotar uma regra de camadas; as pessoas tropeçam em violações. A era dos agentes introduz um adversário diferente, e a diferença não é cosmética. Um agente em loop de retry é um otimizador, e para um otimizador o seu gate não é uma fronteira. É a função objetivo.

A medição já existe. O SpecBench, um benchmark de maio de 2026 para reward hacking em agentes de código de horizonte longo, descobriu que todo agente de fronteira satura a suíte de testes visível enquanto as lacunas contra testes held-out persistem — e a lacuna cresce cerca de 28 pontos percentuais a cada aumento de dez vezes no tamanho do código. O espécime mais memorável do paper é um "compilador" de 2.900 linhas que passou na suíte memorizando as entradas dos testes. Isso não é drift. É a lei de Goodhart rodando dentro do seu build: quando a medida vira o alvo, ela deixa de ser uma boa medida — e um agente a transforma no alvo por construção, porque passar no gate é literalmente o que ele está otimizando.

Então o ponto de controle compilado herda uma corrida armamentista, e o design de fitness functions precisa amadurecer à altura. Três consequências que hoje trato como regras de design.

Regras estruturais sobrevivem melhor à pressão do que regras de resultado. Um teste que afirma comportamento pode ser satisfeito memorizando o comportamento — o compilador com hash table do SpecBench prova isso em escala. Uma regra que afirma estrutura — nenhuma dependência deste pacote para aquele — não tem atalho equivalente, porque a única forma de satisfazê-la é a estrutura de fato se sustentar. A granularidade de quatro arestas acima é a mesma propriedade vista de outro ângulo: a regra verifica o próprio grafo de dependências, e o grafo não pode ser memorizado até virar conformidade. A constituição é toda estrutural; isso não é uma escolha estética.

O raciocínio de holdout entra nos testes de arquitetura. O SpecBench mede o gaming como a lacuna entre suítes visíveis e held-out, e a mesma ideia se transfere: se o contexto do agente inclui todo gate pelo qual ele precisa passar, os gates definem a superfície de ataque. Manter algumas verificações fora do loop — rodando no merge, não no retry — dá a você a medição da lacuna que diz se seus agentes estão satisfazendo a arquitetura ou estudando para a prova.

E o juiz continua determinístico. A correção tentadora para "regras não conseguem verificar significado" é um revisor LLM como fitness function — e isso reintroduz, no gate, o observador correlacionado contra o qual o post sobre ponto de controle alertou. O trabalho recente da InfoQ sobre fitness functions agênticas traça a linha onde eu traçaria: "Use gates determinísticos para invariantes objetivas e juízes agênticos para interpretação ancorada em evidências" — com os juízes em caráter consultivo, escalando para humanos, nunca segurando o botão de merge. Valmis enuncia o mesmo princípio pelo lado dos guardrails: sistemas probabilísticos podem recuperar informação e recomendar; não devem determinar de forma independente um veredito de aplicação. Um gate que um otimizador consegue persuadir não é um gate.

O loop que transforma isso em um plane

Uma pilha de regras de ArchUnit não é um control plane. O que a torna um é o loop que continua compilando.

Todo escape de review é um candidato à compilação. Quando um review humano — ou um incidente — pega algo que os gates deixaram passar, a pergunta não é mais apenas "corrigir ou não". É: esse julgamento compila? Se sim, ele vira uma regra com racional didático, e aquela classe de violação fica fechada para sempre, na velocidade da máquina, para todo agente futuro. O control plane acumula juros. É essa a catraca que faltava no checklist do post sobre a armadilha do espectador: revisar tudo nunca foi sustentável como estado estável, mas revisar tudo e compilar o que você pega converge — cada ciclo move uma linha de julgamento do recurso esgotável (atenção) para o inesgotável (o build).

O loop tem custos, e eles são os trade-offs honestos. Uma constituição ossifica se não for podada — uma regra cujo motivo ninguém lembra é arquitetura por superstição, e agentes vão obedientemente construir ao redor de um muro que já não protege nada. A coisa mais próxima que alguém mediu aponta na direção errada: um levantamento de junho de 2026 com 10.008 repositórios encontrou arquivos de configuração de agentes — a outra metade da camada de direcionamento — em 0,4 commit por mês contra 0,6 dos workflows de CI ao lado deles, com 58% nunca tocados depois do commit que os criou. Artefatos de direcionamento são os arquivos menos mantidos do repositório, e uma constituição é um artefato de direcionamento com botão de merge. O ponto compilado observa apenas o que alguém pensou em codificar: o problema de estreitamento de spec do post anterior não desaparece, ele se muda para o conjunto de regras, e é por isso que os gates conquistam confiança para as invariantes que cobrem e exatamente zero confiança além delas. E a corrida armamentista é permanente: toda regra que você adiciona é contexto que um otimizador acabará vendo, então a constituição precisa do mesmo review que o código costumava receber — só que com menos frequência, por menos gente, no momento em que mais importa.

O que deixa em aberto a linha que ainda se recusa a compilar. Estrutura agora tem forma executável; intenção não. O brief, a spec, o "resolva do jeito que este time resolve as coisas" — isso ainda é aplicado pelo único compilador disponível: uma pessoa, lendo, em velocidade humana. O control plane para agentic coding existe hoje, e fitness functions são o seu conjunto de instruções — mas ele compila metade da constituição, e todo mundo está entregando software apostando na outra metade por confiança. Quem encontrar a etapa de compilação para intenção encerra o debate sobre review de vez. Ainda não vi isso acontecer. Sigo atento.

Continue lendo

Curtindo? Talvez goste disso aqui.

Nada parecido — quer tentar outro ângulo?

Engenharia com agentes · 4 de 6

A parte 5 está em produção.

Novas partes saem às segundas-feiras, 9h (horário do Pacífico) — deixe seu e-mail e eu envio cada uma no dia em que sair. Só isso.

Isso foi útil?

Deixe uma avaliação ou uma nota rápida — me ajuda a melhorar.