Startseite / Artikel

Ein ReAct-Agent mit reinem Python entwickeln: Denken, Handeln, Beobachten

Schreiben Sie einen ReAct-Agenten-Loop von Grund auf in Python: das Thought-, Action- und Observation-Konzept, einen durch PAUSE gesteuerten while-Loop, Fehlerfeedback sowie zwei mögliche Fehlerrouten bei der Produktion.

Verlag: REACTAPP.TOP8 Min. Lesezeit1785 Wörter

Quellen: Building a ReAct AI Agent from Scratch in Pure Python

Ein Plan-und-Ausführen-Agent verfügt über einen cleveren Trick: Das Modell erstellt im Voraus eine nummerierte Liste von Schritten, und reiner Python durchläuft diese nacheinander. Das funktioniert, solange die Realität nicht mit dem Plan übereinstimmt – ein Endpunkt verschiebt sich, eine Suche liefert einen 404-Fehler, oder das Modell hat einfach bei Schritt zwei falsch geraten. Der Agent setzt dennoch mit Schritt drei bis zehn fort, denn nichts in seiner Konstruktion ermöglicht es ihm, nachzuschauen.

ReAct behebt das Problem, indem er den vollständigen Plan durch eine Schleife ersetzt. Unten wird diese Schleife in reinem Python ohne Framework erstellt, bevor erläutert wird, wie sie bei Interaktion mit echten Nutzern in die Irre geführt werden kann.

Von einem festen Zeitplan zu einer Feedbackschleife

Betrachten Sie zwei Möglichkeiten, einen unbekannten Wald zu durchqueren. Ein Reisender notiert sich „1 km nach Norden, dann 500 m nach Osten“ und hält sich genau daran, was bedeutet, dass er weitergeht, auch wenn bereits nach 200 Metern eine Klippe auftaucht. Der andere hält alle paar Schritte an und überlegt laut: Der Weg nach Norden ist durch eine Klippe blockiert, also versucht er es mit einem Umweg nach Westen – tatsächlich gibt es dort einen sicheren Pfad.

Der erste Reisende ist ein Plan-und-Ausführen-Agent. Er verbraucht wenig Ressourcen und ist gut organisiert, doch erbricht bei Fehlern zusammen: Wenn Schritt 2 fehlschlägt, sind die Schritte 3 bis 10 nutzlos. Der zweite ist ein ReAct-Agent (Überlegen plus Handeln). Er führt jeweils nur einen Schritt aus und überprüft vor dem nächsten seine Umgebung erneut. Das kostet zwar mehr Ressourcen, doch er hält deutlich besser stand, wenn etwas schiefgeht.

Die laufende Kommentierung, die der zweite Reisende führt, ist ein innerer Monolog – und genau dieser Mechanismus sorgt dafür, dass ein Agent als anpassungsfähig wahrgenommen wird statt als nach einem Skript handelnd. Um jedoch nützlich zu sein, muss dieser Monolog in der Lage sein, tatsächliche Aktionen auszulösen.

Toolaufruf in einem Absatz

Ein Sprachmodell kann nur Text erzeugen. Um es handlungsfähig zu machen, verwendet man ein einfaches Wörterbuch, das Namen mit Python-Funktionen verknüpft – beispielsweise {"get_weather": get_weather_api}. Wenn die Ausgabe des Modells den String get_weather enthält, erkennt der Code dies und ruft die entsprechende Funktion auf. Die Tools bleiben in einem ReAct-Agenten exakt gleich – was sich ändert, ist die Art und Weise, wie das Modell entscheidet, sie zu verwenden.

Hier liegt der etwas seltsame Aspekt des gesamten Designs: Damit ein Modell so wirkt, als würde es selbstständig denken, muss man es stark einschränken.

Der Vertrag hinter dem Monolog

Anstatt nach einer Antwort zu fragen, bindet man das Modell an ein strenges Ausgabeformat und erlaubt ihm nicht, irgendein Tool zu verwenden, bevor es diesem Format gefolgt ist. Der Zyklus besteht aus drei Schritten:

  1. Gedanke. Das Modell prüft die Frage, alles, was bisher gesammelt wurde, sowie frühere Fehler und notiert anschließend, was als Nächstes zu tun ist und warum.
  2. Aktion. Auf Basis dieses Gedankens wählt es ein Tool aus der Liste aus und gibt die genauen Eingabedaten dafür an.
  3. Beobachtung. Das ist der entscheidende Übergangspunkt. Das Modell muss anhalten. Ihr Python-Code übernimmt, liest die Aktion aus, ruft die eigentliche Funktion auf (einen Live-Wetterabruf, eine Datenbankabfrage usw.) und gibt das rohe Ergebnis an das Modell zurück.

Jeder Durchlauf des Zyklus liefert dem Modell neue Hinweise, auf denen es schließen kann. Der Zyklus aus Hypothesen aufstellen, ausführen und überprüfen wiederholt sich, bis eine Überlegung schließlich zu einem Schluss führt, den der Benutzer verstehen kann. Kurz gesagt: Das Modell denkt nach und trifft Entscheidungen, während gewöhnlicher Code als Schiedsrichter fungiert, jede Tool-Aufruf abfängt und mitteilt, was tatsächlich geschehen ist.

Auf dem Papier klingt das ordentlich. Die praktische Frage ist jedoch, wie man einen Textvorhersager dazu bringt, mitten im Vorgang anzuhalten, auf ein Skript zu warten und anschließend seinen Gedankengang wieder aufzunehmen.

Die Schleife von Hand schreiben

Frameworks wie LangChain, CrewAI und AutoGen verbergen all das hinter einem einzigen .run()-Aufruf. Das ist angenehm – bis der Agent im Kreis dreht oder ein nicht existierendes Tool erfindet. Das Debuggen einer Black Box ist äußerst schwierig, daher lohnt es sich, zu wissen, was sich darin befindet.

Begonnen wird mit dem Systemprompt, der die Spielregeln festlegt und den Vertrag definiert:

system_prompt = """
You are a dynamic AI Agent. You work in a continuous
loop of
- Thought,
- Action,
- Observation.
- PAUSE

Your available tools are:
- get_weather(location: str): Returns the current weather.
- calculate(expression: str): Evaluates a math expression.

Use the following format exactly:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [get_weather, calculate]
Action Input: the input to the action
PAUSE
(You will then receive an Observation from the environment)
Thought: I now know the answer
Final Answer: the final answer to the original input question
"""

Die PAUSE-Zeile übernimmt die Hauptaufgabe, und die Anweisung legt außerdem die genauen Feldnamen (Question, Thought, Action, Action Input, Final Answer) fest, damit eine reguläre Ausdrucksregel sie in freiformigen Ausgaben zuverlässig finden kann. Sie weist das Modell an, mit der Erstellung aufzuhören, wodurch die Kontrolle an Python zurückgegeben wird, sodass das Tool ausgeführt werden kann und sein Ausgabeinhalt als Observation im Kontext gespeichert werden kann.

Modelle befolgen die Formatanweisungen nicht immer perfekt, wie Sie unten sehen werden. Ein Prompt ist jedoch lediglich ein Regelbuch – etwas muss diese Regeln durchsetzen: Es muss den PAUSE-Befehl erfassen, die Funktion ausführen und den Ablauf am Laufen halten. Das ist eine while-Schleife, das Herzstück des Agents. Bei jedem Durchlauf stellt sie dem Modell eine Frage, analysiert die Antwort, ruft ggf. angeforderte Tools auf und fügt den gesamten Austausch der Historie hinzu, wobei sie aufhört, sobald das Modell eine endgültige Antwort gibt oder der Turn-Budget erschöpft ist.

import re

def react_agent(self, user_question, max_turns=5):
    # Initialize our conversational memory with the system prompt
    messages = [
        {"role": "system", "content": self.system_prompt},
        {"role": "user", "content": f"Question: {user_question}"}
    ]

    turn_count = 0

    while turn_count < max_turns:
        print(f"\n--- Turn {turn_count + 1} ---")

        # 1. Get the LLM's response (The Thought + Action)
        response = my_react_agent.call_llm(messages)
        print(response)

        # Add the LLM's generation to the memory
        messages.append({"role": "assistant", "content": response})

        # 2. Check if the agent has reached a conclusion
        if "Final Answer:" in response:
            print("\n✅ Task Complete.")
            #print(json.dumps(messages, indent=2))
            return response.split("Final Answer:")[-1].strip()

        # 3. Parse the Action and Action Input using Regex
        action_match = re.search(r"Action: (.*)", response)
        input_match = re.search(r"Action Input: (.*)", response)

        if action_match and input_match:
            action = action_match.group(1).strip()
            action_input = input_match.group(1).strip()

            # 4. The Observation Phase (Python takes control)
            print(f"⚙️ System Executing: {action}({action_input})")
            try:
                action_input = json.loads(action_input)
                observation_result = my_react_agent.execute_tool(action, action_input)
            except (TypeError, ValueError, json.JSONDecodeError) as error_messgae:
                observation_result = error_messgae
            # Format the observation and feed it back to the agent
            observation_text = f"Observation: {observation_result}"
            messages.append({"role": "user", "content": observation_text})
            print(observation_text)

        else:
            # If the LLM breaks the API contract, gently correct it
            messages.append({"role": "user", "content": "Error: Invalid format. Please provide an Action and Action Input, or a Final Answer."})

        turn_count += 1
    return "❌ Agent timed out before reaching a final answer."

Was das Besitzen der Schleife Ihnen bringt

Weil die Schleife Ihr eigener Code ist, entscheiden Sie selbst, was bei Fehlern passiert. Ein Ausdruck aus einem Lauf mit einem tool im Stil von Weather zeigt, warum das wichtig ist.

Zunächst erkennt das Modell korrekt, dass es ein Tool namens get_planet_mass benötigt, schreibt die Eingabe jedoch in einfachen Anführungszeichen, {'planet': 'Earth'}. Das ist kein gültiges JSON, weshalb der Parser auf der Python-Seite eine Standardmeldung auslöst: Er erwartete Eigennamen in doppelten Anführungszeichen.

Ein typisches Skript würde an dieser Stelle abbrechen. Diese Schleife wirft keinen Fehler aus. Sie nimmt den rohen Fehlertext und fügt ihn als nächste Observation in das Gespräch ein.

In Schritt zwei liest das Modell diese Nachricht und erkennt in seinem Gedankengang, dass die Eingabeformatierung falsch war und die JSON-Regeln eingehalten werden müssen. Es sendet den Aufruf erneut mit {"planet": "Earth"}, der Router führt das Tool aus und die Aufgabe ist abgeschlossen.

Bemerken Sie, dass für diesen Fall nichts Besonderes programmiert wurde. Es gibt keinen Wiederholungsmechanismus für fehlerhaftes JSON, lediglich die Gewohnheit, Fehler als Beobachtungen zurückzugeben, wobei die eigene Lesekompetenz des Modells den Rest übernimmt. So sieht ein internes Monolog in der Praxis aus. Ein statischer Planer wäre bereits in Schritt eins abgestürzt und hätte eine menschliche Intervention zum Neustart benötigt. Der ReAct-Loop ermöglichte es dem Modell, seine eigene Syntax während des Laufs zu diagnostizieren und zu beheben. Es folgte nicht einfach nur einem Plan, sondern reagierte auf das, was tatsächlich geschah.

Wo ReAct in der Produktion versagt

Der obige Code wirkt beim ersten Ausführen fast magisch. Bevor Sie ihn veröffentlichen, sollten Sie die beiden Stellen kennen, an denen er versagt.

Die Halluzinationsspirale. Manchmal ist ein Modell stur davon überzeugt, dass ein defektes Tool gleich funktionieren wird. Es ruft get_weather auf, sieht einen Fehler, ruft get_weather erneut auf, sieht denselben Fehler und setzt dies fort, während Ihre API-Kredite aufgebraucht werden. Der Schutz dagegen ist der Schutzmechanismus max_turns=5 in der Schleife. Ein solcher fester Schaltkreisbruchschutz ist unverzichtbar, und kein ReAct-Agent sollte unbegrenzt laufen dürfen.

Kontextüberflutung. Jeder Zyklus fügt seine Gedanken, Aktionen und Beobachtungen zur Konversation hinzu. Wenn ein Tool einen 10.000 Zeilen umfassenden JSON-Datenstrom zurückgibt, landet alles in dem Kontextfenster. Im nächsten Schritt ist das Modell von Daten überflutet, die Qualität seiner Schlussfolgerungen sinkt und es kann die ursprüngliche Frage aus den Augen verlieren. Führen Sie niemals rohe API-Antworten oder vollständige Webscrapings direkt als Beobachtungen ein. Selbst eine grobe Obergrenze, wie die ersten paar tausend Zeichen zusammen mit einer Anmerkung, dass der Rest gekürzt wurde, ist besser als eine Überflutung des Modells. Setzen Sie dazu eine kleine Middleware-Funktion ein, die die Ergebnisse zunächst zusammenfasst oder kürzt, damit der Monolog kurz und übersichtlich bleibt.

Auch hier gibt es eine Grenze, die kein Schutzmechanismus überwinden kann. Wenn ein einzelner Agent zu viele große Werkzeuge sowie zu viel angesammelten Kontext bewältigen muss, wird sein Denken überladen und er verliert den Fokus auf die Aufgabe. Ein ReAct-Agent ist ein geschickter Taktiker – doch ein schlechter Ausführender.

Kombination beider Ansätze

Die beiden Muster lösen unterschiedliche Probleme. Planen und Ausführen sorgen für Struktur, während ReAct Widerstandsfähigkeit bietet. Viele Produktionsysteme nutzen letztendlich beide Ansätze: Ein Planer erstellt die übergeordneten Schritte und weist anschließend jede unübersichtliche Unteraufgabe einem eigenen ReAct-Agenten zu, der die Details direkt vor Ort klärt. Die Koordination mehrerer solcher Agenten, die von Grund auf ohne Framework entwickelt werden, ist der natürliche nächste Schritt nach diesem Zyklus.