Aller au contenu principal
~/GiwiSoft
Oa0pn3oa0pn3oa0p

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

Giwi 5 min de lecture JavaScript, IA

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 :

Un outil concret pour interroger un serveur :

import { tool } from "ai";
import { z } from "zod";

export const serverHealthTool = tool({
description: "Vérifier la santé d'un serveur",
parameters: z.object({
host: z.string().describe("Nom ou IP du serveur"),
}),
execute: async ({ host }: { host: string }) => {
return JSON.stringify({ status: "ok", cpu: 45, ram: 62 });
},
});

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: serverHealthTool
};

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 :

import { generateText, InvalidToolArgumentsError } from "ai";
import { ollama } from "ollama-ai-provider";
import type { CoreMessage } from "ai";

export class Agent {
private tools: Record<string, any>;

constructor(toolDefs: readonly (readonly [string, any])[]) {
this.tools = Object.fromEntries(toolDefs);
}

async run(task: string) {
const messages: CoreMessage[] = [{ role: "user", content: task }];

for (let i = 0; i < 10; i++) {
let result;
try {
result = await generateText({
model: ollama("qwen2.5:7b", {
simulateStreaming: true,
}) as any,
tools: this.tools,
maxSteps: 1,
messages,
});
} catch (error) {
if (error instanceof InvalidToolArgumentsError) {
messages.push({
role: "user",
content: `L'appel à l'outil "${error.toolName}" a échoué: les arguments ${error.toolArgs} sont invalides. Corrige les paramètres requis.`,
} as CoreMessage);
continue;
}
throw error;
}

if (result.finishReason === "stop") {
return result.text;
}

messages.push(...result.response.messages);
}

return "Limite d'itérations atteinte";
}
}

Exemple concret : agent DevOps

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

import { exec } from "node:child_process";
import { promisify } from "node:util";
import { tool } from "ai";
import { z } from "zod";
import { serverHealthTool } from "./tools/server-info.js";
import { Agent } from "./agent.js";

const execAsync = promisify(exec);

function execSSH(command: string): Promise<string> {
return execAsync(command).then(({ stdout, stderr }) => {
if (stderr) return `stdout: ${stdout}\nstderr: ${stderr}`;
return stdout.trim();
}).catch((error: any) => {
return `Erreur SSH: ${error.message}`;
});
}

const devOpsTools = [
["check_server_health", serverHealthTool] as const,
[
"slack_notify",
tool({
description: "Envoyer une notification sur Slack",
parameters: z.object({
message: z.string().describe("Le message à envoyer"),
}),
execute: async ({ message }: { message: string }) => {
console.log(JSON.stringify({ text: message }));
return "Notification envoyée";
},
}),
] as const,
[
"run_ssh_command",
tool({
description: "Exécuter une commande SSH",
parameters: z.object({
command: z.string().describe("Commande SSH à exécuter"),
}),
execute: async ({ command }: { command: string }) => {
return await execSSH(command);
},
}),
] as const,
];

const agent = new Agent(devOpsTools);

const result = 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");
console.log(result);

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.

Voici l’exemple en plus complet sur github.com/Giwi/test-agent

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