MCP und Human-in-the-Loop
Track KI · M4 Bausteine 03 und 04 · ca. 50 Min.
Worum es geht
In Lektion 14 hast du die Tool-Use-Schleife (die Agentenschleife) gebaut: Das Modell wünscht ein Tool, dein Code führt es aus, das Ergebnis geht zurück. Diese Lektion macht aus der Schleife etwas, das man verantworten kann. Teil 1 behandelt zwei Bausteine:
- MCP (Model Context Protocol): ein Standard, damit Agent und Tool-Anbieter sich verstehen. Die Tool-Definitionen kommen vom Server statt aus deinem Code.
- Human-in-the-Loop (HITL): Bei folgenreichen Aktionen oder Unsicherheit pausiert der Agent und fragt einen Menschen.
In Teil 2 folgen Fehlerbehandlung, Tracing (Ablaufprotokoll) und der Abschluss-Check von M4 als Projektaufgabe.
Was im Browser läuft: Kein echtes Modell und kein echter MCP-Server. Du arbeitest mit einem Mini-Server-Simulator: eine Klasse im Speicher, die Nachrichten im Stil von JSON-RPC annimmt und beantwortet. Die Schleife selbst brauchst du hier noch nicht, ihre Grundlagen stehen in Lektion 14.
Zeitplan ehrlich: etwa 25 Minuten Lesen und 25 Minuten für die drei Übungen. “(Quelle)” im Text meint das Kursbuch quellen/kursbuch-lerninhalte.md.
Von JS/TS her gedacht
MCP kennst du als Idee aus der Web-Welt: Statt dass jede Frontend-App ihre eigene Anbindung an dieselbe Datenquelle schreibt, gibt es eine gemeinsame Schnittstelle (REST mit OpenAPI, GraphQL, ein Language Server für Editoren). Der Konsument fragt “was kannst du?” und bekommt eine maschinenlesbare Antwort.
| Idee | Web / TypeScript | Hier |
|---|---|---|
| Schnittstelle entdecken | OpenAPI-Dokument, GraphQL-Introspection | MCP: listTools (auf der Leitung tools/list, siehe unten) |
| Aufruf als Nachricht | fetch mit JSON-Body |
JSON-Nachricht mit method und params |
| Fehler-Zweige | HTTP-Status, try / catch |
Antwort mit error oder result mit isError |
| Bestätigungsdialog | window.confirm("Wirklich löschen?") |
HITL-Gate vor der Aktion |
Ein Unterschied zu window.confirm: Der Dialog in der Browser-App ist für den Menschen gedacht, der eh klickt. Bei einem Agenten entscheidet dein Code, wann gefragt wird, denn das Modell selbst wird nicht von sich aus innehalten.
Konzept
Schritt 1: MCP in drei Sätzen
Quelle, Baustein 03: Ein MCP-Server bietet Tools, Ressourcen (z. B. Dateien, Datenbankeinträge) und Prompts über eine standardisierte Schnittstelle an. Ein MCP-Client (dein Agent) verbindet sich, fragt die Tools ab (listTools) und ruft sie im selben Schema auf wie eigene Tools. Für deine Schleife ändert sich strukturell wenig: Die Tool-Definitionen kommen jetzt vom Server.
Der Merksatz der Quelle gilt vorab: MCP löst ein Verbindungsproblem, keine Fähigkeit. Ein Tool wird durch MCP nicht besser, nur leichter über mehrere Agenten wiederverwendbar.
Schritt 2: Der Mini-Server
Wie sehen die Nachrichten aus? Die Quelle nennt nur listTools. Die folgenden Details stammen nicht aus der Quelle, sondern sind allgemeines Fachwissen und bitte gegen die aktuelle MCP-Doku zu prüfen: Auf der Leitung heißen die Methoden tools/list und tools/call, jede Nachricht ist ein JSON-Objekt im Stil von JSON-RPC 2.0 mit jsonrpc, id und method, die Antwort hat dieselbe id und entweder ein result oder ein error. Ein fehlgeschlagener Tool-Lauf kommt als result mit isError: true zurück (Feldnamen bitte prüfen). Der Simulator unten folgt genau diesen Annahmen. Er kann nur zwei Methoden, mehr nicht. Ressourcen und Prompts spielt er nicht nach, und der Verbindungsaufbau (ein initialize-Austausch vor dem ersten Aufruf) sowie der Transport (zum Beispiel stdio oder HTTP) fehlen ganz (bitte gegen die MCP-Doku prüfen). Ein echter Client macht das alles zusätzlich.
Ein Beispiel-Server mit zwei Tools für Notizen. Beachte: Die Tools sind ganz normale Python-Funktionen. Der Server verpackt sie nur.
Erst fragt der Client, was der Server kann (listTools), dann ruft er ein Tool auf. Jede Nachricht ist ein dict, jede Antwort auch:
Lies die Antwort des Aufrufs von außen nach innen: gleiche id wie die Anfrage (so ordnet der Client Antworten zu), dann result, darin content (eine Liste von Blöcken, hier ein Textblock) und isError. Das Ergebnis der Funktion steht als Text im ersten Block, deshalb ist die Liste der Notizen ein String.
Drei Fehlerfälle, jeder sieht anders aus:
Der Unterschied ist wichtig: Ein Tool-Fehler (leerer Text) ist ein normales result mit isError: True, das Modell darf den Text lesen und reagieren. Ein Protokollfehler (unbekanntes Tool, unbekannte Methode) kommt als error mit Code und Meldung. Dein Client behandelt beides, aber verschieden.
Schritt 3: Vom Server zur eigenen Schleife
Die Tools des Servers sehen fast aus wie deine eigenen Tool-Definitionen (Lektion 06). Es gibt eine Abweichung, die man leicht übersieht: Der Server nennt das Schema inputSchema, die Messages API nennt es input_schema (beides bitte gegen die Doku prüfen). Ein kleiner Adapter genügt:
Wünscht das Modell später ein Tool, schickst du statt eines lokalen Funktionsaufrufs eine tools/call-Nachricht an den Server und baust aus der Antwort deinen tool_result-Block. Mehr ändert sich nicht. Das übst du in Übung 1.
Schritt 4: Human-in-the-Loop, das Gate
Quelle, Baustein 04: Ein Agent, der bei Unsicherheit die wahrscheinlichste Aktion ausführt, ist bei folgenreichen Aktionen (Zahlung auslösen, Datensatz löschen) zu riskant. Es gibt zwei Auslöser für eine Rückfrage:
- Die selbst eingeschätzte Sicherheit ist niedrig (Quelle: unter 0.7).
- Die Aktion steht in einer festen Liste kritischer Aktionen und braucht unabhängig von der Sicherheit eine Bestätigung.
Die Quelle zeigt dafür ein Pydantic-Modell Entscheidung mit aktion, sicherheit (0 bis 1) und begruendung. Hier ein dict mit denselben Feldern, damit der Simulator klein bleibt. Der Mensch ist eine Funktion, die True (Freigabe) oder False (abgelehnt) liefert. Im echten Programm wäre das die Konsolen-Rückfrage input("... [j/n]"):
Lies die Ausgabe: Zeile 2 fällt wegen niedriger Sicherheit durch, Zeile 3 trotz höchster Sicherheit wegen der Liste. Zeile 4 zeigt die Grenze: < 0.7 heißt, genau 0.7 reicht noch. Das ist eine Festlegung, die du bewusst triffst und testest.
Das Gate selbst ist die Funktion, die zwischen Entscheidung und Ausführung sitzt. Der Mensch wird nur gefragt, wenn nötig (sonst nervt der Agent), und die Ablehnung ist ein vollwertiges Ergebnis:
Unsicherheit erkennen heißt auch nachfragen statt raten. Bei einer mehrdeutigen Aufgabe (“Storniere die Rechnung von gestern”, obwohl es drei gibt) ist die richtige Aktion keine Stornierung, sondern eine Rückfrage. Technisch ist das ein Tool wie jedes andere (zum Beispiel nachfragen(frage)): Die Schleife pausiert, der Mensch antwortet, die Antwort geht als tool_result zurück. In der Projektaufgabe von Teil 2 baust du genau das.
Falle
- MCP als Qualitätsversprechen. Ein schlecht beschriebenes Tool bleibt schlecht, auch hinter einem Server. Beschreibung und Schema sind weiter Teil deines Prompts (Lektion 06).
- MCP-Antworten blind vertrauen. Tool-Beschreibungen und Ergebnisse eines fremden Servers sind Eingabe von außen. Sie können Anweisungen enthalten, die das Modell umlenken (Prompt Injection, Thema in M3). Ein fremder Server bekommt nur die Rechte, die er braucht, und folgenreiche Aktionen laufen weiter durch dein Gate.
- Sicherheit des Modells als Wahrheit. Die selbst gemeldete Sicherheit ist eine Schätzung, keine kalibrierte Wahrscheinlichkeit (Stolperfalle der Quelle). Sie ergänzt die feste Liste kritischer Aktionen, ersetzt sie nie.
- Ungültige Sicherheit. Fehlt der Wert, ist er
nan,Trueoder 1.7, dann istwert < 0.7falsch oder gar nicht definiert (nan < 0.7istFalse, also würde ausgeführt). Im Zweifel fragst du, du rätst nicht.
Übungen
Übung 1: MCP-Anfrage bauen und Antwort lesen (leicht)
Der Simulator MiniServer aus Schritt 2 liegt bereit (du siehst ihn nicht, er ist wie im Text). Dazu ein Lager-Server mit den Tools bestand_abfragen (Eingabe artikel) und reservieren (Eingabe artikel, menge). Die Funktion baue_lager_server() liefert eine frische Instanz.
Schreibe rufe(server, name, argumente). Sie soll genau so vorgehen:
- Zuerst die Tools erfragen: Nachricht
{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}. - Steht
namenicht in der Tool-Liste,Nonezurückgeben und nichts weiter senden. - Sonst die Nachricht
{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": ..., "arguments": ...}}senden, mitargumenteunverändert. - Den Text des ersten
content-Blocks zurückgeben. IstisErrorwahr, stattdessen"FEHLER: "plus Text.
Beispiel: rufe(s, "bestand_abfragen", {"artikel": "schrauben"}) gibt "120" zurück, ein unbekannter Artikel "FEHLER: Artikel nieten unbekannt".
Die Namen stehen in liste["result"]["tools"], jeder Eintrag hat ein Feld name. Und wo liegt in der Antwort des Aufrufs der Text, wo das Fehlerkennzeichen?
def rufe(server, name, argumente):
liste = server.handle({"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
if name not in [t["name"] for t in liste["result"]["tools"]]:
return None
antwort = server.handle({"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": name, "arguments": argumente}})
ergebnis = antwort["result"]
text = ergebnis["content"][0]["text"]
return "FEHLER: " + text if ergebnis["isError"] else text
rufeÜbung 2: Das Human-in-the-Loop-Gate (mittel)
Schreibe steuere(entscheidung, kritische, freigabe). entscheidung ist ein dict mit aktion, sicherheit und begruendung. kritische ist eine Sammlung kritischer Aktionen. freigabe(entscheidung) ist die Rückfrage an den Menschen und liefert True oder False.
Regeln:
- Eine Freigabe wird gebraucht, wenn
sicherheitunter0.7liegt (genau0.7reicht), oder die Aktion inkritischesteht. - Als ungültige Sicherheit zählen: fehlt, keine Zahl (auch
TrueundFalsenicht),nanoder außerhalb von 0 bis 1. Dann wird ebenfalls gefragt, nicht geraten. - Wird eine Freigabe gebraucht, ruft
steuerefreigabegenau einmal auf:Trueergibt"ausgefuehrt",Falseergibt"abgelehnt". - Wird keine gebraucht, ergibt es
"ausgefuehrt"undfreigabewird nicht aufgerufen.
Bestimme zuerst, ob eine Freigabe nötig ist, und mache dafür aus allen Spielarten “ungültig” einen einzigen Zweig. Wie erkennst du nan, und was ist mit True?
import math
def steuere(entscheidung, kritische, freigabe):
s = entscheidung.get("sicherheit")
gueltig = (isinstance(s, (int, float)) and not isinstance(s, bool)
and not math.isnan(s) and 0 <= s <= 1)
noetig = (not gueltig) or s < 0.7 or entscheidung["aktion"] in kritische
if noetig:
return "ausgefuehrt" if freigabe(entscheidung) else "abgelehnt"
return "ausgefuehrt"
steuereÜbung 3: Wann MCP, wann eigene Tool-Definition? (leicht)
Die Ticket-Datenbank deiner Firma soll von drei Agenten abgefragt werden: einem Support-Bot, einem Auswertungs-Agenten und einem Assistenten in der IDE. Drei Teams bauen sie, jedes in einer anderen Sprache. Das Datenbank-Team will die Anbindung einmal bauen und allein pflegen. Was passt am besten?
- a) Jedes Team beschreibt die Datenbank-Tools selbst in seinem Agenten, dann passt jedes Schema genau zum eigenen Bedarf.
- b) Es wird MCP gewählt, weil die Datenbank-Abfragen dadurch schneller und treffsicherer werden als mit eigenen Tools.
- c) Die Zugangsdaten stehen im Prompt jedes Agenten, damit alle drei die Datenbank direkt abfragen können.
- d) Das Datenbank-Team bietet einen MCP-Server an, und die Agenten holen sich die Tools per Tool-Liste vom Server.
Trage den Buchstaben als String ein.
Was genau löst MCP, und was löst es ausdrücklich nicht? Wer pflegt bei jeder Variante die Anbindung, wenn sich die Datenbank ändert?
antwort = "d"
antwortMerksatz
MCP löst ein Verbindungsproblem, keine Fähigkeit, und ein Agent fragt bei Unsicherheit oder kritischer Aktion einen Menschen, wobei dein Code entscheidet, wann gefragt wird, nicht das Modell.
Prüfstein
Dein Agent soll Rechnungen stornieren können, und ein fremder MCP-Server liefert die Tool-Beschreibung dafür. Welche zwei Dinge sicherst du in deinem Code ab, auch wenn der Server “vertrauenswürdig” wirkt, und was meldest du dem Menschen, wenn die selbst gemeldete Sicherheit des Modells genau 0.7 beträgt?
Weiter mit Teil 2: Fehlerbehandlung, Tracing und M4-Abschluss.
Quelle: quellen/kursbuch-lerninhalte.md, Modul M4, Bausteine “03 MCP (Model Context Protocol)” und “04 Human-in-the-Loop & Unsicherheit erkennen” (Warum, Kernidee, Merksatz, Stolperfalle, Übungen). Aus der Quelle stammen: listTools, Tools, Ressourcen und Prompts, die Schwelle 0.7 mit der Liste kritischer Aktionen, das Modell Entscheidung. Über die Quelle hinaus (allgemeines Fachwissen, bitte gegen die aktuelle MCP-Doku prüfen): die Methodennamen tools/list und tools/call, der Nachrichtenaufbau im Stil von JSON-RPC 2.0 (jsonrpc, id, method, params, result, error), die Fehlercodes, die Feldnamen inputSchema, content und isError. Der Mini-Server, die Tools und die Fälle sind von Hand geschrieben und erfunden.