Die Architektur
Die ReAct-Engine implementiert ein zweiphasiges Ausführungsmodell. Die erste Phase ist eine iterative Tool-Nutzungsschleife: der Agent fragt das LLM wiederholt nach einer Aktion, führt angeforderte Tools aus, fügt die Beobachtung hinzu und setzt fort, bis das LLM „fertig” signalisiert. Die zweite Phase ist die Antwortsynthesize: ein separater Streaming-LLM-Aufruf, der die vollständige Ausführungsspur liest und die benutzergerichtete Antwort erzeugt. Diese Aufteilung ist absichtlich. Tool-Iterationen sind für Geschwindigkeit optimiert — jeder LLM-Aufruf in der Schleife verwendet nicht-streamingchat(), da der Benutzer keine partiellen JSON-Aktionen oder Zwischenreasoningtoken sehen muss. Die Antwortgenerierung ist für UX optimiert — sie verwendet streaming stream_chat(), damit der Benutzer Token in Echtzeit erscheinen sieht. Das Ergebnis ist das Beste aus beiden Welten: schnelle Tool-Ausführung mit responsiver Antwortbereitstellung.
Die Tool-Schleife erzeugt ein AgentResult, das die vollständige Konversationshistorie enthält — Systemprompt, Benutzerabfrage, jede Assistentnachricht, jedes Tool-Ergebnis. Die stream_answer()-Methode destilliert diese Spur in eine prägnante, kohärente Antwort. Tool-Ergebnisse werden im Synthesekontext auf 2.000 Zeichen gekürzt, um den Prompt auch nach komplexen Multi-Tool-Workflows schlank zu halten.
Modellbindung. Das LLM wird in ReActAgent.__init__() eingefügt und als self._llm gespeichert. Jeder Aufruf innerhalb einer einzelnen run()-Invokation — alle Tool-Schleifeniterationen und die abschließende Antwortsynthesize — verwendet diese gleiche Instanz. Das Modell ändert sich nicht zwischen Iterationen. Um ein anderes Modell zu verwenden, muss ein neuer ReActAgent konstruiert werden. Im DAG-Modus nutzt DAGExecutor._resolve_agent() dieses Muster: Es erstellt einen frischen Agent pro Schritt (wählt das Modell aus ModelRegistry basierend auf step.model_hint) unmittelbar bevor die ReAct-Schleife dieses Schritts beginnt. Siehe DAG Engine — Per-step override für Details.
Dual-Mode-Ausführung
Die ReAct-Engine unterstützt zwei unterschiedliche Modi für die Interaktion mit dem LLM während der Tool-Schleife. JSON Mode (_run_json) bettet Tool-Beschreibungen direkt in den System-Prompt ein und weist das LLM an, mit einem JSON-Objekt zu antworten — entweder eine tool_call-Aktion mit einem Tool-Namen und Argumenten oder ein final_answer-Signal. Der Agent analysiert das JSON aus dem Antwortinhalt, führt das Tool aus und fügt die Beobachtung als Benutzernachricht an.
Native Function Calling (_run_native) nutzt die integrierte Tool-Calling-API des LLM-Anbieters. Tool-Beschreibungen werden über den tools-Parameter übergeben, und das LLM gibt strukturierte tool_calls in der API-Antwort zurück, anstatt JSON in seinem Inhalt auszugeben. Dies ist der bevorzugte Modus für Modelle, die ihn unterstützen.
Die Modusauswahl erfolgt automatisch. Die Eigenschaft _native_mode_active gibt True nur zurück, wenn beide Bedingungen erfüllt sind: Der Agent wurde mit use_native_tools=True (Standard) erstellt und das LLM gibt abilities["tool_call"] = True an. Wenn eine der Bedingungen nicht erfüllt ist, fällt die Engine auf JSON Mode zurück.
| Aspekt | JSON Mode | Native Function Calling |
|---|---|---|
| LLM-Ausgabe | JSON-Objekt im Nachrichteninhalt | tool_calls in API-Antwort |
| System-Prompt | Bettet vollständige Tool-Beschreibungen in Text ein | Tools über tools-Parameter übergeben |
| Parallele Tool-Aufrufe | Ein Tool pro Iteration | Mehrere über asyncio.gather |
| Parse-Fehlerbehandlung | Wiederholung mit Reformatierungs-Prompt | N/A (strukturiert durch API) |
| Loop-LLM-Aufrufe | Nicht-Streaming chat() | Nicht-Streaming chat() |
| Optimal für | Modelle ohne Tool-Call-Unterstützung | GPT-4, Claude und ähnliche |
stream_answer() funktioniert identisch, unabhängig davon, wie die Tool-Schleife ausgeführt wurde.
structured_llm_call — einheitliche Ausgabeextraktion
Jede Aufrufstelle, die benötigt, dass das LLM Daten zurückgibt, die einem JSON-Schema entsprechen, verwendetstructured_llm_call(). Dies ist der einzelne Einstiegspunkt für strukturierte Ausgaben im gesamten Framework — der DAG-Planer, der Plan-Analyzer, die Werkzeugauswahl und jede zukünftige Komponente, die geparste JSON von einem LLM benötigt.
Die Funktion implementiert eine 3-stufige Degradationskette, die jede Stufe nacheinander versucht, basierend auf den beworbenen Fähigkeiten des LLM:
Stufe 1: Native Function Calling. Verwendet die tool_call / tool_choice API des LLM, um eine strukturierte Antwort zu erzwingen. Verfügbar, wenn abilities["tool_call"] = True. Wenn das LLM tool_calls zurückgibt, werden die Argumente direkt extrahiert. Wenn das Parsing fehlschlägt, wird zur nächsten Stufe übergegangen.
Stufe 2: JSON Mode. Setzt response_format={"type": "json_object"}, um das Ausgabeformat des LLM einzuschränken. Verfügbar, wenn abilities["json_mode"] = True. Wenn die Antwort nicht geparst werden kann, wird einmal mit einer Reformatierungseingabeaufforderung erneut versucht („Your previous response could not be parsed as valid JSON…”), dann wird zur nächsten Stufe übergegangen.
Stufe 3: Klartext. Ruft das LLM ohne Formateinschränkungen auf und extrahiert JSON aus Freitext mit extract_json(). Wenn die Extraktion fehlschlägt, wird eine optionale regex_fallback Funktion versucht. Wird einmal mit der Reformatierungseingabeaufforderung erneut versucht, bevor aufgegeben wird.
Die Degradationskette bedeutet, dass jedes Modell — von GPT-4 mit vollständiger Tool-Call-Unterstützung bis zu einem lokalen LLM, das nur Klartext produzieren kann — an strukturierten Ausgabeszenarien teilnehmen kann. Der schlimmste Fall sind 5 LLM-Aufrufe (1 native + 1 JSON + 1 JSON-Wiederholung + 1 Klartext + 1 Klartext-Wiederholung), aber in der Praxis werden die meisten Aufrufe auf Stufe 1 in einem einzigen Versuch gelöst.
| Modellkapazität | Pfad | Max. LLM-Aufrufe |
|---|---|---|
| tool_call + json_mode | L1 → L2 → L3 | 5 |
| nur json_mode | L2 → L3 | 4 |
| nur Klartext | L3 | 2 |
StructuredCallResult, das den geparsten Wert, das rohe Dict, welche Stufe erfolgreich war, und die kumulative Token-Nutzung enthält. Aufrufstellen verwenden parse_fn, um das rohe Dict in ein Domänenobjekt (z. B. einen DAG-Plan) umzuwandeln, und default_value, um einen Fallback bereitzustellen, wenn totales Versagen akzeptabel ist.
structured_llm_call wird verwendet von: dem DAG-Planer (Plan-Schema), dem Plan-Analyzer (Analyse-Schema), der Werkzeugauswahl (Werkzeuglisten-Schema) und jeder Komponente, die zuverlässige strukturierte Ausgaben benötigt. Es wird auch in Planning Landscape diskutiert.
Werkzeugauswahl
Wenn ein Agent Zugriff auf viele Werkzeuge hat — häufig im Hub-Modus, wo mehrere Konnektoren jeweils mehrere Aktionen bereitstellen — ist das Einfügen des vollständigen Schemas jedes Werkzeugs in den Gesprächskontext verschwenderisch. Ein Konnektoren-Hub mit 20 Werkzeugen verbraucht etwa 5K Token nur für Werkzeugbeschreibungen und verdrängt damit Platz für Gesprächsverlauf und Werkzeugergebnisse. Die Engine adressiert dies mit einer leichtgewichtigen Auswahlphase. Wenn die Gesamtzahl der registrierten WerkzeugeTOOL_SELECTION_THRESHOLD (12) überschreitet, führt der Agent einen vorbereitenden LLM-Aufruf durch, bevor die Hauptschleife beginnt. Dieser Aufruf erhält einen kompakten Katalog — etwa 80 Zeichen pro Werkzeug, enthaltend nur den Namen und eine einzeilige Beschreibung, keine Parameterschemas — und wählt die relevantesten Werkzeuge für die aktuelle Anfrage aus, bis zu _TOOL_SELECTION_MAX (6).
Die Auswahl verwendet structured_llm_call mit einem einfachen Schema ({"tools": ["tool_name_1", "tool_name_2"]}), sodass sie von der gleichen 3-stufigen Degradation profitiert. Die ausgewählten Werkzeugnamen werden verwendet, um eine gefilterte ToolRegistry zu erstellen, die die Hauptschleife sowohl für die Konstruktion des Systemprompts als auch für die Werkzeugausführung verwendet.
Auswahlfehlschlag ist absichtlich nicht fatal. Wenn der LLM nicht analysierbare Ausgabe zurückgibt, wenn alle ausgewählten Namen ungültig sind, oder wenn eine Ausnahme auftritt, fällt der Agent auf den vollständigen Werkzeugsatz zurück. Dies stellt sicher, dass eine fehlerhafte Auswahl den Agent nie am Funktionieren hindert — er verwendet einfach mehr Kontext als optimal.
Die Iterationsschleife
Die Kernschleife treibt sowohl den JSON-Modus als auch den nativen Modus an, mit geringen Unterschieden in der Nachrichtenbehandlung. Jede Iteration folgt dem gleichen übergeordneten Muster: Kontextbudget prüfen, das LLM aufrufen, die Antwort verarbeiten und entweder ein Tool ausführen oder unterbrechen. JSON-Modus-Schleife. Die Antwort des LLM wird über_parse_action() analysiert, die extract_json() verwendet, um ein JSON-Objekt im Inhalt zu finden. Wenn die Analyse fehlschlägt, hängt der Agent die rohe Antwort und eine Umformatierungsanfrage an und setzt fort – dies zählt gegen max_iterations und verhindert Endlosschleifen. Bei Erfolg ist die Aktion entweder ein tool_call (das Tool ausführen, die Beobachtung als Benutzernachricht anhängen) oder eine final_answer (unterbrechen und zur Synthese übergehen).
Nativer Modus-Schleife. Die Antwort des LLM kann einen oder mehrere tool_calls enthalten. Alle Tool-Aufrufe in einer einzelnen Antwort werden parallel über asyncio.gather ausgeführt, und alle Tool-Ergebnisnachrichten werden angehängt, bevor andere Nachrichten hinzugefügt werden. Diese Reihenfolgebeschränkung ist kritisch – die OpenAI API (und kompatible Anbieter) erfordert, dass tool-Nachrichten unmittelbar auf die assistant-Nachricht folgen, die die tool_calls erzeugt hat. Das Einfügen einer anderen Nachricht (wie z. B. eine Benutzerunterbrechung) zwischen ihnen würde das Protokoll unterbrechen. Wenn keine tool_calls vorhanden sind, wird die Antwort als endgültige Antwort behandelt.
Maximale Iterationen. Das Standardlimit beträgt 50 Iterationen. Wenn die Schleife dieses Limit erschöpft, ohne eine final_answer zu erzeugen, synthetisiert der Agent eine Fallback-Antwort aus den gesammelten Schrittergebnissen – eine Zusammenfassung, welche Tools aufgerufen wurden und ob sie erfolgreich waren oder fehlschlugen. Dies ist ein Sicherheitsnetz, keine normale Ausstiegsmöglichkeit.
Context Management erklärt, wie ContextGuard das Token-Budget bei jeder Iteration durchsetzt, einschließlich des Hinweissystems, das dem Kompaktions-LLM mitteilt, dass es aktuelle Argumentationsketten bewahren soll.
Antwortsynthese (stream_answer)
Die Trennung zwischen der Werkzeugschleife und der Antwortsynthese ist eine grundlegende architektonische Entscheidung. Werkzeugiterationen erzeugen Rohdaten — JSON-Aktionen, Werkzeugbeobachtungen, Fehlermeldungen. Der Benutzer benötigt eine kohärente, gut formatierte Antwort, keine Ausgabe der internen Verfolgung des Agenten.stream_answer() erstellt einen Syntheseprompt aus zwei Komponenten. Der Systemprompt weist das LLM an, als Synthesizer zu fungieren: Ergebnisse direkt präsentieren, Markdown-Formatierung verwenden, Meta-Kommentare vermeiden („basierend auf der Werkzeugausgabe…”) und die Sprache der ursprünglichen Anfrage beachten. Die Benutzernachricht enthält die ursprüngliche Frage und eine formatierte Ausführungsverfolgung — jeden Werkzeugaufruf und sein Ergebnis, wobei Werkzeugergebnisse auf 2.000 Zeichen gekürzt werden.
Der Syntheseaufruf verwendet stream_chat() und liefert Token schrittweise. Die Web-Schicht umhüllt diese Token in SSE-answer-Events mit delta-Status, damit das Frontend sie bei Ankunft rendern kann.
Wenn stream_answer() fehlschlägt — Netzwerkfehler, LLM-Timeout, eine beliebige Ausnahme — fällt die Web-Schicht auf result.answer zurück, den kurzen Text aus der letzten Iteration der Werkzeugschleife. Dies ist eine beeinträchtigte Erfahrung (kein Streaming, möglicherweise weniger polierte Prosa), aber es stellt sicher, dass der Benutzer immer eine Antwort erhält.
Unterbrechungsbehandlung
Benutzer können Folgefragen senden, während der Agent noch verarbeitet wird. Diese werden über eineinterrupt_queue — eine InterruptQueue, die pro Konversation registriert ist — bereitgestellt und sammeln Nachrichten zwischen Iterationen an.
Der Zeitpunkt der Warteschlangen-Entleerung unterscheidet sich zwischen Modi aufgrund der Einschränkung der Tool-Call-Reihenfolge:
-
JSON-Modus: Die Warteschlange wird unmittelbar nach jeder Assistenten-Nachricht geleert, bevor überprüft wird, ob die Aktion eine
final_answerist. Dies ist sicher, da der JSON-Modus einfache Benutzer-/Assistenten-Nachrichten ohne strukturelle Paarungsanforderungen verwendet. -
Native FC-Modus: Die Warteschlange wird nur geleert, nachdem Tool-Ergebnis-Nachrichten hinzugefügt wurden. Die
tool-Nachrichten müssen unmittelbar derassistant-Nachricht folgen, dietool_callsenthält — das Einfügen einer Benutzernachricht dazwischen würde gegen das API-Protokoll verstoßen und Fehler verursachen.
pinned=True gekennzeichnet, um sicherzustellen, dass sie alle nachfolgenden Komprimierungen durch ContextGuard überstehen. Siehe Pinned Messages für weitere Informationen darüber, wie der Pinning-Mechanismus verhindert, dass kritische Nachrichten durch Komprimierung verworfen werden.
Wenn eine final_answer ausstehend ist, aber eingefügte Nachrichten angekommen sind, unterdrückt der Agent die finale Antwort und setzt die Schleife fort, um auf die Folgefrage des Benutzers eingehen zu können. Mehrere Einfügungen aus derselben Entleerung werden zu einer einzigen [USER INTERRUPT]-Nachricht kombiniert — dies verhindert, dass das LLM eine fragmentierte Abfolge von kurzen Nachrichten sieht, und fördert, dass es alle Folgefragen ganzheitlich adressiert.
Fehlerbehandlung und Fallbacks
Die Engine ist so konzipiert, dass sie bei LLM- oder Werkzeugfehlern nie abstürzt. Jeder Fehlerpfad führt entweder zu einer stillen Wiederherstellung oder zeigt dem Benutzer eine aussagekräftige Nachricht. JSON-Parse-Fehler. Wenn das LLM im JSON-Modus Inhalte zurückgibt, die kein JSON sind, umhüllt_parse_action() diese als final_answer mit der Begründung "(could not parse LLM output as JSON)". Die Schleife erkennt diesen Sentinel, hängt den Rohinhalt und eine Neuformatierungsanweisung an und setzt fort. Wenn der Wiederholungsversuch auch fehlschlägt, wird der Rohinhalt zur Antwort — nicht perfekt, aber kein Absturz.
Werkzeugfehler. Sowohl „Werkzeug nicht gefunden” als auch „Werkzeugausführungsausnahme” erzeugen Fehlerbeobachtungen, die zum Gespräch hinzugefügt werden. Das LLM sieht den Fehler in der nächsten Iteration und kann entscheiden, ob es mit anderen Argumenten erneut versuchen oder fortfahren soll. Dies macht den Agenten selbstheilend für vorübergehende Werkzeugfehler.
Erweitertes Denken. Modelle wie DeepSeek R1 geben Denkinhalte in einem separaten reasoning_content-Feld zurück, anstatt sie im JSON-Body zu platzieren. Die Engine prüft darauf und verwendet es als Fallback, wenn das JSON-Feld reasoning leer ist.
Rich Content. Wenn ein Werkzeug HTML- oder Markdown-Artefakte erzeugt, wird die an das LLM gesendete Beobachtung durch eine kurze Zusammenfassung ersetzt ("[Artifact generated: filename] The content is rendered as a preview in the UI..."). Dies verhindert, dass das LLM große HTML-Blöcke in seiner endgültigen Antwort wiederholt — ein häufiger Fehlermodus, bei dem das Modell hilfreich die gesamte Werkzeugausgabe zurückpastet.
SSE-Ereignisprotokoll
Die Web-Schicht übersetzt die Iterations-Callbacks des Agenten in Server-Sent Events für das Frontend. Ereignisse werden auf zwei SSE-Kanälen ausgegeben:step für die Tool-Schleife und answer für die Synthesephase.
| Ereignis | Kanal | Payload | Wann |
|---|---|---|---|
| Thinking start | step | {type: "thinking", status: "start", iteration} | Vor jedem LLM-Aufruf |
| Thinking done | step | {type: "thinking", status: "done", iteration, reasoning} | Nach LLM-Antwort, vor Tool-Ausführung |
| Iteration start | step | {type: "iteration", status: "start", iteration, tool_name, tool_args} | Tool-Ausführung beginnt |
| Iteration done | step | {type: "iteration", status: "done", iteration, tool_name, observation, error, iter_elapsed} | Tool-Ausführung abgeschlossen |
| Answer signal | step | {type: "answer", status: "start"} | Agent signalisiert final_answer |
| Answer start | answer | {status: "start"} | Synthesis-Streaming beginnt |
| Answer delta | answer | {status: "delta", content} | Jedes gestreamte Token |
| Answer done | answer | {status: "done"} | Synthesis-Streaming abgeschlossen |
| Compact | compact | {original_messages, kept_messages} | Kontext wurde beim Laden komprimiert |
| Phase | phase | {phase: "selecting_tools", total_tools} | Tool-Auswahlphase aktiv |
| Inject | inject | {type: "inject", content} | Benutzerunterbrechung empfangen |
| Done | done | {answer, iterations, usage, elapsed} | Finales Ergebnis-Payload |
step-Ereignisse, um die zusammenklappbaren Tool-Call-Karten zu rendern (zeigt, welches Tool läuft, seine Argumente und die Beobachtung), die answer-Deltas zum Streamen des Antworttexts und compact zur Anzeige des Kontext-Zusammenfassungs-Trennzeichens. Das done-Ereignis enthält die vollständigen Metadaten – Gesamtiterationen, Token-Nutzung und verstrichene Zeit – für die Antwort-Fußzeile.