La plupart des frameworks d'agents IA rendent la partie facile facile — connecter un LLM à quelques outils — et la partie difficile invisible. Dès que l'agent appelle le mauvais outil, boucle à l'infini ou gaspille des jetons sur une invite défaillante, vous vous retrouvez à scruter des sorties console.log pour reconstituer ce qui s'est passé.
VoltAgent adopte la posture inverse. C'est un framework TypeScript open source où l'observabilité est un citoyen de première classe, et non une arrière-pensée. Chaque exécution d'agent, chaque appel d'outil, chaque délégation à un sous-agent et chaque étape de workflow est tracée et visible dans une console visuelle nommée VoltOps — un canevas de style n8n pour observer vos agents « penser ».
Dans ce tutoriel, vous allez créer un agent de support client de zéro, lui donner des outils et une mémoire persistante, l'exposer en HTTP, coordonner une équipe de sous-agents spécialisés sous un superviseur, et observer chaque étape en direct dans la console développeur.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé (
node --version) - Des bases en TypeScript et
async/await - Une clé API d'un fournisseur de LLM (ce tutoriel utilise OpenAI, mais tout fournisseur de l'AI SDK fonctionne)
- Un éditeur de code — VS Code recommandé
Vous n'avez besoin ni de base de données, ni de Docker, ni de compte cloud. VoltAgent stocke par défaut la mémoire et les traces dans un fichier SQLite local.
Ce que vous allez créer
Un assistant de support qui :
- Répond aux questions à l'aide d'un outil de recherche de commande personnalisé
- Se souvient des conversations entre les requêtes grâce à une mémoire persistante
- S'exécute comme un serveur HTTP appelable depuis n'importe quel frontend
- Délègue le travail spécialisé (résumer, formater) à des sous-agents
- Exécute un workflow déterministe pour l'automatisation en plusieurs étapes
- Diffuse chaque trace d'exécution vers la console VoltOps
Entrons dans le vif du sujet.
Étape 1 : Initialiser le projet
VoltAgent fournit un générateur de projet qui configure TypeScript, le serveur de développement et un agent de départ. Exécutez :
npm create voltagent-app@latest support-agentLa CLI vous demande un nom de projet, un fournisseur d'IA et une clé API. Choisissez OpenAI (ou le fournisseur dont vous avez la clé). Une fois terminé :
cd support-agent
npm run devOuvrez l'URL affichée (la console développeur VoltOps). Vous avez déjà un agent fonctionnel et traçable. Comprenons-le maintenant et reconstruisons-le délibérément.
Les dépendances clés ajoutées par le générateur :
# Déjà installées par le générateur — affichées pour référence
npm install @voltagent/core @voltagent/server-hono @voltagent/libsql @voltagent/logger
npm install @ai-sdk/openai # le fournisseur de modèleUne note sur le champ du modèle : VoltAgent utilise directement le Vercel AI SDK. Vous pouvez passer une simple chaîne comme "openai/gpt-4o-mini" et laisser la passerelle la résoudre, ou passer un objet LanguageModel entièrement construit depuis @ai-sdk/openai. Nous utiliserons la forme chaîne par souci de concision.
Étape 2 : Créer votre premier agent
Créez src/agents/support.ts. Un agent n'est qu'un nom, un ensemble d'instructions (son invite système) et un modèle :
import { Agent } from "@voltagent/core";
export const supportAgent = new Agent({
name: "SupportAssistant",
instructions:
"You are a friendly customer-support assistant for an online store. " +
"Answer concisely. If you need order details, use the available tools. " +
"Never invent order information.",
model: "openai/gpt-4o-mini",
});Maintenant, raccordez-le à une instance VoltAgent et exposez-le en HTTP. Créez src/index.ts :
import { VoltAgent } from "@voltagent/core";
import { honoServer } from "@voltagent/server-hono";
import { supportAgent } from "./agents/support";
new VoltAgent({
agents: { support: supportAgent },
server: honoServer(), // démarre sur le port 3141 par défaut
});Relancez npm run dev. Le serveur démarre sur le port 3141 et votre agent apparaît dans la console VoltOps. Vous pouvez discuter avec lui directement depuis la console — et chaque message produit une trace.
Pour appeler l'agent dans le code, utilisez plutôt generateText :
const response = await supportAgent.generateText(
"What are your shipping options?"
);
console.log(response.text);Pour les interfaces en temps réel, diffusez la réponse jeton par jeton :
const stream = await supportAgent.streamText("Explain your return policy");
for await (const chunk of stream.textStream) {
process.stdout.write(chunk);
}Étape 3 : Donner un outil à l'agent
Un agent conversationnel qui ne sait que parler n'est pas un agent. Les outils permettent au modèle d'agir — interroger une base de données, appeler une API, effectuer un calcul. VoltAgent définit les outils avec createTool et valide leurs entrées avec Zod, de sorte que les arguments produits par le modèle sont contrôlés en type avant même que votre code ne s'exécute.
Créez src/tools/order.ts :
import { createTool } from "@voltagent/core";
import { z } from "zod";
// Imaginons que ceci soit votre base de données
const ORDERS: Record<string, { status: string; eta: string }> = {
"1001": { status: "shipped", eta: "2026-06-29" },
"1002": { status: "processing", eta: "2026-07-02" },
};
export const lookupOrderTool = createTool({
name: "lookup_order",
description: "Look up the status and ETA of a customer order by its ID.",
parameters: z.object({
orderId: z.string().describe("The numeric order ID, e.g. 1001"),
}),
execute: async ({ orderId }) => {
const order = ORDERS[orderId];
if (!order) {
return { found: false, message: "No order with that ID." };
}
return { found: true, ...order };
},
});Attachez-le à l'agent :
import { Agent } from "@voltagent/core";
import { lookupOrderTool } from "../tools/order";
export const supportAgent = new Agent({
name: "SupportAssistant",
instructions:
"You are a friendly customer-support assistant. " +
"Use the lookup_order tool whenever a customer asks about an order. " +
"Never invent order information.",
model: "openai/gpt-4o-mini",
tools: [lookupOrderTool], // [!code highlight]
});Demandez-lui maintenant : « Où est la commande 1001 ? ». Le modèle décide d'appeler lookup_order, transmet { orderId: "1001" }, reçoit le résultat, puis répond en langage naturel. Dans la console VoltOps, vous verrez l'appel d'outil comme un span distinct — ses entrées, sa sortie et sa durée.
Gardez des descriptions d'outils spécifiques et orientées action. Le modèle choisit les outils uniquement d'après le name et la description ; « Rechercher le statut de commande par ID » l'emporte donc sur un vague « assistant commandes ». Utilisez .describe() sur chaque champ Zod — ces indications atterrissent directement dans le schéma d'outil vu par le modèle.
Étape 4 : Ajouter une mémoire persistante
Par défaut, chaque appel est sans état. Pour que l'agent se souvienne d'une conversation entre les requêtes, attachez un fournisseur Memory adossé à LibSQL (SQLite). Créez src/memory.ts :
import { Memory } from "@voltagent/core";
import { LibSQLMemoryAdapter } from "@voltagent/libsql";
export const sharedMemory = new Memory({
storage: new LibSQLMemoryAdapter({
url: "file:./.voltagent/memory.db",
}),
});Attachez-le à l'agent et transmettez un userId (et éventuellement un conversationId) lors de l'appel, afin que VoltAgent sache quel fil charger et compléter :
import { sharedMemory } from "../memory";
export const supportAgent = new Agent({
name: "SupportAssistant",
instructions: "You are a friendly customer-support assistant.",
model: "openai/gpt-4o-mini",
tools: [lookupOrderTool],
memory: sharedMemory, // [!code highlight]
});// Premier tour
await supportAgent.generateText("My name is Sami and order 1002 is late.", {
userId: "cust-42",
conversationId: "ticket-7",
});
// Tour suivant — même fil, l'agent se rappelle le contexte
const reply = await supportAgent.generateText("What was my name again?", {
userId: "cust-42",
conversationId: "ticket-7",
});
console.log(reply.text); // fait référence à « Sami »La conversation persiste dans le fichier SQLite et survit donc aux redémarrages du serveur. Remplacez l'adaptateur LibSQL par une URL Turso (libsql://your-db.turso.io) ou un adaptateur Postgres au passage en production — le code de l'agent ne change pas.
Étape 5 : Coordonner des sous-agents avec un superviseur
Les agents uniques deviennent ingérables à mesure que les responsabilités s'accumulent. La réponse de VoltAgent, ce sont les agents superviseurs : un coordinateur qui délègue à des sous-agents spécialisés, chacun avec ses instructions étroites et ses outils.
Construisez deux spécialistes et un superviseur dans src/agents/team.ts :
import { Agent } from "@voltagent/core";
import { lookupOrderTool } from "../tools/order";
const orderAgent = new Agent({
name: "OrderAgent",
purpose: "Look up and explain order status.",
instructions: "Use lookup_order to answer questions about orders.",
model: "openai/gpt-4o-mini",
tools: [lookupOrderTool],
});
const policyAgent = new Agent({
name: "PolicyAgent",
purpose: "Answer shipping and returns policy questions.",
instructions:
"Answer questions about shipping, returns, and refunds. " +
"Free returns within 30 days; standard shipping is 3 to 5 days.",
model: "openai/gpt-4o-mini",
});
export const supervisor = new Agent({
name: "SupportSupervisor",
instructions:
"Route each customer question to the right specialist. " +
"Use OrderAgent for order-specific questions and PolicyAgent for policy questions.",
model: "openai/gpt-4o-mini",
subAgents: [orderAgent, policyAgent], // [!code highlight]
});Le champ optionnel purpose contrôle la façon dont le superviseur perçoit chaque sous-agent — c'est le résumé court que le coordinateur lit pour décider à qui déléguer, distinct des instructions plus longues que suit le sous-agent lui-même.
Désormais, un seul appel se ramifie automatiquement :
const answer = await supervisor.generateText(
"Is order 1001 shipped, and can I return it if I don't like it?"
);Le superviseur délègue la moitié « commande » à OrderAgent et la moitié « politique » à PolicyAgent, puis fusionne leurs réponses. Dans VoltOps, vous verrez l'arbre de délégation : le span du superviseur, deux spans enfants delegate_task et l'appel d'outil imbriqué dans OrderAgent.
Annuler une exécution
Les appels multi-agents de longue durée doivent être annulables. Transmettez un AbortController et le signal se propage à chaque sous-agent et outil :
const controller = new AbortController();
setTimeout(() => controller.abort("Deadline reached"), 10_000);
const response = await supervisor.streamText("Research and summarize all open tickets", {
abortController: controller,
});Étape 6 : Construire un workflow déterministe
Les agents sont parfaits lorsque vous voulez que le modèle décide quoi faire. Parfois, vous voulez une séquence fixe — valider, puis enrichir, puis notifier — où le LLM n'est utilisé qu'à des étapes précises. C'est le rôle des workflows. Ils s'exécutent comme des chaînes typées, étape par étape, avec leur propre historique d'exécution persistant.
Créez src/workflows/triage.ts :
import { createWorkflowChain } from "@voltagent/core";
import { z } from "zod";
import { supportAgent } from "../agents/support";
export const triageWorkflow = createWorkflowChain({
id: "ticket-triage",
name: "Ticket Triage",
input: z.object({ message: z.string() }),
result: z.object({ category: z.string(), reply: z.string() }),
})
.andThen({
id: "classify",
execute: async ({ data }) => {
const category = data.message.toLowerCase().includes("order")
? "order"
: "general";
return { ...data, category };
},
})
.andThen({
id: "respond",
execute: async ({ data }) => {
const res = await supportAgent.generateText(data.message);
return { category: data.category, reply: res.text };
},
});Enregistrez le workflow sur l'instance VoltAgent pour qu'il apparaisse dans la console et obtienne un historique d'exécution persistant :
import { VoltAgent, Memory } from "@voltagent/core";
import { honoServer } from "@voltagent/server-hono";
import { LibSQLMemoryAdapter } from "@voltagent/libsql";
import { supervisor } from "./agents/team";
import { triageWorkflow } from "./workflows/triage";
new VoltAgent({
agents: { support: supervisor },
workflows: { triage: triageWorkflow },
workflowMemory: new Memory({
storage: new LibSQLMemoryAdapter({ url: "file:./.voltagent/workflows.db" }),
}),
server: honoServer({ port: 3141 }),
});Les entrées et sorties de chaque étape andThen sont typées de bout en bout via Zod, et chaque exécution est rejouable dans la console. C'est là que l'observabilité de VoltAgent prend tout son sens : un workflow qui casse à l'étape 3 sur 5 vous montre exactement quelle étape, avec les données qui y sont entrées.
Étape 7 : Activer l'observabilité complète
Jusqu'ici, les traces vivent en mémoire pour la session de développement. Pour les persister — et utiliser la console VoltOps hébergée pour la supervision en production — ajoutez un fournisseur VoltAgentObservability et un logger structuré.
import {
VoltAgent,
VoltAgentObservability,
} from "@voltagent/core";
import { honoServer } from "@voltagent/server-hono";
import { createPinoLogger } from "@voltagent/logger";
import { LibSQLObservabilityAdapter } from "@voltagent/libsql";
import { supervisor } from "./agents/team";
const logger = createPinoLogger({ name: "support-agent", level: "info" });
new VoltAgent({
agents: { support: supervisor },
server: honoServer(),
logger,
observability: new VoltAgentObservability({
logger,
storage: new LibSQLObservabilityAdapter({
// Fichier local (par défaut) : ./.voltagent/observability.db
// En production via Turso :
// url: "libsql://your-db.turso.io",
// authToken: process.env.TURSO_AUTH_TOKEN,
}),
}),
});Pour diffuser les traces vers la console VoltOps hébergée, créez-y un projet, puis renseignez les clés dans .env :
# .env
VOLTAGENT_PUBLIC_KEY=pk_...
VOLTAGENT_SECRET_KEY=sk_...
OPENAI_API_KEY=sk-...VoltAgent récupère ces variables automatiquement et transmet les traces ; vous obtenez donc un traçage de niveau production, des bacs à sable d'invites et la relecture d'exécutions sans rien instrumenter à la main.
Ne versionnez jamais le fichier .env. Ajoutez-le à .gitignore. La VOLTAGENT_SECRET_KEY accorde un accès complet à votre projet d'observabilité — traitez-la comme un mot de passe et chargez-la depuis un gestionnaire de secrets en production.
Tester votre implémentation
Démarrez le serveur et sollicitez-le avec curl. Le serveur Hono de VoltAgent expose un point de génération par agent :
curl -X POST http://localhost:3141/agents/support/text \
-H "Content-Type: application/json" \
-d '{"input": "Where is order 1001?"}'Vous devriez recevoir un JSON avec la réponse, et une trace correspondante devrait apparaître dans la console en une seconde. Vérifiez chaque capacité :
- Usage d'outil — demandez « Où est la commande 1001 ? » et confirmez l'apparition d'un span
lookup_order - Mémoire — envoyez deux messages avec le même
userIdet vérifiez que le second se rappelle le premier - Délégation — posez une question mêlant commande et politique et confirmez deux spans de sous-agents
- Workflow — déclenchez le workflow de tri et confirmez que les deux étapes s'exécutent dans l'ordre
Dépannage
L'agent n'appelle jamais mon outil. Le modèle décide d'après le name et la description de l'outil. Rendez-les explicites et orientés verbe, et assurez-vous que la requête de l'utilisateur correspond bien à l'outil. Vérifiez la trace : si le modèle a répondu sans span d'outil, la description était probablement trop vague.
La mémoire ne persiste pas entre les redémarrages. Confirmez que vous avez passé une url (un chemin de fichier) à LibSQLMemoryAdapter. Sans URL, il peut utiliser un stockage en mémoire réinitialisé au redémarrage. Confirmez aussi que vous passez les mêmes userId et conversationId à chaque tour.
Erreurs de fournisseur/d'authentification. Assurez-vous que le paquet de fournisseur AI SDK correspondant est installé (@ai-sdk/openai) et que la clé API est dans votre environnement. La chaîne "provider/model" ne se résout que si ce fournisseur est disponible.
Les traces n'atteignent pas la console hébergée. Vérifiez deux fois que VOLTAGENT_PUBLIC_KEY et VOLTAGENT_SECRET_KEY sont définies et que le processus a bien chargé le fichier .env.
Une note pour les équipes MENA
La résidence des données importe au regard de l'INPDP en Tunisie et de la PDPL en Arabie saoudite. VoltAgent aide ici de deux façons. D'abord, la mémoire et l'observabilité utilisent par défaut des fichiers SQLite locaux (LibSQL) — rien ne quitte votre machine tant que vous n'optez pas pour la console hébergée ou une URL Turso/Postgres distante ; vous contrôlez donc où vivent les données de conversation et les traces. Ensuite, parce que la couche modèle est l'AI SDK standard, vous pouvez pointer les agents vers un modèle hébergé régionalement ou auto-hébergé (via un point de terminaison compatible OpenAI) sans réécrire la logique de l'agent — gardant l'inférence, la mémoire et les traces dans votre juridiction quand la conformité l'exige.
Étapes suivantes
- Ajoutez un outil de recherche vectorielle pour que l'agent réponde depuis votre propre base de connaissances
- Connectez des outils externes via le Model Context Protocol (MCP) au lieu d'en écrire chacun à la main
- Remplacez LibSQL par Postgres lorsque vous dépassez un fichier unique
- Raccordez le point HTTP à un frontend Next.js avec une interface en streaming
- Explorez le bac à sable d'invites dans VoltOps pour tester vos instructions en A/B avant la mise en production
Tutoriels connexes sur noqta.tn : créer des agents avec le framework Mastra, le Claude Agent SDK, et ajouter une mémoire persistante avec Mem0.
Conclusion
Vous avez construit un système d'agents complet et observable en TypeScript : un agent de support utilisant des outils, une mémoire persistante, un superviseur déléguant à des spécialistes, un workflow typé et un traçage complet via VoltOps. La leçon que martèle VoltAgent, c'est que les agents que vous ne voyez pas sont des agents auxquels vous ne pouvez pas vous fier en production — et rendre chaque étape traçable dès la première ligne de code transforme le débogage d'un système LLM de devinette en lecture d'une chronologie. Construisez petit, observez les traces, et faites grandir votre équipe d'agents une étape observable à la fois.