ReActEngine v1 : Finalizer et StreamParser

, par Bertrand Degoy

Les modules ReActFinalizer, ReActStreamParser, font partie du niveau supérieur ou "Orchesrtrateur" di traitement. Ils ont été développés spécialement pour atteindre les objectifs du ReActEngine v1.


Architecture ReAct : Finalizer et StreamParser

Rôles et responsabilités

ReActStreamParser

Fait le lien entre le niveau applicatif et le ReActEngine.

Responsable de :

  • La segmentation du flux brut en blocs ReActBlock

  • L’identification du type de chaque bloc (ReActType)

  • La gestion du buffer et du flush final

  • La conservation de l’historique des blocs extraits

Ne décide jamais de l’arrêt ou de la modification du raisonnement.

Expose :

  • has_final_answer() : indique si un bloc FINAL_ANSWER est présent dans les blocs extraits

  • get_final_answer() : retourne le contenu du dernier bloc FINAL_ANSWER si présent

ReActFinalizer

Responsable de :

  • L’orchestration du raisonnement jusqu’à résolution

  • L’arrêt du flux dès qu’un bloc de type FINAL_ANSWER, ANSWER, RESPONSE ou CONCLUSION est rencontré

  • La détection de boucles ou d’anomalies de raisonnement, avec relance du raisonnement via un Thought injecté.

Ne connaît pas l’état interne du parser. Il ne peut pas appeler parser.has_final_answer().


Règle de séparation stricte

  • Le Finalizer ne doit pas inspecter parser.blocks ni utiliser ses méthodes internes.

  • Le Parser ne doit pas décider de l’arrêt ou la modification du raisonnement.

  • La détection de la résolution se fait uniquement dans le Finalizer, en fonction du type des blocs yieldés.


Exemple de flux

Thought: I need to search.
Action: search[weather]
Observation: It's raining.
Final Answer: Bring an umbrella.
  • Le Parser extrait 4 blocs typés.

  • Le Finalizer s’arrête dès réception du bloc FINAL_ANSWER.

La classe ReActType

Héritage :

class ReActType(str, Enum):
  • Hérite de str pour permettre des comparaisons directes avec des chaînes (block.type == "thought").

  • Hérite de Enum pour garantir l’énumération stricte des types.


Types ReAct fondamentaux

Type Rôle dans le flux ReAct
THOUGHT Raisonnement interne
ACTION Appel d’outil
OBSERVATION Résultat d’outil
FINAL_ANSWER Clôture explicite
ANSWER Réponse directe
RESPONSE Variante de réponse
CONCLUSION Clôture synthétique
TOOL_CALL Appel structuré (optionnel)
TOOL_RESULT Résultat structuré
UNKNOWN Valeur de secours

3. Cohérence avec les finalizers et parsers

  • Les types FINAL_ANSWER, ANSWER, RESPONSE, CONCLUSION sont utilisés comme types de clôture dans le Finalizer.

  • Les types TOOL_CALL et TOOL_RESULT sont utiles pour des flux structurés (JSON, agents avancés).

  • UNKNOWN est une bonne pratique pour la robustesse du parsing.

Pourquoi deux parsers ?

Les parsers ont pour fonction d’assembler un flux de réponse brut (les tokens) en messages typés ReAct.

Les lecteurs attentifs auront remarqué que :

  • il existe un ReActOutputParser directement à la sortie du LLM et, pourtant, ReActEngine émet un flux brut de token ;

  • l’application cliente doit reconstruire les messages ReAct, par exemple avec StreamlitChatTracer.

Voici la distinction essentielle — et pourquoi elle est nécessaire — entre :

  • ReActOutputParser (côté LLM, logique interne de l’agent)

  • ReActEngine / Workflow (côté runtime, flux brut de tokens)

  • StreamlitChatTracer / client (côté application, reconstruction des messages)

PNG - 1.3 Mo
Deux parser en parallèle avec des objectifs et une temporalité différents

1. Le ReActOutputParser n’est pas utilisé pendant le streaming

ReActOutputParser est conçu pour analyser un bloc complet de texte produit par le LLM, contenant typiquement :

Thought: ...
Action: tool_name
Action Input: {...}

ou bien :

Thought: ...
Answer: ...

Ce parser fonctionne uniquement lorsque le LLM a fini de produire tout le message.
Il ne peut pas fonctionner sur un flux token-par-token, car :

  • le pattern ReAct n’est pas encore complet,

  • les sections Thought / Action / Observation / Answer peuvent arriver dans n’importe quel ordre,

  • le LLM peut réviser sa sortie en cours de génération.

Donc : pendant le streaming, LlamaIndex ne peut pas appliquer ReActOutputParser.

2. Pourquoi ReActEngine émet un flux brut de tokens

Le moteur ReAct dans LlamaIndex Workflow est conçu pour :

  • exposer l’intégralité du raisonnement ReAct,

  • permettre au client de suivre la progression en temps réel,

  • laisser la liberté au développeur de tracer, filtrer ou visualiser les étapes.

Le flux brut contient donc :

  • les Thought : intermédiaires,

  • les Action : et Action Input :,

  • les Observation :,

  • les Answer : finales.

Ce flux est intentionnellement non structuré, car :

  • il reflète exactement ce que le LLM produit,

  • il permet d’afficher la réflexion pas à pas,

  • il évite d’imposer un format unique de parsing pendant le streaming.

Le moteur ne structure pas le flux, car cela casserait la transparence et la flexibilité du workflow.

3. Pourquoi le client doit reconstruire les messages ReAct

Comme le flux est brut, c’est l’application cliente qui doit :

  • détecter les segments Thought / Action / Observation / Answer,

  • les afficher proprement,

  • éventuellement les agréger ou les filtrer,

  • ou les transformer en messages de chat.

Pour une application de chatbot fondée sur StreamLit, nous avons développé StreamlitChatTracer qui a l’avantage de produire en streaming un flux décoré (police, couleur etc.).

LlamaIndex fournit des helpers comme :

  • LlamaTrace

  • OpenInference instrumentation

Ces outils :

  • écoutent les événements AgentStream, ToolCall, ToolCallResult, etc.,

  • reconstruisent une vue structurée du raisonnement,

  • permettent une visualisation claire dans le client.

La reconstruction côté client est volontaire : elle permet d’adapter l’affichage au contexte (UI, logs, monitoring, etc.).

4. Pourquoi cette séparation est nécessaire

A. Le LLM produit du texte libre → pas structuré

Même si ReAct impose un format, le LLM reste libre dans sa génération.
Le parser ne peut fonctionner qu’une fois la sortie complète.

B. Le streaming impose de ne pas parser prématurément

Un parser ReAct ne peut pas fonctionner sur :

  • un Thought incomplet,

  • un Action Input partiellement généré,

  • un Answer tronqué.

C. Le workflow doit rester générique

Le moteur ReAct doit :

  • fonctionner avec n’importe quel LLM,

  • ne pas dépendre d’un format strict,

  • permettre des outils de tracing externes.

D. Le client peut choisir son mode de visualisation

Certains veulent :

  • tout afficher (Thoughts inclus),

  • masquer les Thoughts,

  • afficher uniquement les Tool Calls,

  • ou ne montrer que la réponse finale.

Impossible de satisfaire tous les cas dans le moteur lui-même.

Conclusion

La différence est nécessaire car :

  • ReActOutputParser est un outil post-hoc pour analyser une sortie complète.

  • ReActEngine doit fournir un flux brut pour permettre le streaming et la transparence.

  • Le client doit reconstruire les messages ReAct selon ses besoins (UI, logs, monitoring).

Cette architecture garantit :

  • flexibilité,

  • transparence,

  • compatibilité avec tous les LLM,

  • et contrôle total côté application.