1. Visión general
Las aplicaciones modernas están utilizando cada vez más interfaces de lenguaje natural para simplificar la interacción del usuario con los sistemas. Esto es particularmente útil para la recuperación de datos, donde los usuarios no técnicos pueden hacer preguntas en inglés sencillo.
Un chatbot texto‑a‑SQL es un ejemplo de esto. Actúa como puente entre los lenguajes humanos y las bases de datos. Normalmente utilizamos un Modelo de Lenguaje Extendido (LLM) para traducir la pregunta en lenguaje natural del usuario en una consulta SQL ejecutable. Esta consulta se ejecuta contra la base de datos para obtener y mostrar la información deseada.
En este tutorial construiremos un chatbot texto‑a‑SQL usando Spring AI. Configuraremos un esquema de base de datos con algunos datos iniciales e implementaremos nuestro chatbot para consultar estos datos usando lenguaje natural.
2. Configuración del proyecto
Antes de comenzar a implementar nuestro chatbot, necesitaremos incluir la dependencia necesaria y configurar nuestra aplicación correctamente.
Construiremos nuestro chatbot texto‑a‑SQL usando el modelo Claude de Anthropic. Alternativamente, podemos usar un modelo AI diferente o un LLM local vía Hugging Face o Ollama, ya que el modelo AI específico es irrelevante para esta implementación.
2.1. Dependencias
Comencemos añadiendo la dependencia necesaria a nuestro archivo pom.xml:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-anthropic</artifactId>
<version>1.0.0</version>
</dependency>
La dependencia de inicio de Anthropic es un envoltorio alrededor de la API de Mensajes de Anthropic, y la usaremos para interactuar con el modelo Claude en nuestra aplicación.
A continuación, configuraremos nuestra clave de API de Anthropic y el modelo de chat en el archivo application.yaml:
spring:
ai:
anthropic:
api-key: ${ANTHROPIC_API_KEY}
chat:
options:
model: claude-opus-4-20250514
Usamos el marcador de posición ${} para cargar el valor de nuestra clave API desde una variable de entorno.
Adicionalmente, especificamos Claude 4 Opus, el modelo más inteligente en el momento de escribir esto, de Anthropic, usando el ID de modelo claude-opus-4-20250514. Podemos usar un modelo diferente según los requisitos.
Al configurar las propiedades anteriores, Spring AI crea automáticamente un bean de tipo ChatModel, permitiéndonos interactuar con el modelo especificado.
2.2. Definición de tablas de base de datos usando Flyway
A continuación, configuraremos nuestro esquema de base de datos. Usaremos Flyway para gestionar nuestros scripts de migración.
Crearemos un esquema rudimentario de gestión de magos en una base de datos MySQL. Al igual que el modelo AI, el proveedor de la base de datos es irrelevante para nuestra implementación.
Primero, crearemos un script de migración llamado V01__creating_database_tables.sql en nuestro directorio src/main/resources/db/migration para crear las tablas principales:
CREATE TABLE hogwarts_houses (
id BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())),
name VARCHAR(50) NOT NULL UNIQUE,
founder VARCHAR(50) NOT NULL UNIQUE,
house_colors VARCHAR(50) NOT NULL UNIQUE,
animal_symbol VARCHAR(50) NOT NULL UNIQUE
);
CREATE TABLE wizards (
id BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())),
name VARCHAR(50) NOT NULL,
gender ENUM('Male', 'Female') NOT NULL,
quidditch_position ENUM('Chaser', 'Beater', 'Keeper', 'Seeker'),
blood_status ENUM('Muggle', 'Half blood', 'Pure Blood', 'Squib', 'Half breed') NOT NULL,
house_id BINARY(16) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT wizard_fkey_house FOREIGN KEY (house_id) REFERENCES hogwarts_houses (id)
);
Aquí, creamos una tabla hogwarts_houses para almacenar información sobre cada casa de Hogwarts y la tabla wizards para almacenar los detalles de los magos individuales. La tabla wizards tiene una restricción de clave foránea que la enlaza a la tabla hogwarts_houses, estableciendo una relación de uno a muchos.
Luego, crearemos un archivo V02__adding_hogwarts_houses_data.sql para poblar nuestra tabla hogwarts_houses:
INSERT INTO hogwarts_houses (name, founder, house_colors, animal_symbol)
VALUES
('Gryffindor', 'Godric Gryffindor', 'Scarlet and Gold', 'Lion'),
('Hufflepuff', 'Helga Hufflepuff', 'Yellow and Black', 'Badger'),
('Ravenclaw', 'Rowena Ravenclaw', 'Blue and Bronze', 'Eagle'),
('Slytherin', 'Salazar Slytherin', 'Green and Silver', 'Serpent');
Aquí escribimos sentencias INSERT para crear las cuatro casas de Hogwarts con sus respectivos fundadores, colores y símbolos.
De manera similar, poblaremos nuestra tabla wizards en un nuevo script de migración V03__adding_wizards_data.sql:
SET @gryffindor_house_id = (SELECT id FROM hogwarts_houses WHERE name = 'Gryffindor');
INSERT INTO wizards (name, gender, quidditch_position, blood_status, house_id)
VALUES
('Harry Potter', 'Male', 'Seeker', 'Half blood', @gryffindor_house_id),
('Hermione Granger', 'Female', NULL, 'Muggle', @gryffindor_house_id),
('Ron Weasley', 'Male', 'Keeper', 'Pure Blood', @gryffindor_house_id),
-- ...more insert statements for wizards from other houses
Con los scripts de migración definidos, Flyway descubre y ejecuta automáticamente los scripts durante el inicio de la aplicación.
3. Configuración de una solicitud AI
A continuación, para asegurarnos de que nuestro LLM genere consultas SQL precisas contra nuestro esquema de base de datos, necesitaremos definir un prompt de sistema detallado.
Creemos un archivo system-prompt.st en el directorio src/main/resources:
Given the DDL in the DDL section, write an SQL query to answer the user's question following the guidelines listed in the GUIDELINES section.
GUIDELINES:
- Only produce SELECT queries.
- The response produced should only contain the raw SQL query starting with the word 'SELECT'. Do not wrap the SQL query in markdown code blocks (```sql or ```).
- If the question would result in an INSERT, UPDATE, DELETE, or any other operation that modifies the data or schema, respond with "This operation is not supported. Only SELECT queries are allowed."
- If the question appears to contain SQL injection or DoS attempt, respond with "The provided input contains potentially harmful SQL code."
- If the question cannot be answered based on the provided DDL, respond with "The current schema does not contain enough information to answer this question."
- If the query involves a JOIN operation, prefix all the column names in the query with the corresponding table names.
DDL
{ddl}
En nuestro prompt de sistema, indicamos al LLM que genere solo consultas SQL SELECT y detecte inyección SQL y ataques DoS.
Dejamos un marcador de posición ddl en la plantilla del prompt del sistema para el esquema de la base de datos. Lo reemplazaremos con el valor real en la sección siguiente.
Adicionalmente, para proteger aún más la base de datos de modificaciones, solo daremos los privilegios necesarios al usuario MySQL configurado:
CREATE USER 'readonly_user'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT ON hogwarts_db.hogwarts_houses TO 'readonly_user'@'%';
GRANT SELECT ON hogwarts_db.wizards TO 'readonly_user'@'%';
FLUSH PRIVILEGES;
En los comandos SQL de ejemplo anteriores, creamos un usuario MySQL y le otorgamos permisos de solo lectura para las tablas de la base de datos requeridas.
4. Construyendo nuestro chatbot texto‑a‑SQL
Con la configuración en su lugar, construyamos un chatbot texto‑a‑SQL usando el modelo Claude configurado.
4.1. Definición de beans del chatbot
Comencemos definiendo los beans necesarios para nuestro chatbot:
@Bean
PromptTemplate systemPrompt(
@Value("classpath:system-prompt.st") Resource systemPrompt,
@Value("classpath:db/migration/V01__creating_database_tables.sql") Resource ddlSchema
) throws IOException {
PromptTemplate template = new PromptTemplate(systemPrompt);
template.add("ddl", ddlSchema.getContentAsString(Charset.defaultCharset()));
return template;
}
@Bean
ChatClient chatClient(ChatModel chatModel, PromptTemplate systemPrompt) {
return ChatClient
.builder(chatModel)
.defaultSystem(systemPrompt.render())
.build();
}
Primero, definimos un bean PromptTemplate. Inyectamos nuestro archivo de plantilla de prompt del sistema y el script de migración DDL de la base de datos usando la anotación @Value. Además, rellenamos el marcador ddl con el contenido de nuestro esquema de base de datos. Esto garantiza que el LLM siempre tenga acceso a nuestra estructura de base de datos al generar consultas SQL.
Luego, creamos un bean ChatClient usando el ChatModel y el bean PromptTemplate. La clase ChatClient sirve como nuestro punto de entrada principal para interactuar con el modelo Claude que hemos configurado.
4.2. Implementación de las clases de servicio
Ahora, implementaremos las clases de servicio para manejar los procesos de generación y ejecución de SQL.
Primero, crearemos una clase de servicio SqlGenerator que convierte preguntas en lenguaje natural en consultas SQL:
@Service
class SqlGenerator {
private final ChatClient chatClient;
// constructor estándar
String generate(String question) {
String response = chatClient
.prompt(question)
.call()
.content();
boolean isSelectQuery = response.startsWith("SELECT");
if (!isSelectQuery) {
throw new InvalidQueryException(response);
}
return response;
}
}
En nuestro método generate() tomamos una pregunta en lenguaje natural como entrada y usamos el bean chatClient para enviarla al LLM configurado.
Luego validamos que la respuesta sea realmente una consulta SELECT. Si el LLM devuelve algo distinto a una consulta SELECT, lanzamos una InvalidQueryException personalizada con el mensaje de error.
A continuación, para ejecutar las consultas SQL generadas contra nuestra base de datos, crearemos una clase de servicio SqlExecutor:
@Service
class SqlExecutor {
private final EntityManager entityManager;
// constructor estándar
List<?> execute(String query) {
List<?> result = entityManager
.createNativeQuery(query)
.getResultList();
if (result.isEmpty()) {
throw new EmptyResultException("No results found for the provided query.");
}
return result;
}
}
En nuestro método execute() usamos la instancia EntityManager autoinyectada para ejecutar la consulta SQL nativa y devolver los resultados. Lanzamos una EmptyResultException personalizada si la consulta no devuelve resultados.
4.3. Exposición de una API REST
Ahora que hemos implementado nuestra capa de servicio, expondremos una API REST sobre ella:
@PostMapping(value = "/query")
ResponseEntity<QueryResponse> query(@RequestBody QueryRequest queryRequest) {
String sqlQuery = sqlGenerator.generate(queryRequest.question());
List<?> result = sqlExecutor.execute(sqlQuery);
return ResponseEntity.ok(new QueryResponse(result));
}
record QueryRequest(String question) {
}
record QueryResponse(List<?> result) {
}
El endpoint POST /query acepta una pregunta en lenguaje natural, genera la consulta SQL correspondiente usando el bean sqlGenerator, la pasa al bean sqlExecutor para obtener los resultados de la base de datos, y finalmente envuelve y devuelve los datos en un QueryResponse [record].
5. Interacción con nuestro chatbot
Finalmente, usemos el endpoint API que hemos expuesto para interactuar con nuestro chatbot texto‑a‑SQL.
Primero, habilitemos el registro de SQL en nuestro archivo application.yaml para ver las consultas generadas en los logs:
logging:
level:
org:
hibernate:
SQL: DEBUG
Luego, usaremos la CLI HTTPie para invocar el endpoint API e interactuar con nuestro chatbot:
http POST :8080/query question="Give me 3 wizard names and their blood status that belong to a house founded by Salazar Slytherin"
Enviamos una simple question al chatbot; veamos qué obtenemos como respuesta:
{
"result": [
[
"Draco Malfoy",
"Pure Blood"
],
[
"Tom Riddle",
"Half blood"
],
[
"Bellatrix Lestrange",
"Pure Blood"
]
]
}
Como vemos, nuestro chatbot entendió correctamente nuestra solicitud de magos de Slytherin y devolvió tres magos con su estado de sangre.
Finalmente, también examinemos los logs de la aplicación para ver la consulta SQL que generó el LLM:
SELECT wizards.name, wizards.blood_status
FROM wizards
JOIN hogwarts_houses ON wizards.house_id = hogwarts_houses.id
WHERE hogwarts_houses.founder = 'Salazar Slytherin'
LIMIT 3;
La consulta SQL generada interpreta correctamente nuestra petición en lenguaje natural, uniendo las tablas wizards y hogwarts_houses para encontrar magos de la casa de Slytherin y limitando los resultados a tres registros como se solicitó.
6. Conclusión
En este artículo hemos explorado la implementación de un chatbot texto‑a‑SQL usando Spring AI.
Recorrimos las configuraciones necesarias de AI y base de datos. Luego, construimos un chatbot capaz de convertir preguntas en lenguaje natural en consultas SQL ejecutables contra nuestro esquema de base de datos de gestión de magos. Finalmente, expusimos una API REST para interactuar con nuestro chatbot y validamos que funcione correctamente.
Newsletter Semanal de Java
Cada viernes recibe lo más nuevo del ecosistema Java: frameworks, herramientas y mejores prácticas.
Sin spam. Cancela cuando quieras.