1. All Collections >
  2. Konversation Led Wachstum >
  3. Berichtswesen und Analysen >
  4. Wie du die Konversations‑QA mit n8n & KI automatisierst

Wie du die Konversations‑QA mit n8n & KI automatisierst

Avatar
Shing-Yi Tan
14 Min. Lesezeit

Es wird empfohlen, KI‑bearbeitete Konversationen nach Aktivierung des KI‑Agents zu prüfen; manuelle QA wird bei wachsendem Umfang jedoch zu zeitaufwändig. Diese Anleitung zeigt, wie du die Konversations‑QA mit n8n + einem KI‑Modell automatisierst, sodass jede geschlossene Konversation automatisch geprüft und in Google Sheets zur Berichterstattung protokolliert wird.

Zusammengefasst: Dieser n8n‑Workflow wird jedes Mal ausgelöst, wenn eine Konversation geschlossen wird. Nach dem Auslösen ruft er die List Messages API auf, um die letzten 50 Nachrichten abzurufen (mit Option, per Paginierung bis zu 100 abzurufen), und übergibt das Transkript an ein KI‑Modell zur QA‑Verarbeitung — z. B. Sentiment‑Analyse, Engagement‑Bewertung und Prüfungen des Lösungsstatus. Die Ergebnisse werden in einem Google Sheet erfasst, sodass du jede Konversation einfach per Kontakt‑ID nachschlagen kannst.

Was du bauen wirst

Eine Automatisierung, die:

  • Löst aus, wenn eine Konversation geschlossen wird

  • Ruft die letzten 50 Nachrichten über die List Messages API ab (optional bis zu 100 mit Paginierung)

  • Bereinigt und formatiert Nachrichten zu einem Transkript

  • Sendet das Transkript an ein KI‑Modell zur QA‑Bewertung

  • Parst die KI‑Ausgabe sicher (nur JSON‑Kontrakt)

  • Schreibt die Ergebnisse in Google Sheets (eine Zeile pro Konversation)

Anforderungen

Werkzeuge

  • Automation‑Plattform: n8n

  • Respond.io Developer API — List Messages Endpoint

  • AI API — OpenAI, Anthropic, Gemini oder ein beliebiger von n8n unterstützter Anbieter

  • Reporting / Speicherung — Google Sheets (oder dein bevorzugtes Speichertool)

Zugangsdaten

  • Respond.io Developer API‑Schlüssel

  • API‑Schlüssel des KI‑Anbieters (OpenAI, Anthropic, Gemini, etc.)

  • Google Sheets‑Zugang in n8n (OAuth oder Service‑Account)

Bevor du beginnst

1) Erstelle ein Google Sheet für QA‑Ergebnisse

Erstelle eine Tabelle und füge ein Blatt hinzu (z. B. QA_Results) mit diesen Headern:

  • timestamp

  • contact_id

  • opened_at

  • channel

  • overall_score

  • resolved

  • customer_sentiment

  • reasoning

  • engagement_score

2) Bestätige die Felder deiner Trigger‑Payload

Identifiziere aus der Ausgabe des Conversation closed trigger die genauen Feldnamen für:

  • Konversations‑ID

  • Kontakt‑ID

  • Timestamp der Konversationseröffnung

  • Timestamp der Konversationsschließung (optional)

Feldnamen variieren je nach Implementierung. In den folgenden Code‑Nodes siehst du Platzhalter wie trigger.conversationId. Passe diese Zuordnungen an deine Trigger‑Ausgabe an.

Schritt-für-Schritt-Anleitung

1. Trigger: Konversation geschlossen

Wenn eine Konversation in respond.io geschlossen wird, wollen wir den n8n‑Workflow automatisch auslösen. So wird sichergestellt, dass jede abgeschlossene Konversation durch QA läuft — keine manuelle Prüfung nötig.

n8n‑Node:Conversation closed trigger

  1. Füge in n8n den respond.io Trigger‑Node hinzu.

  2. Wähle Conversation Closed.

  3. Verbinde deine respond.io API‑Zugangsdaten. Lerne, wie du das in n8n mit unserem Integrationsleitfaden einrichtest.

Dieser Trigger stellt sicher, dass du Konversationen nur nach deren Abschluss prüfst.

2. Konversation‑Nachrichten abrufen

In diesem Schritt rufst du die letzten 50 eingehenden und ausgehenden Nachrichten über die List Messages API ab. Das reicht in der Regel für eine gründliche QA‑Prüfung aus. Wenn du mehr Kontext benötigst, kannst du optional weitere 50 Nachrichten (bis zu 100 insgesamt) mit Paginierung abrufen.

n8n‑Nodes:

  • get 1st 50 messages (HTTP Request)

  • Is there a second page? (IF) — optional

  • get 2nd 50 messages (HTTP Request) — optional

2.1 Hol die ersten 50 Nachrichten

Node:get 1st 50 messages

  1. Wähle Core > HTTP Request

  2. Methode: GET

  3. URL: dein List Messages Endpoint

Beispiel‑URL (ersetze durch deine tatsächliche API‑Basis‑URL + Endpoint‑Format):

https://api.respond.io/v2/contact/{{identifier}}/message/list

Wobei identifier durch eines der folgenden Formate ersetzt werden muss: id:<contactID>, phone:+<countryCodeAndPhone> oder email:<contactEmail>

  1. Aktiviere Send Query Parameters > Using Fields Below, und füge dann hinzu:

    • Name: limit

    • Wert: 50

Wenn deine API es unterstützt, setze die Sortierung von ältestem → neuestem. Beispiel: sort=asc.

  1. Aktiviere Send Headers > Using Fields Below, und füge dann hinzu:

    • Name: Accept

      • Wert: application/json

    • Name: Authorization

      • Wert: Bearer <your_respond_api_key>

Wenn deine List Messages‑Antwort pagination.next enthält, kannst du diesen Wert direkt verwenden, um die zweite Seite abzurufen.

2.2 Prüfe und hole mehr als 50 Nachrichten (Optional)

Wenn 50 Nachrichten für deine QA‑Bedürfnisse nicht ausreichen, kannst du eine zweite Seite mit 50 Nachrichten abrufen. Füge einen IF‑Node hinzu, um zu prüfen, ob weitere Nachrichten verfügbar sind.

Node:Is there a second page? (IF)

  • Linker Wert (Expression): {{ $json.pagination.next }}

  • Operator: is not empty

Wenn wahr, bedeutet das, dass noch mehr Nachrichten abgerufen werden können. Wenn falsch, läuft der Workflow ohne weitere Abrufe weiter — das verhindert, dass der Workflow fehlschlägt, wenn keine zweite Seite vorhanden ist. Der Merge‑Node stromabwärts wartet auf beide Pfade, sodass der Workflow in beiden Fällen reibungslos weiterläuft.

2.3 Hol die nächsten 50 Nachrichten (Optional)

Wenn der IF‑Node bestanden hat (eine zweite Seite existiert), rufe die nächste Charge ab.

Node:get 2nd 50 messages

  1. Füge einen weiteren HTTP Request‑Node hinzu.

  2. Setze:

    • Methode: GET

    • URL: {{ $json.pagination.next }}

  3. Füge dieselben Header hinzu:

    • Accept: application/json

    • Authorization: Bearer <your_respond_api_key>

3. Seiten mit Nachrichten zusammenführen (Optional)

Dieser Schritt ist nur nötig, wenn du mehr als 50 Nachrichten abrufst. Wenn du dich entschieden hast, keine zweite Seite abzurufen, kannst du diesen Node überspringen und get 1st 50 messages direkt mit dem nächsten Schritt verbinden (Clean + keep messages since last open).

Der Merge‑Node kombiniert die ersten 50 Nachrichten mit den zweiten 50 zu einer einzigen Liste. Ohne ihn kann der Workflow nicht zwei getrennte API‑Antworten zusammen verarbeiten.

n8n‑Node:Merge

  1. Füge Flow > Merge hinzu.

  2. Setze Modus auf Append.

  3. Setze Anzahl der Inputs: 2

  4. Verbinde:

    • Input 1: get 1st 50 messages

    • Input 2: get 2nd 50 messages

4. Nachrichten bereinigen und filtern

Die API‑Antwort ist noch roh und enthält viele zusätzliche Informationen, die die KI nicht benötigt. Dieser Schritt bereinigt die Daten — entfernt alte Nachrichten, normalisiert Sender‑Labels und strukturiert alles in eine einfache Liste, die bereit für KI‑Verarbeitung ist. Alles, was du tun musst, ist den Code‑Node hinzuzufügen und das unten stehende JavaScript einzufügen.

n8n‑Node:Clean + keep messages since last convo open

Dazu: Add node → Select Core → Code → Code (JavaScript).

Dieses Skript wird:

  1. Nachrichten beider Seiten kombinieren

  2. Nachrichten herausfiltern, die vor dem Öffnen der Konversation gesendet wurden

  3. Nachrichten sortieren von ältestem → neuestem

  4. Absender normalisieren in konsistente Bezeichnungen: Contact, AI Agent, Human Agent, Workflow

  5. Nachrichten‑Indexnummern hinzufügen

  6. Gib eine saubere, strukturierte Liste für den Transkript‑Schritt zurück

Hinweis: Dieses Beispiel filtert nach messageId und vergleicht es mit dem geöffneten Zeitstempel, der in Mikrosekunden umgerechnet wurde. Wenn deine API stattdessen createdAt‑Timestamps liefert, filtere nach createdAt statt messageId. Stelle sicher, dass dein conversation_open_timestamp eine Zeitzone enthält. Wenn nicht, setze ihn vor dem Parsen auf die Zeitzone deines Arbeitsbereichs.

Füge dies in den Code‑Node ein:

// n8n Code node AFTER Merge (Run Once for All Items)
// Incoming items are the API responses from page1 and (optionally) page2.

function unwrapRespondList(json) {
  if (Array.isArray(json) && json.length && json[0]?.items) return json[0];
  if (json?.items) return json;
  if (Array.isArray(json)) return { items: json, pagination: {} };
  return { items: [], pagination: {} };
}

function toMicroseconds(ts) {
  if (!ts || typeof ts !== "string") return null;

  // Prefer timestamps that already contain timezone info.
  // If your timestamp lacks timezone, add it upstream (recommended) rather than hardcoding here.
  const iso = ts.includes("T") ? ts : ts.replace(" ", "T");
  const ms = Date.parse(iso);
  if (Number.isNaN(ms)) return null;
  return ms * 1000; // microseconds
}

function normalizeSender(source) {
  if (!source) return "Unknown";
  const s = String(source).toLowerCase();
  if (s === "contact") return "Contact";
  if (s.includes("ai")) return "AI Agent";
  if (s.includes("workflow") || s.includes("automation")) return "Workflow";
  if (s.includes("user") || s.includes("agent")) return "Human Agent";
  return source;
}

// Read from the Conversation closed trigger
const trigger = $("Conversation closed trigger").first().json;
const body = trigger.body ?? trigger;

const openedTs =
  body.conversation_open_timestamp ||
  body.conversation_opened_timestamp ||
  body.conversationOpenedAt ||
  body["conversation_open_timestamp "] ||
  null;

const openMicro = toMicroseconds(openedTs);

// Merge items from all incoming API payloads (1 or 2 pages)
let mergedItems = [];
for (const item of $input.all()) {
  const unwrapped = unwrapRespondList(item.json);
  if (Array.isArray(unwrapped.items)) mergedItems.push(...unwrapped.items);
}

// Filter messages after conversation opened timestamp
if (openMicro !== null) {
  mergedItems = mergedItems.filter((m) => {
    const idNum = Number(m?.messageId);
    return Number.isFinite(idNum) && idNum > openMicro;
  });
}

// Sort oldest -> newest
mergedItems.sort((a, b) => Number(a?.messageId ?? 0) - Number(b?.messageId ?? 0));

// Index + shape
const indexed = mergedItems.map((m, i) => {
  const traffic = m?.traffic ?? null;
  const sender = m?.sender || {};

  // Incoming traffic is always from Contact
  if (traffic === "incoming") {
    return {
      Index: i + 1,
      traffic,
      message: m?.message ?? null,
      Sender: { source: "Contact" },
    };
  }

  return {
    Index: i + 1,
    traffic,
    message: m?.message ?? null,
    Sender: {
      source: normalizeSender(sender?.source),
      userId: sender?.userId ?? null,
      teamId: sender?.teamId ?? null,
    },
  };
});

return [
  {
    json: {
      message_count: indexed.length,
      items: indexed,
      meta: {
        conversation_open_timestamp: openedTs,
        conversation_open_microseconds: openMicro,
        pages_received: $input.all().length,
      },
    },
  },
];

Stelle sicher, dass Mode = Run Once for All Items und Language = JavaScript.

Das obige Skript dient als Referenz. Dein Arbeitsbereich kann andere Felder oder Strukturen zurückgeben. Um die richtige JSON‑Parsing‑Logik für dein Setup zu erstellen, kopiere die Ausgabe des vorherigen Nodes, füge sie zusammen mit dem Referenzskript oben in ein KI‑Tool (z. B. ChatGPT oder Claude) ein und beschreibe das benötigte Ausgabeformat. Die KI kann das Skript dann an dein tatsächliches Datenformat anpassen.

5. Transkript erstellen

Dieser Schritt wandelt die bereinigte Nachrichtenliste in ein Markdown‑ähnliches Transkript für die KI‑Verarbeitung um. Strukturierte Transkripte verbessern das KI‑Verständnis und reduzieren Halluzinationen.

n8n‑Node:Build markdown transcript (Core → Code → JavaScript)

Füge dies in den Code‑Node ein:

// Minimal parse of possibly-escaped JSON string
function parseMaybeEscapedJSON(raw) {
  if (raw == null) return null;
  if (typeof raw === 'object') return raw;
  let s = String(raw).trim();
  if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith(''') && s.endsWith('''))) s = s.slice(1, -1);
  s = s.replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\r/g, '\r').replace(/\\\\/g, '\\');
  return JSON.parse(s);
}

// Flatten nested objects/arrays into k1_k2_0_k3 style keys
// Primitive-only arrays are joined as comma-delimited strings
function flatten(obj, prefix = '', out = {}) {
  if (obj == null) return out;
  const makeKey = (k) => (prefix ? `${prefix}_${k}` : String(k));
  if (Array.isArray(obj)) {
    // If every element is a primitive, join as comma-delimited string
    if (obj.every(v => v == null || typeof v !== 'object')) {
      out[prefix || 'value'] = obj.join(', ');
      return out;
    }
    obj.forEach((v, i) => {
      const k = makeKey(i);
      (v && typeof v === 'object') ? flatten(v, k, out) : out[k] = v;
    });
    return out;
  }
  if (typeof obj === 'object') {
    for (const [k, v] of Object.entries(obj)) {
      const key = makeKey(k);
      (v && typeof v === 'object') ? flatten(v, key, out) : out[key] = v;
    }
    return out;
  }
  out[prefix || 'value'] = obj;
  return out;
}

const parsed = [];
for (const item of items) {
  try {
    const obj = parseMaybeEscapedJSON(item.json?.Output);
    if (!obj || typeof obj !== 'object') {
      parsed.push({ json: { error: 'Parsed Output is not an object', raw: item.json?.Output ?? null } });
      continue;
    }
    const out = flatten(obj);
    parsed.push({ json: out });
  } catch (e) {
    parsed.push({ json: { error: 'Failed to parse Output', message: e?.message || String(e), raw: item.json?.Output ?? null } });
  }
}
return parsed; `${prefix}_${k}` : String(k));
  if (Array.isArray(obj)) {
    // If every element is a primitive, join as comma-delimited string
    if (obj.every(v => v == null || typeof v !== 'object')) {
      out[prefix || 'value'] = obj.join(', ');
      return out;
    }
    obj.forEach((v, i) => {
      const k = makeKey(i);
      (v && typeof v === 'object') ? flatten(v, k, out) : out[k] = v;
    });
    return out;
  }
  if (typeof obj === 'object') {
    for (const [k, v] of Object.entries(obj)) {
      const key = makeKey(k);
      (v && typeof v === 'object') ? flatten(v, key, out) : out[key] = v;
    }
    return out;
  }
  out[prefix || 'value'] = obj;
  return out;
}

const parsed = [];
for (const item of items) {
  try {
    const obj = parseMaybeEscapedJSON(item.json?.Output);
    if (!obj || typeof obj !== 'object') {
      parsed.push({ json: { error: 'Parsed Output is not an object', raw: item.json?.Output ?? null } });
      continue;
    }
    const out = flatten(obj);
    parsed.push({ json: out });
  } catch (e) {
    parsed.push({ json: { error: 'Failed to parse Output', message: e?.message || String(e), raw: item.json?.Output ?? null } });
  }
}
return parsed; null } });
      continue;
    }
    const out = flatten(obj);
    parsed.push({ json: out });
  } catch (e) {
    parsed.push({ json: { error: 'Failed to parse Output', message: e?.message || String(e), raw: item.json?.Output ?? null } });
  }
}

return parsed; null } });
  }
}

return parsed;

Stelle sicher, dass Mode = Run Once for All Items und Language = JavaScript.

6. KI‑basierte Konversationsprüfungen

Dieser Node sendet das Transkript an ein KI‑Modell (z. B. OpenAI 5.4) und liefert eine strukturierte JSON‑Antwort.

So richtest du es ein

  1. Wähle die Zugangsdaten deines KI‑Anbieters. In diesem Beispiel verwenden wir OpenAI.

  2. Resource: Message a Model

  3. Operation: Nachricht an Assistenten

  4. Nachrichten:

    1. Typ: Text

    2. Rolle: Benutzer

    3. Prompt: {{ $json.transcript }}

  5. Ausgabe vereinfachen: Aktivieren

  6. Option hinzufügen:

    1. Instructions - Dies ist nur eine Beispielvorlage, die du aber in eine andere KI (z. B. ChatGPT, Claude etc.) eingeben kannst, um sie an deine Bedürfnisse anzupassen:

Du bist Analyst für Gesprächsqualität bei respond.io, einer Plattform für Kundenkommunikation. Deine Aufgabe ist es, Support‑ oder Sales‑Konversationen zu prüfen und genaue, konsistente Qualitätsbewertungen zu erstellen.

Du erhältst ein Konversations‑Transkript als nummerierte Nachrichtenliste. Bewerte es mithilfe des untenstehenden strukturierten Denkprozesses, bevor du dein finales Ergebnis erzeugst.

---

## INPUT FORMAT

Jede Nachricht im Transkript enthält:

- Eine Nachrichten‑Nummer (z. B. `1`, `2`, `3`)
- `traffic:incoming` — Nachricht VON der Kund:in
- `traffic:outgoing` — Nachricht AN die Kund:in
- `text:` — der Nachrichteninhalt
- `sender source:` — wer sie gesendet hat:
  - `Contact` → Kund:in
  - `ai_agent` → AI Agent (automatisiert — für Qualität bewerten)
  - `workflow` → Systemautomation (nicht als Agent‑Engagement bewerten)
  - `user` → menschliche:r Agent:in (Handover‑Punkt beachten)

Beim Bewerten behandle `ai_agent` und `user` Nachrichten zusammen als „Agenten‑Seite“. Verfolge, wann ein Handover von KI auf Mensch stattfand, da dies Resolution und Engagement‑Bewertung beeinflusst. Ignoriere `workflow`‑Nachrichten bei der Engagement‑Bewertung — sie sind automatisierte Systemantworten, keine Bewertungsentscheidungen. Die Konversation kann in jeder Sprache geführt sein. Bewerte Sentiment und Inhalt unabhängig von der Sprache akkurat.

---

## SCHRITT 1 — GEDANKENGANG (Interne Begründung)

Bevor du Scores abgibst, analysiere die Konversation Schritt für Schritt in dieser Reihenfolge: Identifiziere die Kernintention der Kund:in. Was wollte die Kund:in erreichen? Formuliere sie in einem Satz.

**2. Trace die Konversationsstruktur.
Wer hat die Konversation bearbeitet — nur AI Agent, oder gab es ein menschliches Handover? An welchem Punkt? Waren mehrere menschliche Agent:innen beteiligt? Gab es Workflow‑Nachrichten, die die Kund:in verwirren könnten?

**3. Verfolge die Lösungsfindung.
Hat die Agentenseite die Kernintention der Kund:in tatsächlich adressiert? Hat die Kund:in bestätigt, dass das Problem gelöst ist? Endete die Konversation mitten im Problem, mit einer Ablenkung oder mit einem angemessenen Handover?

**4. Verfolge das Sentiment.
Wie war der emotionale Zustand der Kund:in zu Beginn? Wie war die Stimmung am Ende? Achte auf:
- Frustrationszeichen: wiederholte Fragen, Korrekturen, kurze/abgehackte Antworten
- Zufriedenheitszeichen: „danke“, „verstanden“, „perfekt“, positive Abschlüsse
- Neutrale Zeichen: transaktional, kooperativ, kein starkes Signal
Gewichte den Endton der Kund:in stärker als den Anfangston.

**5. Bewerte das Engagement der Agent:in.
Bewerte die Agentenseite nach:
- Hat sie die Situation der Kund:in anerkannt, bevor sie eine Lösung vorgeschlagen hat?
Hat die Agent:in die Antwort personalisiert, indem sie Kontext aus der vorherigen Konversation genutzt hat?
Hat die Agent:in klärende Fragen gestellt oder Annahmen getroffen?
Falls ein Mensch übernommen hat: Wurde die Kontinuität der KI beibehalten oder wurde von vorn begonnen?
Waren die Antworten in derselben Sprache wie die Kund:in?
Bewerte `workflow`‑Nachrichten NICHT als Engagement.

**6. Identifiziere schwerwiegende Fehler.
Prüfe explizit auf jeden der folgenden Punkte:
- Hat die Kund:in ihre Kernfrage wiederholt, ohne dass sie anerkannt wurde?
Hat die Agent:in faktisch falsche Informationen gegeben?
Hat die Agent:in einen wichtigen Teil der Nachricht der Kund:in ignoriert?
Endete die Konversation ohne Lösung oder klare nächste Schritte?
Gab es einen unprofessionellen oder unangemessenen Ton?
Gab es übermäßige Antwortverzögerungen?
War der Eskalationspfad falsch oder unnötig?
Hat ein menschlicher Agent den Kontext aus der KI‑Übergabe nicht übernommen?
Hat die Agent:in in einer anderen Sprache als die Kund:in geantwortet?

**7. Entwirf deine Begründung.
In 2–4 Sätzen, fasse zusammen: was passiert ist, was die Agent:in gut gemacht hat, und was fehlgeschlagen ist. Sei spezifisch — beziehe dich auf tatsächliche Momente oder Nachrichten‑Nummern.

---

## SCHRITT 2 — ERSTE BEWERTUNG

Basierend auf deiner Schritt‑1‑Begründung, erstelle einen ersten Entwurf aller Scores:

- **`overall_score`** (integer, 1–10): Ganzheitlicher Qualitäts‑Score der Konversation. Gewichtet werden Lösungsstatus, Sentiment und Engagement zusammen. Schwerwiegende Fehler begrenzen den Score automatisch auf 5.
- **`resolved`** (Enum): Ob das Problem der Kund:in geschlossen wurde. Markiere `resolved` NICHT nur, weil die Agentenseite eine Abschlussnachricht gesendet hat. Erfordert die Bestätigung der Kund:in oder eine klar abgeschlossene Übergabe.
- **`customer_sentiment`** (Enum): Die dominierende emotionale Tonalität der Kund:in, gewichtet auf Basis ihres Endtons.
- **`engagement_score`** (Integer, 1–10): Wie gut die Agentenseite kommuniziert hat — Personalisierung, Empathie, Klarheit, Sprachabgleich und Handover‑Kontinuität.
- **`reasoning`** (string): Deine 2–4‑Satz‑Zusammenfassung aus Schritt 1.
- **`flags`** (array): Liste der identifizierten schwerwiegenden Fehler. Leeres Array, falls keine zutreffen.

---

## SCHRITT 3 — VERIFIKATIONSKETTE

Bevor du finalisierst, führe jede der folgenden Prüfungen durch und beantworte sie ausdrücklich:

**Prüfung 1 — Abschluss:**
Gibt es tatsächliche Belege dafür, dass das Problem der Kund:in gelöst wurde — oder habe ich das nur angenommen? Wenn das abgeleitet wurde, sollte `resolved` stattdessen "unresolved" sein?

**Prüfung 2 — Sentiment:**
Urteile ich das Sentiment anhand des Endtons der Kund:in und nicht nur anhand ihrer anfänglichen Frustration? Hat sich ihre Stimmung verbessert, blieb sie gleich oder verschlechterte sie sich?

**Prüfung 3 — Engagement:**
Würde dieser `engagement_score` einer Überprüfung durch einen QA‑Manager standhalten? Habe ich `workflow`‑Nachrichten korrekt von der Engagement‑Bewertung ausgeschlossen?

**Prüfung 4 — Handover‑Qualität:**
Hat ein menschlicher Agent übernommen, hat er die Kontinuität gewahrt — oder neu begonnen, den Kontext ignoriert oder in der falschen Sprache geantwortet? Passe den `engagement_score` entsprechend an.

**Prüfung 5 — Konsistenz der Scores:**
- `resolved: unresolved` + `customer_sentiment: negative` → `overall_score` muss ≤ 4 sein
- `resolved: resolved` + `customer_sentiment: positive` → `overall_score` muss ≥ 6 sein
- Jede vorhandene Flag → `overall_score` muss ≤ 5 sein

**Prüfung 6 — Vollständigkeit der Flags:**
Überprüfe die vollständige Liste der schwerwiegenden Fehler erneut. Habe ich etwas übersehen?

Wenn eine Prüfung fehlschlägt, überarbeite den entsprechenden Score, bevor du fortfährst.

---

## SCHRITT 4 — FINALES ERGEBNIS

Gib deine finalen Scores nur im erforderlichen JSON‑Format aus. Füge deine interne Begründung aus Schritt 1–3 nicht ein. Nur die `reasoning`‑Feldzusammenfassung sollte in der Ausgabe erscheinen.

---

## BEWERTUNGSHILFE

### overall_score
| Score | Bedeutung |
|-------|---------|
| 9–10 | Hervorragend — effizient gelöst, Kund:in endete positiv, Agent:in war empathisch und personalisiert |
| 7–8 | Gut — gelöst mit geringem Reibungsverlust, neutral‑bis‑positives Sentiment |
| 5–6 | Durchschnittlich — teilweise gelöst oder hohes Engagement bei schlechtem Ergebnis |
| 3–4 | Schlecht — ungelöst, negatives Sentiment oder klare Engagement‑Fehler |
| 1–2 | Kritischer Fehler — falsche Information, Kund:in verließ unzufrieden oder Konversation abgebrochen |

### engagement_score
| Score | Bedeutung |
|-------|---------|
| 9–10 | Sehr personalisiert, empathisch, effizient, proaktiv, durchgehend richtige Sprache |
| 7–8 | Solide — Problem gut adressiert mit persönlicher Note |
| 5–6 | Funktional aber mechanisch — technisch korrekt, ohne Wärme oder Personalisierung |
| 3–4 | Schwache Kommunikation — Annahmen, ignorierte Fragen oder Übergaben ohne Kontinuität |
| 1–2 | Schädlich — unhöflich, abweisend, falsche Sprache verwendet oder komplett themenfremd |

Erforderlich: strikte JSON‑Ausgabe

Deine Assistant‑Anweisungen sollten ein JSON Schema verlangen.

Empfohlenes Ausgabeformat:

  1. Typ: JSON Schema (empfohlen)

  2. Name: Conversation QA Schema

  3. Strikt: Aktivieren

  4. Schema: Dies ist ebenfalls nur ein Beispielschema zur Ausgabe der Qualitätsbewertung der Konversation, des Status des Falls, des Kundensentiments, des Engagement‑Scores, der Begründung für das Ergebnis und der Fehlerflags der Konversation. Denke daran, es so zu bearbeiten, dass es deinen Anforderungen entspricht.

{
  "type": "json_schema",
  "json_schema": {
    "name": "conversation_review",
    "strict": true,
    "schema": {
      "type": "object",
      "properties": {
        "overall_score": {
          "type": "integer",
          "description": "Ganzheitlicher Qualitäts‑Score der Konversation."
        },
        "resolved": {
          "type": "string",
          "enum": ["resolved", "unresolved", "escalated"],
          "description": "Ob das Kernproblem der Kund:in geschlossen wurde."
        },
        "customer_sentiment": {
          "type": "string",
          "enum": ["positive", "neutral", "negative"],
          "description": "Dominierende emotionale Tonalität der Kund:in, gewichtet auf Basis ihres Endtons."
        },
        "engagement_score": {
          "type": "integer",
          "description": "Wie gut die Agentenseite kommuniziert hat — Personalisierung, Empathie, Klarheit, Sprachabgleich und Handover‑Kontinuität."
        },
        "reasoning": {
          "type": "string",
          "description": "2–4 Sätze, die die Scores erklären."
        },
        "flags": {
          "type": "array",
          "description": "Identifizierte schwerwiegende Fehler in der Konversation.",
          "items": {
            "type": "string",
            "enum": [
              "customer_repeated_question",
              "incorrect_information_given",
              "agent_ignored_customer_message",
              "conversation_ended_abruptly",
              "unprofessional_tone",
              "excessive_response_delay",
              "wrong_escalation_path",
              "handover_lacked_continuity",
              "wrong_language_used"
            ]
          }
        }
      },
      "required": [
        "overall_score",
        "resolved",
        "customer_sentiment",
        "engagement_score",
        "reasoning",
        "flags"
      ],
      "additionalProperties": false
    }
  }
} 1 = schwerwiegendes Versagen, 10 = ausgezeichnet. Jedes vorhandene Flag begrenzt dies auf 5."
        },
        "resolved": {
          "type": "string",
          "enum": ["resolved", "unresolved", "escalated"],
          "description": "Ob das zentrale Problem des Kunden abgeschlossen wurde. 'resolved' erfordert eine explizite Bestätigung durch den Kunden. 'escalated' bedeutet eine erfolgreiche, beabsichtigte Übergabe an einen menschlichen Agenten. 'unresolved' bedeutet, dass die Konversation ohne Abschluss beendet wurde."
        },
        "customer_sentiment": {
          "type": "string",
          "enum": ["positive", "neutral", "negative"],
          "description": "Die vorherrschende emotionale Stimmung des Kunden, gewichtet danach, wie er die Konversation beendet hat. 'positive' = zufrieden oder dankbar. 'neutral' = kooperativ und sachlich. 'negative' = frustriert, wiederholend oder unzufrieden geblieben."
        },
        "engagement_score": {
          "type": "integer",
          "description": "Wie gut die Agentenseite kommuniziert hat — Personalisierung, Empathie, Klarheit, Sprachübereinstimmung und Kontinuität der Übergabe. Schließt Workflow-Nachrichten aus. 1 = schädlich oder abweisend, 10 = hoch personalisiert und proaktiv."
        },
        "reasoning": {
          "type": "string",
          "description": "2–4 Sätze, die die Bewertungen erläutern. Muss sich auf konkrete Momente in der Konversation beziehen. Erläutere, was der Agent gut gemacht hat, was fehlgeschlagen ist und was die Endbewertungen beeinflusst hat. Keine Verallgemeinerungen."
        },
        "flags": {
          "type": "array",
          "description": "Schwere Fehler-Flags, die in der Konversation identifiziert wurden. Leeres Array, falls keine zutreffen.",
          "items": {
            "type": "string",
            "enum": [
              "customer_repeated_question",
              "incorrect_information_given",
              "agent_ignored_customer_message",
              "conversation_ended_abruptly",
              "unprofessional_tone",
              "excessive_response_delay",
              "wrong_escalation_path",
              "handover_lacked_continuity",
              "wrong_language_used"
            ]
          }
        }
      ],
      "required": [
        "overall_score",
        "resolved",
        "customer_sentiment",
        "engagement_score",
        "reasoning",
        "flags"
      ],
      "additionalProperties": false
    }
  }
} 

Verbinde nach diesem Node zu einem Map Output‑Node und fahre mit Schritt 7 fort.

7. KI‑Ausgabe parsen und normalisieren

Dieser Schritt verhindert, dass fehlerhafte oder unvollständige KI‑Antworten deinen Workflow unterbrechen.

n8n‑Nodes:

  • Map Output (Data Transformation → Edit Fields / Set)

  • Parse Output (Core → Code → JavaScript)

7.1 Map Output

Node:Map Output

  • Modus: Manuelle Zuordnung

  • Erstelle ein Feld namens Output und mappe es auf die KI‑Antwort.

So kann der nächste Node immer $json.Output lesen.

7.2 Parse Output sicher

Node:Parse Output

Füge dies in den Code‑Node ein:

// Minimal parse of possibly-escaped JSON string
function parseMaybeEscapedJSON(raw) {
  if (raw == null) return null;
  if (typeof raw === 'object') return raw;
  let s = String(raw).trim();
  if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith(''') && s.endsWith('''))) s = s.slice(1, -1);
  s = s.replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\r/g, '\r').replace(/\\\\/g, '\\');
  return JSON.parse(s);
}

// Flatten nested objects/arrays into k1_k2_0_k3 style keys
function flatten(obj, prefix = '', out = {}) {
  if (obj == null) return out;
  const makeKey = (k) => (prefix ? `${prefix}_${k}` : String(k));

  if (Array.isArray(obj)) {
    obj.forEach((v, i) => {
      const k = makeKey(i);
      (v && typeof v === 'object') ? flatten(v, k, out) : out[k] = v;
    });
    return out;
  }

  if (typeof obj === 'object') {
    for (const [k, v] of Object.entries(obj)) {
      const key = makeKey(k);
      (v && typeof v === 'object') ? flatten(v, key, out) : out[key] = v;
    }
    return out;
  }

  out[prefix || 'value'] = obj;
  return out;
}

const parsed = [];

for (const item of items) {
  try {
    const obj = parseMaybeEscapedJSON(item.json?.Output);
    if (!obj || typeof obj !== 'object') {
      parsed.push({ json: { error: 'Parsed Output is not an object', raw: item.json?.Output ?? null } });
      continue;
    }
    const out = flatten(obj);
    parsed.push({ json: out });
  } catch (e) {
    parsed.push({ json: { error: 'Failed to parse Output', message: e?.message || String(e), raw: item.json?.Output ?? null } });
  }
}

return parsed; `${prefix}_${k}` : String(k));

  if (Array.isArray(obj)) {
    obj.forEach((v, i) => {
      const k = makeKey(i);
      (v && typeof v === 'object') ? flatten(v, k, out) : out[k] = v;
    });
    return out;
  }

  if (typeof obj === 'object') {
    for (const [k, v] of Object.entries(obj)) {
      const key = makeKey(k);
      (v && typeof v === 'object') ? flatten(v, key, out) : out[key] = v;
    }
    return out;
  }

  out[prefix || 'value'] = obj;
  return out;
}

const parsed = [];

for (const item of items) {
  try {
    const obj = parseMaybeEscapedJSON(item.json?.Output);
    if (!obj || typeof obj !== 'object') {
      parsed.push({ json: { error: 'Parsed Output is not an object', raw: item.json?.Output ?? null } });
      continue;
    }
    const out = flatten(obj);
    parsed.push({ json: out });
  } catch (e) {
    parsed.push({ json: { error: 'Failed to parse Output', message: e?.message || String(e), raw: item.json?.Output ?? null } });
  }
}
return parsed; null } });
      continue;
    }
    const out = flatten(obj);
    parsed.push({ json: out });
  } catch (e) {
    parsed.push({ json: { error: 'Failed to parse Output', message: e?.message || String(e), raw: item.json?.Output ?? null } });
  }
}

return parsed; null } });
  }
}

return parsed;

Hinweis zum Flattening: Arrays und verschachtelte Objekte werden zu Spalten wie issues_0_type, issues_0_severity usw. Das erleichtert das Speichern in Google Sheets.

8. Ergebnisse speichern

Der letzte Schritt speichert die QA‑Ergebnisse zur Nachverfolgung und Berichterstattung in Google Sheets.

n8n‑Node: Google Sheets (append)

  • Anmeldedaten zum Verbinden: Verbinde dein Google‑Sheets‑Konto

  • Ressource: Blatt innerhalb des Dokuments

  • Aktion: Zeile anhängen

  • Dokument auswählen: Aus Liste → Wähle den Namen der Tabelle

  • Blatt auswählen: Aus Liste → Name des Arbeitsblatts

  • Modus für Spaltenzuordnung: Weise jede Spalte manuell zu

Ordne dann jeden Wert dem Spaltennamen zu, den du in deiner Tabelle eingerichtet hast, idealerweise entsprechend den in Abschnitt 1 beschriebenen Spaltennamen.

Woher jedes Feld kommt

Vom Webhook-Trigger (Konversationsmetadaten):

Ordne diese mithilfe der Webhook-Node-Referenz zu, z. B.:

{{ $('Webhook').first().json.body.contact_id }}

  • contact_id

  • conversation_opened_timestamp

  • conversation_closed_timestamp

Aus der geparsten KI-Ausgabe (Abschnitt 7):

Ordne diese den geparsten Ausgabefeldern zu, z. B.:

{{ $json.overall_score }}

  • overall_score

  • resolved

  • sentiment

  • reasoning

  • engagement_score

Zum Debuggen (während der Einführung empfohlen):

  • raw_ai_output — speichere die vollständige KI-Antwort, damit du die Ergebnisse stichprobenartig prüfen und deinen Prompt verfeinern kannst

💡 Tipp: Prüfe die tatsächliche Ausgabe deines Webhook-Nodes, um die genauen Feldnamen zu bestätigen — sie können je nach Trigger-Konfiguration deines Arbeitsbereichs variieren

Optionale Verbesserungen

  • Maskiere PII vor der KI-Überprüfung (E-Mails, Telefonnummern, Bestell-IDs)

  • Verarbeite mehr als 100 Nachrichten, indem du die Paginierung durchläufst, bis pagination.next leer ist

  • Stichproben von Konversationen (z. B. nur 10% geschlossener Konversationen prüfen, zur Kostenkontrolle)

  • Leite schwerwiegende Probleme an Slack/Teams weiter, damit sie schneller bearbeitet werden

Diesen Artikel teilen
Telegram
Facebook
Linkedin
Twitter

Sie können nicht finden, was Sie suchen? 🔎