Der Agent-Loop: Plan, Aktion, Beobachtung
Kurz gesagt: Ein KI-Agent ist eine Schleife: Das Modell nennt einen Plan und bittet um eine Aktion, das Programm führt sie aus und gibt die Beobachtung zurück, bis es eine Antwort gibt, das Schrittlimit erreicht ist oder ein Mensch gebraucht wird.
Problem
Der Assistent antwortet, erledigt aber nichts
Der Kunde schreibt: „Die Kopfhörer aus Bestellung 1187 sind kaputt. Ich möchte sie zurückgeben.“
Das ist keine Frage mit einer Antwort. Jemand muss prüfen, ob die Bestellung zugestellt wurde, eine Rückgabeanfrage öffnen und dem Kunden sagen, wie es weitergeht. Ist die Bestellung noch nicht zugestellt, sieht der Weg anders aus. Will der Kunde eine Entschädigung, wieder anders.
Die Schleife aus der Lektion über Tool Use hat eine Frage beantwortet: Das Modell bittet um Daten, das Programm gibt sie, das Modell antwortet. Hier müssen mehrere Aktionen nacheinander passieren, und welche als Nächste kommt, weiß man erst nach der vorigen.
Du kannst es mit einem if für jeden Fall schreiben. Das funktioniert für die Fälle, die du vorhergesehen hast. Kunden schreiben aber auch die anderen.
Die Idee
Der Agent: Plan, Aktion, Beobachtung
Ein Agent ist ein Programm, in dem das Modell den nächsten Schritt zu einem Ziel wählt. Jeder Schritt besteht aus drei Dingen:
- Plan. Das Modell sagt in Worten, was es tun will: „Zuerst prüfe ich, ob die Bestellung zugestellt wurde.“
- Aktion. Das Modell bittet um ein Tool (ein
tool_use-Block), und das Programm führt es aus. - Beobachtung. Das Ergebnis geht zurück in den Verlauf (ein
tool_result-Block). Im nächsten Schritt sieht das Modell es und wählt neu.
Das ist der Agent-Loop. Das Programm hält drei Ausgänge daraus bereit:
- Antwort. Das Modell ist fertig und liefert
end_turn. - Schrittlimit. Nach 5 Schritten ohne Antwort hält das Programm an und übergibt an einen Kollegen.
- Ein Mensch wird gebraucht. Das Modell selbst bittet um das Tool
hand_to_human. Das Programm führt es nicht aus, sondern hält an.
Die Tools des Agenten sind sicher: Bestellstatus, das Lieferdatum des Kuriers und eine Rückgabeanfrage. Die Anfrage wartet nur auf einen Kollegen. Geld läuft nie über den Agenten.
Sieh es dir an
Die Tools und die Daten
Die vier Tools. Die Beschreibung ist für das Modell: Es wählt danach.
def tool(name, description, field): schema = {"type": "object", "properties": {field: {"type": "string"}}, "required": [field]} return {"name": name, "description": description, "input_schema": schema}
TOOLS = [ tool("get_order_status", "Status und Datum einer Bestellung nach ihrer Nummer.", "order_id"), tool("estimate_delivery", "Das Lieferdatum, das der Kurier erwartet.", "order_id"), tool("open_return_request", "Öffnet eine Rückgabeanfrage. Ein Kollege gibt sie frei.", "order_id"), tool("hand_to_human", "Übergibt das Gespräch an einen Kollegen, mit dem Grund.", "reason"),]Das Programm führt aus, worum gebeten wurde, gegen seine eigenen Daten. Die Rückgabeanfrage kommt in eine Liste, die ein Kollege ansieht:
def run_tool(use): order_id = use["input"].get("order_id", "") order = ORDERS.get(order_id) if order is None: return result(use, f"keine Bestellung {order_id}", error=True) delivered = order["status"] == DELIVERED if use["name"] == "get_order_status": text = f"{order_id} — {order['status']}, Lieferung {order['date']}" elif use["name"] == "estimate_delivery": if delivered: text = f"{order_id} — schon zugestellt" else: text = f"{order_id} — erwartete Lieferung {order['date']}, kein neuer Scan" elif use["name"] == "open_return_request": if not delivered: return result(use, f"{order_id} ist noch nicht zugestellt", error=True) request = f"R-{len(REQUESTS) + 1} für {order_id}" REQUESTS.append(request) text = f"{request} — wartet auf die Freigabe durch einen Kollegen" else: return result(use, f"kein Tool {use['name']}", error=True) return result(use, text)
def result(use, text, error=False): block = {"type": "tool_result", "tool_use_id": use["id"], "content": text} if error: block["is_error"] = True return blockhand_to_human steht hier nicht. Es ist keine Aktion, sondern ein Ausgang aus der Schleife.
Sieh es dir an
Die Schleife
def run_agent(model, task): print(f"Aufgabe: {task}") first = {"type": "text", "text": task} history = [{"role": "user", "content": [first]}] for step in range(1, MAX_STEPS + 1): reply = model.create(SYSTEM, history, TOOLS) head = f" Schritt {step}. " text = text_of(reply) if reply["stop_reason"] != "tool_use": print(f"{head}Ende: {text}") return use = tool_use_of(reply) print(f"{head}Plan: {text}") action = describe(use) print(f" Aktion: {action}") if use["name"] == "hand_to_human": print(" Stopp: ein Mensch wird gebraucht —", use["input"]["reason"]) return history.append({"role": "assistant", "content": reply["content"]}) result = run_tool(use) print(f" Beobachtung: {result['content']}") history.append({"role": "user", "content": [result]}) print(f" Stopp: {MAX_STEPS} Schritte ohne Antwort", "→ an einen Kollegen")Schritt für Schritt, in Worten
- Die Aufgabe ist die erste Nachricht. Der Agent hat 5 Schritte (MAX_STEPS) und vier Tools.
- Schritt 1. Das Modell nennt einen Plan und bittet um eine Aktion: get_order_status(1187).
- Es ist nicht hand_to_human, also führt das Programm es aus. Die Beobachtung kommt in den Verlauf.
- Schritt 2. Das Modell sieht „zugestellt“, also ändert sich der Plan: open_return_request(1187).
- Das Tool öffnet nur die Anfrage R-1. Sie wartet auf einen Kollegen: Der Agent erstattet nie Geld.
- Schritt 3. Das Modell antwortet mit end_turn: Die Schleife endet mit einer Antwort, nach 3 von 5 Schritten.
- Zwei weitere Ausgänge stehen bereit: das Schrittlimit und hand_to_human. Ohne sie kann der Agent endlos kreisen.
Die Hilfsfunktionen finden den Plan (den Textblock) und die Aktion (den tool_use-Block):
def text_of(reply): return next((b["text"] for b in reply["content"] if b["type"] == "text"), "")
def tool_use_of(reply): return next(b for b in reply["content"] if b["type"] == "tool_use")
def describe(use): return f"{use['name']}({', '.join(use['input'].values())})"def run_agent(model, task): print(f"Aufgabe: {task}") first = {"type": "text", "text": task} history = [{"role": "user", "content": [first]}] for step in range(1, MAX_STEPS + 1): reply = model.create(SYSTEM, history, TOOLS) head = f" Schritt {step}. " text = text_of(reply) if reply["stop_reason"] != "tool_use": print(f"{head}Ende: {text}") return use = tool_use_of(reply) print(f"{head}Plan: {text}") action = describe(use) print(f" Aktion: {action}") if use["name"] == "hand_to_human": print(" Stopp: ein Mensch wird gebraucht —", use["input"]["reason"]) return history.append({"role": "assistant", "content": reply["content"]}) result = run_tool(use) print(f" Beobachtung: {result['content']}") history.append({"role": "user", "content": [result]}) print(f" Stopp: {MAX_STEPS} Schritte ohne Antwort", "→ an einen Kollegen")model = FakeModel()run_agent(model, "Die Kopfhörer aus Bestellung 1187 sind kaputt. Ich möchte sie zurückgeben.")print("wartet auf Freigabe:", ", ".join(REQUESTS))Aufgabe: Die Kopfhörer aus Bestellung 1187 sind kaputt. Ich möchte sie zurückgeben. Schritt 1. Plan: Zuerst prüfe ich, ob die Bestellung zugestellt wurde. Aktion: get_order_status(1187) Beobachtung: 1187 — zugestellt, Lieferung 26. Oktober Schritt 2. Plan: Sie ist zugestellt. Ich öffne eine Rückgabeanfrage. Aktion: open_return_request(1187) Beobachtung: R-1 für 1187 — wartet auf die Freigabe durch einen Kollegen Schritt 3. Ende: Ich habe die Anfrage R-1 zur Rückgabe von Bestellung 1187 geöffnet. Ein Kollege gibt sie frei und schreibt dir.wartet auf Freigabe: R-1 für 1187Nirgends steht im Code „nach dem Status öffne eine Anfrage“. Das Modell wählt es, weil die Beobachtung sagt „1187 — zugestellt, Lieferung 26. Oktober“. Das Programm hält das Limit ein, führt aus und protokolliert.
Das Modell hier ist ein falscher Client, wie in den früheren Lektionen: Anhand der Wörter der Aufgabe und der bisherigen Beobachtungen wählt es den nächsten Schritt. Seine Regel steht unter „Mehr“.
Probier es aus
Ergänze
Bei welcher Aktion führt das Programm nichts aus, sondern hält die Schleife an?
if use["name"] == "":hand_to_human. Das Modell bittet darum, wenn die Aufgabe nichts für es ist: Entschädigung, eine Beschwerde, etwas außerhalb der Regeln. Das Programm gibt den Grund aus und hält an. Ein echter Shop würde das Gespräch mit dem ganzen Verlauf an einen Kollegen schicken.
Die Idee
Wenn der Agent nicht aufhört
Der Kunde schreibt „Wann kommt 1042 an? Der Kurier hat gesagt, heute.“. Der Kurier hat keinen neuen Scan, und das Modell fragt immer wieder:
model = FakeModel()run_agent(model, "Wann kommt 1042 an? Der Kurier hat gesagt, heute.")Aufgabe: Wann kommt 1042 an? Der Kurier hat gesagt, heute. Schritt 1. Plan: Ich frage den Kurier nach dem Termin. Aktion: estimate_delivery(1042) Beobachtung: 1042 — erwartete Lieferung 3. November, kein neuer Scan Schritt 2. Plan: Keine Änderung. Ich frage noch einmal. Aktion: estimate_delivery(1042) Beobachtung: 1042 — erwartete Lieferung 3. November, kein neuer Scan Schritt 3. Plan: Keine Änderung. Ich frage noch einmal. Aktion: estimate_delivery(1042) Beobachtung: 1042 — erwartete Lieferung 3. November, kein neuer Scan Schritt 4. Plan: Keine Änderung. Ich frage noch einmal. Aktion: estimate_delivery(1042) Beobachtung: 1042 — erwartete Lieferung 3. November, kein neuer Scan Schritt 5. Plan: Keine Änderung. Ich frage noch einmal. Aktion: estimate_delivery(1042) Beobachtung: 1042 — erwartete Lieferung 3. November, kein neuer Scan Stopp: 5 Schritte ohne Antwort → an einen KollegenJeder Schritt schickt den ganzen Verlauf, also kostet jeder Schritt mehr als der vorige (die Zahlen stehen in der Lektion über Kosten). Ohne das Limit dreht sich das Programm, bis das Geld alle ist. Mit ihm erreicht der Kunde nach 5 Schritten einen Menschen.
Das Limit ist die letzte Verteidigung, nicht die einzige. Hier fragt das Modell fünfmal dasselbe, und das Programm hätte es schon im zweiten Schritt sehen können. Das ist die Übung dieser Lektion.
Probier es aus
Sag die Ausgabe voraus
Der Kunde will eine Entschädigung. Was gibt runAgent aus?
model = FakeModel()run_agent(model, "Ich möchte 20 € Entschädigung, weil 1042 zu spät kommt.")Zwei Schritte. Das Modell prüft die Bestellung und bittet dann um hand_to_human: Über eine Entschädigung entscheidet es nicht. Das Programm führt diese Aktion nicht aus und hält mit dem Grund an. Es öffnet keine Rückgabeanfrage und erreicht das Limit nicht.
Probier es aus
Finde den Fehler
Ein Kollege schreibt runAgent, und jedes Gespräch endet mit „5 Schritte ohne Antwort“, auch wenn der Kunde eine Antwort bekommen hat. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
break verlässt nur die Schleife. Danach erreicht das Programm die Zeile nach der Schleife und gibt aus, dass das Limit aufgebraucht ist. Der Kunde bekommt also eine Antwort und gleich danach „an einen Kollegen“. Es muss return sein: Die Antwort ist ein Ausgang aus der ganzen Funktion.
Die Idee
Bei dir ausführen
Dieses Programm ist nicht Teil der Prüfung der Lektion: Du brauchst ein Konto bei einem Modell-Anbieter, einen Schlüssel und Geld für jeden Aufruf. Der Schlüssel steht in LLM_API_KEY, nie im Code. Anbieter liefern offizielle SDKs für Java, C#, Python und JavaScript mit derselben Form (Nachrichten, Rollen, Tokens, Tools); die genauen Namen von Klassen und Methoden stehen in der Doku des Anbieters.
Die erste Anfrage trägt die Aufgabe und die vier Tools. Der Name des Modells ist ein Platzhalter:
{ "model": "<Name des Modells>", "max_tokens": 1024, "system": "Du bist der Support-Agent eines Online-Shops. Nutze die Tools; über Geld entscheidet ein Mensch.", "tools": [ { "name": "get_order_status", "description": "Status und Datum einer Bestellung nach ihrer Nummer.", "input_schema": { "type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"] } }, { "name": "estimate_delivery", "description": "Das Lieferdatum, das der Kurier erwartet.", "input_schema": { "type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"] } }, { "name": "open_return_request", "description": "Öffnet eine Rückgabeanfrage. Ein Kollege gibt sie frei.", "input_schema": { "type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"] } }, { "name": "hand_to_human", "description": "Übergibt das Gespräch an einen Kollegen, mit dem Grund.", "input_schema": { "type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"] } } ], "messages": [ { "role": "user", "content": "Die Kopfhörer aus Bestellung 1187 sind kaputt. Ich möchte sie zurückgeben." } ]}Die Antwort trägt sowohl den Plan als auch die Aktion: einen Textblock und einen tool_use-Block in einer Nachricht. Die Zahlen in usage sind grob, nach der Regel aus der Lektion über Tool Use:
{ "content": [ { "type": "text", "text": "Zuerst prüfe ich, ob die Bestellung zugestellt wurde." }, { "type": "tool_use", "id": "call_1", "name": "get_order_status", "input": {"order_id": "1187"} } ], "stop_reason": "tool_use", "usage": {"input_tokens": 81, "output_tokens": 13}}Jede Anfrage wird mit dem Standard-HTTP-Client der Sprache gesendet, und die Schleife darum ist runAgent. Die Adresse ist ein Platzhalter:
# request_json ist das JSON der Anfrage oben.# Der genaue Header für den Schlüssel steht in der Doku des Anbieters.import osimport urllib.request
request = urllib.request.Request( "https://<anbieter>/messages", data=request_json.encode("utf-8"), headers={"Authorization": "Bearer " + os.environ["LLM_API_KEY"], "Content-Type": "application/json"},)with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8"))Die Idee
Wenn es wehtut
- Der Weg ist bekannt. Wenn „Bestellstatus“ immer dieselbe Anfrage ist, schreib sie im Code. Der Agent ist langsamer, kostet mehr und wählt vielleicht jedes Mal etwas anderes. Er lohnt sich, wenn die Schritte davon abhängen, was unterwegs auftaucht.
- Aktionen mit Folgen. Erstattungen, Stornierungen, eine E-Mail an alle Kunden. Der Agent bereitet sie vor, ein Mensch gibt sie frei. Das Modell kann die falsche Bestellung wählen, und der Text, den es liest, kann es in die Irre führen. Wie das geschieht, steht in der Lektion über Prompt Injection, und die Prüfungen um das Modell herum stehen in der Lektion über Guardrails.
- Kein Limit. Ein Limit für Schritte, für Tokens (oder Euro) und für die Zeit. Jedes davon hält den Agenten an und übergibt an einen Menschen, statt zu kreisen.
- Kein Protokoll. Wenn der Agent sich irrt, musst du Plan, Aktion und Beobachtung jedes Schritts sehen. Die Ausgabe von
runAgentist genau so ein Protokoll. - Keine Tests. Ein Agent wird gemessen wie jede App mit einem Modell: ein Testset aus Aufgaben mit Prüfungen der Endantwort, aber auch des Wegs (welche Aktionen, wie viele Schritte).
Genauer, für Neugierige
Eine Aktion pro Schritt. Ein echtes Modell kann in einer Antwort um mehrere Tools bitten, wie in der Lektion über Tool Use. Hier ist jeder Schritt eine Aktion, damit die Reihenfolge sichtbar bleibt. Ein Programm mit mehreren Aktionen führt sie alle aus und gibt alle Ergebnisse in einer user-Nachricht zurück.
Der Plan ist Text. Das Modell schreibt „Zuerst prüfe ich“, aber nichts zwingt es, genau das zu tun. Das Programm vertraut dem Plan nicht; es führt nur die Aktion aus, und nur, wenn sie auf der Liste der Tools steht. Der Plan ist nützlich für das Protokoll und für den Menschen, der es danach liest.
Ein Name aus den Papers. Der Rhythmus „Gedanke, Aktion, Beobachtung“ ist als ReAct bekannt (reason + act). Es gibt auch Agent-Bibliotheken, die die Schleife für dich ausführen. Innen sind es dieselben drei Dinge und dieselben Ausgänge.
Das falsche Modell innen. Es sammelt die Beobachtungen aus dem Verlauf und zählt sie. Bei einer Rückgabe: zuerst der Status; ist er „zugestellt“, eine Anfrage; dann eine Antwort mit der Nummer der Anfrage. Bei einer Entschädigung: der Status, dann hand_to_human. Beim Kurier: Es fragt in jedem Schritt erneut. Die tool_use-IDs sind call_1, call_2.
# Steht anstelle des echten Modells: aufgezeichnete Antworten,# ohne Netz und jedes Mal dieselben.class FakeModel: NOT_DELIVERED = "Die Bestellung ist noch nicht zugestellt. Du kannst sie nach der Lieferung zurückgeben." THANKS = "Gern geschehen! Schreib, wenn du eine Frage hast."
def __init__(self): self.last_id = 0
def create(self, system, messages, tools): task = messages[0]["content"][0]["text"] seen = [b["content"] for m in messages for b in m["content"] if b["type"] == "tool_result"] n = len(seen) order_id = "1187" if "1187" in task else "1042" if "zurückgeben" in task: if n == 0: return self.act("Zuerst prüfe ich, ob die Bestellung zugestellt wurde.", "get_order_status", "order_id", order_id) if n == 1 and "zugestellt" in seen[0]: return self.act("Sie ist zugestellt. Ich öffne eine Rückgabeanfrage.", "open_return_request", "order_id", order_id) if n == 1: return self.say(self.NOT_DELIVERED) request = seen[-1].split(" ")[0] return self.say(f"Ich habe die Anfrage {request} zur Rückgabe von Bestellung {order_id} geöffnet. Ein Kollege gibt sie frei und schreibt dir.") if "Entschädigung" in task: if n == 0: return self.act("Ich prüfe die Bestellung.", "get_order_status", "order_id", order_id) return self.act("Über eine Entschädigung entscheidet ein Mensch, nicht ich.", "hand_to_human", "reason", f"Entschädigung für {order_id}") if "Kurier" in task: plan = "Ich frage den Kurier nach dem Termin." if n == 0 else "Keine Änderung. Ich frage noch einmal." return self.act(plan, "estimate_delivery", "order_id", order_id) return self.say(self.THANKS)
# Ein Plan (Text) und eine Aktion (tool_use) in einer Antwort. def act(self, plan, tool, key, value): self.last_id += 1 use = {"type": "tool_use", "id": f"call_{self.last_id}", "name": tool, "input": {key: value}} return {"content": [{"type": "text", "text": plan}, use], "stop_reason": "tool_use"}
@staticmethod def say(text): return {"content": [{"type": "text", "text": text}], "stop_reason": "end_turn"}Übung
Dieselbe Aktion zweimal
Löse es in deinem Editor.
Der Kunde schreibt „Wann kommt 1042 an? Der Kurier hat gesagt, heute.“, und der Agent fragt den Kurier fünfmal, bevor er anhält. Füge runAgent eine Prüfung hinzu: Bittet das Modell um dieselbe Aktion wie im vorigen Schritt (derselbe Name und dieselbe Eingabe), gibt das Programm Stopp: dieselbe Aktion zweimal → an einen Kollegen aus und hält an.
Aufgabe: Wann kommt 1042 an? Der Kurier hat gesagt, heute. Schritt 1. Plan: Ich frage den Kurier nach dem Termin. Aktion: estimate_delivery(1042) Beobachtung: 1042 — erwartete Lieferung 3. November, kein neuer Scan Schritt 2. Plan: Keine Änderung. Ich frage noch einmal. Aktion: estimate_delivery(1042) Stopp: dieselbe Aktion zweimal → an einen KollegenWeitere Aufgaben
- Ein Token-Budget. Zähle die Tokens jedes Schritts (Wörter × 1,3, wie in der Lektion über Tool Use) und halte an, sobald die Summe 300 übersteigt. In welchem Schritt hält die Kurier-Aufgabe an?
- Eine noch nicht zugestellte Bestellung. Führe eine Rückgabe-Aufgabe für Bestellung 1042 aus, die noch nicht zugestellt ist. Welchen Ausgang nimmt der Agent, und was sagt er dem Kunden?
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Strukturierte Ausgabe: JSON nach einem Schema · Tool Use: das Modell fragt, das Programm handelt · Evaluation: ein Testset für den Assistenten · Kosten und Latenz: Budget, Cache, ein kleineres Modell
- Geht weiter in: Prompt Injection: die E-Mail, die Befehle gibt · Guardrails: Prüfungen vor und nach dem Modell · Der Support-Assistent
- Aufgaben zum Thema: Agenten