Technologie

Voici comment les Agents fonctionnent dans l'ai-service d'Omnilude

AAnonymous
12 min de lecture

Introduction

Cet article fait suite à Pourquoi l'ai-service d'Omnilude a choisi la structure Assistant / Thread / Run. Dans l'article précédent, j'expliquais le rôle de assistant, thread et run. Cette fois, je voudrais montrer comment agent fonctionne réellement au-dessus de cette base.

Quand j'explique cette structure, il y a d'abord un malentendu que je voudrais écarter. Le AiAgent d'Omnilude n'est pas une grande boîte magique. Ce n'est pas non plus une couche qui transforme les assistants en quelque chose de totalement différent. C'est plutôt l'inverse. AiAgent est une structure qui regroupe plusieurs assistants sous forme de nœuds et qui enregistre leur ordre d'exécution sous la forme d'un workflow.

Dit plus simplement :

Si AiAssistant est un preset LLM individuel, AiAgent est le flux de travail qui relie ces presets.

Dans cet article, je vais expliquer comment ce flux est implémenté dans le code, de la manière la plus accessible possible.

Un Agent n'est pas une autre IA, mais un workflow

La première chose à regarder est l'entité AiAgent. Elle contient bien un nom, une description et un type, mais son vrai cœur est le champ workflow. Cette valeur n'est pas stockée dans une table séparée : elle est conservée en JSONB.

Autrement dit, dans Omnilude, un agent ressemble moins à « un objet qui appelle un modèle de façon plus intelligente » qu'à un graphe d'exécution exprimé avec des nœuds et des arêtes.

Par exemple, un agent peut ressembler à ceci :

  • Il reçoit une entrée dans un nœud de départ.
  • Un nœud generate-text utilise un assistant pour générer du texte.
  • Un nœud router décide du chemin suivant.
  • Le nœud final finish organise le résultat.

À ce stade, la différence entre assistant et agent devient beaucoup plus nette.

  • Un assistant est un preset pour une seule inférence.
  • Un agent est le workflow qui définit dans quel ordre et sous quelles conditions ces inférences sont reliées.

Un Agent n'est pas une autre IA, mais un workflow (référence texte)

AiAssistant
  - preset pour un seul appel LLM
  - définit quel model/provider/instructions utiliser

AiAgent
  - workflow qui relie plusieurs nœuds
  - chaque nœud peut utiliser un assistant ou se comporter comme un tool
  - le point clé n'est pas « un appel » mais « l'ordre d'exécution et les connexions »

Une requête ne s'exécute pas immédiatement

Dans Omnilude, l'exécution de AiAgent ne se termine généralement pas par un simple appel synchrone. Le composant important ici est DTE (Distributed Task Executor).

Le flux ressemble globalement à ceci.

  1. Le client ou le backoffice demande l'exécution de l'agent.
  2. Le serveur ne traite pas cela immédiatement et le transforme en job DTE.
  3. WorkflowTaskHandler récupère ce job dans la file.
  4. SingleWorkflowExecutor prend ensuite en charge l'exécution réelle du workflow.
  5. À l'intérieur, BasicWorkflowEngine fait avancer les nœuds un par un.

Cette structure est importante pour une raison simple. L'exécution d'un agent peut durer plus longtemps que prévu, inclure du streaming, nécessiter la conservation d'un état intermédiaire et parfois devoir tourner en arrière-plan. Au lieu de faire entrer tout cela de force dans un cycle request-response classique, il est bien plus stable de traiter l'exécution elle-même comme un job.

Une requête ne s'exécute pas immédiatement (référence texte)

Controller
  -> AiAgentExecutor
  -> DistributedTaskQueue
  -> WorkflowTaskHandler
  -> SingleWorkflowExecutor
  -> BasicWorkflowEngine

Points clés
  - l'exécution est gérée comme un job
  - le streaming et le traitement en arrière-plan deviennent plus simples
  - les exécutions longues peuvent être gérées dans une seule structure

Tout commence par un seul start node

Le fait d'avoir un workflow ne signifie pas que le moteur parcourt simplement une liste du début à la fin. BasicWorkflowEngine cherche d'abord un nœud où startNode=true et dont le type se termine par -input. Il injecte ensuite l'entrée initiale dans ce nœud et démarre l'exécution à partir de là.

J'aime beaucoup cette partie. Elle considère un agent non pas comme « une ligne de commande », mais comme un graphe d'exécution avec un point d'entrée clair.

Par exemple, le nœud text-input prépare la chaîne fournie par l'utilisateur et transmet cette valeur au nœud suivant. Ensuite, le nœud generate-text reçoit cette valeur et déclenche un appel au LLM. Puis l'output se propage vers le nœud suivant en suivant l'edge.

Autrement dit, un agent n'est pas une énorme fonction. C'est un flux composé de petites unités d'exécution reliées entre elles.

Ce qui exécute réellement les nœuds, c'est NodeExecutor

C'est ici qu'apparaît la couche vraiment importante : NodeExecutor. Chaque nœud possède une chaîne de type, et NodeExecutorFactory trouve l'exécuteur correspondant à ce type. De son côté, NodeExecutorRegistry scanne l'annotation @NodeType et enregistre automatiquement quel exécuteur gère quel type.

Grâce à cette structure, ajouter un nouveau nœud ne nécessite pas de réécrire tout le moteur. Il suffit de créer un nœud, de déclarer son type et de lui rattacher son provider pour qu'il rejoigne le runtime.

Il y a quatre nœuds représentatifs qu'il est particulièrement utile de comprendre dans cet article.

  • TextInputNode : prépare l'entrée initiale.
  • GenerateTextNode : utilise un assistant pour générer le texte réel.
  • RouterNode : utilise le LLM pour décider du chemin suivant.
  • FinishNode : rassemble le résultat final et met fin à l'exécution.

Comprendre ces quatre nœuds suffit déjà à se faire une bonne idée du fonctionnement de la structure d'agents d'Omnilude.

Même dans un Agent, l'inférence réelle est faite par les assistants

C'est ici que cet article rejoint à nouveau le précédent.

Même si AiAgent existe, l'agent lui-même n'appelle pas directement le modèle. L'appel réel au LLM a lieu à l'intérieur d'un nœud, et à ce moment-là AiAssistantService réapparaît.

Par exemple, GenerateTextNode lit aiAssistantId dans sa configuration. Il charge ensuite l'assistant correspondant, combine AiModel, AiProvider et AiApiKey, puis construit un RunnableAiAssistant. Ce n'est qu'après cela que l'appel réel au chat model se produit.

La bonne manière de lire cette structure est donc la suivante :

  • les assistants sont des composants d'inférence
  • les agents sont des workflows qui décident dans quel ordre ces composants s'exécutent

Quand on comprend cela, il devient naturel de ne plus voir assistant et agent comme des concepts en concurrence. Ce ne sont pas des alternatives. Ce sont des couches.

Même dans un Agent, l'inférence réelle est faite par les assistants (référence texte)

AiAgent
  - unité d'exécution de niveau supérieur qui possède un workflow

Node
  - sélectionne et appelle un assistant quand c'est nécessaire

RunnableAiAssistant
  - objet exécutable réel composé de assistant + model + provider + key

Sur la route /ai-runs, l'agent se place au-dessus du contexte conversationnel

Un autre point intéressant est la route /ai-runs. Cette route n'est pas simplement une API qui exécute directement un agent stocké. Elle lit d'abord les messages d'un thread, puis exécute un workflow AiAgent spécifique au-dessus de ce contexte.

Dans l'implémentation actuelle, ChatAgentTaskHandler sélectionne un type d'agent comme AiAgentType.USING_WEB_SEARCH_TOOL et le transmet à SingleWorkflowExecutor. Cela signifie qu'à ce stade, un run n'est plus seulement un appel LLM simple. Il devient une commande qui lance un workflow d'agent sur le contexte d'une conversation.

C'est l'une des raisons pour lesquelles l'ai-service d'Omnilude ressemble davantage à une plateforme qu'à un simple wrapper de modèle. Une même structure d'inférence prend un sens produit différent dès lors qu'elle est placée sur l'interface thread/message/run.

Je laisse volontairement multiagent en dehors de cet article

En lisant le code, on voit aussi le package multiagent. Mais sa logique est un peu différente de celle du workflow AiAgent expliqué ici. Il ressemble davantage à une couche séparée qui orchestre plusieurs agents et traite des problèmes plus larges, comme le routage dynamique, les retries et la review humaine.

Plus important encore, certaines parties sont encore en cours, si bien que je ne souhaite pas l'expliquer exactement au même niveau que le runtime actuel de AiAgent. C'est pour cela que je l'ai volontairement laissé de côté ici. Le périmètre de cet article est strictement la structure qui exécute un seul workflow d'agent en combinant des assistants.

En resserrant ainsi le périmètre, l'article devient plus facile à suivre et l'explication reste plus nette.

Je trouve cette structure assez réaliste

Si on la regarde à nouveau, l'agent dans l'ai-service d'Omnilude n'est pas une autre IA au nom impressionnant. Ce n'est pas non plus un wrapper supplémentaire autour des assistants. Si quelque chose ressort, c'est plutôt son côté pragmatique.

  • Le flux d'exécution est stocké dans AiAgent.workflow
  • L'exécution réelle est transmise sous la forme d'un job DTE
  • Le moteur avance depuis le nœud de départ en suivant les edges
  • Chaque nœud appelle un assistant lorsqu'il a besoin du LLM

Je pense que cette structure fonctionne parce qu'elle n'exagère pas son extensibilité. Assistant, thread, run et agent ont tous des rôles différents, et la limite de responsabilité de chacun reste relativement claire.

Plus on ajoute de fonctionnalités IA dans un produit, plus ce genre de structure devient important. Ce qui reste dans la durée, ce n'est pas seulement l'appel réussi à un modèle une fois, mais la manière dont on stocke les choses, l'interface par laquelle elles s'exécutent et le flux par lequel elles sont reliées.

Conclusion

Si, dans l'article précédent, assistant / thread / run formaient les concepts de base de la plateforme d'exécution d'Omnilude, alors dans celui-ci agent est la couche de composition qui vient au-dessus. AiAgent n'est pas une autre IA : c'est une entité qui stocke un workflow, et son exécution passe par DTE -> WorkflowExecutor -> NodeExecutor.

Et l'inférence réelle est toujours prise en charge par les assistants. Au final, l'ai-service d'Omnilude grandit non pas en opposant assistants et agents, mais en construisant des agents à partir d'assistants utilisés comme composants.

La prochaine fois, j'irai un cran plus loin dans le concret et j'expliquerai, avec des exemples, comment je conçois réellement les workflow JSON et les compositions de nœuds.