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:
timestampcontact_idopened_atchanneloverall_scoreresolvedcustomer_sentimentreasoningengagement_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
Di n8n, tambahkan node trigger respond.io.
Pilih Conversation Closed.
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) — opsionalget 2nd 50 messages(HTTP Request) — opsional
2.1 Ambil 50 pesan pertama
Node:get 1st 50 messages
Pilih Core > HTTP Request
Method:
GETURL: 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>
Nyalakan Send Query Parameters > Using Fields Below, lalu tambahkan:
Name:
limitValue:
50
Jika API Anda mendukungnya, atur pengurutan dari terlama → terbaru. Contoh: sort=asc.
Aktifkan Send Headers > Using Fields Below, lalu tambahkan:
Nama:
AcceptNilai:
application/json
Nama:
AuthorizationNilai:
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
Tambahkan satu node HTTP Request lagi.
Atur:
Method:
GETURL:
{{ $json.pagination.next }}
Tambahkan header yang sama:
Accept:
application/jsonAuthorization:
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 messageslangsung 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
Tambahkan Flow > Merge.
Atur Mode ke Append.
Atur Jumlah Input:
2Hubungkan:
Input 1:
get 1st 50 messagesInput 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:
Gabungkan pesan dari kedua halaman
Saring pesan yang dikirim sebelum percakapan dibuka
Urutkan pesan dari terlama → terbaru
Normalisasikan pengirim menjadi label yang konsisten:
Contact,AI Agent,Human Agent,Workflow.Tambahkan nomor indeks pesan
Kembalikan daftar bersih dan terstruktur untuk langkah transkrip
Catatan: Contoh ini menyaring menggunakan
messageIddibandingkan dengan stempel waktu pembukaan yang dikonversi ke mikrodetik. Jika API Anda menyediakan stempel waktucreatedAtsebagai gantinya, saring berdasarkancreatedAtdaripadamessageId. Pastikanconversation_open_timestampAnda 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
Pilih kredensial penyedia AI Anda. Dalam contoh ini, kami akan menggunakan OpenAI.
Resource: Message a Model
Operation: Message an Assistant
Pesan:
Tipe: Teks
Peran: Pengguna
Prompt:
{{ $json.transcript }}
Sederhanakan Output: Aktifkan
Tambahkan Opsi:
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:
Tipe: JSON Schema (direkomendasikan)
Nama: Skema QA Percakapan
Ketat: Aktifkan
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
Outputdan 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_idconversation_opened_timestampconversation_closed_timestamp
Dari keluaran AI yang diurai (Bagian 7):
Petakan ini menggunakan field keluaran yang diurai, misalnya:
{{ $json.overall_score }}
overall_scoreresolvedsentimentreasoningengagement_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.nextkosongSampel 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