1. All Collections >
  2. Pertumbuhan yang Dipimpin Percakapan >
  3. Pelaporan dan Analisis >
  4. Cara Mengotomatiskan QA Percakapan Menggunakan n8n & AI

Cara Mengotomatiskan QA Percakapan Menggunakan n8n & AI

Avatar
Shing-Yi Tan
12 menit baca

Pelanggan disarankan meninjau percakapan yang ditangani AI setelah mengaktifkan Agen AI, tetapi QA manual menjadi terlalu memakan waktu pada skala besar. Panduan ini menjelaskan cara mengotomatiskan QA percakapan menggunakan n8n + model AI, sehingga setiap percakapan yang ditutup dapat ditinjau secara otomatis dan dicatat ke dalam Google Sheets untuk pelaporan.

Singkatnya, alur kerja n8n ini dipicu setiap kali percakapan ditutup. Setelah dipicu, alur ini memanggil List Messages API untuk mengambil 50 pesan terakhir (dengan opsi hingga 100 menggunakan pagination), lalu meneruskan transkrip ke model AI untuk pemrosesan QA — seperti analisis sentimen, penilaian keterlibatan, dan pemeriksaan penyelesaian. Hasilnya dicatat di Google Sheets sehingga Anda dapat dengan mudah mencari percakapan berdasarkan ID kontak.

Apa yang Akan Anda Bangun

Otomatisasi yang:

  • Dipicu saat percakapan ditutup

  • Mengambil 50 pesan terakhir menggunakan List Messages API (opsional hingga 100 dengan paginasi)

  • Membersihkan dan memformat pesan menjadi transkrip

  • Mengirimkan transkrip ke model AI untuk evaluasi QA

  • Menguraikan output AI dengan aman (kontrak JSON saja)

  • Menambahkan hasil ke Google Sheets (satu baris per percakapan)

Persyaratan

Alat

  • Platform otomatisasi: n8n

  • Respond.io Developer API — endpoint List Messages

  • AI API — OpenAI, Anthropic, Gemini, atau penyedia lain yang didukung oleh n8n

  • Pelaporan / penyimpanan — Google Sheets (atau alat penyimpanan pilihan Anda)

Kredensial

  • Kunci Respond.io Developer API

  • Kunci API penyedia AI (OpenAI, Anthropic, Gemini, dll.)

  • Kredensial Google Sheets di n8n (OAuth atau akun layanan)

Sebelum Anda Mulai

1) Buat Google Sheet untuk hasil QA

Buat spreadsheet dan tambahkan sebuah sheet (mis. QA_Results) dengan header berikut:

  • timestamp

  • contact_id

  • opened_at

  • channel

  • overall_score

  • resolved

  • customer_sentiment

  • reasoning

  • engagement_score

2) Konfirmasi field payload pemicu Anda

Dari output Conversation closed trigger, identifikasi nama field yang tepat untuk:

  • ID Percakapan

  • ID Kontak

  • stempel waktu pembukaan percakapan

  • stempel waktu penutupan percakapan (opsional)

Nama field bervariasi tergantung implementasi. Di node kode di bawah, Anda akan melihat placeholder seperti trigger.conversationId. Perbarui mapping tersebut agar sesuai dengan output pemicu Anda.

Panduan Langkah demi Langkah

1. Pemicu: Conversation Closed

Ketika sebuah percakapan ditutup di respond.io, kami ingin secara otomatis memicu alur kerja n8n. Ini memastikan setiap percakapan yang selesai melewati QA — tidak perlu tinjauan manual.

n8n node:Conversation closed trigger

  1. Di n8n, tambahkan node trigger respond.io.

  2. Pilih Conversation Closed.

  3. Hubungkan kredensial kunci API respond.io Anda. Pelajari cara mengaturnya di n8n dengan panduan integrasi kami.

Pemicu ini memastikan Anda hanya meninjau percakapan setelah selesai.

2. Ambil pesan dari percakapan

Pada langkah ini, Anda akan mengambil 50 pesan masuk dan keluar terakhir menggunakan List Messages API. Ini biasanya cukup untuk tinjauan QA yang menyeluruh. Jika Anda membutuhkan konteks lebih, Anda dapat secara opsional mengambil tambahan 50 pesan (hingga 100 total) menggunakan paginasi.

Node n8n:

  • get 1st 50 messages (HTTP Request)

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

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

2.1 Ambil 50 pesan pertama

Node:get 1st 50 messages

  1. Pilih Core > HTTP Request

  2. Method: GET

  3. URL: endpoint List Messages Anda

Contoh URL (ganti dengan URL dasar API + format endpoint Anda yang sebenarnya):

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

Di mana identifier perlu diganti dengan salah satu format berikut: id:<contactID>, phone:+<countryCodeAndPhone>, atau email:<contactEmail>

  1. Nyalakan Send Query Parameters > Using Fields Below, lalu tambahkan:

    • Name: limit

    • Value: 50

Jika API Anda mendukungnya, atur pengurutan dari terlama → terbaru. Contoh: sort=asc.

  1. Aktifkan Send Headers > Using Fields Below, lalu tambahkan:

    • Nama: Accept

      • Nilai: application/json

    • Nama: Authorization

      • Nilai: Bearer <your_respond_api_key>

Jika respon List Messages Anda menyertakan pagination.next, Anda dapat menggunakannya langsung untuk mengambil halaman kedua.

2.2 Periksa dan ambil lebih dari 50 pesan (Opsional)

Jika 50 pesan tidak cukup untuk kebutuhan QA Anda, Anda dapat mengambil halaman kedua berisi 50 pesan. Tambahkan node IF untuk memeriksa apakah masih ada pesan yang tersedia.

Node:Is there a second page? (IF)

  • Nilai kiri (Expression): {{ $json.pagination.next }}

  • Operator: tidak kosong

Jika bernilai true, berarti ada lebih banyak pesan untuk diambil. Jika bernilai false, alur kerja akan berlanjut tanpa mengambil lebih banyak — ini mencegah alur kerja gagal ketika tidak ada halaman kedua. Node Merge di tahap hilir akan menunggu kedua jalur selesai, sehingga alur kerja tetap berjalan lancar dalam kedua kasus.

2.3 Ambil 50 pesan berikutnya (Opsional)

Jika node IF terpenuhi (ada halaman kedua), ambil batch berikutnya.

Node:get 2nd 50 messages

  1. Tambahkan satu node HTTP Request lagi.

  2. Atur:

    • Method: GET

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

  3. Tambahkan header yang sama:

    • Accept: application/json

    • Authorization: Bearer <your_respond_api_key>

3. Gabungkan Halaman Pesan (Opsional)

Langkah ini hanya diperlukan jika Anda mengambil lebih dari 50 pesan. Jika Anda memilih untuk tidak mengambil halaman kedua, Anda dapat melewati node ini dan menghubungkan get 1st 50 messages langsung ke langkah berikutnya (Clean + keep messages since last open).

Node Merge menggabungkan 50 pesan pertama dengan 50 pesan kedua menjadi satu daftar. Tanpa ini, alur kerja tidak dapat memproses dua respons API terpisah bersama-sama.

n8n node:Merge

  1. Tambahkan Flow > Merge.

  2. Atur Mode ke Append.

  3. Atur Jumlah Input: 2

  4. Hubungkan:

    • Input 1: get 1st 50 messages

    • Input 2: get 2nd 50 messages

4. Bersihkan dan Saring Pesan

Respons API masih mentah dan berisi banyak informasi tambahan yang tidak dibutuhkan AI. Langkah ini membersihkannya — menghapus pesan lama, menormalkan label pengirim, dan menyusun semuanya menjadi daftar sederhana yang siap untuk pemrosesan AI. Yang perlu Anda lakukan adalah menambahkan node Code dan menyalin-tempel JavaScript di bawah.

n8n node:Clean + keep messages since last convo open

Untuk melakukan ini: Tambahkan node → Pilih Core → Code → Code in JavaScript.

Kode ini akan:

  1. Gabungkan pesan dari kedua halaman

  2. Saring pesan yang dikirim sebelum percakapan dibuka

  3. Urutkan pesan dari terlama → terbaru

  4. Normalisasikan pengirim menjadi label yang konsisten: Contact, AI Agent, Human Agent, Workflow.

  5. Tambahkan nomor indeks pesan

  6. Kembalikan daftar bersih dan terstruktur untuk langkah transkrip

Catatan: Contoh ini menyaring menggunakan messageId dibandingkan dengan stempel waktu pembukaan yang dikonversi ke mikrodetik. Jika API Anda menyediakan stempel waktu createdAt sebagai gantinya, saring berdasarkan createdAt daripada messageId. Pastikan conversation_open_timestamp Anda menyertakan zona waktu. Jika tidak, atur ke zona waktu ruang kerja Anda sebelum mengurai.

Tempelkan ini ke node 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,
      },
    },
  },
];

Pastikan mengatur Mode = Run Once for All Items dan Language = JavaScript.

Skrip di atas hanya sebagai referensi. Ruang kerja Anda mungkin mengembalikan bidang atau struktur yang berbeda. Untuk membuat logika parsing JSON yang tepat untuk pengaturan Anda, salin output dari node sebelumnya, tempelkan ke alat AI (mis. ChatGPT atau Claude) bersama skrip referensi di atas, dan jelaskan format output yang Anda butuhkan. AI kemudian dapat menyesuaikan skrip agar cocok dengan data aktual Anda.

5. Buat Transkrip

Langkah ini mengonversi daftar pesan yang telah dibersihkan menjadi sebuah transkrip gaya Markdown untuk pemrosesan AI. Transkrip terstruktur meningkatkan pemahaman AI dan mengurangi halusinasi.

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

Tempelkan ini ke node 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;

Pastikan mengatur Mode = Run Once for All Items dan Language = JavaScript.

6. Tinjauan percakapan berbasis AI

Node ini mengirimkan transkrip ke model AI (mis. OpenAI 5.4) dan mengembalikan sebuah respons JSON terstruktur.

Cara mengatur

  1. Pilih kredensial penyedia AI Anda. Dalam contoh ini, kami akan menggunakan OpenAI.

  2. Resource: Message a Model

  3. Operation: Message an Assistant

  4. Pesan:

    1. Tipe: Teks

    2. Peran: Pengguna

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

  5. Sederhanakan Output: Aktifkan

  6. Tambahkan Opsi:

    1. Instruksi - Ini hanya contoh template, tetapi Anda dapat memasukkan ini ke AI lain (mis. ChatGPT, Claude, dll.) untuk mengeditnya sesuai kebutuhan Anda:

Anda adalah analis kualitas percakapan untuk respond.io, sebuah platform pesan pelanggan. Tugas Anda adalah meninjau percakapan dukungan atau penjualan dan menghasilkan skor kualitas yang akurat dan konsisten.

Anda akan menerima transkrip percakapan sebagai daftar pesan bernomor. Evaluasilah menggunakan proses penalaran terstruktur di bawah sebelum menghasilkan output akhir Anda.

---

## INPUT FORMAT

Setiap pesan dalam transkrip berisi:

- Nomor pesan (mis. `1`, `2`, `3`)
- `traffic:incoming` — pesan DARI pelanggan
- `traffic:outgoing` — pesan KEPADA pelanggan
- `text:` — isi pesan
- `sender source:` — siapa pengirimnya:
  - `Contact` → pelanggan
  - `ai_agent` → Agen AI (otomatis — nilai untuk kualitas)
  - `workflow` → otomatisasi sistem (JANGAN hitung sebagai keterlibatan agen)
  - `user` → agen manusia (catat titik penyerahan)

Saat mengevaluasi, anggap pesan `ai_agent` dan `user` bersama sebagai "sisi agen". Lacak kapan terjadi handover dari AI ke manusia, karena ini memengaruhi skor penyelesaian dan skor keterlibatan. Abaikan pesan `workflow` saat menilai keterlibatan — mereka adalah respons sistem otomatis, bukan bahan penilaian. Percakapan dapat menggunakan bahasa apa pun. Nilailah sentimen dan konten secara akurat terlepas dari bahasanya.

LANGKAH 1 — RANTAI PEMIKIRAN (Penalaran Internal)

Sebelum menghasilkan skor apa pun, pikirkan percakapan langkah demi langkah dalam urutan ini:

**1. Identifikasi maksud inti pelanggan.
Apa yang ingin dicapai pelanggan? Nyatakan dalam satu kalimat.

**2. Lacak struktur percakapan.
Siapa yang menangani — hanya agen AI, atau apakah ada penyerahan ke manusia? Pada titik mana? Apakah beberapa agen manusia terlibat? Apakah ada pesan alur kerja yang bisa membingungkan pelanggan?

**3. Lacak penyelesaian.
Apakah agen benar-benar menanggapi maksud inti pelanggan? Apakah pelanggan mengonfirmasi bahwa masalah telah terselesaikan? Apakah percakapan berakhir di tengah masalah, dengan pengalihan, atau dengan handover yang tepat?

**4. Lacak sentimen.
Bagaimana emosi pelanggan pada awalnya? Bagaimana akhir emosinya? Perhatikan:
- Tanda frustrasi: pertanyaan berulang, koreksi, balasan singkat/terputus
- Tanda kepuasan: "terima kasih", "mengerti", "sempurna", penutup positif
- Tanda netral: transaksional, kooperatif, tanpa sinyal kuat ke arah mana pun
Beratkan nada akhir pelanggan lebih daripada nada awalnya.

**5. Nilai keterlibatan agen.
Nilai sisi agen berdasarkan:
- Apakah mereka mengakui situasi pelanggan sebelum langsung memberikan solusi?
- Apakah mereka mempersonalisasi respons menggunakan konteks dari percakapan sebelumnya?
- Apakah mereka mengajukan pertanyaan klarifikasi yang cerdas, atau membuat asumsi?
- Jika manusia mengambil alih, apakah mereka mempertahankan kontinuitas dari AI — atau memulai dari awal?
- Apakah respons diberikan dalam bahasa yang sama dengan pelanggan?
- JANGAN menghitung pesan `workflow` sebagai engagement.

**6. Identifikasi kegagalan serius.
Periksa secara eksplisit setiap hal berikut:
- Apakah pelanggan mengulang pertanyaan inti mereka tanpa diakui?
- Apakah agen memberikan informasi yang keliru secara faktual?
- Apakah agen mengabaikan bagian penting dari pesan pelanggan?
- Apakah percakapan berakhir tanpa penyelesaian atau langkah selanjutnya yang jelas?
- Apakah ada nada yang tidak profesional atau tidak pantas?
- Apakah terdapat penundaan respons yang berlebihan?
- Apakah jalur eskalasi salah atau tidak perlu?
Apakah agen manusia gagal mengambil konteks dari penyerahan oleh agen AI?
- Apakah agen merespons dalam bahasa yang berbeda dari pelanggan?

**7. Susun alasan Anda.
Dalam 2–4 kalimat, ringkas: apa yang terjadi, apa yang agen lakukan dengan baik, dan apa yang gagal. Jadilah spesifik — sebutkan momen nyata atau nomor pesan.

---

## LANGKAH 2 — PENILAIAN AWAL

Berdasarkan alasan Langkah 1 Anda, buat rancangan awal semua skor:

- **`overall_score`** (bilangan bulat, 1–10): Kualitas keseluruhan percakapan. Beratkan penyelesaian, sentimen, dan keterlibatan secara bersama. Kegagalan serius secara otomatis membatasi skor maksimum menjadi 5.
- **`resolved`** (enum): Apakah masalah pelanggan ditutup. JANGAN tandai `resolved` hanya karena agen mengirim pesan terakhir. Membutuhkan konfirmasi dari pelanggan atau handover yang jelas selesai.
- **`customer_sentiment`** (enum): Nada emosional dominan pelanggan, diberi bobot pada bagaimana mereka mengakhiri percakapan.
- **`engagement_score`** (bilangan bulat, 1–10): Seberapa baik agen berkomunikasi — personalisasi, empati, kejelasan, kecocokan bahasa, dan kontinuitas di seluruh handover.
- **`reasoning`** (string): Ringkasan 2–4 kalimat Anda dari Langkah 1.
- **`flags`** (array): Daftar kegagalan serius yang teridentifikasi. Array kosong jika tidak ada.

---

## LANGKAH 3 — RANTAI VERIFIKASI

Sebelum finalisasi, jalankan setiap pemeriksaan berikut dan jawab secara eksplisit:

**Pemeriksaan 1 — Penyelesaian:**
Apakah ada bukti nyata bahwa masalah pelanggan telah diselesaikan — atau apakah saya hanya menyimpulkannya? Jika itu disimpulkan, apakah `resolved` seharusnya bernilai `"unresolved"`?

**Pemeriksaan 2 — Sentimen:**
Apakah saya menilai sentimen berdasarkan nada akhir pelanggan, bukan hanya frustrasi awal mereka? Apakah suasana hati mereka membaik, tetap sama, atau memburuk?

**Pemeriksaan 3 — Keterlibatan:**
Apakah `engagement_score` ini akan tetap valid jika seorang manajer QA meninjau percakapan? Apakah saya sudah benar mengecualikan pesan `workflow` dari penilaian engagement?

**Pemeriksaan 4 — Kualitas penyerahan:**
Jika agen manusia mengambil alih, apakah mereka mempertahankan kontinuitas — atau memulai dari awal, mengabaikan konteks, atau merespons dalam bahasa yang salah? Sesuaikan `engagement_score` sesuai.

**Pemeriksaan 5 — Konsistensi skor:**
- `resolved: unresolved` + `customer_sentiment: negative` → `overall_score` harus ≤ 4
- `resolved: resolved` + `customer_sentiment: positive` → `overall_score` harus ≥ 6
- Ada flag apa pun → `overall_score` harus ≤ 5

**Pemeriksaan 6 — Kelengkapan flags:**
Tinjau kembali daftar lengkap kegagalan serius. Apakah saya melewatkan sesuatu?

Jika ada pemeriksaan yang gagal, perbaiki skor terkait sebelum melanjutkan.

---

## LANGKAH 4 — HASIL AKHIR

Keluarkan skor akhir Anda hanya dalam format JSON yang diminta. Jangan sertakan alasan internal Langkah 1–3 Anda. Hanya ringkasan pada bidang `reasoning` yang harus muncul di keluaran.

---

## REFERENSI PENILAIAN

### overall_score
| Skor | Makna |
|-------|---------|
| 9–10 | Sangat baik — terselesaikan secara efisien, pelanggan berakhir positif, agen empatik dan personal |
| 7–8 | Baik — terselesaikan dengan sedikit gesekan, sentimen netral-ke-positif |
| 5–6 | Rata-rata — sebagian terselesaikan, atau keterlibatan tinggi tetapi hasil buruk |
| 3–4 | Buruk — tidak terselesaikan, sentimen negatif, atau kegagalan keterlibatan yang jelas |
| 1–2 | Kegagalan kritis — informasi salah diberikan, pelanggan pergi marah, atau percakapan ditinggalkan |

### engagement_score
| Skor | Makna |
|-------|---------|
| 9–10 | Sangat personal, empatik, efisien, proaktif membantu, bahasa yang benar sepanjang percakapan |
| 7–8 | Baik — masalah ditangani dengan baik dengan sentuhan personal |
| 5–6 | Fungsional tapi kaku — secara teknis benar, tanpa kehangatan atau personalisasi |
| 3–4 | Komunikasi buruk — asumsi dibuat, pertanyaan diabaikan, atau serah terima tidak berkelanjutan |
| 1–2 | Merugikan — kasar, meremehkan, penggunaan bahasa yang salah, atau sama sekali tidak relevan |

Diperlukan: keluaran JSON yang ketat

Instruksi asisten Anda harus mewajibkan JSON Schema.

Format Keluaran yang Direkomendasikan:

  1. Tipe: JSON Schema (direkomendasikan)

  2. Nama: Skema QA Percakapan

  3. Ketat: Aktifkan

  4. Skema: Ini juga hanya contoh skema untuk mengeluarkan skor kualitas percakapan, status masalah, sentimen pelanggan, skor keterlibatan, alasan di balik hasilnya, dan flag kegagalan percakapan. Ingat untuk mengeditnya agar sesuai kebutuhan Anda.

{
  "type": "json_schema",
  "json_schema": {
    "name": "conversation_review",
    "strict": true,
    "schema": {
      "type": "object",
      "properties": {
        "overall_score": {
          "type": "integer",
          "description": "Skor kualitas holistik dari percakapan."}
 1 = kegagalan kritis, 10 = sangat baik. Setiap flag yang ada membatasi nilai ini menjadi 5.",
        },
        "resolved": {
          "type": "string",
          "enum": ["resolved", "unresolved", "escalated"],
          "description": "Apakah masalah inti pelanggan telah ditutup. 'resolved' membutuhkan konfirmasi eksplisit dari pelanggan. 'escalated' berarti penyerahan secara sengaja dan berhasil kepada agen manusia. 'unresolved' berarti percakapan berakhir tanpa penyelesaian.",
        },
        "customer_sentiment": {
          "type": "string",
          "enum": ["positive", "neutral", "negative"],
          "description": "Tonalitas emosional dominan dari pelanggan, ditimbang berdasarkan bagaimana mereka mengakhiri percakapan. 'positive' = puas atau berterima kasih. 'neutral' = kooperatif dan bersifat transaksional. 'negative' = frustrasi, berulang, atau merasa tidak puas.",
        },
        "engagement_score": {
          "type": "integer",
          "description": "Seberapa baik pihak agen berkomunikasi — personalisasi, empati, kejelasan, kecocokan bahasa, dan kontinuitas dalam penyerahan. Mengecualikan pesan alur kerja. 1 = merugikan atau mengabaikan, 10 = sangat personal dan proaktif.",
        },
        "reasoning": {
          "type": "string",
          "description": "2–4 kalimat yang menjelaskan skor tersebut. Harus merujuk pada momen spesifik dalam percakapan. Jelaskan apa yang agen lakukan dengan baik, apa yang gagal, dan faktor yang memengaruhi skor akhir. Tidak boleh membuat generalisasi.",
        },
        "flags": {
          "type": "array",
          "description": "Identifikasi flag kegagalan keras dalam percakapan. Array kosong jika tidak ada yang berlaku.",
          "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
    }
  }
}

Setelah node ini, hubungkan ke node Map Output dan lanjutkan ke langkah 7.

7. Mengurai dan menormalisasi output AI

Langkah ini mencegah respons AI yang salah format atau parsial agar tidak merusak alur kerja Anda.

Node n8n:

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

  • Parse Output (Core → Code → JavaScript)

7.1 Map Output

Node:Map Output

  • Mode: Pemetaan manual

  • Buat field bernama Output dan petakan ke respons AI.

Ini membuat node berikut konsisten (selalu dapat membaca $json.Output).

7.2 Parse Output secara aman

Node:Parse Output

Tempelkan ini ke node 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;

Catatan tentang perataan (flattening): array dan objek bersarang akan menjadi kolom seperti issues_0_type, issues_0_severity, dll. Ini memudahkan penyimpanan di Google Sheets.

8. Simpan Hasil

Langkah terakhir menyimpan hasil QA ke Google Sheets untuk pelacakan dan pelaporan.

n8n node: Google Sheets (append)

  • Kredensial untuk terhubung: Hubungkan akun Google Sheets Anda

  • Sumber: Sheet Within Document

  • Operation: Append Row

  • Pilih dokumen: Dari daftar → Pilih nama spreadsheet

  • Pilih sheet: Dari daftar → Nama worksheet

  • Mode Pemetaan Kolom: Peta setiap kolom secara manual

Lalu, petakan setiap nilai ke nama kolom yang sudah Anda siapkan di sheet Anda, idealnya mengikuti nama kolom yang dijelaskan di Bagian 1.

Dari mana setiap field berasal

Dari pemicu Webhook (metadata percakapan):

Petakan ini menggunakan referensi node Webhook, mis.:

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

  • contact_id

  • conversation_opened_timestamp

  • conversation_closed_timestamp

Dari keluaran AI yang diurai (Bagian 7):

Petakan ini menggunakan field keluaran yang diurai, misalnya:

{{ $json.overall_score }}

  • overall_score

  • resolved

  • sentiment

  • reasoning

  • engagement_score

Untuk debugging (disarankan saat peluncuran):

  • raw_ai_output — simpan seluruh respons AI agar Anda bisa memeriksa hasil secara acak dan menyempurnakan prompt Anda

💡 Tip: Periksa keluaran sebenarnya dari node Webhook Anda untuk memastikan nama field yang tepat — nama-nama tersebut mungkin berbeda tergantung pada konfigurasi pemicu di ruang kerja Anda.

Peningkatan Opsional

  • Sembunyikan PII sebelum peninjauan AI (email, nomor telepon, ID pesanan)

  • Proses lebih dari 100 pesan dengan melakukan loop melalui paginasi sampai pagination.next kosong

  • Sampel percakapan (misalnya, tinjau hanya 10% percakapan yang ditutup untuk mengendalikan biaya)

  • Alihkan masalah dengan tingkat keparahan tinggi ke Slack/Teams untuk tindak lanjut yang lebih cepat

Bagikan artikel ini
Telegram
Facebook
Linkedin
Twitter

Tidak dapat menemukan yang Anda cari? 🔎