*Sécurisez vos analytics Postgres en quelques minutes.
Forkez notre Postgres SQL Analytic Toolkit, verrouillez-le avec des rôles à moindre privilège, adaptez-le à votre schéma et déployez sur Arcade.dev en moins de 6 minutes.*
Explorer et exploiter les données de vos bases SQL est l’un des cas d’usage les plus courants pour les agents IA/LLM. Comment construire des agents sûrs et fiables ? Voyons ça de près.

Commencez par les limites, pas par les prompts
Le prompt indique au modèle ce que vous voulez, pas ce qu’il est autorisé à faire. Pensez-y comme à une intention : utile pour l’UX, mais jamais une garantie de sécurité. L’application réelle se joue une couche plus bas : dans le moteur de base de données lui-même ou dans une couche de service à périmètre étroit que vous contrôlez entièrement.
Pour y parvenir, vous aurez besoin de :
- Rôles dédiés - créez un rôle DB par toolkit, votre modèle de permissions devient ainsi auto-documenté.
- Réduire la surface d’exposition
- Accès - Si votre agent n’a pas besoin d’accès en écriture, ne le lui accordez pas.
- Tables - n’exposez que celles dont l’agent a réellement besoin.
- Colonnes - excluez ssn, password_hash et toute autre donnée personnelle.
- Lignes - activez la Row‑Level Security (RLS) pour que les agents ne voient que leur portion de données.
- Associez les connexions à ces rôles - utilisez un pool de connexions auquel le LLM ne peut pas accéder, via un serveur d’outils distant. N’accordez jamais à l’agent la possibilité de modifier cela avec des commandes comme SET ROLE. De plus, pour accélérer votre agent, maintenez un pool de connexions actif créé au démarrage de l’agent, et ne laissez pas l’agent le modifier.
Exemple 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;
D’autres moteurs proposent des mécanismes similaires : les Secure Views de Snowflake, les Authorized Views de BigQuery, et le Dynamic Data Masking de SQL Server appliquent tous le même principe : moindre privilège par défaut, appliqué là où les données résident.
C’est la raison pour laquelle des problèmes de sécurité récents ont émergé : des outils IA (p. ex. des serveurs MCP) créés avec des accès trop permissifs (ex.). Ne faites pas ça !
Lorsque vous concevez votre outil, réfléchissez à la relation entre les permissions du compte de base de données et les rôles de vos utilisateurs finaux. Plusieurs options s’offrent à vous :
- Utilisateur unique : L’agent est mono-utilisateur, et son accès à la base de données est identique à celui de l’utilisateur. Dans ce cas, stocker la chaîne de connexion en variable d’environnement suffit.
- Rôle unique : L’agent est multi-utilisateur, mais son accès à la base de données est le même pour tous. Par exemple, l’« agent finance » sera uniquement utilisé par les membres du département financier, ou l’« agent analytics » accède à des tables visibles par toute l’entreprise. Là encore, stocker la chaîne de connexion comme variable d’environnement globale est approprié.
- Rôles hétérogènes : Plusieurs types d’utilisateurs utilisent les agents, chacun avec des accès différents. L’agent doit alors gérer plusieurs connexions selon le profil de chaque utilisateur. Chacun peut être amené à stocker sa propre chaîne de connexion comme secret, ou vous irez chercher les permissions de chaque utilisateur dans un système commeDreamFactory ou un serveur d’habilitations similaire.
Outils opérationnels ou exploratoires ?
Ces deux grandes catégories d’outils SQL pour agents IA ont des conceptions et des exigences de sécurité très différentes. On peut les classer en « Opérationnels » et « Exploratoires ». Les outils opérationnels ont des cas d’usage clairement définis et peuvent même modifier des données en toute sécurité. Les outils exploratoires servent à l’exploration et au reporting, et doivent gérer des cas d’usage inconnus sur des schémas étendus.
Outils opérationnels : précision et contrôle
Les outils SQL opérationnels sont conçus pour des interactions spécifiques, souvent transactionnelles, avec la base de données. Ils servent généralement à modifier des données (insertions, mises à jour, suppressions) ou à récupérer des données très structurées. La priorité : précision, prévisibilité et sécurité.
Les agents sont plus exposés aux attaques par injection SQL que les logiciels traditionnels, en raison d’une couche d’interprétation supplémentaire dès lors que le LLM rédige une partie de la requête. Vous devez vous protéger contre ces attaques comme pour toute application interagissant avec une base de données. Évitez autant que possible de laisser votre LLM écrire des instructions SQL. Construisez plutôt des outils opérationnels avec les propriétés suivantes :
- Requêtes préparées : Utilisez systématiquement des instructions SQL préparées pour éviter les vulnérabilités par injection. L’agent IA fournit des paramètres liés à l’instruction pré-compilée, plutôt que de construire des requêtes SQL brutes.
- Méthodes spécifiques et validation des entrées : Chaque outil opérationnel doit exposer des méthodes (fonctions) clairement définies que l’agent IA peut appeler. Ces méthodes doivent inclure une validation robuste des entrées, pour s’assurer que toutes les données reçues respectent les types, formats et plages attendus.
- Énumérer les valeurs autorisées : Pour les champs n’acceptant qu’un ensemble limité de valeurs, utilisez des enums ou des tables de référence pour restreindre les choix de l’agent IA. Cela évite la génération de données invalides ou malveillantes.
- Moindre privilège : Comme évoqué précédemment, ces outils doivent fonctionner avec le minimum absolu de permissions sur la base de données.
Exemples d’outils opérationnels
Les outils opérationnels ont des descriptions de types comme :
# 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
"""
Ce qui produit une requête du type :
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
Et
# 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.
"""
Ce qui produit une requête du type :
UPDATE users SET payment_plan = $1 WHERE id = $2 LIMIT 1
Notez l’argument ToolContext dans les exemples ci-dessus. C’est la façon d’Arcade de transmettre les secrets et les informations utilisateur à l’outil sans passer par le LLM (non sécurisé). Vous ne voulez jamais qu’un élément aussi sensible qu’une chaîne de connexion ou un mot de passe soit accessible au LLM : il pourrait l’afficher, le divulguer, voire s’entraîner dessus. En savoir plus sur ToolContextici.
Outils exploratoires : exploration et insights
Les outils SQL exploratoires sont, eux, conçus pour interroger les données et en extraire des insights. Le cas d’usage le plus courant : permettre aux utilisateurs internes de votre organisation d’accéder à votre entrepôt de données. Un entrepôt de données est une base de données massive (souventSnowflake ou Databricks) qui regroupe toutes les données de tous les outils utilisés par votre entreprise. Elle est synchronisée via un outil ELT/ETL commeAirbyte.
La principale contrainte de ces outils est qu’ils doivent être en lecture seule. Cette restriction fondamentale réduit considérablement la surface d’attaque.
- Lecture seule obligatoire : Les rôles de base de données associés aux outils d’exploration doivent disposer explicitement des seuls privilèges `SELECT`, sans aucune permission `INSERT`, `UPDATE` ou `DELETE`. Les outils SQL bien conçus s’assurent également que seules les requêtes SELECT peuvent être exécutées. Encore une fois, Les agents sont encore plus vulnérables aux attaques par injection SQL ! Bloquez-les au niveau de la connexion.
- Compréhension du schéma : Vos agents doivent savoir quelles tables existent et à quoi elles servent. La meilleure approche consiste à charger à l’avance les descriptions des tables dont vous avez besoin dans le contexte, ainsi que toutes les métadonnées de référence disponibles. Vous disposez peut-être d’une couche sémantique (par ex. dbt ou Cube) que vous pouvez utiliser, ou d’annotations de tables chargeables depuis votre base de données. À défaut, décrivez votre schéma en texte libre.
- Descriptions des tables : Pour permettre à l’agent IA d’interroger efficacement la base, fournissez des descriptions claires et concises des tables et de leurs colonnes. Toutes vos tables ne seront probablement pas utiles pour tous les cas d’usage (ce qui gaspillerait des tokens) ; encouragez donc le LLM/Agent à apprendre le schéma des tables pertinentes avant de les interroger. Un workflow « Observer → Planifier → Requêter » fonctionne très bien.
- Bonnes pratiques de requêtage : Même si les outils exploratoires peuvent être plus généraux, encouragez des pratiques telles que :
- Limiter les résultats : Exigez une clause `LIMIT` dans les requêtes pour éviter des résultats excessivement volumineux.
- Sélection de colonnes précise : Privilégiez la sélection des seules colonnes nécessaires plutôt que `SELECT *`.
- `EXPLAIN ANALYZE` (pour le développement et le débogage) : Sans être destiné à être exécuté directement par le LLM en production, analyser le plan de requête peut s’avérer utile lors du développement des outils.
- RetryableToolErrors pour l’apprentissage du workflow : Implémentez des types d’erreurs personnalisés comme RetryableToolError lorsque le LLM tente une requête exploratoire invalide (par ex. en hallucinant des colonnes inexistantes). Cela lui signale qu’il doit inspecter les tables disponibles et leurs descriptions avant de tenter la requête suivante, lui enseignant ainsi un workflow plus robuste. Pour en savoir plus sur les erreurs d’outil réessayables, consultez cet article.
Exemples d’outils exploratoires
Un bon ensemble d’outils SQL exploratoires pour l’exploration de données pourrait ressembler à :
Découvrir le schéma :
@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.
"""
Obtenir le schéma d’une table :
@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.
"""
Exécuter une requête 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.
"""
Vous pouvez voir un exemple de toolkit Arcade Postgres exploratoire ici.
Vous vous souvenez que nous avons évoqué plus haut compréhension du schéma ? Remarquez que ces outils généralistes en sont dépourvus, ce qui les rend moins efficaces qu’ils pourraient l’être. Imaginez que chacun de ces outils puisse recevoir un contexte supplémentaire sur la structure de votre base de données :
- Quelles sont les tables finales/gold dans votre Architecture Medallion, et donc celles que le LLM devrait privilégier pour la plupart des requêtes ?
- Quelles tables sont les plus utiles pour l’analyse en général (ex. : utilisateurs ou comptes ?).
- Quels types de questions correspondent à quelles tables (ex. : les questions financières devraient commencer par les tables normalized_accounts).
- Les « traductions » dont le LLM pourrait avoir besoin pour trouver vos données (ex. : toutes les informations de paiement et de transaction sont en USD, exprimées en centimes).
Indiquer au LLM vos préférences officielles permettra d’économiser beaucoup de temps et de tokens !
Sur le chargement dynamique du schéma :
À mesure que votre schéma grossit, vous rencontrerez des limites de performance et de contexte : impossible de pré-charger l’intégralité du schéma dans le LLM, ce sera trop volumineux. Le moment venu, vous devrez explorer le chargement dynamique du schéma à la demande via des outils de « découverte », ou des techniques de compression mémoire.
Reprenons l’exemple du début de cet article. Un agent multi-tour a utilisé les indications de nos outils pour construire lui-même le workflow d’appel d’outils adapté :

Les outils tels que définis ci-dessus nous ont permis de guider le LLM pour qu’il inspecte la base de données, ne charge que les tables nécessaires, puis récupère leur schéma (économisant ainsi temps et tokens).
Personnalisation vs. généralité
Les outils SQL généralistes peuvent être utiles, mais gardez à l’esprit que des outils conçus spécifiquement pour un cas d’usage seront plus fiables et moins sujets aux erreurs que laisser le LLM construire des requêtes SQL arbitraires. L’objectif final de tous les outils Exploratoires est de les convertir en outils Opérationnels, une fois votre requête bien rodée. Nous faisons monter les outils du niveau « service » au niveau « workflow », ce qui améliore la qualité et réduit la latence.
Nous pouvons classer les étapes de ce parcours et quelques critères de conception qui évoluent :
- Exploratoire (niveau service) :
- Outils peu précis qui nécessitent un accompagnement humain
- Outils très généralistes qui requièrent des permissions élevées
- faible consommation de tokens
- forte dépendance au LLM
- Probablement un petit nombre d’outils parmi lesquels le LLM peut choisir
- Exemple :
execute_query()
- Hybride :
- Plus précis,
- Encore généraliste, mais limité à un domaine spécifique,
- consommation élevée de tokens
- Exemple
GetRecentSalesWins()
- Opérationnel (niveau workflow)
- Très précis, adapté à l’opérationnalisation
- Très spécifique et compatible avec des permissions strictement délimitées
- consommation très élevée de tokens,
- faible dépendance au LLM
- Probablement un grand nombre d’outils parmi lesquels le LLM peut choisir
- Exemple :
GetRecentSalesWinsBySalesperson()
Et maintenant ?
Pour conclure, est possible de créer des outils SQL efficaces et sécurisés pour les agents. Mais vous devez définir clairement quels outils l’agent peut appeler, et fixer des limites précises pour les garder sous contrôle.
*Sécurisez votre analytics Postgres en quelques minutes.
Forkez notre Postgres SQL Analytic Toolkit, verrouillez-le avec des rôles à moindres privilèges, adaptez-le à votre schéma et déployez sur Arcade en moins de 6 minutes.*

