Technical Guide

KI-Workflows entwerfen, die in der Produktion funktionieren

Ein praktischer Leitfaden zum Aufbau robuster KI-Pipelines. Lerne Pipeline-Architektur, Schrittsequenzierung, Verzweigungslogik, Fehlerbehandlung und Prompt-Chaining von Ingenieuren, die diese Systeme ausgeliefert haben.

10. Juli 202518 Min. LesezeitOronts Engineering Team

Warum die meisten KI-Workflows in der Produktion scheitern

Hier ist etwas, das wir auf die harte Tour gelernt haben: Eine KI dazu zu bringen, in einer Demo etwas Beeindruckendes zu machen, ist einfach. Sie dazu zu bringen, dasselbe zuverlässig zu tun, tausende Male am Tag, mit echten Daten und echten Grenzfällen? Da stoßen die meisten Teams an ihre Grenzen.

Wir haben KI-Workflows für Dokumentenverarbeitung, Kundenservice-Automatisierung, Content-Generierung und Datenanalyse gebaut. Dabei haben wir jeden denkbaren Fehler gemacht. Dieser Leitfaden ist das, was wir uns gewünscht hätten, dass uns jemand vorher gesagt hätte.

Der Unterschied zwischen einem Prototyp und einem Produktionssystem ist nicht das Modell, das du verwendest. Es ist alles drum herum.

Lass mich dir zeigen, wie wir heute tatsächlich KI-Workflows entwerfen, nachdem wir diese Lektionen gelernt haben.

Die Anatomie einer Produktions-KI-Pipeline

Stell dir einen KI-Workflow als eine Reihe von Verarbeitungsstufen vor, jede mit einer spezifischen Aufgabe. Hier ist die Grundstruktur, die wir verwenden:

Eingabe → Validierung → Vorverarbeitung → KI-Verarbeitung → Nachverarbeitung → Validierung → Ausgabe
           ↓               ↓                  ↓                  ↓               ↓
        [Fehler]      [Transformation]     [LLM-Aufruf]      [Parsen]      [Qualitäts-
        Handler       + Anreicherung       + Tools           + Format       prüfung]

Jede Stufe hat klare Ein- und Ausgaben sowie Fehlermodi. Lass sie uns aufschlüsseln.

Stufe 1: Eingabevalidierung und Normalisierung

Vertraue eingehenden Daten niemals. Niemals. Das haben wir gelernt, als ein Kunde uns ein "Textdokument" schickte, das eigentlich eine 50MB-Binärdatei war. Die Pipeline verschluckte sich, die Queue staute sich, und wir verbrachten ein Wochenende mit der Reparatur.

const validateInput = async (input) => {
  const checks = {
    exists: input !== null && input !== undefined,
    sizeOk: Buffer.byteLength(input, 'utf8') < MAX_INPUT_SIZE,
    formatOk: isValidFormat(input),
    contentOk: !containsMaliciousPatterns(input)
  };

  const failures = Object.entries(checks)
    .filter(([_, passed]) => !passed)
    .map(([check]) => check);

  if (failures.length > 0) {
    throw new ValidationError(`Eingabe fehlgeschlagen: ${failures.join(', ')}`);
  }

  return normalizeInput(input);
};

Was du prüfen solltest:

  • Dateigrößenlimits (setze sie niedriger als du denkst)
  • Formatvalidierung (ist es wirklich JSON, nicht nur .json benannt?)
  • Zeichenkodierung (UTF-8-Probleme werden dich verfolgen)
  • Content-Sicherheit (sende keine bösartigen Inhalte an dein LLM)

Stufe 2: Vorverarbeitung und Anreicherung

Rohe Eingaben gehen selten direkt an ein LLM. Normalerweise musst du sie transformieren, Kontext hinzufügen oder in Chunks aufteilen.

VorverarbeitungsaufgabeWann verwendenBeispiel
ChunkingLange Dokumente über Kontextlimits100-seitiges PDF in 2000-Token-Chunks aufteilen
AnreicherungZusätzlicher Kontext nötigKundenhistorie vor Support-Ticket-Verarbeitung hinzufügen
ExtraktionNur Teile der Eingabe relevantNur das "description"-Feld aus JSON-Payload ziehen
TransformationFormatkonvertierung nötigHTML zu Markdown für saubere Verarbeitung konvertieren
DeduplizierungWiederholter Content verschwendet TokensDoppelte Absätze aus gescraptem Content entfernen

Hier ist eine Chunking-Strategie, die wir tatsächlich verwenden:

const chunkDocument = (text, options = {}) => {
  const {
    maxTokens = 2000,
    overlap = 200,
    preserveParagraphs = true
  } = options;

  const chunks = [];
  let currentChunk = '';

  const paragraphs = text.split(/\n\n+/);

  for (const para of paragraphs) {
    const combined = currentChunk + '\n\n' + para;

    if (estimateTokens(combined) > maxTokens) {
      if (currentChunk) {
        chunks.push(currentChunk.trim());
        // Overlap für Kontextkontinuität behalten
        currentChunk = getLastNTokens(currentChunk, overlap) + '\n\n' + para;
      } else {
        // Einzelner Absatz zu lang, erzwungener Split
        chunks.push(...forceSplitParagraph(para, maxTokens));
        currentChunk = '';
      }
    } else {
      currentChunk = combined;
    }
  }

  if (currentChunk.trim()) {
    chunks.push(currentChunk.trim());
  }

  return chunks;
};

Der Overlap ist entscheidend. Ohne ihn gehen Informationen verloren, die Chunk-Grenzen überspannen. Das haben wir gelernt, als unser Dokumenten-Summarizer immer wieder wichtige Punkte verpasste, die zufällig zwischen Chunks fielen.

Den KI-Verarbeitungskern entwerfen

Hier passieren die eigentlichen LLM-Aufrufe. Aber ein einzelner Aufruf reicht selten für komplexe Aufgaben.

Das Prompt-Chain-Pattern

Anstatt eines riesigen Prompts, der alles versucht, teile komplexe Aufgaben in fokussierte Schritte auf:

[Verstehen] → [Planen] → [Ausführen] → [Verifizieren] → [Formatieren]

Beispiel: Kundenbeschwerde verarbeiten

const processComplaint = async (complaint) => {
  // Schritt 1: Verstehen - Schlüsselinformationen extrahieren
  const analysis = await llm.call({
    system: `Extrahiere strukturierte Informationen aus Kundenbeschwerden.
             Rückgabe als JSON mit: issue_type, urgency, customer_emotion, key_details`,
    user: complaint
  });

  // Schritt 2: Planen - Antwortstrategie bestimmen
  const strategy = await llm.call({
    system: `Basierend auf dieser Beschwerdeanalyse, bestimme die Antwortstrategie.
             Berücksichtige: Lösungsoptionen, Eskalationsbedarf, Kompensationsberechtigung`,
    user: JSON.stringify(analysis)
  });

  // Schritt 3: Ausführen - Antwort generieren
  const response = await llm.call({
    system: `Schreibe eine Kundenantwort nach dieser Strategie.
             Ton: empathisch, professionell. Länge: 2-3 Absätze.`,
    user: `Strategie: ${JSON.stringify(strategy)}\nOriginale Beschwerde: ${complaint}`
  });

  // Schritt 4: Verifizieren - Qualitätsprüfung
  const verification = await llm.call({
    system: `Prüfe diese Kundenantwort. Checke:
             - Adressiert alle Anliegen? - Angemessener Ton? - Umsetzbare nächste Schritte?
             Rückgabe: {approved: boolean, issues: string[]}`,
    user: response
  });

  if (!verification.approved) {
    return await regenerateWithFeedback(response, verification.issues);
  }

  return response;
};

Jeder Schritt ist einfacher, testbarer und leichter zu debuggen. Wenn etwas schiefgeht, weißt du genau, welche Stufe fehlgeschlagen ist.

Wann verketten vs. wann parallelisieren

Nicht alles muss sequentiell sein. So entscheiden wir:

PatternVerwenden wennBeispiel
Sequentielle KetteJeder Schritt hängt vom vorherigen Output abVerstehen → Planen → Ausführen
Parallele AusführungSchritte sind unabhängigMehrere Dokumente gleichzeitig analysieren
Map-ReduceItems verarbeiten, dann aggregierenJeden Chunk zusammenfassen, dann Zusammenfassungen kombinieren
Bedingte VerzweigungVerschiedene Pfade für verschiedene EingabenEinfache Anfragen vs. komplexe Analyse

Beispiel parallele Ausführung:

const analyzeDocuments = async (documents) => {
  // Alle Dokumente parallel verarbeiten
  const analyses = await Promise.all(
    documents.map(doc => analyzeDocument(doc))
  );

  // Reduce: In finalen Report kombinieren
  const combinedReport = await llm.call({
    system: 'Synthetisiere diese einzelnen Analysen zu einem kohärenten Bericht',
    user: analyses.map((a, i) => `Dokument ${i + 1}:\n${a}`).join('\n\n---\n\n')
  });

  return combinedReport;
};

Verzweigungslogik: Zum richtigen Handler routen

Eingaben aus der realen Welt variieren stark. Eine "Kundennachricht" könnte eine Beschwerde, eine Frage, ein Lob oder Spam sein. Jede braucht unterschiedliche Behandlung.

Klassifizierungsbasiertes Routing

const routeCustomerMessage = async (message) => {
  const classification = await llm.call({
    system: `Klassifiziere diese Kundennachricht in genau eine Kategorie:
             - complaint: Kunde drückt Unzufriedenheit aus
             - question: Kunde sucht Informationen
             - feedback: Allgemeines Feedback oder Vorschläge
             - urgent: Sicherheitsprobleme, rechtliche Drohungen, Executive-Eskalation
             - spam: Irrelevante oder automatisierte Nachrichten

             Gib nur den Kategorienamen zurück.`,
    user: message
  });

  const handlers = {
    complaint: handleComplaint,
    question: handleQuestion,
    feedback: handleFeedback,
    urgent: escalateToHuman,
    spam: markAsSpam
  };

  const handler = handlers[classification.toLowerCase()] || handleUnknown;
  return await handler(message);
};

Konfidenzbasiertes Routing

Manchmal ist die KI unsicher. Baue diese Unsicherheit in dein Routing ein:

const routeWithConfidence = async (input) => {
  const result = await llm.call({
    system: `Analysiere und route diese Anfrage. Rückgabe als JSON:
             {
               "category": "string",
               "confidence": 0.0-1.0,
               "reasoning": "warum diese Kategorie"
             }`,
    user: input
  });

  if (result.confidence < 0.7) {
    // Niedrige Konfidenz - menschliche Eingabe holen oder Fallback nutzen
    return await handleLowConfidence(input, result);
  }

  if (result.confidence < 0.9 && isHighStakes(result.category)) {
    // Mittlere Konfidenz bei wichtigen Entscheidungen - verifizieren
    return await verifyThenRoute(input, result);
  }

  // Hohe Konfidenz - automatisch fortfahren
  return await routeToHandler(result.category, input);
};

Fehlerbehandlung: Weil Dinge kaputtgehen werden

KI-Workflows scheitern auf Arten, die traditionelle Software nicht kennt. Das LLM könnte ungültiges JSON zurückgeben, Informationen halluzinieren oder einfach... Anweisungen nicht befolgen. Plane dafür.

Die Fehler-Hierarchie

class AIWorkflowError extends Error {
  constructor(message, stage, recoverable = true) {
    super(message);
    this.stage = stage;
    this.recoverable = recoverable;
  }
}

class ValidationError extends AIWorkflowError {
  constructor(message) {
    super(message, 'validation', true);
  }
}

class LLMError extends AIWorkflowError {
  constructor(message, type) {
    super(message, 'llm_call', type !== 'rate_limit');
    this.type = type; // 'timeout', 'rate_limit', 'invalid_response', 'refused'
  }
}

class OutputError extends AIWorkflowError {
  constructor(message) {
    super(message, 'output', true);
  }
}

Retry-Strategien, die funktionieren

Verschiedene Fehler brauchen verschiedene Retry-Ansätze:

FehlertypRetry-StrategieMax. RetriesBackoff
Rate LimitWarten und retry5Exponentiell mit Jitter
TimeoutSofort retry3Linear
Ungültige AntwortRetry mit Feedback2Keins
Modell verweigertUmformulieren und retry2Keins
Server-FehlerWarten und retry3Exponentiell
const withRetry = async (operation, options = {}) => {
  const {
    maxRetries = 3,
    backoffMs = 1000,
    backoffMultiplier = 2,
    shouldRetry = () => true
  } = options;

  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;

      if (!shouldRetry(error) || attempt === maxRetries) {
        throw error;
      }

      const delay = backoffMs * Math.pow(backoffMultiplier, attempt);
      const jitter = Math.random() * delay * 0.1;
      await sleep(delay + jitter);
    }
  }

  throw lastError;
};

// Verwendung mit LLM-Aufrufen
const safeLLMCall = async (params) => {
  return await withRetry(
    () => llm.call(params),
    {
      maxRetries: 3,
      shouldRetry: (error) => {
        if (error.type === 'rate_limit') return true;
        if (error.type === 'timeout') return true;
        if (error.type === 'server_error') return true;
        return false;
      }
    }
  );
};

Selbstheilung für ungültige Antworten

Wenn das LLM Müll zurückgibt, kannst du es manchmal reparieren:

const parseWithRecovery = async (llmResponse, expectedSchema) => {
  // Erster Versuch: direktes Parsen
  try {
    const parsed = JSON.parse(llmResponse);
    if (validateSchema(parsed, expectedSchema)) {
      return parsed;
    }
  } catch (e) {
    // JSON-Parse fehlgeschlagen, weiter zur Wiederherstellung
  }

  // Wiederherstellungsversuch: LLM bitten, seine Ausgabe zu reparieren
  const fixed = await llm.call({
    system: `Die folgende Antwort sollte gültiges JSON sein, das diesem Schema entspricht:
             ${JSON.stringify(expectedSchema)}

             Repariere die Antwort zu gültigem JSON. Gib NUR das reparierte JSON zurück.`,
    user: llmResponse
  });

  try {
    const parsed = JSON.parse(fixed);
    if (validateSchema(parsed, expectedSchema)) {
      return parsed;
    }
  } catch (e) {
    // Immer noch kaputt
  }

  // Letzter Fallback: extrahieren was geht
  return extractPartialData(llmResponse, expectedSchema);
};

Monitoring und Observability

Du kannst nicht reparieren, was du nicht siehst. Hier ist, was wir tracken:

Schlüsselmetriken

MetrikWarum wichtigAlert-Schwelle
Latenz (p50, p95, p99)User Experience, Timeout-Risikop95 > 10s
ErfolgsrateGesamtgesundheit< 95%
Token-VerbrauchKostenkontrolle> 150% der Baseline
Retry-RateVersteckte Instabilität> 10%
KlassifizierungsverteilungDrift erkennenSignifikante Abweichung von Baseline
Output-Qualitäts-ScoresDegradation erkennenDurchschnitt < 0.8

Strukturiertes Logging

Jeder Workflow-Lauf sollte nachverfolgbare Logs produzieren:

const runWorkflow = async (input, context) => {
  const runId = generateRunId();
  const startTime = Date.now();

  const log = (stage, data) => {
    logger.info({
      runId,
      stage,
      timestamp: Date.now(),
      elapsed: Date.now() - startTime,
      ...data
    });
  };

  try {
    log('start', { inputSize: input.length });

    const validated = await validate(input);
    log('validated', { valid: true });

    const processed = await process(validated);
    log('processed', {
      tokensUsed: processed.usage.total,
      model: processed.model
    });

    const output = await format(processed);
    log('complete', {
      success: true,
      outputSize: output.length,
      totalTime: Date.now() - startTime
    });

    return output;

  } catch (error) {
    log('error', {
      error: error.message,
      stage: error.stage,
      recoverable: error.recoverable
    });
    throw error;
  }
};

Alles zusammenfügen: Ein vollständiges Beispiel

Bauen wir einen Dokumentenanalyse-Workflow, der alles verwendet, was wir besprochen haben:

class DocumentAnalysisPipeline {
  constructor(options = {}) {
    this.maxChunkSize = options.maxChunkSize || 3000;
    this.concurrency = options.concurrency || 5;
  }

  async run(document) {
    // Stufe 1: Validieren
    const validated = await this.validate(document);

    // Stufe 2: Vorverarbeiten
    const chunks = await this.preprocess(validated);

    // Stufe 3: Parallele Analyse mit Concurrency-Kontrolle
    const analyses = await this.analyzeChunks(chunks);

    // Stufe 4: Synthetisieren
    const synthesis = await this.synthesize(analyses);

    // Stufe 5: Qualitätsprüfung
    const final = await this.qualityCheck(synthesis, document);

    return final;
  }

  async validate(document) {
    if (!document || typeof document !== 'string') {
      throw new ValidationError('Dokument muss ein nicht-leerer String sein');
    }

    if (document.length > 1000000) {
      throw new ValidationError('Dokument überschreitet maximale Größe');
    }

    return document;
  }

  async preprocess(document) {
    return chunkDocument(document, {
      maxTokens: this.maxChunkSize,
      overlap: 200
    });
  }

  async analyzeChunks(chunks) {
    const results = [];

    // In Batches verarbeiten für Concurrency-Kontrolle
    for (let i = 0; i < chunks.length; i += this.concurrency) {
      const batch = chunks.slice(i, i + this.concurrency);
      const batchResults = await Promise.all(
        batch.map((chunk, idx) => this.analyzeChunk(chunk, i + idx))
      );
      results.push(...batchResults);
    }

    return results;
  }

  async analyzeChunk(chunk, index) {
    return await withRetry(async () => {
      const result = await llm.call({
        system: `Analysiere diesen Dokumentabschnitt. Extrahiere:
                 - Schlüsselthemen
                 - Wichtige Fakten und Zahlen
                 - Bemerkenswerte Zitate oder Aussagen
                 - Fragen oder Informationslücken

                 Rückgabe als strukturiertes JSON.`,
        user: `Abschnitt ${index + 1}:\n\n${chunk}`
      });

      return parseWithRecovery(result, ANALYSIS_SCHEMA);
    });
  }

  async synthesize(analyses) {
    const combined = analyses.map((a, i) =>
      `Abschnitt ${i + 1}:\n${JSON.stringify(a, null, 2)}`
    ).join('\n\n---\n\n');

    return await llm.call({
      system: `Synthetisiere diese Abschnittsanalysen zu einer umfassenden Dokumentzusammenfassung.
               Struktur:
               1. Executive Summary (2-3 Sätze)
               2. Schlüsselerkenntnisse (Aufzählungspunkte)
               3. Wichtige Details
               4. Lücken oder Fragen
               5. Empfehlungen`,
      user: combined
    });
  }

  async qualityCheck(synthesis, originalDocument) {
    const check = await llm.call({
      system: `Überprüfe diese Analyse auf Qualität. Prüfe:
               - Genauigkeit: Spiegelt sie das Originaldokument wider?
               - Vollständigkeit: Sind wichtige Punkte abgedeckt?
               - Klarheit: Ist sie gut organisiert und klar?

               Rückgabe: {score: 0-1, issues: string[], approved: boolean}`,
      user: `Analyse:\n${synthesis}\n\nOriginal (erste 2000 Zeichen):\n${originalDocument.slice(0, 2000)}`
    });

    if (!check.approved) {
      // Zur Überprüfung loggen, aber nicht fehlschlagen
      logger.warn({ issues: check.issues, score: check.score });
    }

    return {
      analysis: synthesis,
      qualityScore: check.score,
      qualityIssues: check.issues
    };
  }
}

Häufige Fallstricke und wie du sie vermeidest

Nach dem Bau dutzender dieser Systeme sind hier die Fehler, die wir am häufigsten sehen:

1. Keine Timeout-Grenzen

Jeder LLM-Aufruf braucht ein Timeout. Setze sie aggressiv.

// Schlecht: Kein Timeout
const result = await llm.call(params);

// Gut: Explizites Timeout
const result = await Promise.race([
  llm.call(params),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), 30000)
  )
]);

2. Token-Limits ignorieren

Verbrauch tracken und Budgets setzen:

const tokenBudget = {
  max: 10000,
  used: 0,

  canSpend(amount) {
    return this.used + amount <= this.max;
  },

  spend(amount) {
    this.used += amount;
    if (this.used > this.max * 0.8) {
      logger.warn('Token-Budget zu 80% verbraucht');
    }
  }
};

3. Kein Fallback für kritische Pfade

Hab immer einen Plan B:

const processWithFallback = async (input) => {
  try {
    return await primaryProcess(input);
  } catch (error) {
    if (error.recoverable) {
      return await simplifiedProcess(input);
    }
    // Kritischer Pfad - für manuelle Bearbeitung einreihen
    await queueForManualReview(input, error);
    return { status: 'queued_for_review' };
  }
};

Was kommt als nächstes

KI-Workflows werden immer ausgefeilter. Hier ist, wohin wir die Dinge gehen sehen:

  • Bessere Tool-Nutzung: Modelle werden besser darin zu entscheiden, wann und wie externe Tools zu verwenden sind
  • Längerer Kontext: Größere Kontextfenster bedeuten weniger Chunking-Kopfschmerzen
  • Schnellere Inferenz: Latenz sinkt, ermöglicht komplexere Echtzeit-Workflows
  • Spezialisierte Modelle: Fine-tuned Modelle für spezifische Aufgaben übertreffen Allzweck-Modelle

Aber die Grundlagen ändern sich nicht. Validiere Eingaben, behandle Fehler elegant, überwache alles und hab immer einen Fallback. Baue auf diesen Prinzipien auf, und deine KI-Workflows werden den Kontakt mit der realen Welt überstehen.

Wenn du KI-Workflows baust und an Wände stößt, haben wir dein Problem wahrscheinlich schon gesehen. Melde dich, und lass es uns gemeinsam lösen.

Topics covered

KI-Workflow-DesignKI-Pipeline-ArchitekturPrompt-ChainingKI-FehlerbehandlungLLM-OrchestrierungKI-AutomatisierungWorkflow-VerzweigungKI-Systementwicklung

Ready to implement agentic AI?

Our team specializes in building production-ready AI systems. Let's discuss how we can help you leverage agentic AI for your enterprise.

Start a conversation