← Voltar ao Blog

52,8% para 66,5%: como Generator-Evaluator inspirado em GAN eleva accuracy do RAG

52,8% para 66,5%. Foi o salto que peguei quando parei de tratar RAG como uma única passada e comecei a rodar um loop Generator-Evaluator inspirado em GAN. 13,7 pontos de accuracy, no mesmo conjunto de perguntas, no mesmo modelo base, só mudando a arquitetura ao redor do LLM.

A parte incômoda para quem acredita em “o modelo grande resolve”: nenhuma troca de modelo entrega esses 13,7 pontos de graça no meu benchmark. Trocar Haiku por Sonnet num RAG single-shot subiu 2-3 pontos. Colocar o mesmo Haiku dentro de um loop Generator-Evaluator subiu 13,7. Arquitetura ganhou de tamanho de modelo.

Este post é a autópsia do pipeline exato: os 4 componentes, o que eu quebrei antes de fazer funcionar, e por que a maioria dos tutoriais de “self-refine RAG” que li não chegam nesses números.

O pipeline de 4 componentes

A ideia é roubada de GANs: um Generator produz a resposta, um Evaluator critica, e o Generator refaz baseado na crítica. Um Critique loop decide quantas voltas dar, e um Stop condition impede que o loop rode até o fim do universo (nem seu bolso quer isso).

[Query] → Generator (draft answer) → Evaluator (score + critique)
              ↑                              ↓
              └── Critique loop ← Stop condition (score ≥ 0.85 ou iter=3)

Generator-Evaluator loop com os 4 componentes e o delta de accuracy

Generator — LLM comum com o contexto recuperado do índice. Nada especial. No meu setup, Haiku 4.5 com top-k=8 de embedding search. Ele produz o draft_1.

Evaluator — Segundo LLM (mesmo modelo, prompt diferente) que recebe query + draft + contexto e devolve um JSON com dois campos: score (0-1) e critique (o que está faltando ou errado). O Evaluator não sabe qual foi o draft anterior, então não fica leal ao Generator. Isso é o “adversarial” da coisa — ele tenta reprovar.

Critique loop — Se score < 0.85, envia (query, draft_1, critique) de volta pro Generator e pede um draft_2 que endereça as críticas. Repete.

Stop conditionscore ≥ 0.85 (aprovou) ou iteration ≥ 3 (parou por budget). O corte em 3 iterações é o que me impediu de queimar 40 vezes o custo original por uma pergunta difícil.

Isso é essencialmente o padrão que o ATM (arXiv 2405.18111) formalizou como “adversarial tuning multi-agent”, e que a survey de Agentic RAG (arXiv 2501.09136) lista como o padrão “evaluator-optimizer”. Mas você não precisa dos papers para rodar isso, precisa das 4 caixas acima e de disciplina em duas delas: o prompt do Evaluator e o Stop condition.

O experimento e o número

O benchmark: 200 perguntas técnicas sobre um repositório de documentação minha (mistura de Zenn book + notas internas). Ground truth escrita à mão, comparação feita por um LLM juiz terceiro (não o Generator nem o Evaluator) com uma rubrica de 5 pontos.

Resultados médios em 3 rodadas:

SetupAccuracyCusto relativo
RAG single-shot (Haiku 4.5, top-k=8)52,8%1,0x
RAG single-shot (Sonnet 4.6, top-k=8)55,6%3,2x
Generator-Evaluator loop (Haiku 4.5, max 3 iter)66,5%2,1x
Generator-Evaluator loop (Sonnet 4.6, max 3 iter)71,3%6,8x

O que me interessou não foi o topo. Foi a linha do meio: o mesmo Haiku, dentro do loop, passou o Sonnet single-shot em 10,9 pontos por 2/3 do custo do Sonnet. Se você paga por token e tem um budget mensal para RAG, essa é a manchete. Modelo pequeno com arquitetura vence modelo grande sem arquitetura — resultado que já apareceu em outros contextos (grep contra RAG foi uma variante disso, só que ali o “modelo pequeno” era literalmente grep).

Por que a maioria dos tutoriais não chega perto

Li uns 6 tutoriais de “self-refine RAG” antes de montar o meu. Nenhum me deu esses 13,7 pontos. Três coisas explicam a diferença.

1. Evaluator fraco. O erro clássico é usar o mesmo prompt do Generator e só pedir “revise sua resposta”. O modelo tende a concordar consigo mesmo. No meu setup, o prompt do Evaluator começa com “Você é um revisor cético. Assuma que o draft está errado até prova em contrário.” Essa única frase mudou a taxa de aprovação em iter=1 de 78% (falso positivo alto) para 34% (mais críticas úteis, menos loops desnecessários).

2. Stop condition ausente. Vários tutoriais rodam “até convergir”. Convergir para o quê? Se o score flutua entre 0.82 e 0.87, você fica preso em 2 iterations trocando pequenas frases. O corte em iter=3 OU score ≥ 0.85 (o que vier primeiro) foi o que fez o custo ficar previsível.

3. Contexto empilhado no loop. Tem gente que passa [draft_1, critique_1, draft_2, critique_2, ...] acumulado pro Generator a cada iteração. Isso explode o token count e piora a resposta (o LLM começa a “defender” drafts antigos em vez de reescrever). O que funcionou no meu caso: passar só o draft_anterior + critique_anterior, sem histórico. Cada iteration é um refactor limpo.

O que eu quebrei antes de funcionar

Coisas que perdi tempo com e que talvez economizem o seu.

Achei que Evaluator melhor exigia modelo maior. Testei Sonnet como Evaluator e Haiku como Generator. Ganho de accuracy: 1,2 pontos. Custo: 3x. Não vale. O que importa no Evaluator é o prompt cético, não o tamanho.

Tentei paralelizar 3 Generators e agregar. A ideia era pegar 3 drafts e deixar o Evaluator escolher o melhor. Ganho: 0,4 pontos. Custo: 3x no lado Generator. O Evaluator com 1 draft e loop bate 3 drafts sem loop.

Deixei o loop ir até 5 iterations “por segurança”. Nas 200 perguntas do meu benchmark, ninguém precisou de mais do que 3. Iter 4-5 só gastou tokens em resposta que já era boa. Cortei para 3 e a accuracy caiu 0,1 ponto. Não vale a diferença.

Tentei um único modelo fazendo Generator+Evaluator na mesma chamada (via structured output com dois campos). Não funciona. O modelo escreve um draft e imediatamente aprova. A separação em duas chamadas com prompts diferentes é o que quebra a “leniência com si mesmo”.

Quando NÃO usar Generator-Evaluator

Nem toda pergunta merece o loop. Se sua base é 90% “pergunta simples que o top-k=3 resolve” (perguntas factuais diretas), o loop vira overhead. No meu benchmark, 62% das perguntas foram aprovadas em iter=1 (score ≥ 0.85 no primeiro draft). Só 38% precisaram de iterations. Para essas 62%, o custo extra do Evaluator foi de ~40% sobre single-shot.

O que vale a pena: separar o traffic. Um classificador leve (regex ou embedding-based) roteia perguntas “fáceis” para RAG single-shot e “difíceis” para o loop. No meu setup, isso derrubou o custo médio para 1,4x do single-shot (contra 2,1x se todas as perguntas passassem pelo loop) sem perder accuracy.

O padrão maior: harness ao redor do modelo

Se você olha o pipeline de longe, ele não é sobre RAG. É sobre montar um harness (a camada que decide como o modelo é chamado, com o quê, e quando parar) mais rico do que “1 prompt = 1 resposta”. Generator-Evaluator é um harness. Roteador de traffic é outro. Stop condition é o terceiro. Cada um vale alguns pontos de accuracy que nenhum upgrade de modelo compra.

Leitor do TabNews: mostra esses números para seus colegas. A frase “modelo grande resolve” ainda é o default nas discussões e ela deixa performance na mesa.


ken imoto · WebRTC & Voice AI engineer · kenimoto.dev · TabNews

Harness Engineering Livro relacionado Harness Engineering Tutorial de Harness Engineering | design de AGENTS.md · implementação de hooks · operação de agentes de IA Ver a página do livro →