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 blocFINAL_ANSWERest présent dans les blocs extraits -
get_final_answer(): retourne le contenu du dernier blocFINAL_ANSWERsi 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,RESPONSEouCONCLUSIONest 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
Finalizerne doit pas inspecterparser.blocksni utiliser ses méthodes internes. -
Le
Parserne 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
Parserextrait 4 blocs typés. -
Le
Finalizers’arrête dès réception du blocFINAL_ANSWER.
La classe ReActType
Héritage :
class ReActType(str, Enum):
-
Hérite de
strpour permettre des comparaisons directes avec des chaînes (block.type == "thought"). -
Hérite de
Enumpour 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,CONCLUSIONsont utilisés comme types de clôture dans leFinalizer. -
Les types
TOOL_CALLetTOOL_RESULTsont utiles pour des flux structurés (JSON, agents avancés). -
UNKNOWNest 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)
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.