1. All Collections >
  2. Crescimento Orientado por Conversa >
  3. Relatórios e Análises >
  4. Como automatizar a QA de conversas com n8n & IA

Como automatizar a QA de conversas com n8n & IA

Avatar
Shing-Yi Tan
14 minutos de leitura

Recomendamos que os clientes revisem as conversas tratadas pela IA após habilitar o Agente de IA, pois a revisão manual de QA torna-se impraticável em larga escala. Este guia mostra como automatizar a QA de conversas usando n8n + um modelo de IA, para que toda conversa encerrada possa ser revisada automaticamente e registrada no Google Sheets para relatórios.

Em resumo, este fluxo n8n é acionado toda vez que uma conversa é encerrada. Ao ser acionado, ele chama a List Messages API para buscar as últimas 50 mensagens (com opção de buscar até 100 via paginação) e, em seguida, envia a transcrição a um modelo de IA para avaliação de QA — como análise de sentimento, pontuação de engajamento e verificações de resolução. Os resultados são registrados no Google Sheets para que você possa localizar facilmente qualquer conversa pelo ID do contato.

O que você vai criar

Uma automação que:

  • Aciona quando uma conversa é encerrada

  • Recupera as últimas 50 mensagens usando a List Messages API (opcionalmente até 100 com paginação)

  • Limpa e formata as mensagens para gerar uma transcrição

  • Envia a transcrição a um modelo de IA para avaliação de QA

  • Analisa com segurança a saída da IA (contrato somente em JSON)

  • Adiciona os resultados no Google Sheets (uma linha por conversa)

Requisitos

Ferramentas

  • Plataforma de automação: n8n

  • Respond.io Developer API — endpoint List Messages

  • AI API — OpenAI, Anthropic, Gemini ou qualquer provedor suportado pelo n8n

  • Relatórios / armazenamento — Google Sheets (ou sua ferramenta de armazenamento preferida)

Credenciais

  • Chave da Respond.io Developer API

  • Chave da API do provedor de IA (OpenAI, Anthropic, Gemini, etc.)

  • Credencial do Google Sheets no n8n (OAuth ou conta de serviço)

Antes de começar

1) Crie uma planilha do Google para os resultados de QA

Crie uma planilha e adicione uma aba (ex.: QA_Results) com estes cabeçalhos:

  • timestamp

  • contact_id

  • opened_at

  • channel

  • overall_score

  • resolved

  • customer_sentiment

  • reasoning

  • engagement_score

2) Confirme os campos do payload do gatilho

Na saída do Conversation closed trigger, identifique os nomes exatos dos campos para:

  • ID da conversa

  • ID de contato

  • timestamp de abertura da conversa

  • timestamp de fechamento da conversa (opcional)

Os nomes dos campos variam conforme a implementação. Nos nós de código abaixo, você verá espaços reservados como trigger.conversationId. Atualize esses mapeamentos para corresponder à saída do seu gatilho.

Guia passo a passo

1. Gatilho: Conversa encerrada

Quando uma conversa é encerrada no respond.io, queremos acionar automaticamente o fluxo n8n. Isso garante que toda conversa concluída passe pela QA — sem necessidade de revisão manual.

n8n node:Conversation closed trigger

  1. No n8n, adicione o nó de gatilho respond.io.

  2. Escolha Conversation Closed.

  3. Conecte as credenciais da sua Respond.io Developer API. Saiba como configurar isso no n8n com o nosso guia de integração.

Este gatilho garante que você revise apenas as conversas após sua conclusão.

2. Buscar mensagens da conversa

Nesta etapa, você buscará as últimas 50 mensagens de entrada e saída usando a API List Messages. Isso geralmente é suficiente para uma revisão de QA completa. Se precisar de mais contexto, você pode opcionalmente buscar mais 50 mensagens (até 100 no total) usando paginação.

n8n nodes:

  • get 1st 50 messages (HTTP Request)

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

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

2.1 Buscar as primeiras 50 mensagens

Nó:get 1st 50 messages

  1. Selecione Core > HTTP Request

  2. Método: GET

  3. URL: seu endpoint List Messages

Exemplo de URL (substitua pelo seu URL base da API + formato de endpoint):

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

Onde identifier precisa ser substituído por um dos formatos a seguir: id:<contactID>, phone:+<countryCodeAndPhone> ou email:<contactEmail>

  1. Ative Send Query Parameters > Using Fields Below, depois adicione:

    • Nome: limit

    • Valor: 50

Se sua API suportar, defina a ordenação de mais antigas → mais recentes. Exemplo: sort=asc.

  1. Ative Send Headers > Using Fields Below, depois adicione:

    • Nome: Accept

      • Valor: application/json

    • Nome: Authorization

      • Valor: Bearer <your_respond_api_key>

Se a resposta do List Messages incluir pagination.next, você pode usá-lo diretamente para buscar a segunda página.

2.2 Verificar e obter mais de 50 mensagens (Opcional)

Se 50 mensagens não forem suficientes para suas necessidades de QA, você pode buscar uma segunda página de 50 mensagens. Adicione um nó IF para verificar se há mais mensagens disponíveis.

Nó:Is there a second page? (IF)

  • Valor esquerdo (Expressão): {{ $json.pagination.next }}

  • Operador: não está vazio

Se verdadeiro, significa que há mais mensagens a buscar. Se falso, o fluxo continua sem buscar mais — isso evita que o fluxo falhe quando não houver segunda página. O nó Merge na etapa seguinte aguardará que ambos os caminhos sejam concluídos, então o fluxo continuará funcionando normalmente de qualquer forma.

2.3 Buscar as próximas 50 mensagens (Opcional)

Se o nó IF passar (existir uma segunda página), busque o próximo lote.

Nó:get 2nd 50 messages

  1. Adicione outro nó HTTP Request.

  2. Defina:

    • Método: GET

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

  3. Adicione os mesmos headers:

    • Accept: application/json

    • Authorization: Bearer <your_respond_api_key>

3. Mesclar páginas de mensagens (Opcional)

Esta etapa é necessária apenas se você estiver buscando mais de 50 mensagens. Se você optou por não buscar a segunda página, pode pular este nó e conectar get 1st 50 messages diretamente à próxima etapa (Clean + keep messages since last open).

O nó Merge combina as primeiras 50 mensagens com as segundas 50 em uma única lista. Sem ele, o fluxo não consegue processar duas respostas de API separadas em conjunto.

n8n node:Merge

  1. Adicione Flow > Merge.

  2. Defina Mode para Append.

  3. Defina Number of Inputs: 2

  4. Conecte:

    • Input 1: get 1st 50 messages

    • Input 2: get 2nd 50 messages

4. Limpar e filtrar mensagens

A resposta da API ainda está bruta e contém muitas informações extras que a IA não precisa. Esta etapa limpa isso — removendo mensagens antigas, normalizando rótulos de remetente e estruturando tudo em uma lista simples pronta para o processamento pela IA. Tudo o que você precisa fazer é adicionar o nó Code e copiar/colar o JavaScript abaixo.

n8n node:Clean + keep messages since last convo open

Para isso, clique em Add node → selecione Core → Code → Code em JavaScript.

Este código irá:

  1. Combine as mensagens de ambas as páginas

  2. Filtre mensagens enviadas antes da abertura da conversa

  3. Ordene as mensagens de mais antigas → mais recentes

  4. Normalize os remetentes para rótulos consistentes: Contact, AI Agent, Human Agent, Workflow

  5. Adicione números de índice das mensagens

  6. Retorne uma lista limpa e estruturada para a etapa de transcrição

Observações: Este exemplo filtra usando messageId comparado ao timestamp de abertura convertido para microssegundos. Se sua API fornecer timestamps createdAt, filtre por createdAt em vez de messageId. Garanta que seu conversation_open_timestamp inclua um fuso horário. Se não, defina-o para o fuso horário do seu espaço de trabalho antes de analisar.

Cole isto no nó 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; 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,
      },
    },
  },
]; 0) - Number(b?.messageId ??  0));\n\n// Índice + estrutura\nconst indexed = mergedItems.map((m, i) => {\n  const traffic = m?.traffic ?? null;\n  const sender = m?.sender || {};\n\n  // O tráfego de entrada vem sempre de Contact\n  if (traffic === "incoming") {\n    return {\n      Index: i + 1,\n      traffic,\n      message: m?.message ?? null,\n      Sender: { source: "Contact" },\n    };\n  }\n\n  return {\n    Index: i + 1,\n    traffic,\n    message: m?.message ?? null,\n    Sender: {\n      source: normalizeSender(sender?.source),\n      userId: sender?.userId ?? null,\n      teamId: sender?.teamId ?? null,\n    },\n  };\n});\n\nreturn [\n  {\n    json: {\n      message_count: indexed.length,\n      items: indexed,\n      meta: {\n        conversation_open_timestamp: openedTs,\n        conversation_open_microseconds: openMicro,\n        pages_received: $input.all().length,\n      },\n    },\n  },\n];

Certifique-se de definir Mode = Run Once for All Items e Language = JavaScript.

O script acima é uma referência. Seu espaço de trabalho pode retornar campos ou estruturas diferentes. Para construir a lógica correta de análise JSON para sua configuração, copie a saída do nó anterior, cole-a em uma ferramenta de IA (ex.: ChatGPT ou Claude) junto com o script de referência acima e descreva o formato de saída que você precisa. A IA pode então adaptar o script para corresponder aos seus dados reais.

5. Gerar transcrição

Esta etapa converte a lista de mensagens limpa em uma transcrição em estilo Markdown para processamento pela IA. Transcrições estruturadas melhoram a compreensão da IA e reduzem alucinações.

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

Cole isto no nó 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; `${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;

Certifique-se de definir Mode = Run Once for All Items e Language = JavaScript.

6. Revisões de conversas baseadas em IA

Este nó envia a transcrição para um modelo de IA (ex.: OpenAI 5.4) e retorna uma resposta JSON estruturada.

Como configurar

  1. Selecione as credenciais do seu provedor de IA. Neste exemplo, usaremos a OpenAI.

  2. Recurso: Message a Model

  3. Operação: Message an Assistant

  4. Mensagens:

    1. Tipo: Texto

    2. Função: Usuário

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

  5. Simplificar saída: Ativar

  6. Adicionar opção:

    1. Instruções - Este é apenas um modelo de exemplo, mas você pode inserir isto em outra IA (p.ex. ChatGPT, Claude, etc.) para editar conforme suas necessidades:

Você é um analista de qualidade de conversas para a respond.io, uma plataforma de mensagens para clientes. Seu trabalho é revisar conversas de suporte ou vendas e produzir pontuações de qualidade precisas e consistentes.

Você receberá uma transcrição da conversa como uma lista numerada de mensagens. Avalie-a usando o processo de raciocínio estruturado abaixo antes de produzir sua saída final.

---

## FORMATO DE ENTRADA

Cada mensagem na transcrição contém:

- Um número de mensagem (ex.: `1`, `2`, `3`)
- `traffic:incoming` — mensagem DO cliente
- `traffic:outgoing` — mensagem PARA o cliente
- `text:` — o conteúdo da mensagem
- `sender source:` — quem a enviou:
  - `Contact` → cliente
  - `ai_agent` → AI Agent (automatizado — avalie para qualidade)
  - `workflow` → automação do sistema (NÃO pontuar como engajamento do agente)
  - `user` → agente humano (observe o ponto de handover)

Ao avaliar, trate as mensagens `ai_agent` e `user` juntas como "o lado do agente". Acompanhe quando ocorreu uma transferência de IA para humano, pois isso afeta a resolução e a pontuação de engajamento. Ignore mensagens `workflow` ao pontuar engajamento — elas são respostas automatizadas do sistema, não decisões de avaliação. A conversa pode estar em qualquer idioma. Avalie sentimento e conteúdo com precisão independentemente do idioma.

---

## PASSO 1 — CADEIA DE PENSAMENTO (Raciocínio interno)

Antes de produzir quaisquer pontuações, analise a conversa passo a passo nesta ordem exata: Identifique a intenção principal do cliente.
O que o cliente queria? Resuma em uma frase.

**2. Trace a estrutura da conversa. Quem conduziu a conversa — apenas o AI Agent, ou houve transferência para um agente humano? Em que ponto ocorreu a transferência? Vários agentes humanos estiveram envolvidos? Houve mensagens de workflow que poderiam ter confundido o cliente?

**3. Analise a resolução.
O agente realmente atendeu à intenção principal do cliente? O cliente confirmou que o problema foi resolvido? A conversa terminou no meio do problema, foi desviada ou houve um repasse apropriado?

**4. Analise o sentimento.
Qual era o estado emocional do cliente no início? Como terminou? Procure por:
- Indicadores de frustração: perguntas repetidas, correções, respostas curtas/abruptas
- Indicadores de satisfação: "obrigado", "entendi", "perfeito", despedidas positivas
- Indicadores neutros: transacional, cooperativo, sem sinal forte
Pese o tom de encerramento do cliente mais fortemente que o de abertura.

**5. Avalie o engajamento do agente.
Pontue o lado do agente em:
- Eles reconheceram a situação do cliente antes de pular para a solução?
- Personalizaram a resposta usando o contexto anterior da conversa?
- Fizeram perguntas de esclarecimento inteligentes ou fizeram suposições?
- Se um humano assumiu, manteve a continuidade da IA — ou recomeçou do zero?
- As respostas estavam no mesmo idioma do cliente?
- NÃO pontue mensagens `workflow` como engajamento.

**6. Identifique falhas graves.
Verifique explicitamente cada um dos seguintes:
- O cliente repetiu a pergunta principal sem que ela fosse reconhecida?
- O agente forneceu informações factualmente incorretas?
- O agente ignorou uma parte importante da mensagem do cliente?
- A conversa terminou sem resolução ou sem próximos passos claros?
- Houve tom não profissional ou inapropriado?
- Houve atrasos excessivos nas respostas?
- O caminho de escalonamento foi errado ou desnecessário?
- Um agente humano deixou de captar o contexto do repasse da IA?
- O agente respondeu em um idioma diferente do cliente?

**7. Elabore seu raciocínio.
Em 2–4 frases, resuma: o que aconteceu, o que o agente fez bem, e o que falhou. Seja específico — referencie momentos reais ou números de mensagem.

---

## PASSO 2 — PONTUAÇÃO INICIAL

Com base no seu raciocínio do Passo 1, produza um rascunho inicial de todas as pontuações:

- **`overall_score`** (inteiro, 1–10): Qualidade holística da conversa. Pesa resolução, sentimento e engajamento em conjunto. Falhas graves limitam automaticamente a pontuação a 5.
- **`resolved`** (enum): Se o problema do cliente foi encerrado. NÃO marque `resolved` apenas porque o agente enviou uma mensagem final. Requer confirmação do cliente ou uma transferência claramente concluída.
- **`customer_sentiment`** (enum): O tom emocional dominante do cliente, ponderado pelo modo como a conversa terminou.
- **`engagement_score`** (inteiro, 1–10): Quão bem o agente se comunicou — personalização, empatia, clareza, correspondência de idioma e continuidade nas transferências.
- **`reasoning`** (string): Seu resumo de 2–4 frases do Passo 1.
- **`flags`** (array): Lista de falhas graves identificadas. Array vazio se não houver.

---

## PASSO 3 — CADEIA DE VERIFICAÇÃO

Antes de finalizar, execute cada um dos seguintes cheques e responda-os explicitamente:

**Cheque 1 — Resolução:**
Há evidência real de que o problema do cliente foi solucionado — ou eu o inferi? Se inferido, `resolved` deveria ser "unresolved" em vez disso?

**Cheque 2 — Sentimento:**
Estou julgando o sentimento com base no tom de encerramento do cliente, não apenas em sua frustração inicial? O humor deles melhorou, permaneceu o mesmo ou piorou?

**Cheque 3 — Engajamento:**
Essa `engagement_score` resistiria se um gerente de QA revisasse a conversa? Excluí corretamente as mensagens `workflow` da pontuação de engajamento?

**Verificação 4 — Qualidade da transferência:**
Se um agente humano assumiu, manteve a continuidade — ou reiniciou do zero, ignorou o contexto, ou respondeu no idioma errado? Ajuste a `engagement_score` conforme necessário.

**Verificação 5 — Consistência das pontuações:**
- `resolved: unresolved` + `customer_sentiment: negative` → `overall_score` deve ser ≤ 4
- `resolved: resolved` + `customer_sentiment: positive` → `overall_score` deve ser ≥ 6
- Qualquer flag presente → `overall_score` deve ser ≤ 5

**Verificação 6 — Completude das flags:**
Revise novamente a lista completa de falhas graves. Perdi algo?

Se alguma verificação falhar, revise a pontuação relevante antes de prosseguir.

---

## PASSO 4 — SAÍDA FINAL

Produza suas pontuações finais somente no formato JSON exigido. Não inclua seu raciocínio interno dos Passos 1–3. Apenas o resumo do campo `reasoning` deve aparecer na saída.

---

## REFERÊNCIA DE PONTUAÇÃO

### overall_score
| Pontuação | Significado |
|-------|---------|
| 9–10 | Excelente — resolvido de forma eficiente, cliente saiu satisfeito, agente foi empático e ofereceu atendimento personalizado |
| 7–8 | Bom — resolvido com pequenas fricções, sentimento neutro a positivo |
| 5–6 | Médio — parcialmente resolvido, ou alto envolvimento com resultado ruim |
| 3–4 | Ruim — não resolvido, sentimento negativo ou falhas claras no atendimento |
| 1–2 | Falha crítica — informação incorreta fornecida, cliente saiu irritado ou conversa abandonada |

### engagement_score
| Pontuação | Significado |
|-------|---------|
| 9–10 | Altamente personalizado, empático, eficiente, proativamente prestativo, uso correto do idioma durante toda a interação |
| 7–8 | Sólido — tratou bem o problema com algum toque pessoal |
| 5–6 | Funcional porém robótico — tecnicamente correto, sem calor humano ou personalização |
| 3–4 | Comunicação fraca — suposições feitas, perguntas ignoradas ou transferência sem continuidade |
| 1–2 | Prejudicial — rude, desdenhoso, uso de idioma incorreto ou completamente fora do tópico |

Obrigatório: saída JSON estrita

Suas instruções de assistente devem requerer um JSON Schema.

Formato de saída recomendado:

  1. Tipo: JSON Schema (recomendado)

  2. Nome: Conversation QA Schema

  3. Strict: Ativado

  4. Schema: Este é apenas um esquema de exemplo para saída sobre a pontuação de qualidade da conversa, o status do problema, o sentimento do cliente, a pontuação de engajamento, o raciocínio por trás do resultado e os indicadores de falha da conversa. Lembre-se de editá‑lo para que se ajuste às suas necessidades.

{
  "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 = falha crítica, 10 = excelente. Qualquer flag presente limita isso a 5.
        },
        "resolved": {
          "type": "string",
          "enum": ["resolved", "unresolved", "escalated"],
          "description": "Whether the customer's core issue was closed.",
 'resolved' requer confirmação explícita do cliente. 'escalated' significa uma transferência intencional e bem‑sucedida para um agente humano. 'unresolved' significa que a conversa terminou sem fechamento.
        },
        "customer_sentiment": {
          "type": "string",
          "enum": ["positive", "neutral", "negative"],
          "description": "The customer's dominant emotional tone, weighted toward how they ended the conversation.",
 'positive' = satisfeito ou agradecido. 'neutral' = cooperativo e transacional. 'negative' = frustrado, repetitivo ou insatisfeito.
        },
        "engagement_score": {
          "type": "integer",
          "description": "How well the agent side communicated — personalization, empathy, clarity, language match, and handover continuity.",
 Exclui mensagens `workflow`. 1 = prejudicial ou desdenhoso, 10 = altamente personalizado e proativo.
        },
        "reasoning": {
          "type": "string",
          "description": "2–4 sentences explaining the scores.",
 Deve referenciar momentos específicos na conversa. Cubra o que o agente fez bem, o que falhou, e o que motivou as pontuações finais. Sem generalizações.
        },
        "flags": {
          "type": "array",
          "description": "Hard failure flags identified in the conversation.",
 Array vazio se nenhum se aplicar.",
          "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
    }
  }
}

Após este nó, conecte-o a um nó Map Output e continue para a etapa 7.

7. Analisar e normalizar a saída da IA

Esta etapa evita que respostas da IA malformadas ou parciais quebrem seu fluxo de trabalho.

Nós n8n:

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

  • Parse Output (Core → Code → JavaScript)

7.1 Map Output

Nó:Map Output

  • Modo: Mapeamento manual

  • Crie um campo chamado Output e mapeie-o para a resposta da IA.

Isso torna o próximo nó consistente (ele sempre poderá ler $json.Output).

7.2 Parse Output com segurança

Nó:Parse Output

Cole isto no nó 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; `${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 } });

Observação sobre flattening: arrays e objetos aninhados se tornarão colunas como issues_0_type, issues_0_severity, etc. Isso facilita o armazenamento no Google Sheets.

8. Armazenar resultados

A etapa final armazena os resultados de QA no Google Sheets para acompanhamento e relatório.

n8n node: Google Sheets (append)

  • Credencial para conectar: Conecte sua conta Google Sheets

  • Recurso: Sheet Within Document

  • Operação: Append Row

  • Selecionar documento: Na lista → selecione o nome da planilha

  • Selecionar aba: Na lista → selecione o nome da aba

  • Mapping Column Mode: Mapeie cada coluna manualmente

Em seguida, mapeie cada valor para o nome da coluna que você definiu na sua planilha, idealmente seguindo os nomes de coluna descritos na Seção 1.

De onde vem cada campo

Do gatilho Webhook (metadados da conversa):

Mapeie estes usando a referência do nó Webhook, ex.:

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

  • contact_id

  • conversation_opened_timestamp

  • conversation_closed_timestamp

Da saída de IA analisada (Seção 7):

Mapeie-os usando os campos da saída analisada, por exemplo:

{{ $json.overall_score }}

  • overall_score

  • resolved

  • sentiment

  • reasoning

  • engagement_score

Para depuração (recomendado durante o lançamento):

  • raw_ai_output — armazene a resposta completa da IA para que você possa verificar pontualmente os resultados e refinar seu prompt

💡 Dica: Verifique a saída real do seu nó Webhook para confirmar os nomes exatos dos campos — eles podem variar dependendo da configuração de gatilho do seu espaço de trabalho.

Melhorias opcionais

  • Mascarar PII antes da revisão pela IA (e-mails, números de telefone, IDs de pedido)

  • Processe mais de 100 mensagens percorrendo a paginação até que pagination.next esteja vazia

  • Amostras de conversas (por exemplo, revise apenas 10% das conversas encerradas para controlar custos)

  • Encaminhe problemas de alta gravidade para Slack/Teams para acompanhamento mais rápido

Compartilhar este artigo
Telegram
Facebook
Linkedin
Twitter

Não consegue encontrar o que você está procurando? 🔎