Wie KI lokale Funktionen durch einen Service-Aufruf (basierend auf dem OpenAI-Protokoll) bedient
Dieser Artikel basiert auf der OpenAI-kompatiblen Funktionsaufruf-Fähigkeit (Werkzeugaufruf) und zeigt, wie man eine allgemeine, praxistaugliche Implementierung entwerfen kann.
Wird gerendert...
Wenn Großmodelle „aktiv werden“ müssen – Datenbanken abfragen, schreiben, interne APIs aufrufen, Benachrichtigungen senden – erfolgt dies meist durch Ausführung von echtem Code in Ihrem Service. Die Verknüpfung von „Modellentscheidungen“ mit „lokaler Ausführung“ ist genau das, was der Begriff „KI-Aufruf lokaler Funktionen“ lösen möchte.
## Anwendungsszenarien & Ziele
Typische Szenarien umfassen: Konversationelle Geschäftsvorgänge (z.B. Buchhaltung: „Buche einen Betrag“, „Zeige die Zusammenfassung des letzten Monats an“), Erstellung von Tickets/Aufträgen, Datenabfragen und Statistiken, Interpretation interner Systembefehle. Gemeinsam ist allen Szenarien: Der Nutzer drückt seine Absicht in natürlicher Sprache aus, das Modell identifiziert die passenden „Werkzeuge“ und Parameter, die konkrete Logik wird anschließend in Ihrem Backend ausgeführt und das Ergebnis dem Modell zur Generierung der finalen Antwort übergeben.
Ziele lassen sich auf drei Punkte reduzieren:
- **Werkzeuge beschreibbar** (Modell kennt verfügbare Funktionen und Parameterstruktur)
- **Ausführung lokal** (keine Weitergabe sensibler Logik oder Daten an Dritte)
- **Ergebnisse rückkoppelbar** (Werkzeug-Rückgabewerte fließen erneut in den Dialog zur Generierung natürlicher Sprache ein).
## Architektonischer Ansatz
Trennung von „Beschreibung + Ausführung“: Mit OpenAIs `tools` (Function Calling) werden lokale Fähigkeiten als „Name + Parameter-Schema“ beschrieben, das Modell gibt `tool_calls` zurück. Ihr Service analysiert diese `tool_calls`, ruft lokal die entsprechenden Funktionen mit Parametern auf und fügt die Rückgabewerte im Format `role: "tool"` zurück in die Nachrichtenliste ein. Anschließend erfolgt eine erneute Modellanfrage, damit das Modell basierend auf den Werkzeugresultaten die Benutzerantwort generiert. So bleiben sensible Logik und Daten immer in Ihrem Prozesskontext – das Modell übernimmt nur „Werkzeugauswahl, Parameterfüllung, Antwortformulierung“.
Falls Ihr aktuell verwendeter Modell- oder Gateway-Anbieter `tool_calls` nicht unterstützt, kann eine Fallback-Ebene ergänzt werden: Das Modell gibt strukturierten JSON-Code aus (z.B. `{ "action": { "name", "args" }, "reply" }`), den Sie analysieren und wie gewohnt lokal ausführen. So bleibt der „Interpretation → Ausführung → Zusammenfassung“-Ablauf auch ohne `tool_calls` aufrecht.
## Werkzeugdefinition: OpenAI-Format
Das Modell benötigt Informationen über „verfügbare Funktionen und deren Parameter“. OpenAI verwendet dafür ein `tools`-Array, bestehend aus Objekten mit `type: "function"` sowie `function.name`, `function.description` und `function.parameters` (JSON Schema). Je präziser die Beschreibung, desto geringer die Wahrscheinlichkeit, dass das Modell falsche Werkzeuge wählt oder Parameter vergisst.
Beispielgruppe anonymisierter Werkzeugdefinitionen (Namen und Geschäftslogik verallgemeinert):
```ts
import type { ChatCompletionTool } from "openai/resources/chat/completions";
export const CHAT_TOOLS: ChatCompletionTool[] = [
{
type: "function",
function: {
name: "create_item",
description: "Erstellt einen Geschäftsvorgang. Wird genutzt, wenn Nutzer „Buche etwas“, „Füge hinzu“, „Registriere“ etc. sagt.",
parameters: {
type: "object",
properties: {
category: { type: "string", description: "Kategorie, z.B. Typ A, Typ B" },
amount: { type: "number", description: "Betrag oder Menge" },
title: { type: "string", description: "Titel/Zusammenfassung" },
date: { type: "string", description: "Datum YYYY-MM-DD, Standard: heute" },
remark: { type: "string", description: "Bemerkung" },
},
required: ["category", "amount", "title"],
},
},
},
{
type: "function",
function: {
name: "list_items",
description: "Sucht Geschäftsvorgänge nach Kriterien. Wird genutzt, wenn Nutzer „Zeige an“, „Liste“, „Filtere“ etc. sagt.",
parameters: {
type: "object",
properties: {
category: { type: "string", description: "Kategorie filtern" },
startDate: { type: "string", description: "Startdatum YYYY-MM-DD" },
endDate: { type: "string", description: "Enddatum YYYY-MM-DD" },
pageNum: { type: "number", description: "Seitennummer, Standard: 1" },
pageSize: { type: "number", description: "Einträge pro Seite, Standard: 15" },
},
},
},
},
{
type: "function",
function: {
name: "get_summary",
description: "Liefert Zusammenfassungsstatistik. Wird genutzt, wenn Nutzer „Wie viel insgesamt“, „Statistik“, „Zusammenfassung“ etc. fragt.",
parameters: {
type: "object",
properties: {
startDate: { type: "string", description: "Startdatum YYYY-MM-DD" },
endDate: { type: "string", description: "Enddatum YYYY-MM-DD" },
month: { type: "string", description: "Monat YYYY-MM, alternativ zum Datumsbereich" },
},
},
},
},
];
```
In realen Projekten können Sie Werkzeuge ergänzen/löschen, `description` und `properties` detaillieren. Bei Bedarf `enum` zur Wertbeschränkung nutzen, um Fehleingaben zu reduzieren.
## Lokaler Executor: Mapping von Namen zu Funktionen
Die Werkzeugausführungsschicht übernimmt zwei Aufgaben: Basierend auf `name` die passende Implementierung finden, `args` samt Laufzeitkontext (z.B. Benutzer-ID, Mandanten-ID) übergeben und Ergebnis als String (typischerweise JSON) zurückgeben, um es in Nachrichten mit `role: "tool"` einzufügen.
Beispiel eines anonymisierten Executors:
```ts
export interface ToolExecutionContext {
userId: number;
// Erweiterbar: tenantId, requestId, locale etc.
}
export async function executeTool(
name: string,
args: Record<string, unknown>,
ctx: ToolExecutionContext,
): Promise<string> {
switch (name) {
case "create_item":
return execCreateItem(args, ctx);
case "list_items":
return execListItems(args, ctx);
case "get_summary":
return execGetSummary(args, ctx);
default:
return JSON.stringify({ success: false, message: `Unbekanntes Werkzeug: ${name}` });
}
}
async function execCreateItem(
args: Record<string, unknown>,
ctx: ToolExecutionContext,
): Promise<string> {
const category = String(args.category ?? "");
const amount = Number(args.amount ?? 0);
const title = String(args.title ?? "");
const date = args.date ? new Date(String(args.date)) : new Date();
const remark = args.remark ? String(args.remark) : null;
if (!title || Number.isNaN(amount)) {
return JSON.stringify({ success: false, message: "Titel und Betrag dürfen nicht leer sein" });
}
// Echte Persistenz (Datenbank, API etc.) integrieren
const created = await yourCreateItemService({
userId: ctx.userId,
category,
amount,
title,
date,
remark,
});
return JSON.stringify({
success: true,
item: { id: created.id, title: created.title, amount: created.amount },
});
}
```
`list_items`, `get_summary` analog: Sicherer Parameterabruf aus `args` (Typkonvertierung, Defaultwerte), Aufruf der Business-Schicht, Rückgabe per `JSON.stringify`. So muss die Agent-Schicht keine Tabellenstrukturen oder RPCs kennen, sondern nutzt nur das vereinbarte „Werkzeugname + Parameterwörterbuch → String-Ergebnis“-Prinzip.
## Dialogagent: Mehrfachaufruf mit tool_calls
Agent-Schicht verwaltet „Nachrichtenverlauf“: Systemprompt + Nutzerhistorie + aktuelle Modellantwort. Bei `tool_calls` werden diese durchlaufen, `function.name` und `function.arguments` (JSON) analysiert, optional Parameter ergänzt (z.B. „heute“, „diesen Monat“ durch echte Daten ersetzen), `executeTool` aufgerufen und Ergebnisse mit `role: "tool"` und passender `tool_call_id` an Nachrichtenverlauf angehängt, bevor erneut Modell aufgerufen wird. Ohne `tool_calls` oder bei Erreichen der Maximalrunden wird `message.content` als finale Antwort genutzt – falls Werkzeuge ausgeführt wurden, erfolgt typischerweise ein letzter Modellaufruf zur Generierung einer natürlichsprachigen Zusammenfassung.
Anonymisiertes Kernloop-Beispiel:
```ts
async function runWithToolCalls(opts: {
userId: number;
messages: ChatCompletionMessageParam[];
maxToolRounds: number;
client: OpenAI;
config: { model: string; temperature?: number; maxTokens?: number };
}): Promise<{ content: string }> {
const { userId, messages, maxToolRounds, client, config } = opts;
const fullMessages: ChatCompletionMessageParam[] = [
{ role: "system", content: SYSTEM_PROMPT },
...messages,
];
let round = 0;
let lastContent = "";
while (round < maxToolRounds) {
const response = await client.chat.completions.create({
model: config.model,
temperature: config.temperature ?? 0.5,
max_tokens: config.maxTokens ?? 3000,
messages: fullMessages,
tools: CHAT_TOOLS,
});
const msg = response.choices[0]?.message;
if (!msg) return { content: "Keine gültige Antwort erhalten" };
fullMessages.push(msg);
const toolCalls = msg.tool_calls;
if (!toolCalls?.length) {
lastContent = msg.content ?? "Operation abgeschlossen.";
break;
}
for (const tc of toolCalls) {
if (tc.type !== "function") continue;
const name = tc.function.name;
let args: Record<string, unknown> = {};
try {
args = JSON.parse(tc.function.arguments || "{}");
} catch {
args = {};
}
const output = await executeTool(name, args, { userId });
fullMessages.push({
role: "tool",
tool_call_id: tc.id!,
content: output,
});
}
round++;
}
// Falls Werkzeuge ausgeführt wurden: Letzte Antwort mit Modell generieren
if (fullMessages.some((m) => m.role === "tool")) {
const finalResponse = await client.chat.completions.create({
model: config.model,
temperature: config.temperature ?? 0.5,
max_tokens: config.maxTokens ?? 3000,
messages: fullMessages,
});
const finalMsg = finalResponse.choices[0]?.message;
lastContent = finalMsg?.content ?? lastContent ?? "Operation abgeschlossen.";
}
return { content: lastContent };
}
```
Hinweis: `tools` nur im Request übergeben, wenn Modellentscheidung erforderlich. Letzte Runde ohne `tools`, um erneute tool_calls zu vermeiden. `maxToolRounds` verhindert Endlosschleifen bei Fehlern.
## Fallback: JSON-Befehle ohne tool_calls
Einige Modelle/Proxies unterstützen keine `tool_calls` oder liefern falsche Formate. Implementieren Sie eine „JSON-Befehl“-Alternative: Systemprompt verlangt strikt strukturierten JSON wie `{ "action": { "name": "create_item" | "list_items" | "get_summary" | "none", "args": { ... } }, "reply": "Optionale Standardantwort" }`. Ihr Code analysiert JSON: Bei `action.name ≠ "none"` wird `executeTool` mit `action.args` aufgerufen, danach erfolgt „Zusammenfassungs“-Request mit Werkzeugergebnis und `reply`, um finale Antwort zu generieren. Bei `action.name === "none"` wird `reply` direkt genutzt. So bleibt „Absicht interpretieren → lokal ausführen → natürliche Sprache generieren“ auch ohne tool_calls aufrecht.
```ts
const JSON_PLAN_SYSTEM = `Sie sind ein Befehlsparser. Geben Sie NUR einen JSON aus, keine markdown oder zusätzlichen Erklärungen.
Format: { "action": { "name": "create_item"|"list_items"|"get_summary"|"none", "args": {...} }, "reply": "Antwort an Nutzer (erforderlich bei name = none) }
Nutzen Sie action, wenn möglich, vermeiden Sie none.`;
async function runWithJsonPlan(opts: {
userId: number;
userText: string;
client: OpenAI;
config: { model: string; maxTokens?: number };
}): Promise<{ content: string }> {
const { userId, userText, client, config } = opts;
const res = await client.chat.completions.create({
model: config.model,
temperature: 0.1,
max_tokens: config.maxTokens ?? 3000,
messages: [{ role: "user", content: `${JSON_PLAN_SYSTEM}\n\nNutzeranfrage: ${userText}` }],
});
const raw = res.choices[0]?.message?.content?.trim();
if (!raw) throw new Error("Kein Inhalt erhalten");
const parsed = parseJsonPlan(raw); // Extrahiert JSON aus raw und parsed
const name = parsed.action?.name;
const args = parsed.action?.args ?? {};
if (name && name !== "none") {
const toolOutput = await executeTool(name, args, { userId });
// Optional: Zweiter Modellaufruf für Zusammenfassung mit toolOutput + parsed.reply
return { content: await summarizeWithModel(client, config, userText, name, toolOutput, parsed.reply) };
}
return { content: parsed.reply?.trim() ?? "Ihre Anfrage kann nicht verarbeitet werden." };
}
```
Hauptzugang kann zunächst `runWithToolCalls` versuchen, bei Fehlern auf `runWithJsonPlan` ausweichen – so bleibtKommentar
Melde dich an, um Kommentare anzuzeigen und zu veröffentlichen
Zur Anmeldung