Cómo usar AI para consultar una base de datos de clientes sin escribir SQL
Por fin llegaste al puesto de Analista de Datos en la empresa de tus sueños (¡genial!). Estás emocionado de usar todas esas habilidades de SQL que fuiste acumulando con los años, quizás hasta probar algunos de los nuevos frameworks de dataframes. Llegas a tu escritorio con una taza de café recién hecho, listo para sumergirte en lo último de lo último. Pero las solicitudes más urgentes son las de siempre: “¡Oye! ¿Me puedes dar una lista de nuestros clientes en Nueva York que compraron al menos 10 aguacates el año pasado?” Entonces abres tu viejo y confiablecustomers-filter.sqlreemplazas “Los Ángeles” con “Nueva York” y “Aguacate” con “Bagel” en la consulta, y adjuntas ese archivo CSV en Slack. Este proceso repetitivo y mecánico (y francamente aburrido) es muy frustrante y consume mucho tiempo. ¿Qué pasaría si tus colegas no técnicos pudieran consultar la base de datos directamente cuando lo necesitaran?
¿Qué es un SQL Toolkit?
El SQL Toolkit permite interactuar con tu base de datos en lenguaje natural. En lugar de escribir consultas complejas, describes lo que necesitas en español llano. El toolkit traduce tu solicitud en un conjunto de parámetros y devuelve resultados formateados, evitando alucinaciones al acceder directamente a tus datos.
Características principales:
- Consultas a la base de datos en lenguaje natural
- Resultados con formato correcto
- Validación de datos integrada
Por qué Arcade.dev lo hace posible
Usamos una llamada de herramienta de Arcade para interactuar con tu base de datos SQL.Tool-callingsignifica que la AI puede realizar acciones específicas en tu nombre, en lugar de solo proporcionar información. En vez de sugerir consultas SQL, el agente usa Arcade para interactuar directamente con tu base de datos con autenticación adecuada; la herramienta consulta la base de datos y devuelve resultados precisos. Así obtienes datos reales, no alucinaciones de AI, manteniendo control total sobre lo que el agente puede acceder.
Primeros pasos
En este tutorial construiremos un toolkit de Arcade que puede interactuar con una base de datos usando lenguaje natural para generar y formatear resultados. A grandes rasgos, le daremos al agente LLM una función que sabe cómo acceder correctamente a los datos para evitar alucinaciones. Para esto haremos lo siguiente:
- Crear un nuevo toolkit
- Escribir una función de generación de consultas para que use el LLM
- Construir evals para tool-calls
- Invocar nuestra nueva herramienta desde el Playground
Configuración
Primero, asegurémonos de tener el software necesario instalado:
- Python 3.10+ y pip
Empezaremos creando un directorio y un entorno virtual para nuestro toolkit
mkdir arcade-sql-agent
cd arcade-sql-agent
python -m venv .venv
source .venv/bin/activate
Luego agregamos los paquetes de Python necesarios al entorno activo.
pip install 'arcade-ai[evals]' python-dotenv psycopg2-binary
Ahora podemos invocar elarcadecomando desde nuestra terminal, que usaremos para crear nuestro nuevo toolkit. Pero primero, entremos a Arcade. Si no tienes una cuenta, este es el momento decrear una nueva cuenta de Arcade. Luego ve a la terminal y ejecuta:
arcade login
Tu navegador se abrirá y, una vez que termines, deberías ver un mensaje en la terminal con tu API key de Arcade.
Crear un nuevo toolkit
Ahora empecemos a desarrollar nuestro toolkit desde cero. Primero, usaremosarcadepara arrancar una plantilla de proyecto para el nuevo toolkit:
arcade new
Te pedirá que le des al toolkit un nombre, una descripción, un usuario de GitHub y un correo.
Usé estos valores:
Name of the new toolkit?: sql_customers
Description of the toolkit?: Query the customer database using natural language
Github owner username?: torresmateo
Author's email?: mateo@arcade.dev
Verás que se creó un directoriosql_customersse creó para ti, con una plantilla de proyecto. Incluye un esqueleto organizado para agregar nuestras funciones, pruebas y evals, con utilidades para instalar el toolkit localmente y probarlo.
Así se ve el directorio:
├── LICENSE
├── Makefile
├── README.md
├── arcade_sql_customers
│ ├── __init__.py
│ └── tools
│ ├── __init__.py
│ └── hello.py
├── codecov.yaml
├── evals
│ └── eval_sql_customers.py
├── pyproject.toml
├── tests
│ ├── __init__.py
│ └── test_sql_customers.py
└── tox.ini
Eltoolses donde va el código de las herramientas, y por defecto obtenemoshello.pyque incluye una herramienta básica que toma un nombre y lo saluda. Lo reemplazaremos con nuestra propia herramienta.
Entender el esquema de la base de datos para el tool calling
Por simplicidad, usaremosSupabasepara hospedar una base de datos. Aquí está el esquema de la tabla:
create table people
(
id INTEGER primary key,
name TEXT,
age INTEGER,
location TEXT,
occupation TEXT,
email TEXT
);
Preparamos unaversión descargable de este esquemacon 100 filas que puedes ejecutar para recrear la misma base de datos. Si quieres seguir el tutorial,crea una cuenta de Supabasey carga el esquema en una nueva base de datos.
Escribir una función de generación de consultas para que use el LLM
Abre el directoriosql_customeren tu editor de código y reemplaza elhello.pyarchivo con uno más descriptivoquery.pybajoarcade_sql_customer/toolsEmpezaremos importando los paquetes necesarios y configurando un logger simple:
import sqlite3
from typing import Annotated, Optional
from arcade.sdk import tool, ToolContext
from arcade_sql_customers.utils import get_database_connection
# Configure the logger
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
Si estás siguiendo el tutorial, descarga el
utils.pyscript desdeel repo, o mejor aún, clona el repositorio completo para que sea más fácil ejecutarlo.
Empecemos con el enfoque clásico de texto a SQL, que le pide al LLM que genere SQL directamente a partir de un prompt en lenguaje natural:
@tool(requires_secrets=["database_url"])
def direct_query(
context: ToolContext,
query: Annotated[str, "The query to run in the database"]
) -> Annotated[dict, "The data returned from the database"]:
"""
Query the data from the 'people' table with a query generated by the LLM.
"""
logger.info("Starting query_customer_data function")
logger.debug(f"Query received: {query}")
# Connect to the database
conn = get_database_connection(context)
cursor = conn.cursor()
# Execute the query and get the rows
cursor.execute(query)
rows = cursor.fetchall()
logger.info(f"Query executed successfully. Rows fetched: {len(rows)}")
# Prepare the results as a list of dictionaries
column_names = [description[0] for description in cursor.description]
results = [dict(zip(column_names, row)) for row in rows]
logger.debug(f"Results prepared: {results}")
conn.close()
return {"results": results}
Mira la implementación completa en el repo de este tutorial:https://github.com/ArcadeAI/arcade-sql-agent-tutorial/blob/main/arcade_sql_customers/tools/query.py#L14
El@tooldecorador sobre nuestra definición le indica al worker de Arcade que lo exporte como una herramienta que el engine puede poner a disposición de los LLMs. ElToolContextque recibimosnos permitirá obtener la información correcta del usuario para configurar nuestra herramienta. En este caso, usamosrequire_secretspara asegurarnos de obtener la ruta a la base de datos SQLite como un secreto para la herramienta.
Vamos a desplegar esta herramienta y ver cómo funciona. Podemos desplegar un worker en la nube con los siguientes comandos (puede tardar varios segundos la primera vez):
arcade deploy
Esto desplegará un worker con las nuevas herramientas en la nube y lo registrará en nuestra cuenta de Arcade. Sientramos ahoraveremos que nuestras herramientas se agregaron a la lista en la pestaña “Tools”, y también una advertencia pidiéndonos que configuremos los secretos requeridos faltantes:

Para configurar el secreto que necesita nuestra herramienta,obtén la cadena de conexión desde Supabase. Haz clic en el enlace naranja y pégala en el campo “Secret Value”.

Ahora podemos probar nuestradirect_queryherramienta. Esta función hace que el LLM genere una consulta SQL y la función intentará ejecutarla directamente en la base de datos. Aunque esto es útil en escenarios simples, no garantiza que funcione siempre:

Los LLMs son propensos a las alucinaciones. En este caso, el modelo asumió incorrectamente que “male” en el campo Occupation o “Mr.” en el nombre del cliente era una forma válida de determinar el género. Sin embargo, según nuestro esquema, debería haber indicado que faltaban las columnas necesarias para generar una respuesta precisa. Este tipo de error puede hacer creer a los usuarios que el Sr. Phillips es el único cliente masculino en la base de datos, llevando a decisiones equivocadas. Una consecuencia aún más peligrosa de darle tanta libertad al LLM es que, si se le convence, podría generarDELETEoDROPsentencias, o alucinaciones de consultas tan complejas que afecten el rendimiento de toda la base de datos. O simplemente usar el dialecto SQL incorrecto, o columnas que no existen:

Podemos mitigar esto dándole al LLM una interfaz más robusta y controlada hacia nuestra base de datos, con parámetros explícitos que puede usar. La estrategia es ofrecer un conjunto claro de parámetros que el LLM pueda usar para filtrar la tabla. La herramienta se encargará de sanear la entrada y construir la consulta, garantizando el dialecto correcto y evitando que llegue a nuestra base de datos cualquier cosa que no queramos:
@tool(requires_secrets=["database_url"])
def query_customer_data(
context: ToolContext,
columns_to_select: Annotated[
Optional[list[str]],
"List of columns to select from the 'people' table."
" If None, all columns are selected.",
] = None,
filter_by_id: Annotated[
Optional[int],
"Filter the results by ID."
] = None,
filter_by_name: Annotated[
Optional[str],
"Filter the results by name."
] = None,
# ... more parameters in the repo!
order_by: Annotated[
Optional[str],
"Column to order the results by. Must be a valid column name."
] = None,
limit: Annotated[
int,
"The maximum number of rows to return."
] = 20,
) -> Annotated[
dict,
"The data returned from the database."]:
"""
Query the data from the 'people' table with the provided parameters.
"""
# Build the base query
valid_columns_list = ["id", "name", "age",
"location", "occupation", "email"]
valid_columns = "*"
if columns_to_select:
# Collect valid columns to select, ignore any others
valid_columns = [
col for col in columns_to_select if col in valid_columns_list]
query = f"SELECT {valid_columns} FROM people"
params = []
logger.debug(f"Initial query: {query}")
# Build WHERE clause with filters
where_clauses = []
if filter_by_id is not None:
where_clauses.append('id = %s')
params.append(filter_by_id)
logger.debug(f"Filtering by id: {filter_by_id}")
if filter_by_name is not None:
where_clauses.append('Name ILIKE %s')
params.append(f"%{filter_by_name}%")
logger.debug(f"Filtering by Name: {filter_by_name}")
# ... handle more parameters
# add the WHERE clause if needed
if where_clauses:
query += " WHERE " + " AND ".join(where_clauses)
logger.debug(f"Added WHERE clauses: {' AND '.join(where_clauses)}")
# Add ORDER BY clause if provided
if order_by:
if order_by not in valid_columns_list:
logger.error(f"Invalid order_by column: {order_by}")
return {"results": []}
query += f' ORDER BY "{order_by}"'
logger.debug(f"Ordering by: {order_by}")
# Add LIMIT clause
query += " LIMIT %s"
params.append(limit)
# Connect to the database
conn = get_database_connection(context)
cursor = conn.cursor()
# Execute the query and get the rows
cursor.execute(query, params)
rows = cursor.fetchall()
logger.info(f"Query executed successfully. Rows fetched: {len(rows)}")
# Prepare the results as a list of dictionaries
column_names = [description[0] for description in cursor.description]
results = [dict(zip(column_names, row)) for row in rows]
logger.debug(f"Results prepared: {results}")
conn.close()
return {"results": results}
Acortamos el código del tutorial para evitar funciones muy largas y verbosas. Mira la implementación completa en el repo de este tutorial:https://github.com/ArcadeAI/arcade-sql-agent-tutorial/blob/main/arcade_sql_customers/tools/query.py#L76
Este es un buen momento para desplegar y probar de nuevo. (Si no recuerdas cómo hacerlo, solo sube 😉)
Construir evals para tool-calls
Las pruebas son una buena forma de validar que la función se comporta de manera predecible al pasar diferentes valores. Sin embargo,también deberíamos probar qué tan bien interactúan los LLMs con nuestras herramientas. Arcade nos da una forma de evaluar qué tan bien los LLMs usan nuestras herramientas, evaluando:
- Que el LLM elija la herramienta correcta en el momento adecuado
- Que pase los parámetros correctos a la herramienta según el contexto
- Que llame a las herramientas en la secuencia correcta para tareas de varios pasos
Aquí le estamos dando una sola herramienta al LLM, así que solo probamos el segundo aspecto, pero usaremos todos los mecanismos de evaluación de Arcade: Rubrics, Evaluation Suites y Critics. Vamos a reemplazar el contenido desql_customers/evals/eval_sql_customers.py.:
from arcade.sdk import ToolCatalog
from arcade.sdk.eval import (
EvalRubric,
EvalSuite,
ExpectedToolCall,
SimilarityCritic,
BinaryCritic,
tool_eval,
)
import arcade_sql_customers
from arcade_sql_customers.tools.query import query_customer_data
# Evaluation rubric
rubric = EvalRubric(
fail_threshold=0.85,
warn_threshold=0.95,
)
catalog = ToolCatalog()
catalog.add_module(arcade_sql_customers)
@tool_eval()
def sql_toolkit_query_customer_eval_suite() -> EvalSuite:
suite = EvalSuite(
name="sql_toolkit Tools Evaluation",
system_message=(
"You are an AI assistant with access to sql_toolkit tools. "
"Use them to help the user with their tasks."
),
catalog=catalog,
rubric=rubric,
)
# We'll add our evaluations here
return suite
Analicemos qué está pasando aquí:
- El
EvalRubricobjeto que usamos le indica a Arcade que considere como fallo cualquier puntaje de evaluación por debajo del 85%, como advertencia los puntajes entre 85% y 95%, y como éxito cualquier cosa por encima del 95%. - El
ToolCatalogobjeto recopila las herramientas que queremos poner a disposición del LLM. - Usamos el
@tool_eval()decorador para indicarle a Arcade que queremos usar elsql_toolkit_query_customer_eval_suitefunción para definir un conjunto de evaluaciones específicas. - En la función, inicializamos el suite con un mensaje del sistema, y ahora agregaremos casos específicos al suite para probar diferentes escenarios usando un LLM:
suite.add_case(
name="Getting names and emails from a given Name",
user_message="Get the names and emails of all customers named David",
expected_tool_calls=[ExpectedToolCall(
func=query_customer_data,
args={
"filter_by_name": "David",
"columns_to_select": ["name", "email"],
})],
rubric=rubric,
critics=[
SimilarityCritic(critic_field="filter_by_name", weight=0.5),
BinaryCritic(critic_field="columns_to_select", weight=0.5),
]
)
Mira el conjunto completo de casos de evaluación en el repo de este tutorial:https://github.com/ArcadeAI/arcade-sql-agent-tutorial/blob/main/evals/eval_sql_customers.py
Cada caso representa un prompt que un usuario puede usar para que el LLM llame a nuestra herramienta. El LLM debe ser capaz de analizar el lenguaje natural incluido enuser_messagey llenar los parámetros de nuestra herramienta con los valores correctos.
Usamos elExpectedToolCallpara definir quequery_customer_dataes la función correcta a llamar, así como qué valores deben usarse en este escenario según el contexto. Para el primer caso, queremos evaluar que el LLM sea capaz de elegir correctamente los valores a pasar afilter_by_name( "David"ycolumns_to_select(["name", "email"] ).
Como estamos usandoILIKEen nuestro query builder, no nos importa que el valor pasado afilter_by_nameel parámetro no seaexactamenteel mismo que el de nuestro prompt ( “david” funcionaría igual que “DAVID”, “David”, o incluso “DaViD” ). En este escenario, usamos un SimilarityCritic, que evalúa la similitud entre el valor esperado que definimos en ExpectedToolCall y el valor real inferido por el LLM del contexto.
Sin embargo, sí nos importa que el valor pasado acolumns_to_selectsea correcto ("E-mail"no funcionará para seleccionar la columna"email" ). En este escenario, usamos elBinaryCriticque verifica que haya una coincidencia exacta entre los valores esperados y reales.
Los parámetrosweightpasados a los critics determinan qué tan relevante es el puntaje del critic para el caso de evaluación general. La mejor práctica más intuitiva es hacer que todos los pesos sumen1.0para cada caso de evaluación, para que pueda interpretarse fácilmente como una importancia basada en porcentaje. Puedes leer más sobre lostipos de critic y cuándo usarlos.
Para evaluar esto, usaremos el Arcade Engine, que ya tiene acceso a nuestras herramientas.
Instala el toolkit localmente:
make install
Y ejecuta el evaluation suite:
arcade evals --cloud evals
Esto se conectará a Arcade y ejecutará cada caso en nuestro evaluation suite. El resultado debería verse así:

Invocar nuestra nueva herramienta desde el Playground
¡Felicidades! Todos los evals pasaron, y ahora estamos listos para ver cómo interactúa nuestra herramienta en un escenario de chat real. Entra aldashboard de Arcadey pídele que recupere algunos clientes

¡Ahora podemos interactuar con nuestra base de datos usando lenguaje natural!
Siguientes pasos
Ahora que viste cómo construir una interfaz de base de datos impulsada por AI con Arcade, puedes crear tu propia solución personalizada:
- Personaliza las consultas según las necesidades de tu equipo
- Prueba a fondo antes de desplegar
El proceso completo toma alrededor de media hora y le abre el acceso a los datos de clientes a todos en tu equipo, sin importar su nivel técnico.
¿Listo para probarlo tú mismo? Visitaarcade.devpara empezar.

