Mihael Costa/índice
← índice
AVALIAÇÃO DE AGENTE

Query Advisor

Um modelo de linguagem recebe uma consulta lenta e a tarefa de acelerá-la. Depois o banco decide se ele conseguiu.

  • .NET 10
  • Microsoft.Extensions.AI
  • MCP
  • PostgreSQL
  • Evals

O modelo propõe, o banco dispõe

Cinco perguntas, cada uma respondida por uma instrução correta e lenta. O modelo recebe a pergunta, a instrução lenta e três ferramentas: o schema, o planejador e um executor somente-leitura. Ele propõe uma reescrita e explica o raciocínio.

Aí roda a parte que não é um modelo de linguagem. As duas instruções executam contra 1.000.000 de pedidos e 1.500.001 linhas de pedido. Cada célula dos dois conjuntos de resultado é hasheada e comparada. Os dois planos são capturados com EXPLAIN (ANALYZE, BUFFERS). O veredicto sai dessa comparação e de mais nada.

Nada aqui pede para um segundo modelo dar nota ao primeiro.

Por que isso importa mais do que parece

Nove em cada dez times com agentes em produção têm observabilidade sobre eles. Cerca de metade tem avaliação. A lacuna não é preguiça — é que a saída de um agente costuma ser prosa, e o único juiz disponível para prosa é outro modelo, de forma que a medição herda justamente aquilo que deveria verificar.

SQL não tem esse problema. Duas instruções ou devolvem as mesmas linhas ou não devolvem, e uma delas ou é mais rápida ou não é. Este demo pega a única tarefa de agente em que existe um oráculo determinístico e constrói a avaliação que ninguém tem como contestar.

São quatro veredictos, e três deles são falhas por causas diferentes. Confirmado é mesmas linhas, comprovadamente mais rápido. Linhas erradas é resposta errada. Sem ganho é uma reescrita honesta que não comprou nada. Inválido nunca chegou a rodar. Reduzir isso a um booleano jogaria fora a metade mais interessante do que foi medido — uma resposta errada e rápida e uma correta e inútil não são o mesmo achado.

O que foi medido

Haiku 4.5 Opus 5
Receita por status em um mês confirmado, 12,3× confirmado, 8,1×
As linhas de pedido de um cliente sem ganho, 1,0× confirmado, 62,2×
Os dez maiores clientes sem ganho, 0,6× sem ganho, 0,8×
O valor de vinte pedidos confirmado, 8,1× confirmado, 2.982×
Página 25.001 da lista de pedidos confirmado, 6.458× confirmado, 3.893×
3 de 5 · $0,17 4 de 5 · $0,62

A última pergunta foi escrita para ser a difícil: uma reescrita por keyset precisa de um cursor, e o cursor daquela página é uma linha que o modelo tem que ir procurar. Os dois acharam. A difícil acabou sendo a terceira — o tipo de coisa que só se descobre rodando o conjunto, e não raciocinando sobre ele.

A pergunta que os dois erraram

Dez clientes, ordenados por quantidade de pedidos, a partir de um count(*) correlacionado avaliado uma vez por linha candidata. A correção de manual é trocar a subconsulta correlacionada por uma agregação com GROUP BY, e foi exatamente isso que os dois fizeram.

Os dois ficaram mais lentos. 213ms viraram 329ms.

O baseline só contava os clientes que casavam com o filtro, usando o índice em customer_id. A reescrita agrupou a tabela inteira de um milhão de pedidos primeiro e filtrou depois — estritamente mais trabalho, obtido ao aplicar uma regra que quase sempre está certa.

Existe uma reescrita que ganha: filtrar os clientes antes de agregar, e ela roda em 146ms. Nenhum dos dois modelos a encontrou. E nenhuma leitura das explicações deles teria revelado isso, porque as duas explicações estão corretas sobre por que o original era lento. Só executando as duas instruções é que se sabe qual é mais rápida.

Essa pergunta fica no conjunto exatamente como está. Um golden set cuja resposta é sempre a que todo mundo já sabe mede memória, não julgamento.

O bug que importou mais do que as notas

A primeira execução reportou que os dois modelos responderam errado a uma das perguntas. Não responderam. O baseline daquela pergunta estourou o orçamento de cinco segundos do papel de leitura, então nunca rodou — e a comparação, que checava a proposta e nunca o baseline, confrontou um hash real com um vazio e culpou o modelo.

Um harness que culpa o modelo pela própria falha é pior do que harness nenhum, porque produz números com cara de achado. A correção é um veredicto próprio — o baseline falhou — verificado antes de qualquer cobrança à resposta, e uma pergunta reescrita para que a versão lenta continue ruinosa e ainda assim termine.

Foi preciso rodar uma consulta na mão contra o banco para perceber. Esse é o argumento para rodar uma avaliação contra dados reais antes de acreditar no que ela imprime, e é por isso que as notas acima valem leitura: elas sobreviveram ao harness ter errado uma vez.

Dois papéis que não fazem o trabalho um do outro

As ferramentas do modelo rodam sob um papel com SELECT em cinco tabelas, somente-leitura no nível da transação e com orçamento de cinco segundos por instrução. O medidor de orçamento roda sob outro papel, que pode inserir no ledger de gasto e não enxerga os dados de jeito nenhum.

Um modelo que alcança a tabela que registra o próprio custo não está sendo medido por ela. O CI assere os quatro privilégios em vez de confiar no arquivo que os concedeu — basta um GRANT para este parágrafo virar ficção.

O orçamento, e o que acontece quando acaba

Este é um endpoint público que gasta dinheiro quando alguém clica. Rate limiting limita a velocidade do gasto, não a quantidade dele, então existe um teto rígido por dia.

Alcançado o teto, o serviço recusa e diz isso. Ele não serve uma resposta guardada, pelo mesmo motivo que o laboratório de SQL não serve: no instante em que um visitante não consegue distinguir uma medição de uma gravação, todo número da página deixa de valer alguma coisa.

p95 ao vivo -uptime -