Technologie

Pourquoi l'ai-service d'Omnilude a choisi une structure Assistant / Thread / Run

AAnonymous
13 min de lecture

Introduction

Dans cet article, je veux partager la manière dont l'ai-service d'Omnilude est implémenté. Il existe un adage célèbre que l'on a presque tous entendu au moins une fois : « ne réinventez pas la roue ». Je suis moi aussi d'accord avec cette idée. Lorsque j'ai conçu le mode d'exécution IA de l'ai-service, j'avais déjà utilisé l'Assistants API d'OpenAI (Beta), et cette expérience n'avait pas été mauvaise. Comme je n'avais pas non plus d'idée plus brillante à ce moment-là, la structure initiale a été adoptée d'une manière très proche de l'imitation.

La structure de l'Assistants API d'OpenAI était assez simple. Elle reposait sur un squelette assistant / thread / message / run / run step, et l'on pouvait comprendre Assistant comme un préréglage, Thread comme un conteneur de conversation et Run comme l'exécution réelle. Cet axe conceptuel apparaît clairement dans le deep dive et la FAQ officiels d'OpenAI.

Cela dit, Omnilude n'a pas recopié cette structure telle quelle. Nous ne construisions pas un service tourné uniquement vers OpenAI. Nous devions gérer, dans une seule plateforme, plusieurs chemins d'exécution : modèles locaux, serveurs compatibles OpenAI, Anthropic, Ollama et LM Studio.

Ainsi, l'ai-service d'Omnilude emprunte des concepts à OpenAI Assistants, mais son implémentation réelle a évolué dans une direction plus découplée. Dans ce texte, je vais essayer d'expliquer cette structure de la manière la plus simple possible.

D'abord, qu'est-ce qui est stocké ?

L'ai-service d'Omnilude contient plusieurs objets liés à l'IA. Les noms peuvent sembler complexes, mais une fois les rôles séparés, l'ensemble est plus simple qu'il n'y paraît.

  • AiProvider : stocke quel fournisseur est utilisé. C'est l'axe OpenAI, Anthropic, Ollama, LM Studio, etc.
  • AiApiKey : stocke la clé d'authentification reliée à ce fournisseur.
  • AiModel : stocke les informations réelles du modèle, comme le nom, le support du reasoning, le prix et la taille du contexte.
  • AiAssistant : définit un préréglage d'exécution indiquant dans quel but le modèle est utilisé et avec quelles instructions.
  • AiThread, AiMessage, AiRun, AiRunStep : stockent la conversation et l'historique d'exécution.
  • AiAgent : la couche supérieure qui combine plusieurs assistants en workflow.

Si je résume cela en une phrase, cela donne ceci.

Provider et Model sont les ingrédients, Assistant est la recette, et Thread et Run sont la commande réelle et le journal de préparation.

Le lien entre ces éléments devient plus clair sous forme de diagramme.

C'est aussi là que l'on retrouve la ressemblance avec l'ancienne Assistants API d'OpenAI. Le squelette Assistant, Thread, Message, Run et Run Step reste proche. La différence est qu'Omnilude sort provider / model / apiKey de l'Assistant et les gère comme des actifs séparés.

Pourquoi les avoir séparés ainsi ?

La raison est simple. Dans Omnilude, je ne voulais pas considérer un Assistant comme un simple bloc de prompt.

Imaginons que l'on crée un assistant. Ce que l'on veut vraiment stocker n'est pas uniquement instructions.

  • quel provider il utilise
  • quel model il utilise
  • s'il prend en charge le reasoning
  • si le response format est du texte ou du JSON
  • comment sont réglés temperature et top-p
  • même si le modèle change plus tard, quel modèle reste le point de référence d'origine

On le voit directement dans l'entité réelle AiAssistant. modelId et primaryModelId sont distincts, et apiKeyId et primaryApiKeyId le sont également. Mais il ne s'agit pas seulement de mémoriser le modèle de référence initial. La raison la plus importante est le fallback. Si un provider entre dans un état de panne, si une clé est bloquée, ou si le modèle actuellement connecté devient instable, le service doit continuer en basculant vers un autre modèle et une autre clé. Autrement dit, séparer le modèle et la clé courants du modèle et de la clé principaux permet d'exploiter le service de manière plus robuste pendant une défaillance.

Grâce à cette structure, un assistant n'est plus un simple modèle de prompt. Il devient un préréglage d'exécution exploitable.

C'est l'un des points où Omnilude se distingue d'OpenAI Assistants. L'Assistant d'OpenAI donnait plutôt l'impression de regrouper model + instructions + tools dans un seul paquet. Omnilude, au contraire, rend d'abord provider, model et api key indépendants, puis conçoit l'assistant comme une couche de composition posée au-dessus de ces actifs.

Assistant ne s'arrête pas à une entité de base de données

Il y a ici une étape supplémentaire importante. Un AiAssistant stocké n'est pas exécuté directement.

Juste avant l'exécution, AiAssistantService assemble d'un seul coup les informations suivantes.

  • les valeurs de preset propres à l'assistant
  • les informations du model relié
  • les informations du provider relié
  • l'api key chiffrée stockée dans le système

Ensuite, le résultat est transformé en objet d'exécution appelé RunnableAiAssistant.

Ce nom est assez important. Dans Omnilude, l'assistant stocké en base et l'assistant réellement exécuté sont traités comme deux choses distinctes.

  • AiAssistant côté base : l'entité qui stocke la configuration
  • RunnableAiAssistant côté exécution : l'objet prêt à appeler le modèle

Cette séparation présente des avantages clairs.

Premièrement, la couche d'exécution n'a plus à se soucier des entités JPA. Deuxièmement, les différences propres à chaque provider peuvent être masquées dans les implémentations de RunnableAiModel. Troisièmement, les options de reasoning, le response format et les paramètres de sampling peuvent être interprétés au même endroit au moment de l'exécution.

En bref, Omnilude sépare le fait de stocker un assistant comme donnée du fait d'exécuter réellement cet assistant.

Comment circule un appel réel ?

Le chemin le plus simple pour exécuter directement un assistant ressemble à peu près à ceci.

L'élément central de ce flux est LlmGateway. Dans Omnilude, la plupart des exécutions directes passent désormais par cette passerelle.

Et cette passerelle en fait beaucoup plus qu'il n'y paraît.

  • application des limites de taux propres à chaque provider
  • enregistrement du début et de la fin de l'exécution
  • création et gestion de l'état de AiRun et AiRunStep
  • journalisation des payloads de requête et de réponse
  • collecte des tokens et du coût

Autrement dit, on ne se contente pas d'appeler assistant.chat(). On encapsule cet appel comme une exécution de plateforme avec traçage.

Cette différence devient vite importante. Dès que l'on commence à utiliser un peu plus sérieusement les fonctions IA, il devient moins important de savoir quel prompt a été utilisé que de savoir quel assistant a produit quel résultat, avec quel modèle et à quel coût.

Pourquoi conserver Thread, Message et Run ?

À ce stade, beaucoup de personnes se posent naturellement la question suivante.

Si l'on peut déjà exécuter un assistant directement, pourquoi conserver séparément Thread, Message et Run ?

Je pense que c'est une partie très importante de la structure d'Omnilude.

Assistant n'est qu'un préréglage. Mais l'expérience réelle de l'utilisateur ne s'arrête pas à un préréglage. L'utilisateur poursuit la conversation, accumule des messages, déclenche une exécution à un moment donné, puis reçoit le résultat sous forme d'un nouveau message.

Omnilude découpe ce flux de la manière suivante.

  • AiThread : la salle de conversation
  • AiMessage : chaque tour échangé entre l'utilisateur et l'assistant
  • AiRun : la demande d'exécution basée sur un message donné
  • AiRunStep : la trace détaillée à l'intérieur de cette exécution

Je vois cela comme une manière assez réaliste de faire entrer dans le runtime d'un produit la structure qu'OpenAI Assistants avait montrée.

Par exemple, un client peut d'abord créer un thread, y accumuler un user message, puis appeler /ai-runs. À partir de là, ce n'est plus un simple appel de chat completion. Cela devient une unité d'exécution dotée d'un contexte conversationnel.

Pourquoi est-ce important ?

Premièrement, cela permet de relier l'historique d'exécution à la conversation. Deuxièmement, cela permet de gérer le point de reprise à l'intérieur d'un même thread. Troisièmement, cela permet d'ajouter au niveau du thread des fonctions annexes comme la génération de titre, les favoris ou la réinitialisation.

Plus on transforme l'IA en produit, plus ce type d'interface a tendance à survivre plus longtemps qu'un appel direct isolé.

Mais cliquer sur Run ne signifie pas toujours lancer un seul Assistant

C'est un autre point où Omnilude se différencie d'OpenAI Assistants.

L'impression laissée par OpenAI Assistants était assez claire : il y a un Assistant, il y a un Thread, et Run exécute cet assistant sur ce thread.

Mais dans Omnilude, /ai-runs ressemble désormais davantage à une entrée de workflow. Dans l'implémentation réelle, AiRunController n'appelle pas immédiatement le modèle. Il pousse d'abord une tâche dans la file DTE. Ensuite, ChatAgentTaskHandler récupère cette tâche, lit les messages du thread et exécute le workflow de AiAgent.

Autrement dit, dans l'Omnilude actuel, Run ne signifie plus seulement exécuter un assistant unique. Selon le cas, il peut être le point de départ qui déclenche un workflow contenant plusieurs assistants.

Vu ainsi, la structure devient plus nette.

  • Assistant : un preset LLM
  • Agent : un workflow composé de plusieurs assistants
  • Run : l'unité d'exécution qui lance réellement ce workflow

C'est pour cette raison que l'ai-service d'Omnilude se rapproche davantage d'une plateforme que d'un simple dépôt d'assistants.

Tous les chemins ne sont pas encore totalement identiques

Je veux aussi laisser ici une remarque plus réaliste. Même si la structure est bien organisée, toutes les entrées ne suivent pas encore exactement le même chemin d'exécution.

Par exemple, le backoffice playground et les chemins de direct inference passent par LlmGateway, ce qui permet d'attacher assez proprement le traçage et l'enregistrement des coûts. En revanche, l'API de chat interne /system/ai/chat suit un chemin plus direct. Ce chemin charge l'assistant, applique le rate limiter, puis appelle directement le chat. Il a donc une texture légèrement différente du chemin de traçage basé sur la gateway.

Je trouve même cela plutôt naturel. Dans les plateformes réelles, les choses ne naissent presque jamais parfaitement organisées autour d'un seul modèle idéal. Le plus souvent, elles convergent progressivement vers une structure commune à mesure que les usages augmentent.

L'important est que l'axe central est déjà en place.

  • les objets de gestion sont séparés
  • les objets d'exécution sont en train d'être unifiés sous RunnableAiAssistant
  • la conversation et l'historique d'exécution restent sous la forme Thread / Message / Run / RunStep
  • l'orchestration de niveau supérieur est prise en charge par AiAgent

J'aime beaucoup cette structure

Lorsqu'on la regarde à nouveau, l'ai-service d'Omnilude n'est pas simplement un service qui sait bien appeler des modèles.

Il essaie de faire quatre choses en même temps.

  • gérer plusieurs providers et models dans une seule plateforme
  • transformer les assistants en préréglages d'exécution réutilisables
  • conserver conversation et exécution dans une structure thread/message/run
  • étendre ensuite cela vers des workflows d'agents en composant les assistants

Je trouve cette direction très réaliste. Dès que l'on commence à intégrer l'IA dans un produit, ce dont on a vraiment besoin à la fin, ce ne sont pas quelques fonctions utilitaires pour appeler des modèles, mais une interface exécutable.

Et cette interface ressemble beaucoup à la structure conceptuelle qu'OpenAI Assistants avait autrefois montrée. Omnilude est simplement allé un peu plus loin, en séparant plus clairement provider et model, et en intégrant plus fortement le tracking et le workflow dans la conception.

Conclusion

Dans l'ai-service d'Omnilude, AiAssistant n'est pas un simple stockage de prompts. Il ressemble davantage à un préréglage d'exécution qui regroupe quel provider et quel model appeler, comment les appeler, sous quel format recevoir le résultat et sur quel runtime l'enregistrer.

Et AiThread, AiMessage, AiRun et AiRunStep sont l'interface de runtime qui permet à ce préréglage de fonctionner dans le flux réel du produit. Une fois AiAgent ajouté au-dessus, l'ai-service d'Omnilude dépasse le simple fait de gérer quelques assistants. Il devient une plateforme capable d'exploiter des workflows.

La prochaine fois, j'irai un peu plus loin avec l'histoire de la manière dont on compose des assistants pour construire des agents.