Retour au blog
Tutoriel Technique · 10 min de lecture

Construire un agent Postgres sécurisé avec function calling (tutoriel)

Affane Daylami · Fondateur· 24 août 2026

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).

L’essentiel
  • 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 mode readOnly: true, une transaction Postgres en lecture seule réelle, pas un simple filtre textuel.
  • Le endpoint /chat natif d’Aurabase n’accepte pas encore de rôle tool ni de paramètre tools (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_role contourne 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.
#
Objectif

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.

Info
Ce tutoriel utilise le SDK JavaScript @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.
#
Sous le capot

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.

tool-schema-dangereux.json (anti-pattern)
JSON
{
"name": "execute_sql",
"parameters": {
"query": { "type": "string" } // le modèle écrit du SQL directement
}
}

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é).

Le prompt système n'est pas un contrôle de sécurité
Un tool 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.
#
Étape 1

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.

lib/agent-tools.ts
TYPESCRIPT
export const tools = [
{
type: 'function',
function: {
name: 'query_database',
description:
"Interroge les données du projet en langage naturel. N'accepte pas de SQL : posez une question.",
parameters: {
type: 'object',
properties: {
question: {
type: 'string',
description: 'Question en français sur les données du projet.'
},
},
required: ['question'],
additionalProperties: false
},
},
},
]
#
Étape 2

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.

server/tools/query-database.ts
TYPESCRIPT
// Client initialisé avec la clé service_role, jamais côté navigateur
import { aura } from '@/lib/aurabase'
export async function queryDatabase(question: string) {
const { data: validated, error } = await aura.ai.nl2sql(
question,
undefined,
{ limit: 50 },
)
if (error) return { error: error.message }
const { data: rows, error: execError } = await aura.db.sql(
validated.sql,
[],
{ readOnly: true },
)
if (execError) return { error: execError.message }
return { sql: validated.sql, rows }
}
Astuce
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.
#
Étape 3

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.

Limitation actuelle, pas un choix définitif
Tant qu’Aurabase n’orchestre pas nativement les tool calls, votre backend doit gérer la boucle lui-même avec le SDK OpenAI, Anthropic ou Gemini. NL2SQL et l’exécution SQL restent des appels Aurabase classiques à l’intérieur de cette boucle.
server/agent.ts
TYPESCRIPT
import OpenAI from 'openai'
import { tools } from './lib/agent-tools'
import { queryDatabase } from './tools/query-database'
const openai = new OpenAI()
export async function askAgent(question: string) {
const messages = [{ role: 'user', content: question }]
const first = await openai.chat.completions.create({
model: 'gpt-4.1', messages, tools,
})
const call = first.choices[0].message.tool_calls?.[0]
if (!call) return first.choices[0].message.content
const args = JSON.parse(call.function.arguments)
const result = await queryDatabase(args.question)
const second = await openai.chat.completions.create({
model: 'gpt-4.1',
messages: [
...messages,
first.choices[0].message,
{ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) },
],
})
return second.choices[0].message.content
}

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.

#
Étape 4

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.

résultat du tool (extrait)
JSON
{
"sql": "SELECT count(*) FROM orders WHERE customer_plan = 'premium' AND created_at >= date_trunc('month', now()) LIMIT 50",
"rows": [{ "count": 128 }]
}

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écurité

Sécuriser l'agent avant la mise en production

  • La clé service_role ne 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: true reste actif sur aura.db.sql() pour ce tool précis, même si votre projet a besoin d’écriture ailleurs dans l’application.
  • service_role contourne 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.
#
Honnêteté

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.

#
Aller plus loin

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.

#
Questions Fréquentes

FAQ

Puis-je donner à l’agent un accès en écriture (INSERT/UPDATE) ?+
Techniquement oui, en retirant l’option readOnly et en pointant vers un tool séparé, mais ce n’est pas ce que fait NL2SQL aujourd’hui : le validateur n’autorise que des requêtes SELECT, quelle que soit l’option d’exécution choisie côté client. Un agent en écriture demande un validateur distinct, avec sa propre liste blanche de fonctions et probablement une confirmation humaine avant exécution.
Est-ce compatible avec LangChain, LlamaIndex ou un service comme Azure AI Agent ?+
Oui : ces frameworks orchestrent la boucle de function calling à votre place, mais l’implémentation du tool reste la vôtre. Le même handler (NL2SQL puis exécution en lecture seule) se câble comme fonction du tool déclaré dans LangChain ou dans l’agent Azure, plutôt que de leur laisser exécuter du SQL brut.
ESSAYEZ MAINTENANT

Ajoutez un agent Postgres sécurisé à votre projet.

NL2SQL et l'exécution en lecture seule sont déjà en place, il ne reste que la boucle agent.

Créer un projet Documentation IA
Aucune carte bancaire requise · 500 MB gratuits · 50 000 MAU