Preparação para entrevista
PadrãoComposição

Avaliador e Otimizador

Evaluator-Optimizer

Avaliador e Otimizador (Evaluator-Optimizer) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Composição. Um modelo gera, outro avalia com critério claro, e o ciclo repete até passar.

Elemento
06/70
Nível
Intermediário
Leitura
9 min
Atualizado
Nesta página 18
  1. 01Intenção
  2. 02Problema
  3. 03Solução
  4. 04Estrutura
  5. 05Pseudocódigo
  6. 06Cenários reais
  7. 07Aplicabilidade
  8. 08Quando não usar
  9. 09Como implementar
  10. 10Variações
  11. 11Custo, latência e falhas
  12. 12Como avaliar
  13. 13Em produção
  14. 14Segurança
  15. 15Prós e contras
  16. 16Código
  17. 17Suporte por framework
  18. 18Teste seu entendimento
  19. ··Perguntas rápidas

Intenção

Um modelo gera, outro avalia contra critérios claros, e o laço se repete até passar ou até um limite. Serve para tarefas em que existe um critério de qualidade bem definido e uma primeira versão raramente basta.

Em uma frase
Um editor que devolve o texto ao autor com a lista do que falta, até o texto estar pronto para ir para a gráfica.

Problema

Gustavo cuida de um gerador de testes unitários. O modelo recebe uma função e escreve testes. Uma parte dos testes não compila, outra parte compila mas não testa nada de útil, e alguns testam o comportamento errado.

Pedir “escreva testes bons” no prompt não mudou nada. Acrescentar uma lista de regras ajudou um pouco. O que mais ajudou foi rodar os testes e mostrar o erro ao modelo: com a mensagem do compilador na frente, ele corrigia quase sempre na segunda tentativa.

O mesmo vale para outras tarefas: uma consulta SQL que não retorna o esperado, um texto que fura o limite de caracteres, um post que não segue o guia da marca. A primeira versão é razoável; a segunda, com uma crítica específica, é boa.

A pergunta que este padrão responde: como transformar “tente de novo” num laço controlado, com critério claro e fim garantido?

Solução

Separe duas funções:

  • Gerador: produz uma versão da saída. Nas rodadas seguintes, recebe também a avaliação anterior.
  • Avaliador: compara a saída com critérios escritos e devolve uma nota e uma lista de problemas específicos. Pode ser código (compilar, rodar testes, validar schema), um modelo com rubrica, ou os dois.

O laço termina quando a avaliação passa, quando o número máximo de rodadas acaba, ou quando a nota para de melhorar. O avaliador precisa ser mais confiável que o gerador no critério que julga; senão, o laço só gira.

O post “Building effective agents” da Anthropic (dezembro de 2024) descreve este padrão com um gerador e um avaliador em ciclo, e avisa: ele vale quando há critérios claros. Sem isso, o laço vira o smell Autocrítica Inútil.

Estrutura

flowchart TD
  IN(["1. Tarefa e critérios"]):::io --> G(["2. Gerador"]):::llm
  G --> E{"3. Avaliador"}
  E -->|"passou"| OUT(["5. Saída aprovada"]):::io
  E -->|"falhou, com problemas"| B{"4. Ainda há rodadas?"}
  B -->|sim| G
  B -->|não| BEST(["6. Melhor versão com o relatório"]):::io
  1. A tarefa chega com critérios escritos, de preferência verificáveis.
  2. O gerador produz uma versão; a partir da segunda rodada, com a avaliação anterior.
  3. O avaliador checa os critérios: código primeiro, modelo com rubrica quando necessário.
  4. O código decide se tenta de novo, com teto de rodadas e detecção de estagnação.
  5. Se passou, a saída sai.
  6. Se acabaram as rodadas, sai a melhor versão até ali, marcada com o que ainda falha.

Exemplo trabalhado

Gustavo pede testes para apply_discount(price, pct). A primeira versão do gerador importa um módulo que não existe. O avaliador em código é o pytest:

ImportError: cannot import name 'moneyfmt' from 'utils'

Essa mensagem volta para o gerador. A segunda versão compila, os testes passam, e todos os asserts são assert True. O pytest está verde. O critério “compila e passa” é frouxo.

Aí entra o segundo critério, ainda em código: mutação. Trocar pct por 0 no código original deveria quebrar pelo menos um teste. Nenhum quebra. O avaliador devolve “mata 0% dos mutantes”. A terceira versão testa o desconto real.

Repare a ordem. Compilação primeiro (barata, infalível no critério dela). Testes depois. Mutação por último. O modelo com rubrica, se existir, só vê o que já passou nisso. Pedir ao mesmo modelo “os testes estão bons?” na primeira versão teria aprovado o assert True com um parágrafo elogioso.

Checagem +20 XP de primeiraO gerador e o avaliador são o mesmo modelo, com o mesmo prompt de critérios, sem rodar os testes. Depois de três rodadas o texto mudou pouco. Qual é o diagnóstico?

Pseudocódigo

schema Evaluation { passed: bool, score: float, problems: list[string] }

function generate_with_feedback(task, criteria, max_rounds=3):
    best, history = null, []
    for round in range(max_rounds):
        draft = llm(GENERATOR_PROMPT, task, history)                 # 2
        # 3. checagens em código antes do modelo
        code_problems = run_checks(draft)                            # compila, testes, schema
        if code_problems:
            ev = Evaluation(passed=false, score=0, problems=code_problems)
        else:
            ev = llm(EVALUATOR_PROMPT, criteria, draft) -> Evaluation
        trace.span("round", round, ev)

        if best is null or ev.score > best.score:
            best = { draft, score: ev.score, problems: ev.problems }
        if ev.passed:
            return draft                                              # 5
        if stagnated(history, ev):
            break                                                     # 4
        history.append({ draft, problems: ev.problems })
    return best                                                       # 6

Cenários reais

  • Gerador de testes. O avaliador é o compilador e o test runner. O laço termina quando os testes compilam, passam e matam mutantes do código original.
  • Perguntas em linguagem natural para SQL. O avaliador roda a consulta num banco de teste e confere erros de sintaxe, colunas inexistentes e resultado vazio.
  • Pipeline de criação de conteúdo. Um avaliador com a rubrica do guia da marca devolve problemas específicos; o redator corrige até passar.

Aplicabilidade

  • Existe um critério de qualidade escrito e, de preferência, verificável por código.
  • A primeira versão costuma falhar de formas que uma crítica específica corrige.
  • O custo de mais rodadas é menor que o custo de uma saída ruim.
  • A latência extra é aceitável.

Quando não usar

  • A primeira versão já passa na maioria dos casos. O avaliador vira custo sem ganho; use só uma checagem em código.
  • Não há critério claro. Um avaliador vago (“está bom?”) tende a aprovar ou a pedir mudanças aleatórias. Veja Autocrítica Inútil.
  • O avaliador é o mesmo modelo, com o mesmo contexto, sem informação nova. Isso costuma só repetir a primeira opinião. Veja Reflexão para quando a crítica vem de um sinal externo.
  • A tarefa precisa de resposta imediata.

Erro comum: guardar a última versão

A rodada 2 tira o ImportError. A rodada 3, tentando matar mutantes, quebra a compilação de novo. Se você devolve a última, o usuário recebe código que nem importa. A melhor versão era a 2, com o relatório do que ainda falha.

O teto também precisa de um motivo de parada visível: passou, estagnou (mesmo erro duas vezes), ou esgotou rodadas. Sem isso, o trace não diz por que a tarefa saiu.

Checagem +20 XP de primeiraA nota do avaliador foi 6, depois 8, depois 5. O teto é 3. Qual versão você entrega?

Como implementar

  1. Escreva os critérios como lista verificável. Separe os que dá para checar em código.
  2. Implemente as checagens em código primeiro: compilação, testes, schema, regras.
  3. Para o que sobrar, escreva uma rubrica para o avaliador, com exemplos de nota alta e baixa.
  4. Faça o avaliador devolver problemas específicos e acionáveis, não só uma nota.
  5. Passe ao gerador a versão anterior e os problemas, não o histórico inteiro de rodadas.
  6. Ponha teto de rodadas e detecção de estagnação.
  7. Guarde a melhor versão, não a última.
  8. Avalie o avaliador: concordância com uma pessoa nos casos em que ele usa modelo.

Na segunda rodada, o gerador não precisa do rascunho 1 e do rascunho 2 e da rubrica inteira de novo. Precisa do rascunho atual e da lista curta de problemas. Histórico longo empurra o modelo para repetir defesas (“eu já tinha testado isso”) em vez de corrigir.

Se o avaliador é código (pytest, compilador, schema), a concordância é trivial: ele não “opina”. Se é modelo, monte um dataset em que uma pessoa já marcou passou/falhou e meça quanto o juiz acerta. Um avaliador que discorda da pessoa em 30% dos casos vai treinar o gerador na direção errada.

Variações

  • Avaliador só em código. Compilador, testes, validadores. O mais barato e confiável.
  • Avaliador com rubrica. Um modelo com critérios escritos. Veja LLM como Juiz.
  • Avaliador híbrido. Código primeiro; o modelo só avalia o que passou nas checagens objetivas.
  • Várias candidatas por rodada. O gerador produz N versões, o avaliador escolhe a melhor. Mais caro, menos rodadas.

O híbrido é o padrão que menos se arrepende. Código primeiro: se não compilou, o modelo-juiz nem roda. Isso corta custo e impede que um parágrafo eloquente aprove um teste que não importa.

Várias candidatas por rodada fazem sentido quando a primeira versão varia muito (dois tons de post, duas formas de SQL) e o avaliador em código consegue ranquear barato. Gerar 5 e julgar com rubrica cara 5 vezes costuma perder para 2 rodadas híbridas.

O avaliador só em código não cobre tom de marca. Aí o híbrido ganha de novo: pytest (ou o limite de caracteres) primeiro, rubrica depois, nos que passaram. Inverter a ordem é o desperdício clássico: o juiz de estilo aprova um teste que nem importa, e só então o compilador fala não.

Guarde também por que o laço parou. “Passou”, “estagnou” e “teto” são três desfechos diferentes no painel. Sem o motivo, um pico de custo parece “o gerador piorou”, quando na verdade o avaliador ficou implicante.

Custo, latência e falhas

Aspecto O que acontece Como aparece no trace
Chamadas Até 2 por rodada (gerador e avaliador), vezes o número de rodadas spans round por tarefa
Custo Proporcional às rodadas; a maioria das tarefas deve passar em 1 ou 2 média de rodadas por tarefa
Latência Soma das rodadas duração total muito acima de uma chamada
Falha: avaliador frouxo Aprova saídas ruins casos aprovados que falham no eval
Falha: avaliador implicante Sempre acha algo e nunca aprova tarefas que sempre batem no teto
Falha: oscilação O gerador corrige um problema e cria outro nota sobe e desce entre rodadas

Como avaliar

  • Taxa de aprovação na primeira rodada e depois do laço. A diferença é o ganho do padrão.
  • Rodadas médias e custo por tarefa.
  • Qualidade do avaliador: concordância com uma pessoa e taxa de falso positivo e falso negativo.

Falso positivo, aqui, é o avaliador reprovar um texto que a pessoa aceitaria. Falso negativo é aprovar o que a pessoa reprovaria. Em conteúdo, falso negativo é o perigo: o laço entrega um post que fura a marca e ninguém vê. Em testes, falso positivo custa rodadas; falso negativo entrega assert True.

Meça as duas taxas num conjunto pequeno rotulado à mão antes de ligar o laço em produção. Se o juiz com modelo erra 1 em 4, ele não está mais confiável que o gerador.

{ "function": "fixtures/apply_discount.py", "criteria": ["compila", "todos os testes passam", "mata pelo menos 80% dos mutantes"], "max_rounds": 3 }

Em produção

  • Span por rodada com a nota e os problemas.
  • Painel com a distribuição de rodadas por tarefa. Muitas tarefas no teto indicam gerador fraco ou avaliador rígido.
  • Alerta quando o custo médio por tarefa sobe, sinal de que o laço está rodando mais.
  • Amostra das saídas aprovadas pelo avaliador com modelo, para revisão humana.

Segurança

O laço reenvia a saída anterior ao gerador. Se a tarefa inclui texto de fora (um PDF, um e-mail), esse texto pode tentar instruir o gerador na segunda rodada com mais força. Trate o rascunho como dado, não como ordem, e não deixe o gerador chamar ferramentas de efeito no meio do laço. Efeito (merge, e-mail, pagamento) fica depois da aprovação, com Validar Antes de Executar.

Um avaliador frouxo que aprova SQL gerado a partir de pergunta de usuário é o mesmo risco de sempre: saída do modelo indo para um motor. O avaliador precisa checar a consulta no banco de teste, não só “parecer SQL”.

Prós e contras

Prós Contras
Qualidade sobe com critério explícito Custo e latência multiplicados pelas rodadas
Problemas específicos guiam a correção O avaliador também precisa ser avaliado
Checagens em código dão garantias reais Pode oscilar sem convergir
A melhor versão sempre fica disponível Critério vago transforma o laço em desperdício

Código

As abas geram, avaliam com código e repetem com os erros até passar ou até o teto. Python e TypeScript escrevem as rodadas num laço. LangGraph separa gerador e avaliador em nós: voltar ou parar é aresta, não pedido do modelo.

Python. Use quando o critério principal é verificável por código, como compilar e rodar testes.

import subprocess
from pathlib import Path

import anthropic

client = anthropic.Anthropic()
MAX_ROUNDS = 3


def evaluate(test_code: str, workdir: Path) -> tuple[bool, str]:  # 3. avaliador em código
    path = workdir / "test_generated.py"
    path.write_text(test_code, encoding="utf-8")
    out = subprocess.run(["pytest", str(path), "-q", "-x"], capture_output=True, text=True, timeout=120)
    return out.returncode == 0, (out.stdout + out.stderr)[-4000:]


def generate_tests(source: str, workdir: Path) -> dict:
    feedback, best = "", None
    for round_ in range(MAX_ROUNDS):
        prompt = f"Escreva testes pytest para esta função.\n\n{source}"
        if feedback:
            prompt += f"\n\nSua versão anterior falhou. Saída do pytest:\n{feedback}\nCorrija."
        reply = client.messages.create(  # 2. gerador
            model="claude-sonnet-5-5", max_tokens=8000,
            system="Responda só com código Python, sem markdown.",
            messages=[{"role": "user", "content": prompt}],
        )
        code = reply.content[-1].text
        passed, output = evaluate(code, workdir)
        best = {"code": code, "passed": passed, "report": output, "rounds": round_ + 1}
        if passed:
            return best  # 5
        if feedback and output == feedback:
            break  # 4. estagnou: mesmo erro duas vezes
        feedback = output
    return best  # 6. melhor versão com o relatório do que falha

O que você ganhou: o avaliador é o próprio pytest, que não erra sobre se o teste passa. O que você perdeu: “passar” não garante testes úteis; acrescente teste de mutação como segundo critério.

TypeScript. Use quando o critério precisa de uma rubrica além das checagens em código.

import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";

const client = new Anthropic();

const Evaluation = z.object({
  passed: z.boolean(),
  score: z.number().min(0).max(10),
  problems: z.array(z.string()),
});

const text = (m: Anthropic.Message) => m.content.find((b) => b.type === "text")?.text ?? "";

export async function writePost(brief: string, rubric: string, maxRounds = 3) {
  let best = { draft: "", score: -1, problems: [] as string[] };
  let feedback: string[] = [];
  for (let round = 0; round < maxRounds; round++) {
    const draft = text(await client.messages.create({ // 2
      model: "claude-sonnet-5-5", max_tokens: 4000,
      messages: [{ role: "user", content: `Escreva o post.\n${brief}\n${feedback.length ? "Corrija:\n- " + feedback.join("\n- ") : ""}` }],
    }));

    const codeProblems = draft.length > 2200 ? ["Passou de 2.200 caracteres."] : []; // 3. código primeiro
    const { parsed_output: ev } = codeProblems.length
      ? { parsed_output: { passed: false, score: 0, problems: codeProblems } }
      : await client.messages.parse({
          model: "claude-opus-5-5", max_tokens: 2000,
          system: `Avalie o texto contra esta rubrica e liste problemas específicos.\n${rubric}`,
          messages: [{ role: "user", content: draft }],
          output_config: { format: zodOutputFormat(Evaluation) },
        });
    if (!ev) break;
    if (ev.score > best.score) best = { draft, score: ev.score, problems: ev.problems };
    if (ev.passed) return { ...best, passed: true }; // 5
    feedback = ev.problems;
  }
  return { ...best, passed: false }; // 6
}

O que você ganhou: checagens baratas em código antes da rubrica, e a melhor versão guardada. O que você perdeu: o avaliador com modelo pode errar; meça a concordância dele com uma pessoa.

Python + LangGraph. Use quando gerador e avaliador são nós, e voltar ou parar é uma aresta.

from typing import Literal

from langchain.chat_models import init_chat_model
from langgraph.graph import END, START, StateGraph
from typing_extensions import TypedDict

class State(TypedDict):
    task: str
    code: str
    errors: str
    best: str
    rounds: int

writer = init_chat_model("anthropic:claude-opus-5-5", temperature=0.2)

def generate(state: State) -> dict:
    # 2. gerador, com a avaliação anterior a partir da segunda rodada
    reply = writer.invoke([("human", f"{state['task']}\n\nErros:\n{state['errors']}\n\nVersão:\n{state['code']}")])
    return {"code": reply.text, "rounds": state["rounds"] + 1}

def evaluate(state: State) -> dict:
    ok, log = pytest_run(state["code"])  # 3. código primeiro
    return {"errors": "" if ok else log, "best": state["code"] if ok or not state["best"] else state["best"]}

def route(state: State) -> Literal["generate", "done"]:
    # 4. teto e estagnação no código
    if not state["errors"] or state["rounds"] >= 4:
        return "done"
    return "generate"

graph = StateGraph(State)
graph.add_node("generate", generate)
graph.add_node("evaluate", evaluate)
graph.add_edge(START, "generate")
graph.add_edge("generate", "evaluate")
graph.add_conditional_edges("evaluate", route, {"generate": "generate", "done": END})
app = graph.compile()

pytest_run é sua. As outras formas de escrever o loop do gerador, se um dia ele virar agente, estão em Loop ReAct.

O que você ganhou: o modelo não escolhe se houve rodada seguinte. O que você perdeu: a rubrica com segundo modelo, se você precisar dela, é outro nó depois do pytest.

Suporte por framework

Stack Quem decide a próxima rodada
Python, TypeScript O for
Python + LangGraph A aresta depois do pytest

Teste seu entendimento

Checagem +20 XP de primeiraO gerador de testes passa em 92% dos casos na primeira rodada. O laço de 3 rodadas sobe isso para 94% e triplica o custo. O que você faz?

Checagem +20 XP de primeiraO avaliador de posts da marca devolve sempre 'adicione mais urgência' e nunca marca passed. O gerador oscila o tom a cada rodada. Qual é o conserto?

Checagem +20 XP de primeiraEm text-to-SQL, o avaliador só checa se a consulta 'parece SQL válido' com um modelo. A consulta roda e apaga uma tabela de teste. O que faltou?

Checagem +20 XP de primeiraO trace mostra 3 rodadas, a nota 7, 7, 7, e o motivo de parada é 'teto'. O que isso pede primeiro?

Perguntas rápidas

O que é Avaliador e Otimizador?

Avaliador e Otimizador (Evaluator-Optimizer) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Composição. Um modelo gera, outro avalia com critério claro, e o ciclo repete até passar.

Onde Avaliador e Otimizador aparece na prática?

Nos cenários reais Pipeline de criação de conteúdo, Gerador de testes e Perguntas em linguagem natural para SQL.

Para ir além

Relações

Buscar