Cómo probar una función de texto a SQL
Un conjunto de evaluación para texto a SQL es una lista de preguntas con respuestas conocidas. Se vuelve a ejecutar cada vez que cambia el prompt o el modelo. Te dice si un cambio ha mejorado o estropeado algo.
Los módulos 68 y 85 puntúan respuestas en palabras. Esta lección puntúa SQL por las filas que devuelve.
Al final de esta lección
Nota
Puedo puntuar consultas candidatas con un conjunto de preguntas de respuesta conocida.
Palabras clave
| Palabra | Qué significa |
|---|---|
| Conjunto de evaluación | preguntas con respuesta conocida que se repiten en cada cambio |
| Consulta de referencia | una consulta comprobada a mano que da las filas correctas |
| Coincidencia por resultado | las mismas filas que la consulta de referencia |
| Regresión | una pregunta que antes pasaba y ahora falla |
| Control | una regla que bloquea un cambio |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Cada caso tiene una pregunta y una referencia
Un caso guarda una pregunta, una consulta de referencia y si el orden importa o no. Escribe tú la referencia y comprueba sus filas a mano con los datos de prueba.
Ejecuta la referencia para obtener las filas esperadas. Así el conjunto sigue funcionando cuando crecen los datos, mientras las referencias sigan siendo correctas.
Ejemplo resuelto: cinco casos de la biblioteca
| Caso | Pregunta | Filas de referencia |
|---|---|---|
| 1 | Préstamos hechos en el mes 3 | 4 |
| 2 | Socios distintos en el mes 3 | 3 |
| 3 | Libros que siguen prestados | 5 |
| 4 | Títulos que tomó el socio 2 | Mapa de la lluvia, Motores en calma, Siete teteras |
| 5 | Libros que nunca se han prestado | Al norte del mediodía |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Puntúa por el resultado, no por el texto
COUNT(id) y COUNT(*) dan aquí el mismo número, y una unión se puede escribir como subconsulta. Dos textos distintos pueden ser correctos los dos.
Por eso ejecuta cada candidata y compara sus filas con las de referencia. Comparar el texto exacto marca como fallidas respuestas correctas, como muestra el Run.
Ejemplo resuelto: texto y resultado no coinciden
| Candidata | Mismo texto que la referencia | Mismas filas que la referencia |
|---|---|---|
| SELECT COUNT(id) FROM loans WHERE month = 3 | No | Sí |
| Una unión escrita como IN (SELECT book_id ...) | No | Sí |
| SELECT COUNT(*) FROM loans | No | No |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Una regresión se esconde tras la misma nota
Un prompt nuevo arregla la pregunta 2 y rompe la pregunta 3. Las dos versiones aciertan cuatro preguntas de cinco: la nota no se ha movido.
Compara caso a caso. Una pregunta que pasaba y ahora falla es una regresión, y el control bloquea el cambio hasta que se corrija.
Un error frecuente
Advertencia
Mucha gente cree que una tasa de acierto igual significa que nada ha empeorado. Una pregunta puede arreglarse mientras otra se rompe. Compara cada caso con la última pasada y bloquea cualquier cambio que pierda un acierto.
Escribe el ayudante del modelo, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Los tokens de una respuesta, o null si no trae ninguno.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' '// Una respuesta puede envolver la consulta en un bloque delimitado: nos quedamos con el interior.' 'function sqlOf(text) {' ' const parts = text.split(String.fromCharCode(96).repeat(3));' ' const inner = (parts.length > 2 ? parts[1] : text).trim();' ' return inner.toLowerCase().startsWith(\"sql\") ? inner.slice(3).trim() : inner;' '}' > model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el ayudante del modelo, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"estado \" + res.status);' ' const d = await res.json();' ' return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask, sqlOf };' >> model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe la base de la biblioteca, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { DatabaseSync } = require(\"node:sqlite\");' '// Una biblioteca inventada: seis libros, cuatro socios, nueve préstamos.' 'const BOOKS = [[1, \"La calle de la sal\", 2019], [2, \"Puerto de cristal\", 2021], [3, \"Mapa de la lluvia\", 2015],' ' [4, \"Motores en calma\", 2023], [5, \"Siete teteras\", 2018], [6, \"Al norte del mediodía\", 2022]];' 'const MEMBERS = [[1, \"Alba\"], [2, \"Bruno\"], [3, \"Carmen\"], [4, \"Diego\"]];' '// id, book_id, member_id, month, returned (1 devuelto, 0 aún fuera)' 'const LOANS = [[1, 1, 1, 3, 1], [2, 2, 1, 3, 1], [3, 3, 2, 3, 0], [4, 1, 3, 3, 1], [5, 1, 1, 4, 0],' ' [6, 4, 2, 4, 0], [7, 2, 3, 4, 1], [8, 5, 2, 5, 0], [9, 2, 4, 5, 0]];' > library.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe la base de la biblioteca, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function put(db, table, rows) {' ' const marks = rows[0].map(function () { return \"?\"; }).join(\", \");' ' const insert = db.prepare(\"INSERT INTO \" + table + \" VALUES (\" + marks + \")\");' ' rows.forEach(function (row) { insert.run(...row); });' '}' 'function open(file) {' ' const db = new DatabaseSync(file || \":memory:\");' ' db.exec(\"CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, year INTEGER)\");' ' db.exec(\"CREATE TABLE members (id INTEGER PRIMARY KEY, name TEXT)\");' ' db.exec(\"CREATE TABLE loans (id INTEGER PRIMARY KEY, book_id INTEGER REFERENCES books(id), \"' ' + \"member_id INTEGER REFERENCES members(id), month INTEGER, returned INTEGER)\");' ' put(db, \"books\", BOOKS);' ' put(db, \"members\", MEMBERS);' ' put(db, \"loans\", LOANS);' ' return db;' '}' 'module.exports = { open };' >> library.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de evaluación, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask, sqlOf } = require(\"./model\");' 'const { open } = require(\"./library\");' 'const db = open();' 'const cases = [' ' { gold: \"SELECT COUNT(*) FROM loans WHERE month = 3\" },' ' { gold: \"SELECT COUNT(DISTINCT member_id) FROM loans WHERE month = 3\" },' ' { gold: \"SELECT COUNT(*) FROM loans WHERE returned = 0\" },' ' { gold: \"SELECT b.title FROM loans l JOIN books b ON l.book_id = b.id WHERE l.member_id = 2\" },' ' { gold: \"SELECT title FROM books WHERE id NOT IN (SELECT book_id FROM loans)\" },' '];' > eval.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de evaluación, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const A = [\"SELECT COUNT(id) FROM loans WHERE month = 3\",' ' \"SELECT COUNT(member_id) FROM loans WHERE month = 3\", cases[2].gold,' ' \"SELECT b.title FROM books b JOIN loans l ON l.book_id = b.id WHERE l.member_id = 2\",' ' \"SELECT b.title FROM books b LEFT JOIN loans l ON l.book_id = b.id WHERE l.id IS NULL\"];' 'const B = [cases[0].gold, cases[1].gold, \"SELECT COUNT(*) FROM loans\",' ' \"SELECT title FROM books WHERE id IN (SELECT book_id FROM loans WHERE member_id = 2)\", cases[4].gold];' 'function rows(sql) {' ' try { return db.prepare(sql).all().map(function (r) { return JSON.stringify(Object.values(r)); }).sort().join(\"|\"); }' ' catch (error) { return null; }' '}' >> eval.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de evaluación, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function score(sql, i) {' ' const got = rows(sql);' ' return got === null ? \"error\" : got === rows(cases[i].gold) ? \"pasa\" : \"falla\";' '}' 'function rate(list, test) {' ' return Math.floor(100 * list.filter(test).length / list.length) + \" por ciento\";' '}' 'const a = A.map(score);' 'const b = B.map(score);' 'const same = function (sql, i) { return sql === cases[i].gold; };' '// Una regresión: pasaba en A y no pasa en B.' 'const lost = [];' 'a.forEach(function (v, i) { if (v === \"pasa\" && b[i] === \"falla\" || v === \"pasa\" && b[i] === \"error\") lost.push(i + 1); });' >> eval.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de evaluación, parte 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' a.forEach(function (v, i) { console.log(\"pregunta \" + (i + 1) + \": A \" + v + \", B \" + b[i]); });' ' const pass = function (v) { return v === \"pasa\"; };' ' console.log(\"por el resultado: A \" + rate(a, pass) + \", B \" + rate(b, pass));' ' console.log(\"por el texto exacto: A \" + rate(A, same) + \", B \" + rate(B, same));' ' console.log(\"regresiones en B: \" + (lost.length ? lost.join(\", \") : \"ninguna\"));' ' console.log(\"control: \" + (lost.length ? \"bloquear, una pregunta que pasaba ahora falla\" : \"publicar\"));' ' await live();' '}' >> eval.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de evaluación, parte 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function live() {' ' const q = \"loans(id, book_id, member_id, month, returned)\nPregunta: ¿Cuántos socios distintos tomaron prestado un libro en el mes 3?\";' ' const sample = \"SELECT COUNT(DISTINCT member_id) FROM loans WHERE month = 3;\";' ' const reply = await ask([{ role: \"system\", content: \"Responde con una sola consulta SELECT de SQLite y nada más.\" },' ' { role: \"user\", content: q }], sample, 120);' ' const sql = sqlOf(reply.text);' ' console.log(\"el modelo escribió: \" + sql);' ' console.log(\" puntuada aparte de los totales: \" + score(sql, 1));' '}' 'main();' >> eval.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Puntúa dos versiones con cinco preguntas
docker exec -w /home/node/project agent node --experimental-sqlite --no-warnings eval.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Lo que imprime el Run
| Línea | Qué muestra |
|---|---|
| pregunta 2: A falla, B pasa | El prompt nuevo arregló la pregunta 2 |
| pregunta 3: A pasa, B falla | El prompt nuevo rompió la pregunta 3 |
| por el resultado: A 80 por ciento, B 80 por ciento | La nota no se ha movido |
| por el texto exacto: A 20 por ciento, B 60 por ciento | El texto marca como fallidas respuestas correctas |
| regresiones en B: 3 | La pregunta perdida |
| control: bloquear, una pregunta que pasaba ahora falla | La decisión |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.