Einheit 3 — Der Router: Regeln zuerst, KI als Rückfall
Was du nach dieser Einheit weißt: Du kannst einen Router bauen, der eingehende Nachrichten in fester Reihenfolge prüft, frei formulierte Anfragen per KI einem Skill zuordnet, bei Unsicherheit nachfragt und riskante Aktionen erst nach Bestätigung ausführt.
Die Aufgabe des Routers
Der Router beantwortet für jede Nachricht genau eine Frage: Was passiert als Nächstes? Mögliche Ergebnisse sind:
| Ergebnis | Beispiel |
|---|---|
| Direkte Antwort | „Hilfe“ → Liste der Fähigkeiten |
| Weitergabe an einen Skill | „Wie hoch ist der Umsatz im Oktober?“ → Skill Kennzahlen |
| Rückfrage | „Mach das mal fertig“ → „Meinst du das Angebot für Firma X?“ |
| Bestätigung einholen | „Schick die Mahnung raus“ → „Soll ich die Mahnung an … senden? ja/nein“ |
| Ablehnung | unbekannter Absender, Assistent pausiert |
Der Router führt selbst nichts aus. Er entscheidet und verteilt.
Der Aufbau im Workflow
[ Eingang ] → [ Kontext laden ] → [ Entscheiden ] → [ Verteilen ] ─→ Skill Wissen
Internal Storage JavaScript Switch Agent ─→ Skill Erinnerung
▲ ─→ Rückkanal (direkte Antwort)
│ ─→ KI-Erkennung ─┐
└─────────── Ergebnis der KI ───────────────┘
Auffällig ist die Schleife: Wenn keine feste Regel greift, geht die Nachricht zur KI-Erkennung und kommt mit deren Ergebnis ein zweites Mal in denselben Entscheidungs-Agent zurück. So steht die gesamte Entscheidungslogik an einer Stelle.
Schritt 1 — Kontext in einem Zug laden
Ein einziger Internal Storage Agent holt alles, was der Router braucht. Jede Unterabfrage liefert eine Spalte:
SELECT
(SELECT COUNT(*) FROM chat_nachricht WHERE nachricht_id = '{{ nachricht_id }}') AS schon,
(SELECT wert FROM assistent_einstellung WHERE schluessel = 'aktiv') AS aktiv,
(SELECT rolle FROM assistent_nutzer
WHERE kanal = '{{ kanal }}' AND absender_id = '{{ absender_id }}' AND aktiv = 1) AS rolle,
(SELECT json_group_array(json_object('s', schluessel, 'n', name, 'b', beschreibung,
'x', beispiele, 'r', risiko, 'z', ziel))
FROM (SELECT * FROM assistent_faehigkeit WHERE aktiv = 1 ORDER BY sortierung)) AS faehigkeiten,
(SELECT json_group_array(json_object('r', rolle, 't', text))
FROM (SELECT rolle, text FROM assistent_verlauf
WHERE unterhaltung_id = '{{ unterhaltung_id }}' ORDER BY id DESC LIMIT 8)) AS verlauf,
(SELECT json_object('id', id, 'f', faehigkeit, 't', text, 's', status)
FROM assistent_vorgang
WHERE unterhaltung_id = '{{ unterhaltung_id }}'
AND status IN ('bestaetigung', 'rueckfrage')
AND erstellt > datetime('now', '-30 minutes')
ORDER BY erstellt DESC LIMIT 1) AS offen
Setze am Internal Storage Agent keep_message: true. Dann enthält die ausgehende Nachricht sowohl die ursprünglichen Felder (text, absender_id …) als auch das Abfrageergebnis unter rows.
Die Tabellen assistent_faehigkeit, assistent_verlauf und assistent_vorgang lernst du in den Einheiten 4 und 5 kennen.
Schritt 2 — Entscheiden in fester Reihenfolge
Der Entscheidungs-Agent ist ein JavaScript Agent. Er prüft die Nachricht von oben nach unten — die erste passende Regel gewinnt:
| # | Prüfung | Ergebnis |
|---|---|---|
| 1 | Nachricht schon verarbeitet? | still beenden |
| 2 | Absender nicht berechtigt? | Ablehnung |
| 3 | Steuerbefehl („pause“, „weiter“)? | Assistent an- oder abschalten |
| 4 | Assistent pausiert? | Hinweis, nichts ausführen |
| 5 | Offene Bestätigung und Antwort „ja“/„nein“? | gemerkten Auftrag ausführen oder verwerfen |
| 6 | Feste Befehle („hilfe“, „was läuft?“) | direkte Antwort |
| 7 | Eindeutige Muster („erinnere mich …“, „neues Ticket …“) | Skill direkt |
| 8 | Gelernte Formulierung (Einheit 6) | Skill direkt |
| 9 | Nichts davon | KI-Erkennung |
Diese Reihenfolge ist kein Zufall:
- Sicherheit vor allem anderen. Doppelte Nachrichten, fremde Absender und der Not-Aus werden geprüft, bevor irgendetwas anderes passiert.
- Offene Gespräche vor neuen. Wer gerade gefragt wurde „Soll ich das ausführen?“ und mit „ja“ antwortet, meint damit diese Frage — und keinen neuen Auftrag.
- Billig vor teuer. Regeln kosten nichts und sind sofort da. Die KI kommt nur zum Zug, wenn keine Regel passt.
Ein gekürztes Beispiel:
Agent.receive = function() {
var p = this.incomingMessage().payload || {};
var r0 = (p.rows && p.rows[0]) || {};
var ki = p.ki || null; // gesetzt im zweiten Durchlauf
var text = String(p.text || '').trim();
var o = kopie(p); // Felder für die nächsten Schritte
function json(v, d) { try { return typeof v === 'string' ? JSON.parse(v) : (v || d); } catch (e) { return d; } }
function kopie(x) { var c = {}; for (var k in x) { if (k !== 'rows') { c[k] = x[k]; } } return c; }
var katalog = ki ? p.katalog : json(r0.faehigkeiten, []);
var offen = ki ? null : json(r0.offen, null);
o.katalog = katalog;
if (ki) { return zweiterDurchlauf(this); }
if (parseInt(r0.schon, 10) > 0) { return; } // 1
if (!r0.rolle) { return antworte(this, 'Deine Nachricht kann ich nicht verarbeiten.'); } // 2
if (/^(pause|stopp)$/i.test(text)) { o.sql_zusatz = setzeAktiv(0); return antworte(this, '⏸ Pausiert.'); } // 3
if (/^(weiter|start)$/i.test(text)) { o.sql_zusatz = setzeAktiv(1); return antworte(this, '▶ Wieder aktiv.'); }
if (r0.aktiv === '0') { return antworte(this, '⏸ Ich bin pausiert. Schreib „weiter“.'); } // 4
if (offen && offen.s === 'bestaetigung' && /^(ja|ok|mach)/i.test(text)) { return fuehreAus(this, offen.f, offen.t); } // 5
if (/^(hilfe|\?|was kannst du)/i.test(text)) { return antworte(this, hilfeText(katalog)); } // 6
if (/^erinnere? mich\b/i.test(text)) { return weiter(this, 'erinnerung'); } // 7
// 9: keine Regel — an die KI-Erkennung
o.route = 'ki';
o.ki_katalog = katalog.map(function(k) { return '- ' + k.s + ': ' + k.b + ' | ' + (k.x || ''); }).join('\n');
this.createMessage(o);
// … Hilfsfunktionen antworte(), weiter(), fuehreAus(), zweiterDurchlauf() siehe Übung 1
};
Die vollständige, lauffähige Fassung baust du in Übung 1.
Schritt 3 — Die KI-Erkennung
Greift keine Regel, ordnet ein Generative AI Agent die Nachricht einem Skill zu. Der Prompt bekommt drei Dinge: den Katalog der Fähigkeiten, den bisherigen Verlauf und die neue Nachricht.
Du ordnest Nachrichten an einen digitalen Assistenten genau einer Fähigkeit zu.
FÄHIGKEITEN (schluessel: Beschreibung | Beispiele)
{{ ki_katalog }}
BISHERIGER VERLAUF (älteste zuerst)
{{ ki_verlauf }}
NEUE NACHRICHT
<<<
{{ text }}
>>>
Regeln:
- Passt keine Fähigkeit, antworte mit "unbekannt".
- Wenn du raten müsstest, setze "sicherheit" unter 0.6.
- Nenne in "alternativen" bis zu zwei weitere Fähigkeiten, die auch passen könnten.
Antworte ausschließlich mit diesem JSON-Objekt:
{"faehigkeit": "<schluessel oder unbekannt>", "sicherheit": <0 bis 1>,
"alternativen": ["<schluessel>"], "zusammenfassung": "<ein Satz, was gewünscht ist>"}
Stelle den Agent auf output_format: json und aktiviere emit_on_error. Dann kommt auch bei einem Modellfehler eine Nachricht an — mit success: false — und der Router kann reagieren, statt stumm zu bleiben.
Ein kleiner JavaScript Agent nach der KI liest das Ergebnis aus, macht genau einen Wiederholversuch, wenn das Modell nicht oder unbrauchbar geantwortet hat, und schickt das Ergebnis als Feld ki zurück an den Entscheidungs-Agent.
Jede Schleife zwischen Agents braucht eine Abbruchbedingung. Zähle die Versuche in einem Feld (ki_versuch) und lass den Switch Agent nach dem ersten Wiederholversuch nicht mehr zurück zur KI verzweigen. Sonst kann ein dauerhaft gestörtes Modell eine Endlosschleife auslösen.
Schritt 4 — Den KI-Vorschlag bewerten
Im zweiten Durchlauf prüft der Router den Vorschlag der KI — wieder mit festen Regeln:
| Situation | Reaktion |
|---|---|
| Modell nicht erreichbar | ehrliche Fehlermeldung, Vorgang auf fehler |
unbekannt | „Das kann ich noch nicht.“ Wunsch wird notiert (Einheit 6) |
sicherheit < 0,6 | Rückfrage mit Auswahl |
| Skill mit Risikostufe ≥ 2 | Bestätigung einholen |
| sonst | an den Skill weitergeben |
Sprachmodelle schätzen ihre eigene Sicherheit nicht zuverlässig ein. Der Wert ist ein grober Hinweis, keine Wahrscheinlichkeit. Verlass dich nicht allein darauf: Die Risikostufe des Skills entscheidet unabhängig davon, ob vor dem Ausführen gefragt wird.
Rückfragen mit Auswahl
Eine offene Rückfrage („Was meinst du genau?“) führt oft zu einer ebenso unklaren Antwort. Besser ist eine Auswahl:
Das habe ich nicht sicher verstanden. Meinst du: 1 Kennzahl abfragen (Umsatz Oktober) 2 Firmenauskunft Antworte mit der Nummer oder schreib es genauer.
Antwortet die Person mit „1“, weiß der Router sofort Bescheid. Und — das ist der eigentliche Gewinn — er hat ein Lernbeispiel gewonnen: Diese Formulierung bedeutet „Kennzahl abfragen“. Wie er sich das merkt, zeigt Einheit 6.
Bestätigungen
Bei riskanten Skills (Mails versenden, Daten in fremden Systemen ändern) antwortet der Router zuerst mit einer Zusammenfassung:
Ich würde Folgendes tun: Zahlungserinnerung senden – an Muster GmbH, Rechnung 2026-117. Soll ich das ausführen? Antworte mit ja oder nein.
Der Auftrag wird als Vorgang mit Status bestaetigung gespeichert. Kommt innerhalb von 30 Minuten ein „ja“, führt Regel 5 ihn aus. Kommt etwas anderes, verfällt er.
Schritt 5 — Verteilen
Ein Switch Agent verteilt anhand des Feldes route:
| Empfänger | Bedingung |
|---|---|
| Rückkanal | '{{ route }}' == 'antwort' |
| KI-Erkennung | '{{ route }}' == 'ki' |
| Skill Wissen | '{{ route }}' == 'wissen' |
| Skill Erinnerung | '{{ route }}' == 'erinnerung' |
Parallel schreiben drei Internal Storage Agents die Ergebnisse des Routers fort: Nachricht als verarbeitet markieren, Verlauf ergänzen, Vorgang anlegen oder aktualisieren. Der Entscheidungs-Agent liefert die fertigen SQL-Befehle dafür in Feldern mit (sql_merken, sql_verlauf, sql_vorgang), die Storage Agents setzen sie nur ein (sql_statement: {{ sql_vorgang }}).
Kurzer Selbst-Check
Jemand schreibt „ja“, ohne dass vorher eine Bestätigung offen war. Was passiert?
Regel 5 greift nicht, weil kein offener Vorgang mit Status bestaetigung existiert. Die Nachricht fällt bis zur KI-Erkennung durch, die mangels Zusammenhang „unbekannt“ oder eine niedrige Sicherheit liefert — der Assistent fragt nach.
Warum ist die Bestätigungsfrist auf 30 Minuten begrenzt?
Ein „ja“ am nächsten Morgen bezieht sich wahrscheinlich auf etwas anderes. Eine abgelaufene Bestätigung ist sicherer als eine, die unerwartet noch Tage später ausgelöst wird.