*Protege tu analytics de Postgres en minutos.*
Haz fork de nuestro Postgres SQL Analytic Toolkit, restríngelo con roles de mínimo privilegio, adáptalo a tu esquema y despliégalo en Arcade.dev en menos de 6 minutos.*
Uno de los casos de uso más populares para los agentes de AI/LLM es explorar y activar datos en bases de datos y warehouses SQL. ¿Cómo se construyen agentes seguros y confiables? Vamos a explorar el tema.

Empieza con Límites, No con Prompts
Los prompts le dicen al modelo lo que quieres, no lo que puede hacer. Un prompt es como una intenciónútil para la UX, pero nunca una garantía de seguridad. La aplicación real vive una capa más abajo: en el motor de base de datos o en una capa de servicio con alcance restringido que tú controlas completamente.
Para hacerlo bien, necesitas:
- Roles con propósito definido - crea un rol de BD por toolkit para que tu esquema de permisos sea autodocumentado.
- Limitar la superficie de acceso
- Acceso - Si tu agente no necesita acceso de escritura, no se lo otorgues.
- Tablas - expón solo las que el agente realmente necesita.
- Columnas - omite ssn, password_hash y otros datos PII por completo.
- Filas - habilita Row-Level Security (RLS) para que los agentes vean únicamente su porción de datos.
- Fija las conexiones a esos roles - usa un pool de conexiones al que el LLM no pueda acceder, mediante un servidor de herramientas remoto. Nunca le des al agente la capacidad de modificarlo con comandos como SET ROLE. Además, para acelerar tu agente, mantén vivo un pool de conexiones creado cuando el agente arrancó y no dejes que el agente lo cambie.
Ejemplo en PostgreSQL:
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY region_policy ON orders
USING (region = current_setting('app.current_region'));
GRANT SELECT (id, customer_id, total_cents, region)
ON orders TO ai_reporting;
Otros motores tienen primitivas similares: las Secure Viewsde Snowflake, las Authorized Viewsde BigQuery y el Dynamic Data Masking de SQL Server permiten aplicar el mismo principio: mínimo privilegio por defecto, aplicado donde viven los datos.
Crear herramientas de AI (por ejemplo, servidores MCP) con acceso demasiado permisivo es la razón detrás de los problemas de seguridad recientes (ej.). ¡No hagas eso!
Al construir tu herramienta, considera cómo se relacionan los permisos de la cuenta de base de datos con los roles de tus usuarios finales. Hay algunas opciones:
- Usuario único: El agente es de un solo usuario y su acceso a la base de datos es exactamente el mismo que el del usuario. En este caso, guardar la cadena de conexión de la base de datos como variable de entorno funciona bien.
- Rol único: El agente es multiusuario y su acceso a la base de datos es el mismo para todos los usuarios del agente. Por ejemplo, el “agente de finanzas” solo lo usan los miembros del departamento de finanzas, o el “agente de analytics” tiene acceso a tablas que toda la empresa puede ver. De nuevo, guardar la cadena de conexión como variable de entorno global es lo adecuado.
- Roles heterogéneos: Múltiples tipos de usuarios usarán los agentes, cada uno con acceso diferente. En este caso, tu agente necesita gestionar múltiples conexiones para cada tipo de usuario. Quizás cada usuario debe guardar su propia cadena de conexión como secreto, o tendrás que consultar los permisos de cada usuario en un sistema como DreamFactory o un servidor de entitlements similar.
¿Herramientas Operativas o Exploratorias?
Hay que diferenciar entre estos dos tipos principales de herramientas SQL para agentes de AI, ya que su diseño y consideraciones de seguridad varían. En términos generales, las herramientas SQL se clasifican en “Operativas” y “Exploratorias”. Las operativas tienen casos de uso muy bien definidos y pueden incluso modificar datos de forma segura. Las exploratorias sirven para explorar datos y generar reportes, y necesitan soportar casos de uso desconocidos y esquemas amplios.
Herramientas Operativas: Precisión y Control
Las herramientas SQL operativas están diseñadas para interacciones específicas, generalmente transaccionales, con la base de datos. Se usan normalmente en tareas que implican modificación de datos (inserciones, actualizaciones e incluso eliminaciones) o recuperación de datos muy estructurada. El énfasis está en la precisión, la predictibilidad y la seguridad.
Los agentes son másvulnerables a ataques de inyección SQL que el software tradicional, debido a una capa adicional de interpretación cuando el LLM escribe cualquier parte de la consulta. Debes protegerte contra ataques igual que con cualquier otra aplicación que interactúa con una base de datos. No deberías dejar que tu LLM escriba sentencias SQL siempre que sea posible. Lo que deberías hacer en cambio es construir Herramientas Operativas con las siguientes propiedades:
- Sentencias preparadas: Usa siempre sentencias SQL preparadas para prevenir vulnerabilidades de inyección SQL. El agente de AI debe proporcionar parámetros que se vinculen a la sentencia precompilada, en lugar de construir consultas SQL en crudo.
- Métodos específicos y validación de entradas: Cada herramienta operativa debe exponer métodos (funciones) claramente definidos que el agente de AI pueda invocar. Estos métodos deben incluir validación robusta de entradas, asegurando que todos los datos recibidos cumplan con los tipos, formatos y rangos esperados.
- Enumerar valores permitidos: Para campos con un conjunto limitado de entradas válidas, usa enums o tablas de referencia para restringir las opciones del agente de AI. Esto evita la generación de datos inválidos o maliciosos.
- Mínimo privilegio: Como se discutió antes, estas herramientas deben operar con los permisos de base de datos mínimos absolutamente necesarios.
Ejemplos de Herramientas Operativas
Las herramientas operativas tienen descripciones de tipo como:
# An example of a typed operational read tool
@tool(requires_secrets=["DATABASE_CONNECTION_STRING"])
async def count_new_users_of_app(
context: ToolContext,
aggregation_window: Annotated[AggregationWindowEnum, "The time range to group new users by. Default: 'day'"] = AggregationWindow.day, # Arcade will expand the options of AggregationWindowEnum into the prompt automatically
exclude_internal_users: Annotated[bool, "Should we ignore internal users? Default: True"] = True
limit: Annotated[int, "The number of rows to return. Default: 100"] = 100,
) -> int:
"""
Count the number of new users within a time window
"""
Lo que produce una consulta como:
SELECT date_trunc($1, created_at) AS period, COUNT(*) AS user_count
FROM users
WHERE internal = false
GROUP BY period
ORDER BY period DESC
LIMIT $2 sq
Y
# An example of a safe tool which would modify the database
@tool(requires_secrets=["DATABASE_CONNECTION_STRING"])
async def update_user_payment_plan(
context: ToolContext,
user_id: Annotated[str, "The user ID to modify"],
payment_plan: Annotated[PaymentPlanEnum, "The payment plan for the user"], # Arcade will expand the options of PaymentPlanEnum into the prompt automatically
) -> PaymentPlanEnum:
"""
Update the payment plan for a specific user.
"""
Lo que produce una consulta como:
UPDATE users SET payment_plan = $1 WHERE id = $2 LIMIT 1
Ten en cuenta el argumento ToolContext en los ejemplos anteriores. Esa es la forma que tiene Arcade de pasar secretos e información del usuario a la herramienta sin que pase por el LLM (que no es seguro). Nunca quieres que algo tan sensible como una cadena de conexión o contraseña de base de datos esté disponible para el LLM: podría mostrarla, filtrarla o, peor aún, entrenarse con ella. Aprende más sobre ToolContext aquí.
Herramientas Exploratorias: Exploración e Insights
Las herramientas SQL exploratorias, en contraste, están diseñadas para consultar y extraer insights de los datos. El caso de uso más común es permitir a los usuarios internos de tu organización acceder a tu data warehouse. Un data warehouse es una base de datos masiva (comúnmente Snowflake o Databricks) que contiene todos los datos de todas las herramientas que usa tu empresa. Se mantiene sincronizada mediante una herramienta ELT/ETL como Airbyte.
La restricción principal de estas herramientas es que deben ser de solo lectura. Esta restricción fundamental reduce significativamente la superficie de seguridad.
- Aplicación de solo lectura: Los roles de base de datos asociados a las herramientas exploratorias deben tener explícitamente solo privilegios `SELECT`, sin permisos de `INSERT`, `UPDATE` ni `DELETE`. Las herramientas SQL bien escritas también fuerzan que solo se ejecuten consultas SELECT. De nuevo, ¡los agentes son aún más vulnerables a ataques de inyección SQL! Prevénlos a nivel de conexión.
- Comprensión del esquema: Tus agentes necesitarán saber qué tablas existen y para qué sirven. El mejor patrón es cargar descripciones de las tablas necesarias en el contexto de antemano, junto con cualquier metadato de referencia disponible. Quizás tienes una Capa Semántica (por ejemplo, dbt o Cube) que puedes usar, o anotaciones de tablas que se pueden cargar desde tu base de datos. Si no, describe tu esquema en texto plano.
- Descripciones de tablas: Para que el agente de AI haga consultas efectivas, proporciona descripciones claras y concisas de las tablas y sus columnas. No todas las tablas serán necesarias para todos los casos de uso (y eso desperdicia tokens), así que hay que motivar al LLM/Agente a aprender el esquema de las tablas relevantes antes de intentar consultarlas. El flujo “Ver -> Planear -> Consultar” funciona muy bien.
- Buenas prácticas de consulta: Aunque las herramientas exploratorias pueden ser más generales, promueve prácticas como:
- Limitar los conjuntos de resultados: Exige una cláusula `LIMIT` en las consultas para evitar conjuntos de resultados excesivamente grandes.
- Selección de columnas específicas: Fomenta seleccionar solo las columnas necesarias en lugar de `SELECT *`.
- “EXPLAIN ANALYZE“ (para desarrollo/debugging): Aunque no es para que el LLM lo ejecute directamente en producción, analizar el plan de consulta ayuda en el desarrollo de herramientas.
- RetryableToolErrors para el aprendizaje del flujo de trabajo: Implementa tipos de error personalizados como RetryableToolError cuando el LLM intente una consulta exploratoria inválida (por ejemplo, alucinando columnas que no existen). Esto le indica al LLM que necesita inspeccionar las tablas disponibles y sus descripciones antes de intentar la siguiente consulta, enseñándole un flujo de trabajo más robusto. Aprende más sobre los errores reintentables de herramientas aquí.
Ejemplos de Herramientas Exploratorias
Un buen conjunto de herramientas SQL exploratorias para exploración de datos podría verse así:
Descubrir esquema:
@tool(requires_secrets=["DATABASE_CONNECTION_STRING"])
async def discover_schemas(
context: ToolContext,
) -> list[str]:
"""
Discover all the schemas in the postgres database.
"""
@tool(requires_secrets=["DATABASE_CONNECTION_STRING"])
async def discover_tables(
context: ToolContext,
schema_name: Annotated[
str, "The database schema to discover tables in (default value: 'public')"
] = "public",
) -> list[str]:
"""
Discover all the tables in the postgres database when the list of tables is not known.
ALWAYS use this tool before any other tool that requires a table name.E.
"""
Obtener esquema de tabla:
@tool(requires_secrets=["DATABASE_CONNECTION_STRING"])
async def get_table_schema(
context: ToolContext,
schema_name: Annotated[str, "The database schema to get the table schema of"],
table_name: Annotated[str, "The table to get the schema of"],
) -> list[str]:
"""
Get the schema/structure of a postgres table in the postgres database when the schema is not known, and the name of the table is provided.
This tool should ALWAYS be used before executing any query. All tables in the query must be discovered first using the <DiscoverTables> tool.
"""
Ejecutar consulta SELECT:
@tool(requires_secrets=["DATABASE_CONNECTION_STRING"])
async def execute_select_query(
context: ToolContext,
select_clause: Annotated[
str,
"This is the part of the SQL query that comes after the SELECT keyword wish a comma separated list of columns you wish to return. Do not include the SELECT keyword.",
],
from_clause: Annotated[
str,
"This is the part of the SQL query that comes after the FROM keyword. Do not include the FROM keyword.",
],
limit: Annotated[
int,
"The maximum number of rows to return. This is the LIMIT clause of the query. Default: 100.",
] = 100,
offset: Annotated[
int, "The number of rows to skip. This is the OFFSET clause of the query. Default: 0."
] = 0,
join_clause: Annotated[
str | None,
"This is the part of the SQL query that comes after the JOIN keyword. Do not include the JOIN keyword. If no join is needed, leave this blank.",
] = None,
where_clause: Annotated[
str | None,
"This is the part of the SQL query that comes after the WHERE keyword. Do not include the WHERE keyword. If no where clause is needed, leave this blank.",
] = None,
having_clause: Annotated[
str | None,
"This is the part of the SQL query that comes after the HAVING keyword. Do not include the HAVING keyword. If no having clause is needed, leave this blank.",
] = None,
group_by_clause: Annotated[
str | None,
"This is the part of the SQL query that comes after the GROUP BY keyword. Do not include the GROUP BY keyword. If no group by clause is needed, leave this blank.",
] = None,
order_by_clause: Annotated[
str | None,
"This is the part of the SQL query that comes after the ORDER BY keyword. Do not include the ORDER BY keyword. If no order by clause is needed, leave this blank.",
] = None,
with_clause: Annotated[
str | None,
"This is the part of the SQL query that comes after the WITH keyword when basing the query on a virtual table. If no WITH clause is needed, leave this blank.",
] = None,
) -> list[str]:
"""
You have a connection to a postgres database.
Execute a SELECT query and return the results against the postgres database. No other queries (INSERT, UPDATE, DELETE, etc.) are allowed.
ONLY use this tool if you have already loaded the schema of the tables you need to query. Use the <GetTableSchema> tool to load the schema if not already known.
The final query will be constructed as follows:
SELECT {select_query_part} FROM {from_clause} JOIN {join_clause} WHERE {where_clause} HAVING {having_clause} ORDER BY {order_by_clause} LIMIT {limit} OFFSET {offset}
When running queries, follow these rules which will help avoid errors:
* Never "select *" from a table. Always select the columns you need.
* Always order your results by the most important columns first. If you aren't sure, order by the primary key.
* Always use case-insensitive queries to match strings in the query.
* Always trim strings in the query.
* Prefer LIKE queries over direct string matches or regex queries.
* Only join on columns that are indexed or the primary key. Do not join on arbitrary columns.
"""
Puedes ver un ejemplo de toolkit de Arcade exploratorio para Postgres aquí.
Recuerda que arriba hablamos de comprensión del esquema? Nota cómo estas herramientas de propósito general carecen de eso, lo que significa que no serán tan efectivas como podrían. Imagina que cada una de estas herramientas pudiera recibir contexto adicional sobre la estructura de tu base de datos:
- ¿Cuáles son las tablas finales/gold en tu Medallion Architecture, y por lo tanto el LLM debería preferirlas para la mayoría de las consultas?
- ¿Qué tablas son más útiles para el análisis en general (por ejemplo, usuarios o cuentas)?
- ¿Qué tipos de preguntas prefieren qué tablas (por ejemplo, las preguntas financieras deben empezar con las tablas normalized_accounts)?
- Cualquier “traducción” que el LLM pueda necesitar para encontrar tus datos (por ejemplo, toda la información de pagos y transacciones está en USD, listada en centavos).
¡Darle al LLM preferencias oficiales como hints ahorrará mucho tiempo y tokens!
Sobre la carga dinámica de esquemas:
A medida que tu esquema crece, encontrarás limitaciones de rendimiento y contexto: no podrás precargar todo el esquema en el LLM porque será demasiado grande. Cuando eso ocurra, necesitarás empezar a cargar dinámicamente tu esquema según sea necesario mediante herramientas de “descubrimiento” o técnicas de compresión de memoria.
Considera el ejemplo al inicio de este artículo. Un agente multi-turno usó los hints de nuestras herramientas para construir correctamente el flujo de llamado de herramientas por sí mismo:

Las herramientas definidas arriba fueron la forma en que le indicamos al LLM que inspeccionara la base de datos y encontrara solo las tablas que necesitaba, cargando luego su esquema: ahorrando tiempo y tokens.
Personalización vs. Generalidad
Aunque las herramientas generales de consulta SQL pueden ser útiles, recuerda que las herramientas diseñadas específicamente para un caso de uso serán más confiables y menos propensas a errores que permitirle al LLM construir consultas SQL arbitrarias. El objetivo final de todas las herramientas Exploratorias es convertirlas en herramientas Operativas una vez que tengas tu consulta ajustada. Estamos subiendo las herramientas del nivel “servicio” al nivel “flujo de trabajo”, lo que ofrece mejor calidad y menor latencia.
Podemos clasificar los pasos de este proceso y algunos de los criterios de diseño que cambian:
- Exploratoria (nivel de servicio):
- Herramientas de baja precisión que requieren supervisión humana
- Herramientas muy generales que requieren permisos elevados
- bajo conteo de tokens
- alta dependencia del LLM
- Probablemente un número bajo de herramientas entre las que el LLM puede elegir
- Ejemplo:
execute_query()
- Híbrida:
- Más precisa,
- Todavía general, pero limitada a un dominio específico,
- alto conteo de tokens
- Ejemplo
GetRecentSalesWins()
- Operativa (nivel de flujo de trabajo)
- Muy precisa, adecuada para la operacionalización
- Muy específica y puede funcionar con permisos de alcance restringido
- conteo de tokens muy alto,
- baja dependencia del LLM
- Probablemente un número alto de herramientas entre las que el LLM puede elegir
- Ejemplo:
GetRecentSalesWinsBySalesperson()
¿Qué sigue?
Para cerrar, síes posible construir herramientas SQL efectivas y seguras para agentes. Pero necesitas tener claro qué herramientas puede invocar el agente y crear límites específicos para mantenerlas seguras.
*Protege tu analytics de Postgres en minutos.*
Haz fork de nuestro Postgres SQL Analytic Toolkit, restríngelo con roles de mínimo privilegio, adáptalo a tu esquema y despliégalo en Arcade en menos de 6 minutos.*

