Python & AI Tutorials Logo
LangChain & LangGraph

14. Die Agent-Schleife erstellen

In Kapitel 13 haben wir gelernt, wie man die Tool-Aufruf-Anfragen des LLM ausführt und die Ergebnisse zurückgibt. Diese Arbeit setzte jedoch voraus, dass jede Aufgabe mit einem einzigen Tool-Aufruf erledigt werden könnte. In der Praxis müssen Tool-Aufrufe oft über mehrere Runden hinweg fortgesetzt werden, bis das LLM alle Informationen gesammelt hat, die es für eine endgültige Antwort benötigt.

Betrachten Sie die Anfrage „Finde die Einwohnerzahl Frankreichs und multipliziere sie dann mit zwei." Diese Aufgabe erfordert mindestens zwei Tool-Aufrufe. Sie müssen zuerst die Einwohnerzahl nachschlagen — erst dann können Sie die Berechnung durchführen. Der zweite Aufruf hängt vom ersten Ergebnis ab, sodass es keine Möglichkeit gibt, ihn in einem einzigen Tool-Aufruf zu erledigen.

In diesem Kapitel verwandeln wir den einzelnen Zyklus aus Kapitel 13 in eine Schleife. Solange das LLM Tool-Aufrufe anfordert, führen wir sie weiter aus — und wiederholen dies, bis das LLM von sich aus keine Tools mehr anfordert. Anschließend fügen wir Sicherheitsgrenzen hinzu, um zu verhindern, dass die Schleife endlos läuft, und behandeln die Fehlerbehandlung, damit der Agent nicht abstürzt, wenn ein Tool fehlschlägt.

14.1) Vom einzelnen Zyklus zur Schleife

14.1.1) Wie funktioniert die Agent-Schleife?

Wie wir in der Einführung gesehen haben, können Aufgaben, bei denen die nächste Aktion vom Ergebnis des vorherigen Schritts abhängt, oft nicht mit einem einzigen Tool-Aufruf erledigt werden. Das LLM muss ein Tool aufrufen, das Ergebnis prüfen und erneut entscheiden. Genau das macht die Agent-Schleife, und sie funktioniert in drei Phasen:

  • Think — Das LLM liest den bisherigen Gesprächsverlauf und entscheidet, was als Nächstes zu tun ist. Wenn es ein Tool benötigt, fordert es einen Tool-Aufruf über tool_calls an. Andernfalls gibt es eine endgültige Antwort zurück.
  • Act — Führt das in tool_calls angegebene Tool aus.
  • Observe — Prüft das Ausführungsergebnis des Tools und fügt es dem Gespräch in einer ToolMessage hinzu.

Die Agent-Schleife wiederholt diese drei Phasen, bis das LLM keine Tools mehr anfordert. Dieses Muster ist auch als ReAct (Reason + Act) bekannt, und die Kernidee ist der Wechsel zwischen Schlussfolgern und Handeln.

Ja

Nein

Benutzereingabe

THINK
LLM aufrufen

tool_calls
vorhanden?

ACT
Tool ausführen

OBSERVE
Ergebnis prüfen

Endgültige Antwort

Wenn tool_calls vorhanden ist, führen Sie das Tool aus, fügen das Ergebnis dem Gespräch hinzu und rufen das LLM erneut auf. Wenn tool_calls leer ist, hat das LLM eine endgültige Antwort zurückgegeben und die Schleife wird beendet. Lassen Sie uns das nun in Code umsetzen.

14.1.2) Die Think-Act-Observe-Schleife implementieren

Lassen Sie uns die Think-Act-Observe-Schleife aus dem vorherigen Abschnitt in Code umsetzen. Zuerst bereiten wir die Tools und das Modell vor.

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langchain.tools import tool
 
# Tools definieren
@tool
def get_weather(city: str) -> str:
    """Ruft das aktuelle Wetter für eine Stadt ab."""
    fake_data = {"Tokyo": "18°C, bewölkt", "Cairo": "31°C, sonnig"}
    return fake_data.get(city, f"Keine Wetterdaten für {city}.")
 
@tool
def calculate(expression: str) -> str:
    """Wertet einen einfachen arithmetischen Ausdruck aus, z. B. '3 * 21'."""
    return str(eval(expression))  # Warnung: eval() ist ein Sicherheitsrisiko. Nicht in der Produktion verwenden.
 
# Tools binden
tools = [get_weather, calculate]
llm = ChatOpenAI(model="gpt-5-mini")
llm_with_tools = llm.bind_tools(tools)
tool_map = {t.name: t for t in tools}

In Kapitel 13 haben wir ein Tool einmal ausgeführt und dann angehalten. Jetzt wiederholen wir dies, bis das LLM keine Tool-Aufrufe mehr anfordert. Innerhalb einer while True-Schleife rufen wir das LLM auf, und wenn die Antwort tool_calls enthält, führen wir die Tools aus und rufen das LLM erneut auf. Wenn es keine tool_calls gibt, hat das LLM eine endgültige Antwort zurückgegeben, sodass wir die Schleife verlassen.

python
def run_agent(user_input: str) -> str:
    """Führt die Think-Act-Observe-Schleife aus, bis das LLM eine endgültige Antwort zurückgibt."""
    messages = [
        SystemMessage(content="Sie sind ein hilfreicher Assistent."),
        HumanMessage(content=user_input),
    ]
 
    while True:
        # THINK: Das LLM bitten, die nächste Aktion zu entscheiden
        print("THINK: LLM wird zur Entscheidung aufgefordert")
        ai_message = llm_with_tools.invoke(messages)
        messages.append(ai_message)
 
        # Abbruchbedingung: keine tool_calls bedeutet, dass dies die endgültige Antwort ist
        if not ai_message.tool_calls:
            return ai_message.content
 
        # ACT + OBSERVE: Angeforderte Tools ausführen und Ergebnisse dem Gespräch hinzufügen
        for tool_call in ai_message.tool_calls:
            selected_tool = tool_map[tool_call["name"]]
            print(f"ACT: rufe '{tool_call['name']}' auf, args={tool_call['args']}")
 
            tool_message = selected_tool.invoke(tool_call)
            print(f"OBSERVE: {tool_message.content}")
 
            messages.append(tool_message)

Vergleichen wir dies mit dem Code aus Kapitel 13. In Kapitel 13 haben wir nach der Ausführung der Tools das LLM ein letztes Mal aufgerufen, um die Antwort zu erhalten. In Kapitel 14 packen wir denselben Prozess in eine while True und prüfen bei jeder Iteration tool_calls, um zu entscheiden, ob wir fortfahren. Die Bausteine sind dieselben wie in Kapitel 13 — wir haben sie nur in eine Schleife eingehüllt.

Lassen Sie es uns ausführen.

python
answer = run_agent("Wie ist das Wetter in Tokyo, und ist es warm genug für einen Spaziergang?")
print(f'Endgültige Antwort: {answer}')

Ausgabe:

THINK: LLM wird zur Entscheidung aufgefordert
ACT: rufe 'get_weather' auf, args={'city': 'Tokyo'}
OBSERVE: 18°C, bewölkt
THINK: LLM wird zur Entscheidung aufgefordert
Endgültige Antwort: Aktuell sind es in Tokyo 18°C (etwa 64°F) und bewölkt.
Diese Temperatur ist im Allgemeinen mild und für die meisten Menschen angenehm für einen Spaziergang.

Die Ausgabe zeigt den THINK → ACT → OBSERVE → THINK-Ablauf. In der ersten Iteration forderte das LLM einen get_weather-Aufruf an, und in der zweiten Iteration sah es das Wetterergebnis und erzeugte eine endgültige Antwort. Da tool_calls leer war, wurde die endgültige Antwort zurückgegeben und die Schleife beendet.

Nun testen wir das Szenario mit abhängigen Schritten aus der Einführung — bei dem der nächste Aufruf erst nach dem Ergebnis des vorherigen erfolgen kann.

python
answer = run_agent("Hole die Temperatur in Cairo und multipliziere die Zahl dann mit 3.")
print(f'Endgültige Antwort: {answer}')

Ausgabe:

THINK: LLM wird zur Entscheidung aufgefordert
ACT: rufe 'get_weather' auf, args={'city': 'Cairo'}
OBSERVE: 31°C, sonnig
THINK: LLM wird zur Entscheidung aufgefordert
ACT: rufe 'calculate' auf, args={'expression': '31 * 3'}
OBSERVE: 93
THINK: LLM wird zur Entscheidung aufgefordert
Endgültige Antwort: Aktuelle Temperatur in Cairo: 31°C. Multipliziert mit 3 = 93.

Diesmal lief die Schleife drei Iterationen.

  1. Erste Iteration — Das LLM fordert get_weather("Cairo") an.
  2. Zweite Iteration — Nachdem es das Ergebnis "31°C, sonnig" gesehen hat, fordert das LLM calculate("31 * 3") an. Es konnte den Ausdruck erst bilden, nachdem es die Temperatur gesehen hatte.
  3. Dritte Iteration — Nachdem es sowohl das Ergebnis "31°C, sonnig" als auch "93" gesehen hat, gab das LLM die endgültige Antwort zurück.

14.2) Sicherheitsgrenzen hinzufügen

Die oben erstellte Schleife hat nur eine Abbruchbedingung: Wenn das LLM ohne tool_calls antwortet, brechen wir die Schleife ab. Unter normalen Umständen ist dies ausreichend, aber was passiert, wenn das LLM nie aufhört, Tool-Aufrufe anzufordern?

Wenn ein Tool beispielsweise immer mehrdeutige Ergebnisse zurückgibt, ruft das LLM es möglicherweise weiterhin auf, in der Hoffnung auf etwas Besseres. Da die Schleife while True ist, stoppt das Programm nicht, wenn das LLM nicht stoppt. Die API-Aufrufkosten steigen weiter, während das Programm unbegrenzt läuft.

Die einfachste Lösung besteht darin, eine Obergrenze dafür festzulegen, wie oft die Schleife laufen darf. Ersetzen Sie while True durch for step in range(max_steps), und die Schleife wird garantiert nach max_steps Iterationen beendet, unabhängig davon, was das LLM tut.

python
def run_agent(user_input: str, max_steps: int = 10) -> str:
    """Führt die Think-Act-Observe-Schleife innerhalb von max_steps Iterationen aus."""
    messages = [
        SystemMessage(content="Sie sind ein hilfreicher Assistent."),
        HumanMessage(content=user_input),
    ]
 
    for step in range(max_steps):
        # THINK
        ai_message = llm_with_tools.invoke(messages)
        messages.append(ai_message)
 
        # Abbruchbedingung: keine tool_calls bedeutet, dass dies die endgültige Antwort ist
        if not ai_message.tool_calls:
            return ai_message.content
 
        # ACT + OBSERVE
        for tool_call in ai_message.tool_calls:
            selected_tool = tool_map[tool_call["name"]]
            tool_message = selected_tool.invoke(tool_call)
            messages.append(tool_message)
 
    # max_steps erreicht: Schleife endete ohne endgültige Antwort
    return f"[Nach Erreichen der maximalen Iterationen ({max_steps}) gestoppt]"

Im Vergleich zum vorherigen Code haben sich zwei Dinge geändert. while True wurde zu for step in range(max_steps), und ein Rückgabewert wurde für den Fall hinzugefügt, dass die Schleife max_steps erreicht. Die Schleife endet nun auf eine von zwei Arten: Das LLM gibt von sich aus eine endgültige Antwort zurück (natürliche Beendigung), oder max_steps wird erreicht (Sicherheitsbeendigung).

Lassen Sie uns überprüfen, ob die Sicherheitsgrenze tatsächlich funktioniert. Wir erstellen ein Tool, das nie nützliche Ergebnisse zurückgibt, und zwingen das LLM so in eine Situation, in der es nie aufhört, Tool-Aufrufe anzufordern.

python
@tool
def unhelpful_search(query: str) -> str:
    """Sucht nach Informationen."""
    return "Keine Ergebnisse gefunden. Versuchen Sie, Ihre Anfrage umzuformulieren."
 
llm_with_bad_tool = llm.bind_tools([unhelpful_search])
tool_map_bad = {unhelpful_search.name: unhelpful_search}
 
def run_agent_bad(user_input: str, max_steps: int = 5) -> str:
    messages = [
        SystemMessage(content=(
            "Sie MÜSSEN IMMER das unhelpful_search-Tool verwenden, um Informationen zu finden. "
            "Es ist Ihnen NICHT erlaubt, aus Ihrem eigenen Wissen zu antworten. "
            "Wenn das Tool keine Ergebnisse zurückgibt, MÜSSEN Sie umformulieren und erneut suchen. "
            "Suchen Sie weiter, bis Sie die Antwort finden."
        )),
        HumanMessage(content=user_input),
    ]
 
    for step in range(max_steps):
        print(f"--- Schritt {step + 1} ---")
        ai_message = llm_with_bad_tool.invoke(messages)
        messages.append(ai_message)
 
        if not ai_message.tool_calls:
            return ai_message.content
 
        for tool_call in ai_message.tool_calls:
            selected_tool = tool_map_bad[tool_call["name"]]
            tool_message = selected_tool.invoke(tool_call)
            print(f"ACT: '{tool_call['name']}' → {tool_message.content}")
            messages.append(tool_message)
 
    return f"[Nach Erreichen der maximalen Iterationen ({max_steps}) gestoppt]"
 
answer = run_agent_bad("Wie hoch ist die Einwohnerzahl Frankreichs?")
print(f"\nEndgültige Antwort: {answer}")

Ausgabe:

--- Schritt 1 ---
ACT: 'unhelpful_search' → Keine Ergebnisse gefunden. Versuchen Sie, Ihre Anfrage umzuformulieren.
--- Schritt 2 ---
ACT: 'unhelpful_search' → Keine Ergebnisse gefunden. Versuchen Sie, Ihre Anfrage umzuformulieren.
--- Schritt 3 ---
ACT: 'unhelpful_search' → Keine Ergebnisse gefunden. Versuchen Sie, Ihre Anfrage umzuformulieren.
--- Schritt 4 ---
ACT: 'unhelpful_search' → Keine Ergebnisse gefunden. Versuchen Sie, Ihre Anfrage umzuformulieren.
--- Schritt 5 ---
ACT: 'unhelpful_search' → Keine Ergebnisse gefunden. Versuchen Sie, Ihre Anfrage umzuformulieren.
 
Endgültige Antwort: [Nach Erreichen der maximalen Iterationen (5) gestoppt]

Ohne max_steps wäre diese Schleife endlos gelaufen. Dank max_steps=5 wurde sie nach fünf Iterationen zwangsweise beendet.

Der richtige Wert für max_steps hängt von der Komplexität Ihres Agents ab. Zu niedrig, und komplexe Aufgaben werden vorzeitig abgebrochen. Zu hoch, und ein fehlerhafter Agent verursacht Kosten, bevor er gestoppt wird. 15–25 ist ein üblicher Ausgangspunkt; passen Sie ihn basierend auf Ihren tatsächlichen Workloads an.

14.3) Tool-Fehler in der Schleife behandeln

Die Schleife, die wir in 14.1 und 14.2 erstellt haben, geht davon aus, dass Tools immer erfolgreich ausgeführt werden. Aber was passiert, wenn ein Tool eine Ausnahme auslöst? Der aktuelle Code hat keine Ausnahmebehandlung, sodass der gesamte Agent abstürzt, wenn während der Tool-Ausführung eine Ausnahme auftritt.

In Kapitel 12 haben wir gelernt, wie man Ausnahmen innerhalb des Tools selbst mit try/except abfängt und Fehlermeldungen als Strings zurückgibt. Wenn ein Tool auf diese Weise erstellt wird, gibt es kein Problem. Aber nicht jedes Tool behandelt Fehler intern. Tools, die externe Bibliotheken oder APIs aufrufen, können unerwartete Ausnahmen auslösen.

Um sich davor zu schützen, ist es eine gute Idee, Fehler auch auf Schleifenebene zu behandeln. Der Ansatz ist unkompliziert: Umschließen Sie die Tool-Ausführung mit einem try/except, und wenn eine Ausnahme auftritt, platzieren Sie die Fehlermeldung in einer ToolMessage und übergeben sie an das LLM. Das LLM kann diese Fehlermeldung lesen und mit korrigierten Argumenten erneut versuchen oder einen anderen Ansatz wählen. Dies wird als Selbstkorrektur bezeichnet.

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage
from langchain.tools import tool
 
# Tools definieren
@tool
def calculate(expression: str) -> str:
    """Wertet einen einfachen arithmetischen Ausdruck aus, z. B. '3 * 21'."""
    return str(eval(expression))  # Warnung: eval() ist ein Sicherheitsrisiko. Nicht in der Produktion verwenden.
 
# Tools binden
tools = [calculate]
llm = ChatOpenAI(model="gpt-5-mini")
llm_with_tools = llm.bind_tools(tools)
tool_map = {t.name: t for t in tools}
 
def run_agent(user_input: str, max_steps: int = 10) -> str:
    """Agent-Schleife, die Tool-Fehler an das LLM weitergibt und Selbstkorrektur ermöglicht."""
    messages = [
        SystemMessage(content="Sie sind ein hilfreicher Assistent."),
        HumanMessage(content=user_input),
    ]
 
    for step in range(max_steps):
        # THINK
        print("THINK: LLM wird zur Entscheidung aufgefordert")
        ai_message = llm_with_tools.invoke(messages)
        messages.append(ai_message)
 
        if not ai_message.tool_calls:
            return ai_message.content
 
        # ACT + OBSERVE
        for tool_call in ai_message.tool_calls:
            selected_tool = tool_map[tool_call["name"]]
            try:
                print(f"ACT: rufe '{tool_call['name']}' auf, args={tool_call['args']}")
                tool_message = selected_tool.invoke(tool_call)
                print(f"OBSERVE: {tool_message.content}")
            except Exception as e:
                print(f"OBSERVE: Fehler - {e}")
                tool_message = ToolMessage(
                    content=f"Fehler: {e}",
                    tool_call_id=tool_call["id"],
                )
            messages.append(tool_message)
 
    return f"[Nach Erreichen der maximalen Iterationen ({max_steps}) gestoppt]"

Im Vergleich zum Code aus 14.2 ist die einzige Änderung das try/except. Wenn ein Tool eine Ausnahme auslöst, wird die Fehlermeldung in einer ToolMessage platziert und dem Gespräch hinzugefügt. Beachten Sie, dass selbst ein fehlgeschlagener Aufruf eine ToolMessage mit der passenden tool_call_id haben muss. Das LLM sieht diesen Fehler in der nächsten Iteration und entscheidet, was als Nächstes zu tun ist.

Lassen Sie uns überprüfen, ob die Selbstkorrektur funktioniert. Wir lösen eine Ausnahme aus, indem wir das calculate-Tool bitten, durch null zu dividieren.

python
answer = run_agent("Verwende das Rechner-Tool, um 10 / 0 zu berechnen")
print(f"Endgültige Antwort: {answer}")

Ausgabe:

THINK: LLM wird zur Entscheidung aufgefordert
ACT: rufe 'calculate' auf, args={'expression': '10 / 0'}
OBSERVE: Fehler - division by zero
THINK: LLM wird zur Entscheidung aufgefordert
Endgültige Antwort: Ich habe das Rechner-Tool verwendet und es gab einen Fehler zurück: „division by zero."
 
Erklärung: 10 / 0 ist in der gewöhnlichen Arithmetik undefiniert und kann daher keine endliche Zahl ergeben.
 
Möchten Sie, dass ich:
- die einseitigen Grenzwerte berechne,
- das IEEE-754-Gleitkommaergebnis anzeige,
- oder einen anderen Ausdruck auswerte?

In der ersten Iteration forderte das LLM calculate("10 / 0") an, und ein ZeroDivisionError wurde ausgelöst. Das try/except fing die Ausnahme ab und übergab die Fehlermeldung an das LLM. In der zweiten Iteration sah das LLM die Fehlermeldung und gab eine endgültige Antwort zurück, die erklärt, dass die Division durch null unmöglich ist. Ohne das try/except wäre das Programm beim ersten ZeroDivisionError abgestürzt.