Accueil / Articles

Agents IA contre backends traditionnels : flux de contrôle, coûts et modes de défaillance

Comparer un backend déterministe avec un cycle d’agent LLM à l’aide d’un exemple de remboursement : latence, coûts des tokens, croissance du contexte, modes de défaillance, et les cas où une conception hybride convient le mieux.

Éditeur : REACTAPP.TOP9 min de lecture1966 mots

Sources: AI Agents vs Traditional Backend Applications: What’s Actually Different?

L’ingénierie backend s’est longtemps appuyée sur une propriété rassurante : le déterminisme. En écrivant un point d’entrée qui prend une commande, vérifie les stocks dans PostgreSQL et charge une carte via Stripe, la même séquence s’exécute à chaque fois. Avec les mêmes entrées et le même état de la base de données, la ligne 42 s’exécute toujours avant la ligne 43.

Puis l’industrie a commencé à qualifier les agents d’intelligence artificielle de la prochaine étape des systèmes backend. Il est facile d’ignorer cela en le considérant comme du marketing, car un agent ressemble également à un service dans un conteneur qui appelle des API externes et écrit dans une base de données. Cependant, au fond, le flux de contrôle a changé. Dans un backend classique, le développeur prévoit à l’avance chaque étape, chaque branche et chaque cas limite. Avec un agent, le développeur fournit un objectif ainsi qu’un ensemble d’outils, et un modèle probabiliste choisit les étapes à suivre, leur ordre et le moment où la tâche est terminée. Si vous traitez un agent comme n’importe quel autre microservice, vous rencontrerez de sérieux problèmes en termes de latence, de fiabilité, de tests et de factures cloud.

Pipeline fixe versus boucle de raisonnement

Un backend standard traite une demande à travers un graphe acyclique dirigé que le programmeur a codé en dur :

[Request] ──> [Validate Input] ──> [Database Query] ──> [Transform Data] ──> [Response]
                     │
                     └── (Invalid) ──> [Return 400]

Chaque branche, qu’il s’agisse de if/else, switch ou try/catch, est déterminée à l’avance. Le code ne peut pas inventer une étape que vous n’avez pas écrite. Une entrée inattendue provoque une erreur ou un mécanisme de secours prédéfini.

Un agent ne suit pas de ligne statique. Il évolue au sein d’une boucle en temps de exécution, souvent basée sur le modèle ReAct, abréviation de Reason plus Act. Le backend transfère le flux de contrôle à un grand modèle de langage :

┌────────────────────────┐
                       ▼                        │
[Objective] ──> [LLM Evaluates State]           │
                       │                        │
             Does it need more info?            │
             ├── Yes ──> [Select Tool & Arguments]
             │                  │
             │           [Execute Tool] ────────┘
             │           (API, DB, Search)
             │
             └── No  ──> [Return Final Answer]

Une itération dans la boucle se compose de quatre étapes :

  • Le modèle reçoit l’objectif, le contexte ainsi que les actions déjà effectuées.
  • Il vérifie s’il dispose d’informations suffisantes ou s’il a besoin d’un outil externe.
  • Si des données sont nécessaires, il envoie une demande structurée à un outil, comme lire un fichier, appeler une API ou exécuter une requête.
  • Votre environnement d’exécution lance cet outil, insère la sortie brute dans la fenêtre de contexte du modèle et relance la boucle.

Ce qui est important, c’est que le modèle choisit dynamiquement la séquence des étapes en temps réel.

Exemple : remboursement d’un envoi retardé

Prenons un outil interne qui rembourse un client dont l’envoi a été retardé.

La méthode traditionnelle

Dans un service Express ou FastAPI, vous pourriez écrire un gestionnaire qui recherche la commande, applique une règle stricte, procède au remboursement et envoie une notification :

// Traditional Backend: Strict, deterministic logic
async function handleRefund(orderId: string, customerReason: string) {
  const order = await db.orders.findById(orderId);
  if (!order) {
    throw new NotFoundError("Order not found");
  }
  // Strict business rule written by an engineer
  const isEligible = order.status === "DELAYED" && order.daysDelayed > 5;

  if (!isEligible) {
    return { status: "rejected", reason: "Delay does not meet refund criteria." };
  }
  const refund = await paymentGateway.refund(order.paymentIntentId);
  await db.orders.update(orderId, { status: "REFUNDED" });
  await emailService.sendRefundNotice(order.customerEmail);
  return { status: "success", refundId: refund.id };
}

Ce code s’exécute en moins de 100 millisecondes, ne coûte presque rien en termes de calculs et se comporte de manière identique à chaque fois. Il est cependant fragile. Supposons qu’un client indique que le colis est arrivé à temps mais que le livreur a écrasé la boîte aux lettres. Le système de traitement refuse le remboursement car daysDelayed vaut zéro, étant donné qu’aucune des règles codées en dur ne lui permet de raisonner au-delà d’elles.

La méthode des agents

Un agent décide quels outils appeler en fonction de l’explication du client :

// Agent Loop: The model decides which tools to call
import { generateText, tool } from "ai";
import { z } from "zod";
const tools = {
  fetchOrder: tool({
    description: "Fetch order details and shipping history",
    parameters: z.object({ orderId: z.string() }),
    execute: async ({ orderId }) => db.orders.findById(orderId),
  }),
  issueRefund: tool({
    description: "Issue a monetary refund to the customer",
    parameters: z.object({
      paymentIntentId: z.string(),
      amount: z.number(),
      reason: z.string()
    }),
    execute: async (args) => paymentGateway.refund(args.paymentIntentId, args.amount),
  }),
  escalateToHuman: tool({
    description: "Escalate edge cases or physical damage to a support manager",
    parameters: z.object({ ticketSummary: z.string() }),
    execute: async ({ ticketSummary }) => supportDesk.createEscalation(ticketSummary),
  })
};
// The execution is governed by an evaluation loop
const response = await generateText({
  model: yourConfiguredLLM,
  tools,
  maxSteps: 5, // Prevents infinite execution loops
  prompt: `Customer Issue: "The package arrived on time, but the driver ran over my mailbox. Order #1234."
           Evaluate policy guidelines and take appropriate action.`,
});

Ici, le modèle lit la plainte, récupère la commande, constate que le problème concerne des dommages matériels et non la logistique d’expédition, saute le contrôle habituel de remboursement et appelle escalateToHuman. Cette flexibilité a un prix : l’exécution nécessite trois appels à l’LLM, consomme quelques centimes de tokens et prend 4 secondes, alors que l’intervention humaine n’en nécessite que 50 millisecondes.

Comparaison des deux approches

Presque toutes les hypothèses opérationnelles changent lorsqu’on passe d’un backend déterministe à un système agissant.

  • Flux de contrôle : dans le premier cas, le code définit les étapes, tandis que dans l’autre, c’est l’LLM qui les choisit à l’intérieur d’une boucle.
  • Parcours d’exécution : connu au moment de la compilation contre découvert en temps de exécution.
  • Latence : d’environ 10 à 200 ms et prévisible pour un backend, contre 2 à 30 secondes ou plus et variable pour un agent, en fonction des itérations.
  • Modes de défaillance : erreurs de syntaxe, exceptions non gérées et valeurs nulles pour les backends, arguments fantaisistes, boucles infinies et contraintes non respectées pour les agents.
  • Tests : assertions unitaires, d’intégration et end-to-end d’un côté, LLM-as-a-judge, benchmarks d’évaluation et assertions sémantiques de l’autre.
  • État : données transactionnelles SQL ou NoSQL par rapport à l’historique des conversations, aux limites de la fenêtre de contexte et à la mémoire de travail.
  • Cost : consommation en CPU et en mémoire, qui reste stable et faible, par rapport aux tokens d’entrée et de sortie à chaque itération de boucle, qui peuvent augmenter rapidement.
  • La fenêtre de contexte en tant que mémoire de travail

    Un backend conserve l’état dans des variables, des clés Redis ou des lignes de base de données, et une fonction ne lit que les champs dont elle a besoin. Pour un agent, la fenêtre de contexte constitue sa mémoire de travail à court terme. Chaque fois qu’une outil est exécuté, son résultat est sérialisé en texte, généralement sous forme JSON, puis ajouté à l’historique des prompts. Un cycle typique évolue comme suit :

    • Étape 1 : le prompt de l’utilisateur, d’environ 50 tokens.
    • Étape 2 : la demande d’appel à l’outil par le modèle, d’environ 40 tokens.
    • Étape 3 : le résultat de l’outil, ajoutant environ 500 tokens.
    • Étape 4 : le modèle analyse le prompt élargi et effectue un deuxième appel, ajoutant environ 80 tokens.
    • Étape 5 : le deuxième résultat arrive, ajoutant environ 1 200 tokens.

    Le prompt s’agrandit à chaque cycle. Comme tant le coût que la latence augmentent avec la taille de l’entrée, la cinquième étape est nettement plus lente et coûteuse que la première.

    Une erreur fréquente de conception consiste à renvoyer aux agents des lignes entières de base de données ou des envois API énormes. Si l’outil searchUsers() affiche 50 profils complets dans le contexte, la fenêtre de raisonnement efficace du modèle sera bientôt épuisée. Concevez les outils pour agents comme des API publiques : traitez et nettoyez les résultats afin qu’ils ne contiennent que les champs nécessaires au modèle pour sa prochaine décision.

    Pourquoi il est plus difficile de faire fonctionner les agents

    Un backend défaillant vous fournit un traceur d’erreur. Vous exécutez à nouveau le même envoi localement, examinez les variables et corrigez la faille. Un agent, quant à lui, échoue de manière probabiliste.

    Boucles infinies. Supposons que l’agent appelle une API et reçoive une erreur de validation subtile comme invalid_date_format. Il tente à nouveau avec une chaîne de date légèrement différente, puis encore une fois. Comme il n’y a pas de limite stricte au nombre d’itérations (maxSteps), il peut consommer des centaines de dollars en crédits API en essayant de contourner un validateur.

    Arguments d’outil inventés. Les schémas et la validation assurent une sécurité de type pour du code ordinaire en temps de exécution. Un LLM peut générer du JSON valide conforme à votre schéma 98 % du temps, mais inclure occasionnellement un paramètre imaginaire. Ce à quoi on s’attend :

    // Expected:
    { "customerId": "cust_123", "amount": 50 }
    

    Ce que le modèle émet parfois à haute température :

    // What the model occasionally outputs under high temperature:
    { "user_id": "cust_123", "refund_value": "$50.00" }
    

    Si le temps d’exécution ne met pas en œuvre une analyse stricte du schéma à l’aide d’outils tels que Zod ou Pydantic avant d’exécuter la fonction réelle, votre backend va planter.

    Encadrement imprévisible des requêtes. Une requête qui nécessitait deux étapes hier peut en exiger quatre aujourd’hui simplement parce que l’utilisateur l’a formulée différemment. Cela rend difficile la garantie de délais de réponse conformes aux SLA.

    Choisir la bonne architecture

    Les agents attirent tellement l’attention que les développeurs tentent de construire des systèmes entiers à partir d’eux, ce qui est une erreur coûteuse.

    Préférez le code backend classique dans les cas suivants :

    • Lorsque la logique métier est connue et stable, comme pour l’émission de factures, l’authentification, les opérations CRUD et le calcul des paniers.
    • Lorsque la latence doit être faible, comme dans les processus de paiement, l’autocomplétion de recherche et la messagerie en temps réel.
    • Lorsque les erreurs entraînent des risques de conformité ou financiers, comme dans les registres comptables, les dossiers médicaux et l’application des permissions.
  • Le coût par requête devient crucial à grande échelle, par exemple lors du traitement de millions d’événements par heure.
  • Recourrez à un agent lorsque :

    • Le chemin vers une solution est ambigu, comme trouver un vol avec une escale de trois heures, des repas végétariens et un prix inférieur à 800 dollars.
    • Les données d’entrée sont chaotiques et non structurées, comme des factures PDF variées, des e-mails libres ou des rapports de bugs des clients.
    • Le système doit coordonner des outils hétérogènes sans API commune, par exemple en naviguant sur des sites externes, en explorant la documentation trouvée en chemin ou en triant les problèmes entre plusieurs produits.

    Le design hybride qui fonctionne

    Dans les applications réelles, le design le plus fiable n’est rarement un agent pur. Il s’agit d’un backend déterministe dans lequel un agent est placé à l’intérieur d’une limite contrôlée :

    [Incoming Request] ──> [Traditional API Gateway] ──> Authentication / Rate Limiting
                                  │
                                  ▼
                          [Standard Business Logic] ──> Fast DB Reads / Validation
                                  │
                                  ▼
                        [Targeted Agent Boundary]  ──> Handles unstructured task
                                  │                    (Restricted to 3 safe tools)
                                  ▼
                          [Standard Business Logic] ──> Validates agent output
                                  │
                                  ▼
    [Final Response] <─── [Structured DB Write]
    

    Gardez le routage, l’autorisation, la persistance ainsi que la validation critique des opérations métier dans du code déterministe. N’appelez l’agent que pour les sous-tâches qui nécessitent réellement un raisonnement de type humain et une orchestration flexible des outils.

    En résumé

    Un agent n’est pas supérieur à un backend traditionnel : c’est simplement un paradigme de calcul différent. Les backends privilégient la prévisibilité, la vitesse et le faible coût en suivant des instructions explicites, tandis que les agents privilégient l’adaptabilité et la résolution de problèmes en permettant à un LLM de choisir le chemin d’exécution au sein d’un cycle de raisonnement.

    Ainsi, avant de développer la prochaine fonctionnalité, évitez de vous demander si un agent peut s’en charger, car avec suffisamment de tokens, il le pourra probablement. Demandez plutôt si vous connaissez déjà les étapes nécessaires pour résoudre le problème. Si c’est le cas, écrivez vous-même le code et réservez les agents aux problèmes ouverts que le code ordinaire n’a jamais été suffisamment flexible pour traiter.