01 · Problem
Wofür braucht man das?
Ein Sprachmodell kann eine Funktion nur beschreiben und anfordern; Anwendungscode muss den Namen sicher auflösen, Argumente validieren, den Handler ausführen und das Ergebnis wieder in den Dialog einspeisen.
02 · Denkmodell
Das mentale Modell
Behandle das Modell als untrusted planner in einer begrenzten Schleife. Tool-Schema und ausführbarer Handler sind getrennte Verträge, verbunden über einen kontrollierten Registry-Lookup.
AIFunctionCaller sendet Nachrichten und Funktionsbeschreibungen an Chat Completions. Die private Ausführungsroutine ist nicht mit query verbunden, Nachrichtentypen passen nicht zum SDK und das Package kompiliert derzeit nicht.
03 · Kontrollfluss
Was passiert in welcher Reihenfolge?
- User-Nachricht an einen unveränderlich nachvollziehbaren Verlauf anhängen.
- Modell mit Nachrichten und ausschließlich öffentlichen Tool-Schemas aufrufen.
- Tool-Calls nach Name auflösen und JSON-Argumente gegen das jeweilige Schema prüfen.
- Handler ausführen, Ergebnis als Tool-Nachricht speichern und erneut anfragen.
- Bei normaler Antwort oder nach einem harten Schrittlimit beenden.
04 · Bauteile
Die entscheidenden Verträge
AIFunctionCaller.query(message)
Startet im Repository genau einen Modellaufruf und gibt die Antwortnachricht zurück.
AIFunction
Verbindet Funktionsbeschreibung, Parameter und lokalen Handler; diese Rollen sollten im Nachbau getrennt werden.
Message / MessageRole
Eigene Verlaufstypen, die aktuell nicht deckungsgleich mit den SDK-Nachrichten sind.
05 · Build it yourself
Selbst implementieren
Baue zuerst eine modellunabhängige Tool-Schleife und injiziere anschließend einen Adapter zum gewünschten Provider.
- Definiere Tool-Definition und Handler getrennt und registriere beide unter einem eindeutigen Namen.
- Validiere jedes vom Modell gelieferte Argumentobjekt, bevor Anwendungscode es sieht.
- Begrenze Schritte, Laufzeit und Ergebnisgröße; leite ein AbortSignal durch alle Aufrufe.
type Tool = { schema: object; run(input: unknown): Promise<unknown> };
async function runAgent(input: string, signal: AbortSignal) {
const messages = [{ role: "user", content: input }];
for (let step = 0; step < 6; step++) {
const reply = await model.complete(messages, signal);
messages.push(reply.message);
if (!reply.toolCalls.length) return reply.message.content;
for (const call of reply.toolCalls) {
const tool = tools.get(call.name);
if (!tool) throw new Error("Unknown tool: " + call.name);
const args = validate(tool.schema, JSON.parse(call.arguments));
const value = await tool.run(args);
messages.push({ role: "tool", content: JSON.stringify(value) });
}
}
throw new Error("Tool-call limit reached");
}06 · Verifizieren
Was du testen solltest
- Ein unbekannter Toolname wird abgelehnt und niemals per Property-Zugriff ausgeführt.
- Ungültiges JSON, Schemafehler und Handlerfehler erscheinen als definierter Fehlerpfad.
- Schrittlimit, Timeout und Abort beenden auch eine endlose Tool-Call-Kette.
07 · Grenzen
Kompromisse und Stolperfallen
- Modellantworten sind untrusted input und dürfen keine direkte Codeausführung auslösen.
- Gesprächsverläufe wachsen; Kontextfenster und sensible Daten benötigen eine eigene Strategie.
- Provider-SDKs ändern Nachrichtentypen und Tool-Protokolle – der Adapter sollte klein bleiben.
API-Schlüssel gehören ausschließlich in eine Serverumgebung. Eine statische Browser-App darf diesen Ablauf nicht direkt mit einem geheimen Provider-Key ausführen.
08 · Weiterdenken