Query Advisor
Un modelo de lenguaje recibe una consulta lenta y el encargo de acelerarla. Después la base de datos decide si lo consiguió.
- .NET 10
- Microsoft.Extensions.AI
- MCP
- PostgreSQL
- Evals
El modelo propone, la base de datos dispone
Cinco preguntas, cada una respondida por una sentencia correcta y lenta. El modelo recibe la pregunta, la sentencia lenta y tres herramientas: el esquema, el planificador y un ejecutor de solo lectura. Propone una reescritura y explica su razonamiento.
Entonces se ejecuta la parte que no es un modelo de lenguaje. Ambas sentencias
corren contra 1.000.000 de pedidos y 1.500.001 líneas de pedido. Cada celda de
ambos conjuntos de resultados se resume en un hash y se compara. Los dos planes
se capturan con EXPLAIN (ANALYZE, BUFFERS). El veredicto sale de esa
comparación y de nada más.
Aquí nada le pide a un segundo modelo que califique al primero.
Por qué esto importa más de lo que parece
Nueve de cada diez equipos con agentes en producción tienen observabilidad sobre ellos. Alrededor de la mitad tiene evaluaciones. La brecha no es pereza: la salida de un agente suele ser prosa, y el único juez disponible para la prosa es otro modelo, de modo que la medición hereda precisamente aquello que debía comprobar.
SQL no tiene ese problema. Dos sentencias o devuelven las mismas filas o no las devuelven, y una de ellas o es más rápida o no lo es. Esta demo toma la única tarea de agente en la que existe un oráculo determinista y construye la evaluación que nadie puede discutir.
Hay cuatro veredictos, y tres de ellos son fallos por causas distintas. Confirmado es mismas filas, mediblemente más rápido. Filas erróneas es una respuesta equivocada. Sin mejora es una reescritura honesta que no compró nada. Inválido nunca llegó a ejecutarse. Reducirlos a un booleano tiraría la mitad más interesante de lo medido: una respuesta rápida y errónea y una correcta e inútil no son el mismo hallazgo.
Lo que se midió
| Haiku 4.5 | Opus 5 | |
|---|---|---|
| Ingresos por estado en un mes | confirmado, 12,3× | confirmado, 8,1× |
| Las líneas de pedido de un cliente | sin mejora, 1,0× | confirmado, 62,2× |
| Los diez mayores clientes | sin mejora, 0,6× | sin mejora, 0,8× |
| El valor de veinte pedidos | confirmado, 8,1× | confirmado, 2.982× |
| Página 25.001 del listado de pedidos | confirmado, 6.458× | confirmado, 3.893× |
| 3 de 5 · $0,17 | 4 de 5 · $0,62 |
La última pregunta se escribió para ser la difícil: una reescritura por keyset necesita un cursor, y el cursor de esa página es una fila que el modelo tiene que ir a buscar. Los dos la encontraron. La difícil resultó ser la tercera, que es la clase de cosa que solo se aprende ejecutando el conjunto en lugar de razonar sobre él.
La pregunta que ambos fallaron
Diez clientes, ordenados por número de pedidos, a partir de un count(*)
correlacionado evaluado una vez por fila candidata. La corrección de manual es
sustituir la subconsulta correlacionada por una agregación con GROUP BY, y eso
es exactamente lo que hicieron los dos.
Los dos se volvieron más lentos. 213ms pasaron a 329ms.
La consulta original solo contaba los clientes que cumplían el filtro, usando el
índice sobre customer_id. La reescritura agrupó primero la tabla entera de un
millón de pedidos y filtró después: estrictamente más trabajo, alcanzado al
aplicar una regla que casi siempre acierta.
Existe una reescritura que gana: filtrar los clientes antes de agregar, y corre en 146ms. Ninguno de los dos modelos dio con ella. Y ninguna lectura de sus explicaciones lo habría revelado, porque ambas explicaciones son correctas sobre por qué la original era lenta. Solo ejecutando ambas sentencias se sabe cuál es más rápida.
Esa pregunta se queda en el conjunto exactamente como está. Un golden set cuya respuesta es siempre la que todo el mundo ya sabe mide memoria, no criterio.
El fallo que importó más que las notas
La primera ejecución informó de que ambos modelos habían respondido mal a una de las preguntas. No lo habían hecho. La consulta de referencia de esa pregunta superó el presupuesto de cinco segundos del rol de lectura, así que nunca llegó a ejecutarse — y la comparación, que comprobaba la propuesta y nunca la referencia, enfrentó un hash real contra uno vacío y culpó al modelo.
Un arnés que culpa al modelo de su propio fallo es peor que no tener arnés, porque produce números con aspecto de hallazgo. La corrección es un veredicto propio — la referencia falló — comprobado antes de exigirle nada a la respuesta, y una pregunta reescrita para que su versión lenta siga siendo ruinosa y aun así termine.
Hizo falta ejecutar una consulta a mano contra la base de datos para darse cuenta. Ese es el argumento para ejecutar una evaluación contra datos reales antes de creerse lo que imprime, y es la razón por la que las notas de arriba merecen leerse: sobrevivieron a que el arnés se equivocara una vez.
Dos roles que no pueden hacer el trabajo del otro
Las herramientas del modelo corren bajo un rol con SELECT sobre cinco tablas,
de solo lectura a nivel de transacción y con un presupuesto de cinco segundos
por sentencia. El medidor de gasto corre bajo otro rol, que puede añadir al
libro de gasto y no ve los datos en absoluto.
Un modelo que alcanza la tabla que registra lo que cuesta no está siendo medido
por ella. CI comprueba los cuatro privilegios en lugar de fiarse del fichero que
los concedió: basta un GRANT para que este párrafo se vuelva ficción.
El presupuesto, y qué pasa cuando se acaba
Este es un endpoint público que gasta dinero cuando alguien hace clic. El rate limiting limita la velocidad del gasto, no su cantidad, así que hay un techo duro por día.
Alcanzado el techo, el servicio se niega y lo dice. No sirve una respuesta guardada, por la misma razón por la que no lo hace el laboratorio de SQL: en el momento en que un visitante no puede distinguir una medición de una grabación, todos los números de la página dejan de valer nada.
p95 en vivo -uptime -