Construire un agent Postgres sécurisé avec function calling (tutoriel)
Un agent qui interroge Postgres pose une question de sécurité précise avant la première ligne de code : quelle fonction exposez-vous au modèle ? Si le tool que le LLM peut appeler exécute directement le SQL qu’il a lui-même écrit, une question ambiguë ou une injection de prompt suffit à lire n’importe quelle table du projet.
Ce tutoriel construit un agent avec function calling où le tool exposé au modèle n’exécute jamais de SQL arbitraire. Il combine deux mécanismes déjà vérifiés dans le code d’Aurabase : le validateur NL2SQL et une transaction Postgres en lecture seule, deux briques de l’IA native intégrée au backend. Prérequis : un projet Aurabase, sa clé service_role, et un compte chez l’un des trois fournisseurs LLM natifs (OpenAI, Anthropic, Gemini).
- Le risque réel n’est pas le function calling en soi, mais le tool exposé au modèle : un
execute_sql(query)brut lui donne un accès SQL total. - L’architecture sûre expose un tool
query_database(question)qui délègue à un validateur d’arbre syntaxique (SELECT seul, LIMIT borné, schéma isolé) plutôt qu’à une exécution directe. - Aurabase expose ce validateur nativement (
/nl2sql) : le réutiliser comme implémentation du tool évite de recoder la validation SQL soi-même. - Le SQL validé s’exécute ensuite via
aura.db.sql()en modereadOnly: true, une transaction Postgres en lecture seule réelle, pas un simple filtre textuel. - Le endpoint
/chatnatif d’Aurabase n’accepte pas encore de rôletoolni de paramètretools(vérifié dans le code) : la boucle agent tourne aujourd’hui via le SDK du fournisseur LLM, pas via le proxy Aurabase. - La clé
service_rolecontourne la RLS par conception : elle ne doit jamais quitter votre backend, et l’agent hérite d’un accès plus large qu’un utilisateur authentifié classique.
Ce que vous allez construire
Vous allez construire un agent qui répond à des questions en langage naturel sur les données d’un projet Postgres, sans jamais laisser le modèle écrire du SQL qui s’exécute tel quel. Le modèle appelle un tool nommé query_database, ce tool traduit la question en SQL validé via NL2SQL, puis exécute ce SQL en lecture seule et renvoie les lignes au modèle pour qu’il formule sa réponse.
@aurabase/aurabase-js côté serveur (jamais côté navigateur, la clé service_role ne doit pas être exposée au client) et l’API function calling d’OpenAI pour la boucle agent. Le même principe s’applique avec le SDK Anthropic ou Gemini.Pourquoi un tool « exécute ce SQL » est dangereux
La plupart des tutoriels d’agent Postgres, y compris certains guides officiels, définissent un tool unique : une fonction execute_sql qui prend une chaîne SQL en argument et l’exécute telle quelle. Le modèle écrit lui-même cette chaîne, à partir de la question de l’utilisateur et du schéma qu’on lui a donné en contexte.
Ce choix transfère au modèle une responsabilité qu’il ne peut pas tenir de façon fiable. Une injection de prompt glissée dans la question peut produire un SQL destructeur que le tool exécute sans discernement, puisqu’il n’a aucune notion de ce qu’une requête « légitime » devrait ressembler. Notre article dédié détaille ce vecteur d’attaque : sécuriser le NL2SQL contre l’injection SQL.
L’alternative construite dans ce tutoriel expose un tool plus étroit, query_database(question). Le modèle ne peut plus écrire de SQL directement : il ne peut que poser une question dans son propre appel de tool. C’est le moteur NL2SQL d’Aurabase qui traduit cette question en SQL, avant de le faire passer par un validateur d’arbre syntaxique (SELECT seul, pas de sous-requêtes, dix fonctions autorisées, LIMIT borné).
execute_sql(query: string) donne au modèle un accès SQL total, quelle que soit la qualité de votre prompt système. Une instruction (« n’exécute que des SELECT ») reste une consigne que le modèle peut suivre, mal interpréter, ou voir contournée par une injection glissée dans la question de l’utilisateur.Définir le schéma du tool exposé au modèle
Les trois fournisseurs LLM natifs d’Aurabase (OpenAI, Anthropic, Gemini) acceptent un tableau de définitions de tools au format JSON Schema. Un seul tool suffit pour cet agent : query_database, qui prend une question en langage naturel et rien d’autre. Le modèle ne voit ni le schéma SQL, ni un champ query qu’il pourrait remplir lui-même.
Implémenter le tool : NL2SQL puis lecture seule
Le handler du tool tourne sur votre backend, jamais dans le navigateur. Il porte la clé service_role du projet, qui contourne la RLS par conception et ne doit donc jamais être exposée à un client. Il enchaîne deux appels au SDK Aurabase.
Le premier appel traduit la question en SQL validé via aura.ai.nl2sql() : SELECT uniquement, LIMIT borné, aucun accès au catalogue système. Le second exécute ce SQL déjà validé via aura.db.sql(), avec l’option readOnly: true : Postgres lui-même refuse alors toute écriture dans cette transaction, indépendamment de la validation textuelle déjà appliquée par NL2SQL en amont.
readOnly: true déclenche une vraie transaction Postgres en lecture seule : le moteur refuse l’écriture, ce n’est pas un filtre appliqué au texte de la requête. Combiné à la validation SELECT-only de NL2SQL, l’agent dispose de deux couches indépendantes : si l’une a une faille, l’autre tient encore.La boucle agent : function calling côté SDK du fournisseur
Aurabase expose trois fournisseurs LLM natifs, mais son endpoint /chat ne relaie pas encore de paramètre tools ni de rôle tool. ChatOptions ne porte que temperature, max_tokens et model, et les rôles acceptés se limitent à system, user et assistant (vérifié dans llm/mod.rs et handlers/chat.rs). La boucle de function calling tourne donc aujourd’hui directement via le SDK du fournisseur, pas via le proxy Aurabase.
Le principe reste identique si vous orchestrez l’agent avec LangChain ou un service comme Azure AI Agent : le tool déclaré au framework doit rester ce même query_database, jamais un exécuteur SQL brut. Notre comparatif détaille où LangChain et LlamaIndex apportent une valeur réelle sur Postgres, et où ils ajoutent surtout de la complexité : agents Postgres avec LangChain ou LlamaIndex.
Tester avec une vraie question
Question envoyée à l’agent : « Combien de clients premium ont passé une commande ce mois-ci ? ». Le modèle appelle query_database avec cette question telle quelle, sans jamais voir ni écrire de SQL. Voici le résultat des deux appels internes déclenchés par le tool.
La réponse finale du modèle s’appuie sur ces lignes réelles, pas sur une supposition. Si le tool renvoie zéro ligne, une hallucination de chiffre devient nettement moins probable qu’avec un modèle qui répondrait sans données vérifiées.
Sécuriser l'agent avant la mise en production
- La clé
service_rolene quitte jamais votre backend : ni dans le prompt envoyé au modèle, ni dans un log, ni dans une variable d’environnement côté client. readOnly: truereste actif suraura.db.sql()pour ce tool précis, même si votre projet a besoin d’écriture ailleurs dans l’application.service_rolecontourne la RLS par conception. Si l’agent doit répondre différemment selon l’utilisateur qui pose la question, filtrez explicitement dans le SQL ou revenez aux endpoints PostgREST classiques, qui respectent la RLS. Voir isolation RLS multi-tenant.- Journalisez chaque appel de tool (question posée, SQL validé, nombre de lignes) : c’est la seule trace exploitable si une question produit un résultat inattendu.
- Le rate limiting et le quota mensuel d’Aurabase s’appliquent déjà par projet sur
/nl2sql: un agent bavard ne peut pas dépasser silencieusement votre budget IA.
Limites actuelles à connaître
Le tool query_database hérite de toutes les limites du validateur NL2SQL : pas de sous-requêtes, pas de CTE/WITH, pas d’UNION, et une liste fermée de dix fonctions SQL. Une question qui appelle naturellement une sous-requête (« les clients qui n’ont jamais commandé ») doit être reformulée, ou traitée par un second tool dédié plutôt que forcée dans NL2SQL.
Aucune orchestration de tool calls n’existe aujourd’hui à l’intérieur du proxy /chat d’Aurabase : la boucle agent décrite ici vit dans votre code applicatif, pas dans un service managé. Si l’agent doit enchaîner plusieurs tools (base de données et RAG documentaire, par exemple), c’est votre backend qui orchestre les deux appels.
RAG et function calling combinés
Ce tutoriel couvre les questions structurées sur des données relationnelles. Pour des questions sur du contenu non structuré (documents, tickets, notes), le même agent peut exposer un second tool branché sur le RAG natif d’Aurabase (pgvector, recherche HNSW). Les deux capacités et leur articulation sont détaillées sur la page IA native sur Postgres.