- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
650 lines
17 KiB
Markdown
650 lines
17 KiB
Markdown
# 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
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
// ✅ 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
|
||
|
||
```typescript
|
||
// ✅ 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
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
// ✅ 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
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
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:
|
||
|
||
1. **Klare Definitionen:** Gute Namen, detaillierte Descriptions, strenge Schemas
|
||
2. **Security First:** Validierung, Least Privilege, Confirmations
|
||
3. **Robustes Error Handling:** Unterscheide User- und Model-Errors
|
||
4. **Performance:** Parallele Ausführung, Tool-Count begrenzen
|
||
5. **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"
|
||
|
||
---
|
||
|
||
## Quellen
|
||
|
||
- [OpenAI: Function Calling Guide](https://platform.openai.com/docs/guides/function-calling)
|
||
- [Anthropic: Tool Use Documentation](https://docs.anthropic.com/claude/docs/tool-use)
|
||
- [Prompting Guide: Function Calling with LLMs](https://www.promptingguide.ai/applications/function_calling)
|
||
- [Composio: Tool Calling Explained](https://composio.dev/blog/ai-agent-tool-calling-guide)
|
||
- [Martinuke Blog: Anatomy of Tool Calling](https://martinuke0.github.io/posts/2026-01-07-the-anatomy-of-tool-calling-in-llms-a-deep-dive/)
|