Aller au contenu principal
Logo GiwiSoft
Construire un agent autonome avec function calling en Node.js/TypeScript

Construire un agent autonome avec function calling en Node.js/TypeScript

Xavier MARIN Xavier MARIN Non classé 5 min

Les LLM seuls, c’est bien. Un LLM qui peut exécuter des actions, c’est mieux. Le function calling (ou tool use) permet à un agent de décider d’appeler des fonctions pour interagir avec le monde réel : API, base de données, système de fichiers, etc.

J’ai construit un agent autonome en TypeScript capable de planifier, exécuter et s’adapter. Voici comment.

Le principe du function calling

L’idée est simple : on fournit au LLM une description des fonctions disponibles, et il peut décider de les invoquer. Le LLM ne les exécute pas lui-même, il renvoie un appel de fonction, c’est à nous de l’exécuter.

Question : “Quel est le temps dispo sur mon serveur ?”

  • LLM décide d’appeler checkServerHealth(“myserver”)
  • Notre code exécute la fonction
  • On renvoie le résultat au LLM
  • LLM formule la réponse finale

Définition des outils

Commençons par définir les fonctions que notre agent pourra utiliser :

interface Tool {
name: string;
description: string;
parameters: Record<string, unknown>;
execute: (args: any) => Promise<string>;
}

Un outil concret pour interroger un serveur :

const serverHealthTool: Tool = {
name: "check_server_health",
description: "Vérifier la santé d'un serveur (CPU, RAM, disque)",
parameters: {
type: "object",
properties: {
host: { type: "string", description: "Nom ou IP du serveur" },
},
required: ["host"],
},
execute: async ({ host }) => {
const cpu = await getServerCpu(host);
const ram = await getServerRam(host);
const disk = await getServerDisk(host);
return JSON.stringify({ host, cpu, ram, disk });
},
};

L’agent avec Vercel AI SDK

J’utilise le Vercel AI SDK qui gère tout le cycle des tool calls de manière élégante (fonctionne aussi en Node.js, pas besoin de Next.js) :

import { generateText } from "ai";
import { ollama } from "ollama-ai-provider";

const tools = {
check_server_health: {
description: "Vérifier la santé d'un serveur",
parameters: {
type: "object",
properties: {
host: { type: "string" },
},
required: ["host"],
},
execute: async ({ host }: { host: string }) => {
// vérification réelle
return JSON.stringify({ status: "ok", cpu: 45, ram: 62 });
},
},
};

const result = await generateText({
model: ollama("qwen2.5:7b"),
tools,
maxSteps: 5, // nombre max de tool calls en chaîne
prompt: "Check la santé de mon serveur web1 et envoie une alerte Slack si CPU > 80",
});

maxSteps permet à l’agent de faire plusieurs appels d’outils séquentiels, en utilisant le résultat d’un appel pour décider du suivant.

Agent avec mémoire et planification

Un agent un peu plus avancé garde un historique et planifie ses actions :

class Agent {
private messages: Array<{ role: string; content: string }> = [];
private tools: Tool[];

constructor(tools: Tool[]) {
this.tools = tools;
}

async run(task: string) {
this.messages.push({ role: "user", content: task });
let done = false;
let iterations = 0;

while (!done && iterations < 10) {
iterations++;

const response = await this.think();
const toolCall = this.parseToolCall(response);

if (!toolCall) {
done = true; // réponse finale
} else {
const result = await this.executeTool(toolCall);
this.messages.push({
role: "tool",
content: `Résultat de ${toolCall.name}: ${result}`,
});
}
}
return this.messages[this.messages.length - 1].content;
}

private async think(): Promise<string> {
// Appel au LLM avec la liste des outils et l'historique
const response = await callLLM(this.messages, this.tools);
this.messages.push({ role: "assistant", content: response });
return response;
}

private parseToolCall(response: string): { name: string; args: any } | null {
const match = response.match(/TOOL_CALL: (\w+)\((.*)\)/);
if (!match) return null;
return { name: match[1], args: JSON.parse(match[2]) };
}

private async executeTool(toolCall: { name: string; args: any }) {
const tool = this.tools.find((t) => t.name === toolCall.name);
if (!tool) throw new Error(`Outil inconnu: ${toolCall.name}`);
return await tool.execute(toolCall.args);
}
}

Exemple concret : agent DevOps

J’ai créé un agent qui combine plusieurs outils pour gérer mon infrastructure :

const devOpsTools = [
serverHealthTool,
{
name: "slack_notify",
description: "Envoyer une notification sur Slack",
execute: async ({ message }) => {
await fetch(process.env.SLACK_WEBHOOK, {
method: "POST",
body: JSON.stringify({ text: message }),
});
return "Notification envoyée";
},
},
{
name: "run_ssh_command",
description: "Exécuter une commande SSH sur un serveur",
execute: async ({ host, command }) => {
return await execSSH(host, command);
},
},
];

const agent = new Agent(devOpsTools);

await agent.run(
"Vérifie l'espace disque de tous mes serveurs, " +
"si l'un d'eux a moins de 10% de libre, " +
"nettoie les logs de plus de 30 jours et envoie une notif Slack"
);

Le résultat : l’agent enchaîne les appels (vérification disque, nettoyage SSH, notification Slack) en adaptant ses décisions aux résultats intermédiaires.

Agents avec Ollama en local

Le function calling fonctionne aussi en local avec Ollama. Les modèles comme llama3.2 et qwen2.5 supportent nativement les tool calls :

import { ollama } from "ollama-ai-provider";

const model = ollama("qwen2.5:7b", {
simulateStreaming: true,
});

Le principal frein est la taille du contexte : chaque tour d’outil ajoute des messages à l’historique. qwen2.5:7b supporte 32K tokens de contexte, ce qui laisse une bonne marge.

Limitations et bonnes pratiques

Quelques leçons apprises en pratiquant :

  • Validez les entrées des outils : ne faites jamais confiance au LLM pour générer des arguments valides
  • Timeout sur les exécutions : un outil qui bloque fait échouer tout l’agent
  • Limitez le nombre d’itérations : maxSteps ou votre propre compteur pour éviter les boucles infinies
  • Journalisez tout : chaque appel d’outil doit être loggé pour debug
async executeWithTimeout(tool: Tool, args: any, timeout = 10000) {
return Promise.race([
tool.execute(args),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), timeout)
),
]);
}

Conclusion

Le function calling transforme un LLM statique en un agent capable d’agir sur le monde réel. Avec le Vercel AI SDK ou une boucle maison en TypeScript, vous pouvez construire des assistants DevOps, des bots de support, ou des automatisations complexes.

J’utilise le mien au quotidien pour gérer mon homelab : il surveille, alerte et répare automatiquement. La prochaine étape, c’est de lui donner accès à Git pour qu’il crée lui-même ses PR de correction.

Et vous, quel genre d’agent aimeriez-vous construire ?