1. All Collections >
  2. Croissance poussée de la conversation >
  3. Rapports et analyses >
  4. Comment automatiser la QA des conversations avec n8n et l'IA

Comment automatiser la QA des conversations avec n8n et l'IA

Avatar
Shing-Yi Tan
14 min de lecture

Nous recommandons aux clients de relire les conversations traitées par l'agent IA après activation, mais le contrôle qualité manuel devient trop chronophage à grande échelle. Ce guide montre comment automatiser le contrôle qualité des conversations en utilisant n8n + un modèle d'IA, afin que chaque conversation clôturée puisse être revue automatiquement et enregistrée dans Google Sheets pour le reporting.

En résumé, ce workflow n8n se déclenche à chaque fois qu'une conversation est clôturée. Une fois déclenché, il appelle l'API List Messages pour récupérer les 50 derniers messages (avec option d'aller jusqu'à 100 via pagination), puis transmet la transcription à un modèle d'IA pour le traitement de contrôle qualité — par exemple l'analyse du sentiment, le score d'engagement et les vérifications de résolution. Les résultats sont suivis dans une feuille Google Sheets afin que vous puissiez retrouver facilement n'importe quelle conversation via l'ID du contact.

Ce que vous allez créer

Une automatisation qui :

  • Récupère les 50 derniers messages via l'API List Messages (optionnellement jusqu'à 100 via la pagination)

  • Récupère les 50 derniers messages via le List Messages API (éventuellement jusqu'à 100 avec pagination)

  • Nettoie et formate les messages pour créer une transcription

  • Envoie la transcription à un modèle d'IA pour une évaluation du contrôle qualité

  • Analyse en toute sécurité la sortie de l'IA (contrat JSON uniquement)

  • Ajoute les résultats dans Google Sheets (une ligne par conversation)

Exigences

Outils

  • Plateforme d'automatisation : n8n

  • Respond.io Developer API — endpoint List Messages

  • AI API — OpenAI, Anthropic, Gemini, ou tout fournisseur pris en charge par n8n

  • Reporting / stockage — Google Sheets (ou votre outil de stockage préféré)

Identifiants

  • Clé Respond.io Developer API

  • Clé API du fournisseur d'IA (OpenAI, Anthropic, Gemini, etc.)

  • Informations d'identification Google Sheets dans n8n (OAuth ou compte de service)

Avant de commencer

1) Créez une feuille Google Sheets pour les résultats QA

Créez un tableur et ajoutez une feuille (ex. QA_Results) avec ces en-têtes :

  • timestamp

  • contact_id

  • opened_at

  • channel

  • overall_score

  • resolved

  • customer_sentiment

  • reasoning

  • engagement_score

2) Confirmez les champs du payload de votre trigger

Dans la sortie du Conversation closed trigger, identifiez les noms exacts des champs pour :

  • ID de conversation

  • ID de contact

  • Horodatage d'ouverture de la conversation

  • Horodatage de clôture de la conversation (optionnel)

Les noms des champs varient selon l'implémentation. Dans les nœuds de code ci‑dessous, vous verrez des placeholders comme trigger.conversationId. Mettez à jour ces mappages pour qu'ils correspondent à la sortie de votre trigger

Guide étape par étape

1. Déclencheur : Conversation fermée

Lorsqu'une conversation est clôturée dans respond.io, nous souhaitons déclencher automatiquement le workflow n8n. Cela garantit que chaque conversation terminée passe par la QA — pas besoin de revue manuelle.

n8n node:Conversation closed trigger

  1. Dans n8n, ajoutez le nœud déclencheur respond.io.

  2. Choisissez Conversation Closed.

  3. Connectez votre clé API respond.io. Apprenez à configurer cela dans n8n avec notre guide d'intégration.

Ce déclencheur garantit que vous ne consultez les conversations qu'après leur clôture.

2. Récupérer les messages de la conversation

Dans cette étape, vous récupérerez les 50 derniers messages entrants et sortants avec l'API List Messages. C'est généralement suffisant pour une revue de contrôle qualité approfondie. Si vous avez besoin de plus de contexte, vous pouvez éventuellement récupérer 50 messages supplémentaires (jusqu'à 100 au total) via la pagination.

nœuds n8n :

  • get 1st 50 messages (HTTP Request)

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

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

2.1 Récupérer les 50 premiers messages

Nœud :get 1st 50 messages

  1. Sélectionnez Core > HTTP Request

  2. Méthode : GET

  3. URL : votre endpoint List Messages

Exemple d'URL (remplacez par votre URL de base API réelle + format d'endpoint) :

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

identifier doit être remplacé par l'un des formats suivants : id:<contactID>, phone:+<countryCodeAndPhone>, ou email:<contactEmail>

  1. Activez Send Query Parameters > Using Fields Below, puis ajoutez :

    • Nom : limit

    • Valeur : 50

Si votre API le permet, définissez le tri du plus ancien au plus récent. Exemple : sort=asc.

  1. Activez Send Headers > Using Fields Below, puis ajoutez :

    • Nom : Accept

      • Valeur : application/json

    • Nom : Authorization

      • Valeur : Bearer <your_respond_api_key>

Si la réponse List Messages contient pagination.next, vous pouvez l'utiliser directement pour récupérer la deuxième page.

2.2 Vérifier et récupérer plus de 50 messages (optionnel)

Si 50 messages ne suffisent pas pour vos besoins de contrôle qualité, vous pouvez récupérer une deuxième page de 50 messages. Ajoutez un nœud IF pour vérifier s'il y a plus de messages disponibles.

Nœud :Is there a second page? (IF)

  • Valeur gauche (Expression) : {{ $json.pagination.next }}

  • Opérateur : is not empty

Si vrai, cela signifie qu'il y a plus de messages à récupérer. Si faux, le workflow continue sans récupérer davantage — cela évite l'échec du workflow lorsqu'il n'y a pas de deuxième page. Le nœud Merge en aval attendra que les deux chemins se terminent, donc le workflow s'exécute correctement dans les deux cas.

2.3 Récupérer les 50 messages suivants (optionnel)

Si le nœud IF est validé (une deuxième page existe), récupérez le lot suivant.

Nœud :get 2nd 50 messages

  1. Ajoutez un autre nœud HTTP Request.

  2. Paramètres :

    • Méthode : GET

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

  3. Ajoutez les mêmes en-têtes :

    • Accept : application/json

    • Authorization : Bearer <your_respond_api_key>

3. Fusionner les pages de messages (optionnel)

Cette étape n'est nécessaire que si vous récupérez plus de 50 messages. Si vous choisissez de ne pas récupérer une deuxième page, vous pouvez ignorer ce nœud et connecter get 1st 50 messages directement à l'étape suivante (Clean + keep messages since last open).

Le nœud Merge combine les 50 premiers messages avec les 50 suivants en une liste unique. Sans cela, le workflow ne peut pas traiter ensemble deux réponses API séparées.

Nœud n8n :Merge

  1. Ajoutez Flow > Merge.

  2. Définissez le Mode sur Append.

  3. Définissez le Nombre d'Entrées : 2

  4. Connectez :

    • Entrée 1 : get 1st 50 messages

    • Entrée 2 : get 2nd 50 messages

4. Nettoyer et filtrer les messages

La réponse de l'API est encore brute et contient beaucoup d'informations supplémentaires dont l'IA n'a pas besoin. Cette étape la nettoie — en supprimant les anciens messages, en normalisant les libellés des expéditeurs et en structurant le tout dans une liste simple prête pour le traitement par l'IA. Il vous suffit d'ajouter le nœud Code et de copier‑coller le JavaScript ci‑dessous.

n8n node:Clean + keep messages since last convo open

Pour cela, ajoutez un nœud → Sélectionnez Core → Code → Code en JavaScript.

Ce code va :

  1. Combiner les messages des deux pages

  2. Filtrer les messages envoyés avant l'ouverture de la conversation

  3. Trier les messages du plus ancien au plus récent

  4. Normaliser les expéditeurs en libellés cohérents : Contact, Agent IA, Agent humain, Workflow

  5. Ajouter des numéros d'index aux messages

  6. Retourner une liste propre et structurée pour l'étape de transcription

Remarques : Cet exemple filtre en utilisant messageId comparé à l'horodatage d'ouverture converti en microsecondes. Si votre API fournit plutôt des horodatages createdAt, filtrez par createdAt plutôt que par messageId. Assurez‑vous que votre conversation_open_timestamp inclut un fuseau horaire. S'il ne l'inclut pas, définissez-le sur le fuseau horaire de votre espace de travail avant l'analyse.

Collez ceci dans le nœud Code :

// 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,
      },
    },
  },
];

Assurez‑vous de définir Mode = Run Once for All Items et Language = JavaScript.

Le script ci‑dessus est une référence. Votre espace de travail peut renvoyer des champs ou des structures différents. Pour construire la logique d'analyse JSON adaptée à votre configuration, copiez la sortie du nœud précédent, collez‑la dans un outil d'IA (par ex. ChatGPT ou Claude) avec le script de référence ci‑dessus, et décrivez le format de sortie dont vous avez besoin. L'IA pourra alors adapter le script pour correspondre à vos données réelles.

5. Générer la transcription

Cette étape convertit la liste de messages nettoyée en une transcription au format Markdown pour le traitement par l'IA. Les transcriptions structurées améliorent la compréhension de l'IA et réduisent les hallucinations.

n8n node:Build markdown transcript (Core → Code → JavaScript)

Collez ceci dans le nœud Code :

// 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;

Assurez‑vous de définir Mode = Run Once for All Items et Language = JavaScript.

6. Revue des conversations basée sur l'IA

Ce nœud envoie la transcription à un modèle d'IA (par ex. OpenAI 5.4) et renvoie une réponse JSON structurée.

Comment configurer

  1. Sélectionnez les identifiants de votre fournisseur d'IA. Dans cet exemple, nous utiliserons OpenAI.

  2. Ressource : Message a Model

  3. Opération : Message an Assistant

  4. Messages :

    1. Type : Texte

    2. Rôle : Utilisateur

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

  5. Simplifier la sortie : activé

  6. Ajouter une option :

    1. Instructions — Ceci n'est qu'un modèle ; vous pouvez le fournir à une autre IA (par ex. ChatGPT, Claude, etc.) pour l'adapter à vos besoins :

Vous êtes analyste qualité des conversations pour Respond.io, une plateforme de messagerie client. Votre mission est d'examiner des conversations de support ou de vente et de produire des scores de qualité précis et cohérents.

Vous recevrez une transcription de conversation sous forme d'une liste numérotée de messages. Évaluez‑la en utilisant le processus de raisonnement structuré ci‑dessous avant de produire votre résultat final.

---

## FORMAT D'ENTRÉE

Chaque message de la transcription contient :

- Un numéro de message (par ex. `1`, `2`, `3`)
- `traffic:incoming` — message DU client
- `traffic:outgoing` — message AU client
- `text:` — le contenu du message
- `sender source:` — qui l'a envoyé :
  - `Contact` → client
  - `ai_agent` → agent IA (automatisé — évaluer pour la qualité)
  - `workflow` → automatisation système (ne PAS noter comme engagement agent)
  - `user` → agent humain (notez le point de transfert)

Lors de l'évaluation, traitez les messages `ai_agent` et `user` ensemble comme « le côté agent ». Suivez le moment d'un transfert de l'IA vers un humain, car cela affecte la résolution et le scoring d'engagement. Ignorez les messages `workflow` lors du calcul de l'engagement — ce sont des réponses système automatisées, pas des éléments à prendre en compte pour l'engagement. La conversation peut être dans n'importe quelle langue. Évaluez le sentiment et le contenu avec précision quelle que soit la langue.

---

## ÉTAPE 1 — CHAÎNE DE PENSÉE (raisonnement interne)

Avant de produire des notes, raisonnez sur la conversation étape par étape dans cet ordre exact : Identifiez l'intention principale du client. Que souhaitait obtenir le client ? Formulez‑la en une phrase.

**2.** Retracez la structure de la conversation. Qui a géré la conversation — seulement l'AI Agent, ou y a‑t‑il eu un transfert vers un humain ? À quel moment ? Plusieurs agents humains ont‑ils été impliqués ? Des messages de workflow ont‑ils pu perturber le client ?

**3.** Analysez la résolution. L'agent a‑t‑il réellement répondu à l'intention principale du client ? Le client a‑t‑il confirmé que c'était résolu ? La conversation s'est‑elle terminée en plein problème, par une esquive, ou par un transfert approprié ?

**4.** Analysez le sentiment. Comment le client a‑t‑il commencé émotionnellement ? Comment a‑t‑il terminé ? Cherchez :
- Signes de frustration : questions répétées, corrections, réponses courtes/abrupte
- Signes de satisfaction : «merci», «reçu», «parfait», formules positives de clôture
- Signes neutres : transactionnel, coopératif, pas de signal fort
Pesez davantage le ton final du client que son ton initial.

**5.** Évaluez l'engagement de l'agent. Notez le côté agent sur :
- Ont‑ils reconnu la situation du client avant de proposer une solution ?
- Ont‑ils personnalisé la réponse en utilisant le contexte antérieur de la conversation ?
- Ont‑ils posé des questions de clarification pertinentes ou fait des suppositions ?
- Si un humain a pris le relais, a‑t‑il maintenu la continuité depuis l'IA ou a‑t‑il tout repris à zéro ?
- Les réponses étaient‑elles dans la même langue que le client ?
- NE NOTEZ PAS les messages `workflow` comme engagement.

**6.** Identifiez les échecs critiques. Vérifiez explicitement chacun des points suivants :
- Le client a‑t‑il répété sa question principale sans qu'elle soit reconnue ?
- L'agent a‑t‑il fourni des informations factuellement incorrectes ?
- L'agent a‑t‑il ignoré une partie clé du message du client ?
- La conversation s'est‑elle terminée sans résolution ni prochaines étapes claires ?
- Y a‑t‑il eu un ton non professionnel ou inapproprié ?
- Y a‑t‑il eu des délais de réponse excessifs ?
- Le chemin d'escalade était‑il incorrect ou inutile ?
- Un agent humain a‑t‑il échoué à reprendre le contexte du transfert IA ?
- L'agent a‑t‑il répondu dans une langue différente de celle du client ?

**7.** Rédigez votre raisonnement. En 2–4 phrases, résumez : ce qui s'est passé, ce que l'agent a bien fait, et ce qui a échoué. Soyez précis — référencez des moments ou numéros de messages concrets.

---

## ÉTAPE 2 — NOTATION INITIALE

Sur la base de votre raisonnement de l'Étape 1, produisez un brouillon initial de toutes les notes : Pesez la résolution, le sentiment et l'engagement ensemble. Les échecs critiques plafonnent automatiquement la note à 5.
- **`resolved`** (énum) : Si le problème du client a été clos. NE marquez PAS `resolved` simplement parce que l'agent a envoyé un message final. Nécessite la confirmation du client ou un transfert clairement complété.
- **`customer_sentiment`** (énum) : Le ton émotionnel dominant du client, pondéré vers la fin de la conversation.
- **`engagement_score`** (entier, 1–10) : Qualité de la communication côté agent — personnalisation, empathie, clarté, adéquation de la langue et continuité lors des transferts.
- **`reasoning`** (chaîne) : Votre résumé en 2–4 phrases issu de l'Étape 1.
- **`flags`** (tableau) : Liste des échecs critiques identifiés. Tableau vide si aucun.

---

## ÉTAPE 3 — CHAÎNE DE VÉRIFICATION

Avant de finaliser, exécutez chacun des contrôles suivants et répondez‑y explicitement : Si c'est inféré, `resolved` devrait‑il être `"unresolved"` à la place ?

**Contrôle 2 — Sentiment :**
Évaluez‑vous le sentiment sur la base du ton final du client, et non uniquement de sa frustration initiale ? Leur humeur s'est‑elle améliorée, est‑elle restée la même ou s'est‑elle détériorée ?

**Contrôle 3 — Engagement :**
Est‑ce que ce `engagement_score` tiendrait si un responsable QA examinait la conversation ? Ai‑je correctement exclu les messages `workflow` du calcul de l'engagement ?

**Contrôle 4 — Qualité du transfert :**
Si un agent humain a pris la relève, a‑t‑il maintenu la continuité — ou a‑t‑il tout repris, ignoré le contexte, ou répondu dans la mauvaise langue ? Ajustez le `engagement_score` en conséquence.

**Contrôle 5 — Cohérence des notes :**
- `resolved: unresolved` + `customer_sentiment: negative` → `overall_score` doit être ≤ 4
- `resolved: resolved` + `customer_sentiment: positive` → `overall_score` doit être ≥ 6
- Toute présence d'un flag → `overall_score` doit être ≤ 5

**Contrôle 6 — Exhaustivité des flags :**
Revois la liste complète des échecs critiques. Ai‑je oublié quelque chose ?

Si un contrôle échoue, révisez la note concernée avant de continuer.

---

## ÉTAPE 4 — RÉSULTAT FINAL

Sortez vos notes finales au format JSON requis uniquement. N'incluez pas votre raisonnement interne des étapes 1–3. Seul le champ `reasoning` (résumé) doit apparaître dans la sortie.

---

## RÉFÉRENCE DE NOTATION

### overall_score
| Note | Signification |
|-------|---------|
| 9–10 | Excellent — résolu efficacement, client satisfait, agent empathique et personnalisé |
| 7–8 | Bon — résolu avec légères frictions, sentiment neutre à positif |
| 5–6 | Moyen — partiellement résolu, ou forte implication mais résultat insatisfaisant |
| 3–4 | Faible — non résolu, sentiment négatif, ou échecs d'engagement visibles |
| 1–2 | Échec critique — information erronée fournie, client insatisfait ou conversation abandonnée |

### engagement_score
| Note | Signification |
|-------|---------|
| 9–10 | Très personnalisé, empathique, efficace, proactif, bonne adéquation linguistique |
| 7–8 | Solide — problème géré correctement avec une touche personnelle |
| 5–6 | Fonctionnel mais mécanique — techniquement correct, sans chaleur ni personnalisation |
| 3–4 | Communication médiocre — hypothèses, questions ignorées, ou transfert sans continuité |
| 1–2 | Nuisible — impoli, condescendant, mauvaise langue utilisée, ou hors sujet |

Requis : sortie JSON stricte

Les instructions de votre assistant doivent exiger un JSON Schema.

Format de sortie recommandé :

  1. Type : JSON Schema (recommandé)

  2. Nom : Conversation QA Schema

  3. Strict : Activé

  4. Schéma : Il s'agit d'un exemple de schéma pour exporter la note de qualité de la conversation, le statut du problème, le sentiment du client, le score d'engagement, la justification du résultat et les indicateurs d'échec de la conversation. N'oubliez pas de l'éditer pour l'adapter à vos besoins.

{
  "type": "json_schema",
  "json_schema": {
    "name": "conversation_review",
    "strict": true,
    "schema": {
      "type": "object",
      "properties": {
        "overall_score": {
          "type": "integer",
          "description": "Holistic quality score of the conversation. 1 = échec critique, 10 = excellent. Toute présence d'un flag le plafonne à 5."
        },
        "resolved": {
          "type": "string",
          "enum": ["resolved", "unresolved", "escalated"],
          "description": "Whether the customer's core issue was closed. 'resolved' nécessite la confirmation explicite du client. 'escalated' signifie un transfert intentionnel réussi vers un agent humain. 'unresolved' signifie que la conversation s'est terminée sans résolution."
        },
        "customer_sentiment": {
          "type": "string",
          "enum": ["positive", "neutral", "negative"],
          "description": "The customer's dominant emotional tone, weighted toward how they ended the conversation. 'positive' = satisfait ou reconnaissant. 'neutral' = coopératif et transactionnel. 'negative' = frustré, répétitif, ou insatisfait."
        },
        "engagement_score": {
          "type": "integer",
          "description": "How well the agent side communicated — personalization, empathy, clarity, language match, and handover continuity. Exclut les messages `workflow`. 1 = nuisible ou dismissif, 10 = très personnalisé et proactif."
        },
        "reasoning": {
          "type": "string",
          "description": "2–4 sentences explaining the scores. Doit référencer des moments précis de la conversation. Couvrir ce que l'agent a bien fait, ce qui a échoué, et ce qui a motivé les notes finales. Aucune généralisation."
        },
        "flags": {
          "type": "array",
          "description": "Hard failure flags identified in the conversation. Tableau vide si aucun ne s'applique.",
          "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
    }
  }
}

Après ce nœud, connectez‑le à un nœud Map Output et continuez à l'étape 7.

7. Analyser et normaliser la sortie de l'IA

Cette étape évite que des réponses IA mal formées ou partielles perturbent votre flux de travail.

nœuds n8n :

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

  • Parse Output (Core → Code → JavaScript)

7.1 Map Output

Nœud :Map Output

  • Mode : cartographie manuelle

  • Créez un champ appelé Output et mappez‑le à la réponse de l'IA.

Ainsi, le nœud suivant sera cohérent (il pourra toujours lire $json.Output).

7.2 Analyser la sortie en toute sécurité

Nœud :Parse Output

Collez ceci dans le nœud Code :

// 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;

Remarque sur l'aplatissement : les tableaux et objets imbriqués deviendront des colonnes telles que issues_0_type, issues_0_severity, etc. Cela facilite le stockage dans Google Sheets.

8. Enregistrer les résultats

L'étape finale enregistre les résultats QA dans Google Sheets pour le suivi et le reporting.

n8n node: Google Sheets (append)

  • Identifiants de connexion : Connectez votre compte Google Sheets

  • Ressource : Sheet Within Document

  • Opération : Append Row

  • Sélectionner le document : Dans la liste → Choisissez le nom du tableur

  • Sélectionner la feuille : Dans la liste → Nom de la feuille de calcul

  • Mode de mappage des colonnes : Mappez chaque colonne manuellement

Ensuite, associez chaque valeur au nom de colonne que vous avez défini dans votre feuille, idéalement en suivant les noms de colonnes décrits à la Section 1.

D'où provient chaque champ

Depuis le déclencheur Webhook (métadonnées de la conversation) :

Mappez‑les en utilisant la référence du nœud Webhook, ex. :

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

  • contact_id

  • conversation_opened_timestamp

  • conversation_closed_timestamp

À partir de la sortie IA analysée (Section 7):

Associez-les aux champs de sortie analysés, p. ex. :

{{ $json.overall_score }}

  • overall_score

  • resolved

  • sentiment

  • reasoning

  • engagement_score

Pour le débogage (recommandé lors du déploiement):

  • raw_ai_output — stockez la réponse IA complète afin que vous puissiez vérifier ponctuellement les résultats et affiner votre prompt

💡 Astuce : Vérifiez la sortie réelle du nœud Webhook pour confirmer les noms exacts des champs — ils peuvent varier selon la configuration du déclencheur de votre espace de travail.

Améliorations optionnelles

  • Masquez les données personnelles (PII) avant l'examen par l'IA (adresses e-mail, numéros de téléphone, numéros de commande)

  • Traitez plus de 100 messages en parcourant la pagination jusqu'à ce que pagination.next soit vide

  • Échantillonnez les conversations (p. ex., ne révisez que 10 % des conversations fermées pour contrôler les coûts)

  • Redirigez les problèmes graves vers Slack/Teams pour un suivi plus rapide

Partager cet article
Telegram
Facebook
Linkedin
Twitter

Vous ne trouvez pas ce que vous recherchez? 🔎