Présentation du workflow de l'agent de chatbot amélioré
Introduction
Cet article prolonge Pourquoi le ai-service d'Omnilude a choisi la structure Assistant / Thread / Run et Comment les agents fonctionnent dans le ai-service d'Omnilude.
Les précédents billets expliquaient la structure. Cette fois, je veux regarder comment cette structure tourne réellement à l'intérieur du canvas du backoffice. L'objet de cette explication est Enhanced Chatbot.
Cet article ne part pas d'un exemple abstrait inventé pour l'occasion. Il s'appuie sur la configuration réelle du workflow et explique, telle quelle, quelle entrée reçoit cet agent, où il bifurque, et par quels assistants et tools il passe avant de produire une réponse.
Que représente ce chatbot amélioré en une phrase
Cet agent est un chatbot à embranchements assez explicite, qui répartit les questions sur quatre chemins.
- Si la question est générale, il répond directement.
- Si elle exige une information récente, il passe par la branche de recherche.
- Si l’entrée est une URL YouTube, il passe par la branche de résumé de transcript.
- Si l’entrée est une URL web classique, il passe par la branche d’analyse d’article.
Si l'on simplifie le workflow réel, on obtient ceci.
Autrement dit, l'enjeu de ce workflow n'est pas de pousser un seul modèle au maximum de manière brutale. Il commence par déterminer quel type de question il a sous les yeux, puis il choisit la bonne chaîne de traitement.
J'aimerais le rendre accessible pour que chacun puisse l'essayer, mais l'utilisation de LLM a un coût réel, et je ne le diffuse donc pas encore publiquement. Quand j'aurai plus de marge, je voudrais l'ouvrir sous une forme réellement exécutable.
Tout commence par une seule entrée texte
Le nœud de départ de cet agent est text-input, avec comme valeur d'exemple Quels sont les sujets de société récents ?. Ce n'est qu'une phrase de démonstration, mais elle montre en même temps très bien le type de problème que ce workflow cherche à résoudre.
Au lieu d'envoyer la question directement au LLM, il la passe d'abord à un nœud router. L'assistant utilisé à cet endroit est Question Router, et les sources de routage réellement configurées sont au nombre de quatre.
llm_direct: réponse directeweb_search: réponse après recherche webyoutube_summary: résumé YouTubearticle_analyze: analyse de page web
Ce router n'est pas un simple if. Les instructions de l'assistant décrivent explicitement quelle source doit être choisie dans quelle situation, et la réponse doit obligatoirement revenir en JSON. Par exemple, les questions qui exigent une information récente, une information liée à une date, ou un fact checking sont envoyées vers web_search ; les URL YouTube vers youtube_summary ; les URL ordinaires vers article_analyze ; et tout le reste vers llm_direct.
À ce moment-là, le router ne laisse pas seulement la source choisie, mais aussi un reason. L'un des nœuds text-visualize placés sur le canvas sert précisément à lire cette raison à l'œil nu. Cela veut dire que cet écran n'est pas seulement un schéma : c'est un écran de travail où l'on peut inspecter directement les traces d'exécution.
Le chemin de réponse directe est le plus court
Lorsque la bifurcation part vers llm_direct, la structure devient simple. La sortie du router entre dans un nœud generate-text qui utilise l'assistant Question Answer, et la sortie de ce nœud va directement vers finish.
Le point important est que la simplicité de ce chemin ne le rend pas moins structuré. Le nœud generate-text recharge l'assistant via aiAssistantId au moment de l'exécution, combine le system prompt et le user prompt, puis seulement lance l'appel réel au modèle. Autrement dit, même si le canvas embarque des métadonnées d'assistant, la référence de runtime reste l'id de l'assistant.
Les assistants branchés sur Enhanced Chatbot sont les suivants.
Router: Question Router (assistantId=1)
Direct answer: Question Answer (assistantId=2)
Search query writer: Web Search Expert (assistantId=12)
Search-based answer: AdaptiveAnswerer (assistantId=3)
YouTube writer: YoutubeSummarizer (assistantId=15)
Article writer: ArticleAnalizer (assistantId=16)
Grâce à cela, un même nœud generate-text peut jouer un rôle complètement différent selon l'assistant qu'on lui branche. Le nœud est l'exécuteur générique ; le rôle est porté par l'assistant.
Le chemin de recherche n'a pas deux étapes mais quatre
Le véritable centre de cet agent est le chemin web_search. Si l'on regarde l'implémentation actuelle, la réponse fondée sur la recherche n'est pas générée en une seule fois. Les étapes sont volontairement séparées.
La première étape est Web Search Expert. Cet assistant reformule la question de l'utilisateur en une requête que l'on peut envoyer telle quelle au moteur de recherche. Au lieu d'utiliser la question brute, il la compacte en une ligne plus adaptée à la recherche.
La deuxième étape est web-search-tool. Dans la configuration sauvegardée du workflow, ce nœud est réglé sur searchTool: "searx", et l'implémentation réelle appelle SearXng, récupère jusqu'à dix extraits de résultats, puis applique un timeout de 200 secondes afin d'éviter une attente infinie.
La troisième étape est prompt-crafter. Ce nœud ne colle pas directement les résultats de recherche dans la réponse finale. Il injecte {{results}} dans un template enregistré et construit un system prompt de réponse. Ce template contient déjà l'heure actuelle, les documents de référence, les règles de réponse et le ton d'écriture attendu.
La quatrième étape est AdaptiveAnswerer. Le détail intéressant est que la question originale de l'utilisateur reste dans prompt, tandis que le contexte construit à partir de la recherche entre dans system. Les résultats ne sont donc pas collés mécaniquement : la question et les preuves sont gardées séparées, puis un assistant orienté rédaction reformule la réponse finale.
Si l'on redessine le flux, on obtient ceci.
Je trouve cette partie particulièrement importante. Le fait d'ajouter de la recherche ne transforme pas immédiatement le système en bloc opaque à l'apparence de RAG. Le raffinement de la question, l'assemblage des preuves et la rédaction finale restent séparés. En exploitation, il devient alors beaucoup plus simple de voir quelle étape vacille.
Les chemins YouTube et Article sont tool-first
Les bifurcations basées sur des URL sont plus directes.
Si l'entrée est une URL YouTube, youtube-summary-tool s'exécute en premier. Ce nœud utilise YoutubeTranscriptTool pour récupérer un transcript avec information temporelle. Ensuite, l'assistant YoutubeSummarizer réécrit ce transcript en un contenu plus lisible.
Si l'entrée est une URL web classique, article-analyzer-tool s'exécute en premier. Ce nœud récupère la page avec Crawl4Ai et, lorsque c'est possible, utilise Readability4J pour n'extraire que le corps principal et le convertir en markdown avec titre et texte. Ensuite, l'assistant ArticleAnalizer le réorganise sous une forme plus lisible.
Autrement dit, les deux branches URL suivent la même logique.
- Le tool récupère d’abord la matière première.
- Ensuite, l’assistant la réorganise en un résultat lisible par un humain.
Cette logique ressemble aussi à celle du chemin de recherche. Dans la conception actuelle des agents d'Omnilude, tools et assistants ne sont pas des concepts en concurrence. Le tool récupère la matière, l'assistant l'interprète et l'ordonne.
Cet écran n'est pas seulement un joli schéma
Si vous regardez le canvas du backoffice, vous verrez des nœuds text-visualize placés au milieu du graphe. Ils ne sont pas indispensables pour produire la réponse finale. Leur rôle est de vous montrer immédiatement quelle valeur circule dans le graphe.
L'implémentation frontend suit la même direction. Lorsqu'une exécution de workflow reçoit un événement NODE_COMPLETED, le frontend injecte la valeur de sortie de ce nœud dans l'état du canvas. Un nœud text-visualize affiche cette valeur telle quelle et, si des deltas de streaming arrivent, il accumule le texte.
Cette différence compte plus qu'il n'y paraît. Beaucoup d'outils de workflow s'arrêtent au dessin des nœuds. Mais l'écran agent du backoffice Omnilude combine deux usages : regarder le graphe sauvegardé et observer les valeurs intermédiaires d'une exécution réelle. Ce canvas est donc à la fois un schéma et un débogueur.
C'est ainsi que le moteur d'exécution le voit
Sortons maintenant du canvas. Ce workflow est stocké en base sous la forme d'un JSONB ai.ai_agent.workflow. Il n'est pas réparti dans des tables de nœuds séparées ; il est porté comme un unique graphe JSON.
Quand l'exécution démarre, BasicWorkflowEngine cherche d'abord un nœud dont le type se termine par -input et dont startNode=true. Ici, ce point de départ est text-input. Ensuite, NodeExecutorFactory sélectionne l'exécuteur correspondant à chaque type de nœud et poursuit l'exécution.
Pour Enhanced Chatbot, les executors importants se résument à peu près ainsi.
TextInputNode: prépare l’entrée initialeRouterNode: choisit la branche en utilisant un assistantGenerateTextNode: génère du texte avec un assistantWebSearchToolNode: exécute la recherche SearXngYoutubeSummaryToolNode: extrait le transcript YouTubeArticleAnalyzerToolNode: nettoie le corps de la page webPromptCrafterNode: construit un system prompt basé sur un templateFinishNode: rassemble les résultats des branches et termine l’exécution
Le point essentiel ici est que generate-text comme router rechargent l'assistant au moment de l'exécution. L'agent ne porte donc pas directement l'intelligence. Chaque nœud va chercher un assistant seulement lorsqu'il en a besoin. Au final, l'agent relève de l'orchestration, et l'inférence réelle continue d'être assurée par les assistants.
Le nœud finish est moins trivial qu'il n'en a l'air
Le dernier nœud finish n'est pas un simple bouton de fin. Il vérifie les edges entrantes, attend s'il reste encore des handles d'entrée en cours, puis ne transmet comme résultat final que les sorties des nœuds source réellement prêtes.
Cette structure est nécessaire à cause des bifurcations. Le fait que le router ouvre quatre chemins ne veut pas dire que ces quatre chemins se terminent toujours de la même manière. Certaines questions s'achèvent avec une réponse directe, d'autres passent par recherche et résumé. finish joue le rôle de collecteur final qui absorbe ces écarts.
Autrement dit, cet agent n'est pas simplement une fonction qui prend une question et retourne une réponse. Il s'exécute au-dessus d'un moteur de graphe avec bifurcation, attente et convergence.
L'intention de cet agent
Je pense que même cet agent relativement simple montre assez bien la direction actuelle d'Omnilude.
D'abord, il n'exagère pas l'idée d'un agent généraliste. Il classe d'abord le type de question, puis branche de manière pragmatique le bon tool et le bon writer.
Ensuite, il ne compresse pas le chemin de recherche dans un seul bloc opaque. Le raffinement de la question, la recherche, l'assemblage du contexte et la rédaction finale restent séparés.
Troisièmement, le canvas du backoffice n'est pas seulement un éditeur. Il affiche aussi les valeurs intermédiaires d'exécution, ce qui permet de faire les expérimentations de prompt et le débogage sur le même écran.
Enfin, les rôles d'assistant et d'agent sont clairs. L'assistant est le composant d'inférence. L'agent est le workflow qui relie ces composants.
Pour cette raison, il me semble plus juste de dire ici « nous avons rendu une pipeline de traitement des questions visuellement exploitable » que « nous avons construit un AI agent ».
Conclusion
Si les précédents articles expliquaient la structure d'assistant, thread, run et agent, celui-ci montre ce que cette explication devient une fois matérialisée dans un canvas réel et un graphe réel de nœuds. Enhanced Chatbot fonctionne déjà comme un chatbot assez pratique en combinant un router, plusieurs writer assistants et des tools de recherche, YouTube et article.
Quand j'aurai un peu plus de marge, j'aimerais revenir sur la composition et l'implémentation de cet agent avec une explication plus facile à suivre, appuyée sur des images et sur l'écran réel en fonctionnement.