Una de las cosas que más ha cambiado en estos últimos meses, desde el Boom de ChatGPT, ha sido la aparición de modelos de lenguaje inmensos, que son capaces de aprender a realizar tareas con tan solo dos o tres ejemplos. Esta capacidad, junto a la facilidad por interpretar las órdenes, ha facilitado la creación de una nueva técnica dentro del mundo de la IA: la ingeniería de prompts.
Utilizamos Ingeniería de Prompt para cambiar el comportamiento de estos modelos de grandes capacidades y conseguir que hagan las tareas que necesitamos.
En este artículo vamos a ver como crear un prompt para que los modelos GPT de OpenAI sean capaces de ejecutar tareas de NL2SQL. Es decir, traducir las peticiones hechas en el lenguaje del usuario a órdenes SQL que le devolverán los datos que pide.
Para ello partiremos de un paper de la universidad de Ohio: How to Prompt LLMs for Text-to-SQL: A Study in Zero-shot, Single-domain, and Cross-domain Settings. Y modificaremos lo que ya vimos en el segundo artículo del curso de Grandes Modelos de Lenguaje.
Estructura del Prompt.
El prompt va a contener cuatro secciones:
- La estructura de las tablas. Con ejemplos de su contenido.
- Instrucciones para guiar al modelo en cuanto al SQL a generar.
- Los ejemplos de SQL correctos. Llamados Few Shot Samples.
- La pregunta del usuario.
El prompt que vamos a crear es directamente el más completo. Va a contener varios ejemplos y con relaciones cruzadas entre las tablas. No siempre es necesario dar ejemplos de SQL al Modelo, si la tabla es sencilla y hemos dado información suficiente, el modelo será capaz de generar un SQL correcto. Pero estos ejemplos también nos permiten influenciar en el estilo y formato en el que queremos que sea devuelto el SQL.
La estructura de las tablas:
create table employees( ID_Usr INT primary key, name VARCHAR); /*3 example rows select * from employees limit 3; ID_Usr name 1344 George StPierre 2122 Jon jones 1265 Anderson Silva */ create table salary( ID_Usr INT, year DATE, salary FLOAT, foreign key (ID_Usr) references employees(ID_Usr)); /*3 example rows select * from salary limit 3 ID_Usr date salary 1344 01/01/2023 61000 1344 01/01/2022 60000 1265 01/01/2023 55000 */ create table studies( ID_study INT, ID_Usr INT, educational_level INT, /* 5=phd, 4=Master, 3=Bachelor */ Institution VARCHAR, Years DATE, Speciality VARCHAR, primary key (ID_study, ID_Usr), foreign key(ID_Usr) references employees (ID_Usr)); /*3 example rows select * from studies limit 3 ID_Study ID_Usr educational_level Institution Years Speciality 2782 1344 3 UC San Diego 01/01/2010 Bachelor of Science in Marketing 2334 1344 5 MIT 01/01/2023 Phd. Data Science. 2782 2122 3 UC San Diego 01/01/2010 Bachelor of Science in Marketing */
Como podéis ver, el formato en el que se indica la estructura es el mismo que usaríamos en SQL si quisiéramos crear las tablas. Podemos usar los comentarios para aumentar la información, como en el caso del campo educational_level de la tabla studies. Donde he indicado a qué nivel de estudios corresponde cada valor.
Debajo de la definición de la tabla, en un comentario de SQL, he puesto un ejemplo del contenido de la tabla. Este ejemplo se acompaña de la orden SQL que devolvería los datos.
Instrucciones.
/* Maintain the SQL order simple and efficient as you can, using valid SQL Lite, answer the following questions for the table provided above. */
En este caso las instrucciones son muy sencillas. GPT3.5-Turbo es un modelo muy potente que no necesita ser excesivamente guiado. Tan solo le decimos que haga un SQL simple, y que utilice la sintaxis de SQLLite. Podriamos indicarle si queremos usar o no alias para las tablas, usar otro formato de SQL.
Few Shot Samples.
Question: How Many employes we have with a salary bigger than 50000? SELECT COUNT(*) AS total_employees FROM employees e INNER JOIN salary s ON e.ID_Usr = s.ID_Usr WHERE s.salary > 50000; Question: Return the names of the three people who have had the highest salary increase in the last three years. SELECT e.name FROM employees e JOIN salary s ON e.ID_usr = s.ID_usr WHERE s.year >= DATE_SUB(CURDATE(), INTERVAL 3 YEAR) GROUP BY e.name ORDER BY (MAX(s.salary) - MIN(s.salary)) DESC LIMIT 3;
Le estamos marcando al modelo, como le va a llegar la pregunta. Precedida por la palabra Question:. El SQL se devuelve justo después de la pregunta, como vemos lo formateamos en múltiples líneas para que sea más sencillo de leer.
Es importante que las ordenes SQL que pongamos como ejemplo sean correctas y tengan el estilo de SQL que se ha marcado en nuestra organización.
Una buena práctica es usar SQLs que estén funcionando en producción, y que el equipo de desarrollo las valide antes de usarlas como ejemplo en el prompt. El número de ejemplos a pasar puede variar entre 1 y 6. Si el modelo no ha aprendido con seis ejemplos es que algo no funciona.
Tenemos que tener en cuenta que estos ejemplos ocupan Tokens en el prompt y que se pasan en cada consulta, por lo que aumenta el coste y tiempo de respuesta de cada consulta al Modelo. Mi recomendación es mantener este número al mínimo.
En el primer ejemplo he usado tan solo los datos de una tabla, mientras que en el segundo sí que se necesitan relaciones entre tablas. Es recomendable mezclar los ejemplos y tener como mínimo uno de cada. De esta forma no tan solo aprende la estructura de las tablas, sino como queremos la forma del SQL.
La pregunta del usuario.
Question: Cual es el nombre del usuario mejor pagado?
Es tan sencillo como que la pregunta debe seguir el mismo patrón que se ha seguido en los ejemplos. En nuestro caso debemos ponerlo detrás de la palabra Question.
Con todos estos apartados tenemos que construir nuestro prompt completo.
create table employees( ID_Usr INT primary key, name VARCHAR); /*3 example rows select * from employees limit 3; ID_Usr name 1344 George StPierre 2122 Jon jones 1265 Anderson Silva */ create table salary( ID_Usr INT, year DATE, salary FLOAT, foreign key (ID_Usr) references employees(ID_Usr)); /*3 example rows select * from salary limit 3 ID_Usr date salary 1344 01/01/2023 61000 1344 01/01/2022 60000 1265 01/01/2023 55000 */ create table studies( ID_study INT, ID_Usr INT, educational_level INT, /* 5=phd, 4=Master, 3=Bachelor */ Institution VARCHAR, Years DATE, Speciality VARCHAR, primary key (ID_study, ID_Usr), foreign key(ID_Usr) references employees (ID_Usr)); /*3 example rows select * from studies limit 3 ID_Study ID_Usr educational_level Institution Years Speciality 2782 1344 3 UC San Diego 01/01/2010 Bachelor of Science in Marketing 2334 1344 5 MIT 01/01/2023 Phd. Data Science. 2782 2122 3 UC San Diego 01/01/2010 Bachelor of Science in Marketing */ -Maintain the SQL order simple and efficient as you can, using valid SQL Lite, answer the following questions for the table provided above. Question: How Many employes we have with a salary bigger than 50000? SELECT COUNT(*) AS total_employees FROM employees e INNER JOIN salary s ON e.ID_Usr = s.ID_Usr WHERE s.salary > 50000; Question: Return the names of the three people who have had the highest salary increase in the last three years. SELECT e.name FROM employees e JOIN salary s ON e.ID_usr = s.ID_usr WHERE s.year >= DATE_SUB(CURDATE(), INTERVAL 3 YEAR) GROUP BY e.name ORDER BY (MAX(s.salary) - MIN(s.salary)) DESC LIMIT 3; Question: Cual es el nombre del usuario mejor pagado.
El notebook de ejemplo.
Veamos cómo se usa este prompt con OpenAI.
Primero instalamos e importamos la librería de OpenAI que nos da acceso a su API.
!pip install openai import openai openai.api_key='your-api-key'
Para crear el prompt usaremos los roles de OpenAI. Las instrucciones de cómo debe trabajar y la estructura de la base de datos, así como los ejemplos los daremos de alta con el Rol de System, mientras que la pregunta la vamos a mandar con el rol de User.
context = [ {'role':'system', 'content':""" create table employees( ID_Usr INT primary key, name VARCHAR); /*3 example rows select * from employees limit 3; ID_Usr name 1344 George StPierre 2122 Jon jones 1265 Anderson Silva */ create table salary( ID_Usr INT, year DATE, salary FLOAT, foreign key (ID_Usr) references employees(ID_Usr)); /*3 example rows select * from salary limit 3 ID_Usr date salary 1344 01/01/2023 61000 1344 01/01/2022 60000 1265 01/01/2023 55000 */ create table studies( ID_study INT, ID_Usr INT, educational_level INT, /* 5=phd, 4=Master, 3=Bachelor */ Institution VARCHAR, Years DATE, Speciality VARCHAR, primary key (ID_study, ID_Usr), foreign key(ID_Usr) references employees (ID_Usr)); /*3 example rows select * from studies limit 3 ID_Study ID_Usr educational_level Institution Years Speciality 2782 1344 3 UC San Diego 01/01/2010 Bachelor of Science in Marketing 2334 1344 5 MIT 01/01/2023 Phd. Data Science. 2782 2122 3 UC San Diego 01/01/2010 Bachelor of Science in Marketing */ """} ]
Ahora añadimos los ejemplos. Los he puesto en dos celdas diferentes, así podemos hacer pruebas con o sin ellos.
context.append( {'role':'system', 'content':""" --Maintain the SQL order simple and efficient as you can, using valid SQL Lite, answer the following questions for the table provided above. Question: How Many employes we have with a salary bigger than 50000? SELECT COUNT(*) AS total_employees FROM employees e INNER JOIN salary s ON e.ID_Usr = s.ID_Usr WHERE s.salary > 50000; Question: Return the names of the three people who have had the highest salary increase in the last three years. SELECT e.name FROM employees e JOIN salary s ON e.ID_usr = s.ID_usr WHERE s.year >= DATE_SUB(CURDATE(), INTERVAL 3 YEAR) GROUP BY e.name ORDER BY (MAX(s.salary) - MIN(s.salary)) DESC LIMIT 3; """ })
Voy a crear una función que concatenará este prompt, con la pregunta del usuario, para pasársela a OpenAI, y que nos devolverá la respuesta del Modelo.
#Functio to call the model. def return_CCRMSQL(user_message, context): newcontext = context.copy() newcontext.append({'role':'user', 'content':"question: " + user_message}) response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=newcontext, temperature=0, ) return (response.choices[0].message["content"])
Como podemos ver la pregunta del usuario la estamos añadiendo al final del prompt con el rol user, así el modelo entiende que esta es una petición del usuario y no una instrucción de cómo debe funcionar.
Veamos un par de ejemplos:
#new context_user = context.copy() print(return_CCRMSQL("The name of the employee best paid", context_user))
`SELECT e.name FROM employees e JOIN salary s ON e.ID_Usr = s.ID_Usr ORDER BY s.salary DESC LIMIT 1;
print(return_CCRMSQL("Return the Institution with a higher average salary", context_user))
SELECT s.Institution, AVG(s.salary) AS average_salary FROM salary s JOIN studies st ON s.ID_Usr = st.ID_Usr GROUP BY s.Institution ORDER BY average_salary DESC LIMIT 1;
Conclusiones.
He usado esta estructura de prompt en varios proyectos para generar SQL desde lenguaje natural y su resultado siempre ha sido bastante bueno. Para bases de datos con una estructura sencilla quizas no hace falta poner los ejemplos del contenido de las tablas, pero si que recomiendo siempre darle un par de ejemplos de SQL generado.
Si tenéis acceso a una base de datos real, intentad replicar el prompt para obtener órdenes SQL con la estructura de vuestra base de datos. Así podéis probar vosotros mismos como funciona.