Menu
Zurück zum Blog
2 min read
KI-Agenten

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.

Function CallingTool UseAI AgentsAPI IntegrationLLM ToolsClaude Function Calling
Tool Use Patterns: Wie AI-Agenten mit externen APIs interagieren

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:

  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