< BACK Vieux standard téléphonique avec des fils de cuivre emmêlés éclairés par une seule ampoule chaleureuse au-dessus, photographié en film 35 mm

Construire un Agent IA avec Vercel AI SDK et Supabase

Il y a trois semaines, un client m'a appelé, un fondateur de SaaS basé à Édimbourg, et m'a demandé si je pouvais lui construire un « assistant intelligent » capable de répondre à des questions sur sa base de données produits, de se souvenir des conversations passées, et de prendre des mesures comme créer des tickets de support. Le budget était serré, la deadline encore plus serrée. Je voulais depuis un moment approfondir vraiment le Vercel AI SDK, et c'était le moment.

Ce qui a suivi, c'est trois jours de vrai développement : certaines parties élégantes, quelques erreurs embarrassantes, beaucoup de lecture de code source. Cet article est le tutoriel fonctionnel que j'aurais aimé avoir au départ.

---

Ce Qu'On Construit Vraiment

Un agent IA. Pas un chatbot. La distinction compte plus que les gens ne le pensent.

Un chatbot accepte une entrée et produit une sortie. Un agent fait cela et décide quels outils appeler, dans quel ordre, et peut boucler sur lui-même si quelque chose échoue. Il a une mémoire entre les sessions. Il peut agir, pas seulement répondre.

Notre agent va :

  • Prendre des questions en langage naturel des utilisateurs
  • Interroger une base de données Supabase Postgres en utilisant des appels d'outils
  • Mémoriser l'historique de conversation entre les sessions (persisté dans Supabase)
  • Retourner des réponses structurées et ancrées dans les données

La pile est Next.js (App Router), Vercel AI SDK, Supabase pour à la fois la base de données et l'authentification, et gpt-4o d'OpenAI comme modèle. Vous pourriez remplacer OpenAI par Anthropic ou Mistral avec environ dix lignes de changement, le SDK abstrait le fournisseur de manière nette.

---

Configuration du projet

Commencez par un projet Next.js neuf.

`` npx create-next-app@latest ai-agent --typescript --app --tailwind cd ai-agent ``

Installez les dépendances dont vous avez réellement besoin :

`` npm install ai @ai-sdk/openai @supabase/supabase-js @supabase/ssr zod ``

Zod valide le schéma de vos entrées d'outils. Sans lui, vous faites confiance au modèle pour passer des arguments sensés, ce qu'il fait généralement jusqu'au moment où il ne le fait pas.

Définissez vos variables d'environnement dans .env.local :

`` OPENAI_API_KEY=sk-... NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... SUPABASE_SERVICE_ROLE_KEY=eyJ... ``

Utilisez la clé de rôle de service côté serveur uniquement. La clé anon est correcte pour les flux d'authentification côté client. Ne les mélangez jamais. (Je l'ai fait, dans un environnement de staging en 2022, et j'ai brièvement donné à tous les utilisateurs un accès en lecture au niveau administrateur au CRM d'un client. Ce n'a pas été un vendredi après-midi agréable.)

---

Configuration de Supabase

Vous avez besoin de deux choses de Supabase : une table pour vos données réelles, et une table pour la mémoire de conversation.

Le Tableau de Données

Pour ce tutoriel, supposons que vous construisez une catalogue de produits. Exécutez ceci dans l'éditeur SQL Supabase :

```sql create table products ( id uuid primary key default gen_random_uuid(), name text not null, description text, price_gbp numeric(10, 2), category text, in_stock boolean default true, created_at timestamptz default now() ); ```

Alimentez-le avec 20-30 lignes. Des données réalistes rendent les tests beaucoup meilleurs, j'utilise toujours Mockaroo pour cela car il génère des valeurs propres au domaine au lieu des bêtises "string1, string2".

Le Tableau d'Historique

C'est là que l'historique des conversations réside entre les sessions.

```sql create table conversation_messages ( id uuid primary key default gen_random_uuid(), session_id text not null, role text not null check (role in ('user', 'assistant', 'tool')), content jsonb not null, created_at timestamptz default now() ); create index on conversation_messages (session_id, created_at); ```

create index on conversation_messages (session_id, created_at); ```

Le type jsonb sur content est intentionnel. Le SDK AI transmet le contenu des messages sous forme d'objets structurés (parties texte, parties d'appel d'outil, parties de résultat d'outil), pas des chaînes simples. Le stocker en tant que texte puis essayer de le réanalyser est un casse-tête dont vous n'avez pas besoin.

---

L'itinéraire de l'agent principal

Créez app/api/agent/route.ts. C'est là que réside la logique réelle.

```typescript import { openai } from '@ai-sdk/openai'; import { streamText, tool } from 'ai'; import { createClient } from '@supabase/supabase-js'; import { z } from 'zod';

const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY! );

export async function POST(req: Request) { const { messages, sessionId } = await req.json();

// Charger l'historique depuis Supabase const { data: history } = await supabase .from('conversation_messages') .select('role, content') .eq('session_id', sessionId) .order('created_at', { ascending: true }) .limit(40);

const priorMessages = (history ?? []).map((row) => ({ role: row.role, content: row.content, }));

const allMessages = [...priorMessages...messages];

const result = await streamText({ model: openai('gpt-4o'), system: Vous êtes un assistant produit utile. Vous avez accès à une base de données produits. Utilisez toujours l'outil queryProducts lorsque l'utilisateur pose des questions sur les produits, les prix ou la disponibilité. Ne devinez jamais les détails des produits. Si l'outil ne retourne aucun résultat, dites-le clairement., messages: allMessages, tools: { queryProducts: tool({ description: 'Interroger le catalogue de produits par catégorie, nom ou statut de stock.', parameters: z.object({ category: z.string().optional().describe('Catégorie de produit à filtrer'), searchTerm: z.string().optional().describe('Nom ou mot-clé à rechercher'), inStockOnly: z.boolean().optional().describe('Filtrer pour afficher uniquement les produits en stock'), }), execute: async ({ category, searchTerm, inStockOnly }) => { let query = supabase.from('products').select('*');

if (category) query = query.eq('category', category); if (inStockOnly) query = query.eq('in_stock', true); if (searchTerm) query = query.ilike('name', %${searchTerm}%);

const { data, error } = await query.limit(10);

if (error) return { error: error.message }; return { products: data ?? [] }; }, }), }, maxSteps: 5, onFinish: async ({ response }) => { // Persist the new messages const newMessages = response.messages.map((msg) => ({ session_id: sessionId, role: msg.role, content: msg.content, }));

await supabase.from('conversation_messages').insert(newMessages); }, });

return result.toDataStreamResponse(); } ```

Quelques points méritent d'être soulignés ici.

maxSteps: 5 est la boucle d'agent. Le SDK continuera à appeler les outils et à renvoyer les résultats au modèle, jusqu'à cinq fois, avant de forcer une réponse finale. Réglez ce paramètre trop bas et l'agent abandonne la tâche en cours de route. Réglez-le trop haut et un modèle confus peut rapidement faire monter les coûts API. Cinq est un défaut raisonnable pour la plupart des tâches.

Le rappel onFinish est où vous persistez la mémoire. N'essayez pas de sauvegarder les messages avant que le flux soit terminé, vous n'aurez pas encore les paires complètes d'appels d'outils/résultats.

---

Construire le Frontend

Gardez cela simple. Un hook useChat du SDK AI fait presque tout le travail lourd.

```typescript // app/page.tsx 'use client';

import { useChat } from 'ai/react'; import { useState } from 'react';

export default function AgentPage() { const [sessionId] = useState(() => crypto.randomUUID());

const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({ api: '/api/agent', body: { sessionId }, });

return ( <div className="max-w-2xl mx-auto p-6"> <div className="space-y-4 mb-6"> {messages.map((m) => ( <div key={m.id} className={m.role === 'user' ? 'text-right' : 'text-left'}> <span className="inline-block bg-gray-100 rounded px-3 py-2 text-sm"> {typeof m.content === 'string' ? m.content : '[tool interaction]'} </span> </div> ))} </div> <form onSubmit={handleSubmit} className="flex gap-2"> <input value={input} onChange={handleInputChange} className="flex-1 border rounded px-3 py-2 text-sm" placeholder="Posez une question sur les produits..." disabled={isLoading} /> <button type="submit" disabled={isLoading} className="bg-black text-white px-4 py-2 rounded text-sm"> Envoyer </button> </form> </div> ); } ```

Le sessionId est généré une seule fois lors du montage de la page. Dans une vraie application, liez-le à l'ID de l'utilisateur authentifié ou à un cookie de session persistant. Sinon, chaque rechargement de page réinitialise la mémoire.

---

Rendre l'Agent Vraiment Utile

Voilà le truc : un agent basique qui interroge une base de données est une démo. Un agent utile gère les cas limites. Spécifiquement :

Gérer les Erreurs d'Outil Gracieusement

Si votre fonction execute lève une exception, le SDK la capture et retourne la chaîne d'erreur au modèle. Le modèle essaiera généralement d'expliquer l'erreur à l'utilisateur, ce qui est acceptable. Mais vous voulez que vos fonctions execute retournent les erreurs sous forme de données plutôt que de les lever. Le modèle gère mieux les objets d'erreur retournés que les exceptions capturées, d'après mon expérience.

Ancrage et Hallucinations

Le prompt système est crucial. « Ne pas deviner les détails des produits » n'est pas du remplissage — sans cette instruction, gpt-4o invente occasionnellement des spécifications quand une requête ne retourne rien. C'est vrai, j'ai testé sans cette protection, j'ai posé une question sur un produit inexistant, et le modèle a inventé un prix et une description plausibles. Impressionnant et complètement inutile.

Contrôler la Longueur de l'Historique

Charger 40 messages depuis l'historique (le .limit(40) ci-dessus) est un plafond raisonnable. Au-delà de ça, vous brûlez des tokens sur un ancien contexte qui aide rarement. Pour les agents de longue durée, envisagez la résumé : tous les 30 messages, demandez au modèle de résumer la conversation jusqu'à présent et stockez cela comme un seul message « résumé ».

La documentation du Vercel AI SDK sur les appels d'outils multi-étapes approfondit le format des messages si vous voulez explorer comment les parties de résultat d'outils sont structurées.

---

Déploiement sur Vercel

En supposant que vous utilisez l'App Router, c'est vraiment simple.

  1. Poussez sur GitHub.
  2. Importez le repo dans le tableau de bord Vercel.
  3. Ajoutez vos variables d'environnement dans les paramètres du projet.
  4. Déployez.

L'unique piège : les réponses en streaming nécessitent un runtime qui supporte Web Streams. L'App Router sur Vercel gère cela nativement. Si vous utilisez Pages Router avec une ancienne API de style Express, vous devrez configurer les choses différemment, honnêtement, utilisez juste l'App Router.

La mise en pool de connexions Supabase vaut aussi le coup d'être vérifiée. Les projets Supabase sur le tier gratuit ont une limite de connexion d'environ 60. Si vous interrogez la base de données à chaque requête d'agent avec plusieurs queries, vous pourriez saturer cette limite plus vite que prévu sous charge. Utilisez le connection pooler de Supabase (PgBouncer) en mode transaction pour les déploiements en production.

---

Ce que je ferais différemment la prochaine fois

Quand j'ai livré ceci pour le client d'Édimbourg, quelques trucs m'ont posé problème :

  • J'ai initialement stocké le contenu des messages sous forme de texte dans Supabase, pas en jsonb. Reconstruire des messages structurés d'outils à partir d'une chaîne a été vraiment pénible et j'ai gaspillé la plupart d'un après-midi là-dessus.
  • Je n'avais pas ajouté de rate limiting sur la route. L'équipe du client s'est immédiatement mise à l'assaillir avec de longues queries complexes pendant l'UAT. Ajoutez quelque chose comme Upstash Rate Limit avant de remettre quoi que ce soit à des humains.
  • J'ai défini maxSteps à 10 lors des tests initiaux. Sur une requête mal formulée, le modèle a bouclé à travers six appels d'outils avant de conclure que le produit n'existait pas. Ça fait six allers-retours en base de données et beaucoup de tokens. Cinq est presque toujours suffisant.

L'architecture centrale, cependant, a bien tenu. Vercel AI SDK m'a épargné l'écriture de mon propre parseur de streaming et de ma machine à état pour les appels d'outils, ce qui à lui seul valait bien la dépendance.

---

FAQ

Quels modèles fonctionnent avec Vercel AI SDK en dehors d'OpenAI ?

Le SDK supporte Anthropic (Claude 3.5 Sonnet et autres), Google (Gemini), Mistral, Cohere, et plus encore via des packages de fournisseurs comme @ai-sdk/anthropic. Changer de fournisseur est vraiment juste une ligne dans la plupart des cas, remplacez openai('gpt-4o') par anthropic('claude-3-5-sonnet-20241022') et le streaming, les appels d'outils, et les hooks useChat fonctionnent tous de manière identique. Certains fournisseurs ont des particularités concernant le support des appels d'outils, alors vérifiez le tableau de compatibilité du SDK avant de vous engager.

L'agent peut-il écrire dans Supabase, pas seulement lire ?

Oui, et c'est là que les agents deviennent intéressants et dangereux à parts égales. Vous pouvez écrire un outil createSupportTicket ou updateProductStock de la même façon que vous avez écrit queryProducts. La fonction execute exécute juste un insert ou un update au lieu d'un select. Je recommande fortement des politiques de sécurité au niveau des lignes sur toute table dans laquelle l'agent peut écrire, et gardez les opérations destructrices (delete) entièrement hors de l'ensemble d'outils à moins d'avoir une étape de confirmation dans votre interface.

Comment gérer l'authentification pour que les utilisateurs ne voient que leurs propres données ?

L'approche la plus propre : générez le client Supabase à l'intérieur du gestionnaire de route en utilisant le JWT de l'utilisateur (depuis un cookie ou un en-tête Authorization) plutôt que la clé de rôle de service. De cette façon, les politiques de sécurité au niveau des lignes de Supabase s'appliquent automatiquement. Le package @supabase/ssr dispose de fonctions d'aide pour extraire la session des cookies Next.js. Ne passez pas les ID utilisateur comme paramètres simples à l'agent, le modèle pourrait être trompé pour interroger les données d'un autre utilisateur si le message système n'est pas parfait.

Le Vercel AI SDK est-il prêt pour la production ?

Je dirais qu'il est prêt pour la production avec des réserves. Il est activement maintenu, Vercel déploie les fonctionnalités rapidement, et les primitives de streaming et d'appels d'outils sont stables. Ce qui bouge davantage, ce sont les fonctionnalités expérimentales (comme generateObject avec des schémas d'union complexes). Épinglez votre version et lisez le changelog avant de mettre à jour. Seahawk a deux projets clients en direct en dessus sans problèmes, mais nous épinglons les versions de manière agressive.

Pourquoi Supabase spécifiquement et pas Postgres sur Railway ou PlanetScale ?

Supabase vous donne Postgres plus un client typé, l'authentification, le temps réel, le stockage et les fonctions edge sous un même toit. Pour un projet comme celui-ci, l'intégration de l'authentification et l'éditeur SQL pour itérer rapidement font genuinely gagner du temps. Cela dit, le code agent réel fonctionne avec n'importe quelle base de données compatible Postgres. Remplacez le client Supabase par pg ou Drizzle ORM et rien ne change structurellement.

---

Honnêtement, cette stack m'a surpris par la rapidité de passage de zéro à un agent fonctionnel et persistant en mémoire. Le Vercel AI SDK fait beaucoup de travail invisible : protocole de streaming, sérialisation des appels d'outils, abstraction des fournisseurs. Supabase gère la persistance et l'authentification sans vous demander grand-chose. La complexité qui reste, et c'est la partie que personne ne peut abstraire pour vous, c'est d'écrire un system prompt qui fait que le modèle se comporte réellement bien. Cette partie demande de l'itération. Commencez strict, relâchez progressivement, et testez avec les questions les plus mal formulées que vous pouvez imaginer.

Lectures connexes : Recherche de mots-clés par IA en 2026 : qu'est-ce que c'est, pourquoi le SEO traditionnel, le SEO technique et la recherche par IA.

< BACK