Tool Use Patterns: Wie AI-Agenten mit externen APIs interagieren
Implementieren Sie robustes Function Calling für KI-Agenten. Best Practices für Tool-Definition, Error Handling, Security und skalierbare Architekturen.

Tool Use Patterns: Wie AI-Agenten mit externen APIs interagieren
Meta-Description: Implementieren Sie robustes Function Calling für KI-Agenten. Best Practices für Tool-Definition, Error Handling, Security und skalierbare Architekturen.
Keywords: Function Calling, Tool Use, AI Agents, API Integration, LLM Tools, Claude Function Calling, OpenAI Functions
Einführung
Function Calling (oder Tool Use) ist das Fundament für produktive KI-Anwendungen. Es ermöglicht LLMs, externe APIs aufzurufen, Datenbanken abzufragen und reale Aktionen auszuführen – statt nur Text zu generieren.
Aber die Implementierung birgt Fallstricke: Security-Risiken, unvorhersehbare Aufrufe, Kostenexplosionen. In diesem Artikel zeige ich bewährte Patterns aus meinen Produktionsprojekten.
Das Grundprinzip
┌─────────────────────────────────────────────────────────────┐
│ TOOL USE FLOW │
│ │
│ User LLM Tools User │
│ │ │ │ │ │
│ │ Request │ │ │ │
│ │──────────→ │ │ │ │
│ │ │ │ │ │
│ │ │ Tool Selection │ │ │
│ │ │─────────────────│ │ │
│ │ │ │ │ │
│ │ │ Tool Call │ │ │
│ │ │────────────────→│ │ │
│ │ │ │ │ │
│ │ │ Tool Result │ │ │
│ │ │←────────────────│ │ │
│ │ │ │ │ │
│ │ Response │ │ │ │
│ │←───────────│ │ │ │
│ │
└─────────────────────────────────────────────────────────────┘Tool-Definition: Best Practices
Die Anatomie eines guten Tools
interface ToolDefinition {
name: string; // Eindeutig, beschreibend
description: string; // Kritisch für LLM-Entscheidung
parameters: JSONSchema; // Strikt typisiert
returns: JSONSchema; // Optional, für Dokumentation
}
// ✅ Gutes Tool
const searchProductsTool = {
name: "search_products",
description: `Durchsucht die Produktdatenbank nach Artikeln.
Nutze dieses Tool wenn der User nach Produkten sucht,
Preise wissen will, oder Verfügbarkeit prüfen möchte.
Gibt maximal 10 Ergebnisse zurück.`,
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "Suchbegriff für Produktname oder Kategorie"
},
category: {
type: "string",
enum: ["electronics", "clothing", "furniture", "sports"],
description: "Optionale Kategorie-Filterung"
},
max_price: {
type: "number",
description: "Maximaler Preis in EUR"
},
limit: {
type: "integer",
default: 5,
minimum: 1,
maximum: 10,
description: "Anzahl der Ergebnisse"
}
},
required: ["query"]
}
};
// ❌ Schlechtes Tool
const badTool = {
name: "search", // Zu vage
description: "Sucht Sachen", // Nicht aussagekräftig
parameters: {
type: "object",
properties: {
q: { type: "string" } // Kryptischer Parametername
}
}
};Naming Conventions
// ✅ Gute Namen
"search_products" // Verb + Nomen
"get_user_profile"
"send_email"
"calculate_shipping"
"book_flight_ticket"
// ❌ Schlechte Namen
"search" // Zu vage
"doThing" // Nicht beschreibend
"handler" // Was handelt es?
"process" // Was wird prozessiert?Enum statt Freitext
// ✅ Gut: Enum für begrenzte Werte
parameters: {
status: {
type: "string",
enum: ["pending", "approved", "rejected"],
description: "Filtert nach Status"
}
}
// ❌ Schlecht: Freitext für begrenzte Werte
parameters: {
status: {
type: "string",
description: "Status: pending, approved, oder rejected"
}
}Implementierung mit Claude
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
// Tool-Definitionen
const tools: Anthropic.Tool[] = [
{
name: "get_weather",
description: "Ruft aktuelle Wetterdaten für einen Ort ab",
input_schema: {
type: "object",
properties: {
location: {
type: "string",
description: "Stadt und Land, z.B. 'Berlin, Deutschland'"
},
unit: {
type: "string",
enum: ["celsius", "fahrenheit"],
default: "celsius"
}
},
required: ["location"]
}
},
{
name: "search_database",
description: "Durchsucht die interne Datenbank",
input_schema: {
type: "object",
properties: {
query: { type: "string" },
table: {
type: "string",
enum: ["users", "products", "orders"]
}
},
required: ["query", "table"]
}
}
];
// Tool-Implementierungen
const toolImplementations = {
get_weather: async (input: { location: string; unit?: string }) => {
const response = await fetch(
`https://api.weather.com/v1/current?location=${input.location}`
);
return response.json();
},
search_database: async (input: { query: string; table: string }) => {
const results = await db[input.table].search(input.query);
return results;
}
};
// Agent-Loop
async function runAgent(userMessage: string) {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userMessage }
];
while (true) {
const response = await anthropic.messages.create({
model: "claude-3-5-sonnet-latest",
max_tokens: 1000,
tools,
messages
});
// Prüfe ob Tool-Calls vorhanden
if (response.stop_reason === "tool_use") {
const toolUseBlocks = response.content.filter(
block => block.type === "tool_use"
);
// Führe alle Tool-Calls aus
const toolResults = await Promise.all(
toolUseBlocks.map(async (toolUse) => {
const impl = toolImplementations[toolUse.name];
const result = await impl(toolUse.input);
return {
type: "tool_result" as const,
tool_use_id: toolUse.id,
content: JSON.stringify(result)
};
})
);
// Füge Results zur Conversation hinzu
messages.push({ role: "assistant", content: response.content });
messages.push({ role: "user", content: toolResults });
} else {
// Keine weiteren Tool-Calls, fertig
return response.content;
}
}
}Security Best Practices
1. Input-Validierung
import { z } from "zod";
// Schema für Tool-Inputs
const searchInputSchema = z.object({
query: z.string().min(1).max(500),
table: z.enum(["users", "products", "orders"]),
limit: z.number().int().min(1).max(100).default(10)
});
async function executeToolSafely(
toolName: string,
input: unknown
): Promise<ToolResult> {
// 1. Validiere Input
const schema = toolSchemas[toolName];
const validatedInput = schema.parse(input);
// 2. Prüfe Berechtigungen
if (!userHasPermission(currentUser, toolName)) {
throw new UnauthorizedError(`Kein Zugriff auf ${toolName}`);
}
// 3. Ausführen
return await toolImplementations[toolName](validatedInput);
}2. Principle of Least Privilege
// ✅ Gut: Spezifische, eingeschränkte Tools
const tools = [
{
name: "read_user_profile", // Nur lesen
description: "Liest das Profil eines Users (nur öffentliche Daten)"
},
{
name: "update_own_profile", // Nur eigenes Profil
description: "Aktualisiert das eigene Profil des eingeloggten Users"
}
];
// ❌ Schlecht: Zu mächtige Tools
const badTools = [
{
name: "execute_sql", // Voller DB-Zugriff
description: "Führt beliebige SQL-Queries aus"
}
];3. User Confirmation für kritische Aktionen
interface ToolWithConfirmation {
name: string;
requiresConfirmation: boolean;
confirmationMessage: (input: any) => string;
}
const sendEmailTool: ToolWithConfirmation = {
name: "send_email",
requiresConfirmation: true,
confirmationMessage: (input) =>
`E-Mail an ${input.to} senden mit Betreff "${input.subject}"?`
};
async function executeWithConfirmation(
tool: ToolWithConfirmation,
input: any
): Promise<ToolResult> {
if (tool.requiresConfirmation) {
const confirmed = await requestUserConfirmation(
tool.confirmationMessage(input)
);
if (!confirmed) {
return { status: "cancelled", reason: "User declined" };
}
}
return await toolImplementations[tool.name](input);
}4. Rate Limiting pro Tool
import { RateLimiter } from "limiter";
const toolRateLimiters = {
send_email: new RateLimiter({
tokensPerInterval: 10,
interval: "hour"
}),
search_database: new RateLimiter({
tokensPerInterval: 100,
interval: "minute"
}),
external_api: new RateLimiter({
tokensPerInterval: 1000,
interval: "day"
})
};
async function rateLimitedExecute(toolName: string, input: any) {
const limiter = toolRateLimiters[toolName];
if (!limiter.tryRemoveTokens(1)) {
throw new RateLimitError(`Rate limit für ${toolName} erreicht`);
}
return await toolImplementations[toolName](input);
}Error Handling
Zwei Arten von Errors
interface ToolError {
type: "user_facing" | "model_facing";
message: string;
retryable: boolean;
}
function handleToolError(error: Error, toolName: string): ToolError {
// User-facing: Zeige dem Enduser
if (error instanceof ValidationError) {
return {
type: "user_facing",
message: "Bitte geben Sie gültige Eingaben an.",
retryable: true
};
}
// Model-facing: Nur für das LLM, nicht dem User zeigen
if (error instanceof DatabaseError) {
return {
type: "model_facing",
message: "Datenbankfehler. Versuche alternative Methode.",
retryable: true
};
}
// Sensitive Errors niemals leaken
if (error instanceof InternalError) {
return {
type: "model_facing",
message: "Interner Fehler aufgetreten.",
retryable: false
};
}
}Retry-Logik
async function executeWithRetry(
toolName: string,
input: any,
maxRetries = 3
): Promise<ToolResult> {
let lastError: Error;
for (let i = 0; i < maxRetries; i++) {
try {
return await toolImplementations[toolName](input);
} catch (error) {
lastError = error;
// Nur bei retryable Errors wiederholen
if (!isRetryable(error)) {
throw error;
}
// Exponential Backoff
await sleep(Math.pow(2, i) * 1000);
}
}
throw lastError;
}Performance-Optimierung
Parallele Tool-Ausführung
// Das LLM kann mehrere Tools gleichzeitig aufrufen
// → Führen Sie sie parallel aus!
async function executeToolCalls(toolCalls: ToolCall[]): Promise<ToolResult[]> {
// ✅ Parallel (schnell)
return await Promise.all(
toolCalls.map(call => executeToolSafely(call.name, call.input))
);
// ❌ Sequentiell (langsam)
// const results = [];
// for (const call of toolCalls) {
// results.push(await executeToolSafely(call.name, call.input));
// }
// return results;
}Tool Count Optimieren
// OpenAI/Anthropic Empfehlung: Max 20 Tools
// ❌ Schlecht: 50 spezifische Tools
const tooManyTools = [
"search_users_by_name",
"search_users_by_email",
"search_users_by_id",
// ... 47 weitere
];
// ✅ Besser: Wenige flexible Tools
const consolidatedTools = [
{
name: "search_users",
description: "Sucht User nach verschiedenen Kriterien",
parameters: {
filter_type: { enum: ["name", "email", "id"] },
filter_value: { type: "string" }
}
}
];Lazy Loading von Tools
// Nicht alle Tools immer laden
function getToolsForContext(context: Context): Tool[] {
const tools: Tool[] = [];
// Basis-Tools immer verfügbar
tools.push(searchTool, helpTool);
// Kontext-spezifische Tools
if (context.user.isAdmin) {
tools.push(adminTool);
}
if (context.conversation.topic === "orders") {
tools.push(orderTool, shippingTool, refundTool);
}
return tools;
}Prompt Engineering für Tool Use
System Prompt Guidance
const systemPrompt = `Du bist ein hilfreicher Assistent mit Zugriff auf Tools.
## Tool-Nutzung
- Nutze search_products wenn der User nach Produkten sucht
- Nutze get_order_status wenn der User nach einer Bestellung fragt
- Nutze contact_support nur wenn du nicht helfen kannst
## Wichtige Regeln
- Frage nach wenn Informationen fehlen (z.B. Bestellnummer)
- Führe keine Tool-Calls aus wenn du die Antwort schon weißt
- Erkläre dem User was du tust bevor du ein Tool aufrufst
## Verboten
- Rufe niemals delete_* Tools ohne explizite User-Bestätigung auf
- Greife nicht auf andere User-Daten zu
`;Clarification Requests
// Instruiere das Modell, bei Unklarheiten nachzufragen
const clarificationPrompt = `
Wenn der User nach Hotelsuche fragt aber kein Datum nennt:
NICHT: tool_call: search_hotels({location: "Berlin"})
SONDERN: "Für welches Datum möchten Sie ein Hotel in Berlin suchen?"
`;Monitoring & Observability
interface ToolCallMetrics {
toolName: string;
duration: number;
success: boolean;
inputTokens: number;
outputTokens: number;
error?: string;
}
class ToolMonitor {
async trackCall(
toolName: string,
input: any,
execute: () => Promise<any>
): Promise<any> {
const startTime = Date.now();
try {
const result = await execute();
await this.record({
toolName,
duration: Date.now() - startTime,
success: true,
inputTokens: estimateTokens(input),
outputTokens: estimateTokens(result)
});
return result;
} catch (error) {
await this.record({
toolName,
duration: Date.now() - startTime,
success: false,
error: error.message
});
throw error;
}
}
async alertOnAnomaly(metrics: ToolCallMetrics) {
// Alert bei ungewöhnlich vielen Tool-Calls
const recentCalls = await this.getRecentCalls(metrics.toolName, "1h");
if (recentCalls.length > 1000) {
await this.alert(`Ungewöhnlich viele Calls für ${metrics.toolName}`);
}
}
}Fazit
Tool Use ist mächtig, aber mit Verantwortung:
- Klare Definitionen: Gute Namen, detaillierte Descriptions, strenge Schemas
- Security First: Validierung, Least Privilege, Confirmations
- Robustes Error Handling: Unterscheide User- und Model-Errors
- Performance: Parallele Ausführung, Tool-Count begrenzen
- Observability: Monitoring für alle Tool-Calls
Tool Calling transformiert LLMs von Text-Generatoren zu handlungsfähigen Agenten. Die Investition in solide Patterns zahlt sich mehrfach aus.
Bildprompts für diesen Artikel
Bild 1 – Hero Image:
"Robot arm reaching into a toolbox filled with API icons and code symbols, industrial yet modern style"
Bild 2 – Integration Diagram:
"Interconnected puzzle pieces showing different API logos (database, email, calendar), smooth 3D render"
Bild 3 – Multi-Tool Architecture:
"AI brain with multiple tentacles connecting to various service icons, octopus-inspired futuristic design"