1. All Collections >
  2. การสนทนานำการเติบโต >
  3. การรายงานและการวิเคราะห์ >
  4. วิธีทำให้การตรวจสอบคุณภาพของการสนทนาเป็นอัตโนมัติด้วย n8n และ AI

วิธีทำให้การตรวจสอบคุณภาพของการสนทนาเป็นอัตโนมัติด้วย n8n และ AI

Avatar
Shing-Yi Tan
6 min read

เราแนะนำให้ลูกค้าตรวจทบทวนการสนทนาที่ AI จัดการหลังจากเปิดใช้ AI Agent แต่การตรวจสอบคุณภาพด้วยมือจะใช้เวลามากเกินไปเมื่อมีปริมาณมาก. คู่มือนี้อธิบายวิธีทำให้การตรวจสอบคุณภาพ (QA) ของการสนทนาเป็นอัตโนมัติด้วย n8n + โมเดล AI เพื่อให้ทุกการสนทนาที่ปิดแล้วได้รับการตรวจโดยอัตโนมัติและบันทึกลงใน Google Sheets สำหรับการรายงาน.

โดยสรุป workflow ของ n8n นี้จะทำงานทุกครั้งที่มีการปิดการสนทนา. เมื่อทริกเกอร์แล้ว มันจะเรียกใช้ List Messages API เพื่อดึง 50 ข้อความล่าสุด (พร้อมตัวเลือกดึงสูงสุด 100 ผ่านการแบ่งหน้า) แล้วส่งบทถอดความไปยังโมเดล AI เพื่อการประเมิน QA — เช่น การวิเคราะห์ความรู้สึก การให้คะแนนการมีส่วนร่วม และการตรวจสอบว่าปัญหาได้รับการแก้ไข. ผลลัพธ์จะถูกบันทึกใน Google Sheet เพื่อให้คุณสามารถค้นหาการสนทนาใดๆ ได้อย่างง่ายดายโดยใช้ ID ผู้ติดต่อ.

สิ่งที่คุณจะสร้าง

ระบบอัตโนมัติที่:

  • ทำงานเมื่อการสนทนาถูกปิด

  • ดึง 50 ข้อความล่าสุดโดยใช้ List Messages API (สามารถดึงได้สูงสุด 100 ข้อความโดยใช้การแบ่งหน้า)

  • ทำความสะอาดและจัดรูปแบบข้อความเป็นถอดความ

  • ส่งถอดความไปยังโมเดล AI เพื่อประเมิน QA

  • แยกวิเคราะห์ผลลัพธ์จาก AI อย่างปลอดภัย (สัญญาแบบ JSON เท่านั้น)

  • เพิ่มผลลัพธ์ลงใน Google Sheets (หนึ่งแถวต่อการสนทนา)

ความต้องการ

เครื่องมือ

  • แพลตฟอร์มอัตโนมัติ: n8n

  • Respond.io Developer API — List Messages endpoint

  • AI API — OpenAI, Anthropic, Gemini หรือผู้ให้บริการใด ๆ ที่ n8n รองรับ

  • ระบบจัดเก็บ/รายงาน — Google Sheets (หรือเครื่องมือจัดเก็บที่คุณใช้งาน)

ข้อมูลรับรอง

  • คีย์ Respond.io Developer API

  • คีย์ API ผู้ให้บริการ AI (OpenAI, Anthropic, Gemini ฯลฯ)

  • ข้อมูลรับรอง Google Sheets ใน n8n (OAuth หรือ service account)

ก่อนเริ่มต้น

1) สร้าง Google Sheet สำหรับผล QA

สร้างสเปรดชีตและเพิ่มแผ่นงาน (เช่น QA_Results) โดยมีหัวคอลัมน์ดังนี้:

  • timestamp

  • contact_id

  • opened_at

  • channel

  • overall_score

  • resolved

  • customer_sentiment

  • reasoning

  • engagement_score

2) ยืนยันฟิลด์ payload ของทริกเกอร์ของคุณ

จากเอาต์พุตของ Conversation closed trigger ให้ระบุชื่อฟิลด์ที่แน่นอนสำหรับ:

  • ID การสนทนา

  • ID ผู้ติดต่อ

  • ไทม์สแตมป์เวลาเปิดการสนทนา

  • ไทม์สแตมป์เวลาปิดการสนทนา (ไม่บังคับ)

ชื่อฟิลด์จะแตกต่างกันตามการติดตั้ง. ในโหนดโค้ดด้านล่าง คุณจะเห็นตัวแปรแทนที่เช่น trigger.conversationId. อัปเดตการแมปเหล่านั้นให้ตรงกับเอาต์พุตทริกเกอร์ของคุณ.

คำแนะนำทีละขั้นตอน

1. ทริกเกอร์: การสนทนาถูกปิด

เมื่อการสนทนาถูกปิดใน respond.io เราต้องการให้เวิร์กโฟลว์ n8n ถูกทริกเกอร์โดยอัตโนมัติ. วิธีนี้จะทำให้การสนทนาที่เสร็จสมบูรณ์ทุกครั้งผ่านกระบวนการ QA — ไม่จำเป็นต้องมีการตรวจทานด้วยมือ.

n8n node:Conversation closed trigger

  1. ใน n8n ให้เพิ่มโหนดทริกเกอร์ respond.io.

  2. เลือก Conversation Closed.

  3. เชื่อมต่อข้อมูลรับรอง API key ของ respond.io ของคุณ. เรียนรู้วิธีตั้งค่านี้ใน n8n จาก integration guide.

ทริกเกอร์นี้รับประกันว่าคุณจะตรวจเฉพาะการสนทนาหลังจากที่เสร็จเรียบร้อยแล้วเท่านั้น.

2. ดึงข้อความการสนทนา

ในขั้นตอนนี้ คุณจะดึง 50 ข้อความขาเข้าและขาออกล่าสุดโดยใช้ List Messages API. โดยปกติแล้วจำนวนนี้เพียงพอสำหรับการตรวจ QA อย่างรอบคอบ. หากต้องการบริบทเพิ่มเติม คุณสามารถดึงข้อความเพิ่มอีก 50 ข้อความ (รวมสูงสุด 100 ข้อความ) โดยใช้การแบ่งหน้า.

โหนด n8n:

  • get 1st 50 messages (HTTP Request)

  • Is there a second page? (IF) — ไม่บังคับ

  • get 2nd 50 messages (HTTP Request) — ไม่บังคับ

2.1 ดึง 50 ข้อความแรก

Node:get 1st 50 messages

  1. เลือก Core > HTTP Request

  2. Method: GET

  3. URL: endpoint สำหรับ List Messages ของคุณ

URL ตัวอย่าง (แทนที่ด้วย base URL ของ API ของคุณ + รูปแบบ endpoint):

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

โดยที่ identifier ต้องถูกแทนที่ด้วยหนึ่งในรูปแบบต่อไปนี้: id:<contactID>, phone:+<countryCodeAndPhone>, หรือ email:<contactEmail>

  1. เปิด Send Query Parameters > Using Fields Below แล้วเพิ่ม:

    • Name: limit

    • Value: 50

หาก API ของคุณรองรับ ให้ตั้งการจัดเรียงจากเก่า → ใหม่. ตัวอย่าง: sort=asc.

  1. เปิด Send Headers > Using Fields Below แล้วเพิ่ม:

    • Name: Accept

      • Value: application/json

    • ชื่อ: Authorization

      • ค่า: Bearer <your_respond_api_key>

หากการตอบของ List Messages ของคุณมี pagination.next คุณสามารถใช้มันโดยตรงเพื่อดึงหน้าที่สองได้.

2.2 ตรวจและดึงมากกว่า 50 ข้อความ (ไม่บังคับ)

หาก 50 ข้อความไม่เพียงพอต่อความต้องการ QA ของคุณ คุณสามารถดึงหน้าที่สองอีก 50 ข้อความได้. เพิ่มโหนด IF เพื่อตรวจว่ามีข้อความเพิ่มเติมหรือไม่.

Node:Is there a second page? (IF)

  • Left value (Expression): {{ $json.pagination.next }}

  • Operator: is not empty

ถ้าเป็นจริง แสดงว่ายังมีข้อความให้ดึงเพิ่ม. ถ้าเป็นเท็จ workflow จะดำเนินต่อโดยไม่ดึงเพิ่มเติม — ป้องกันไม่ให้ workflow ล้มเหลวเมื่อไม่มีหน้าที่สอง. โหนด Merge ด้านล่างจะรอให้ทั้งสองเส้นทางเสร็จสิ้น ดังนั้น workflow ยังคงทำงานได้อย่างราบรื่นทั้งสองกรณี.

2.3 ดึง 50 ข้อความถัดไป (ไม่บังคับ)

หากโหนด IF ผ่าน (มีหน้าที่สอง) ให้ดึงชุดถัดไป.

Node:get 2nd 50 messages

  1. เพิ่มโหนด HTTP Request อีกตัว.

  2. ตั้งค่า:

    • Method: GET

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

  3. เพิ่ม headers เดียวกัน:

    • Accept: application/json

    • Authorization: Bearer <your_respond_api_key>

3. รวมหน้าข้อความ (ไม่บังคับ)

ขั้นตอนนี้จำเป็นเฉพาะเมื่อคุณดึงมากกว่า 50 ข้อความเท่านั้น. หากคุณเลือกที่จะไม่ดึงหน้าที่สอง คุณสามารถข้ามโหนดนี้และเชื่อม get 1st 50 messages กับขั้นตอนถัดไป (Clean + keep messages since last open) โดยตรงได้.

โหนด Merge จะผสานข้อความ 50 ข้อความแรกกับ 50 ข้อความถัดไปเป็นรายการเดียว. หากไม่มีโหนดนี้ workflow จะไม่สามารถประมวลผลการตอบกลับของ API สองรายการพร้อมกันได้.

n8n node:Merge

  1. เพิ่ม Flow > Merge.

  2. ตั้งค่าโหมดเป็น Append.

  3. ตั้งค่า Number of Inputs: 2

  4. เชื่อมต่อ:

    • Input 1: get 1st 50 messages

    • Input 2: get 2nd 50 messages

4. ทำความสะอาดและคัดกรองข้อความ

การตอบของ API ยังคงเป็นข้อมูลดิบและมีข้อมูลมากเกินความจำเป็นที่ AI ไม่ต้องการ. ขั้นตอนนี้จะทำความสะอาด — ลบข้อความเก่า ปรับป้ายผู้ส่งให้เป็นมาตรฐาน และจัดโครงสร้างเป็นรายการเรียบง่ายพร้อมสำหรับการประมวลผลโดย AI. สิ่งที่คุณต้องทำคือเพิ่มโหนด Code และคัดลอก/วาง JavaScript ด้านล่าง.

n8n node:Clean + keep messages since last convo open

ทำได้โดย Add node → เลือก Core → Code → Code ใน JavaScript.

สคริปต์นี้จะ:

  1. รวมข้อความจากทั้งสองหน้า

  2. กรองออก ข้อความที่ส่งก่อนการเปิดการสนทนา

  3. จัดเรียง ข้อความจากเก่า → ใหม่

  4. ปรับชื่อผู้ส่งให้เป็นมาตรฐาน: Contact, เอเจนต์ AI, เอเจนต์มนุษย์, เวิร์กโฟลว์

  5. เพิ่ม หมายเลขดัชนีข้อความ

  6. ส่งคืน รายการที่เป็นระเบียบและมีโครงสร้างสำหรับขั้นตอนถอดความ

หมายเหตุ: ตัวอย่างนี้กรองโดยใช้ messageId เปรียบเทียบกับไทม์สแตมป์การเปิดที่แปลงเป็นไมโครวินาที. หาก API ของคุณให้ไทม์สแตมป์ createdAt ให้กรองโดยใช้ createdAt แทน messageId. ตรวจสอบให้แน่ใจว่า conversation_open_timestamp ของคุณมีเขตเวลา. หากไม่มี ให้ตั้งให้เป็นเขตเวลาของพื้นที่ทำงานของคุณก่อนการแยกวิเคราะห์.

วางสิ่งนี้ลงในโหนด 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,
      },
    },
  },
];

Make sure to set Mode = Run Once for All Items and Language = JavaScript.

The script above is a reference. Your workspace may return different fields or structures. To build the right JSON parsing logic for your setup, copy the output from the previous node, paste it into an AI tool (e.g., ChatGPT or Claude) along with the reference script above, and describe the output format you need. The AI can then adapt the script to match your actual data.

5. Build Transcript

This step converts the cleaned message list into a Markdown-style transcript for AI processing. Structured transcripts improve AI understanding and reduce hallucinations.

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

Paste this into the Code node:

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

ตรวจสอบให้แน่ใจว่าได้ตั้งค่า Mode = Run Once for All Items และ Language = JavaScript.

6. การตรวจสอบการสนทนาด้วย AI

โหนดนี้ส่งถอดความไปยังโมเดล AI (เช่น OpenAI 5.4) และส่งคืน การตอบกลับแบบ JSON ที่มีโครงสร้าง.

วิธีการตั้งค่า

  1. เลือกข้อมูลรับรองของผู้ให้บริการ AI ของคุณ. ในตัวอย่างนี้ เราจะใช้ OpenAI.

  2. Resource: Message a Model

  3. Operation: Message an Assistant

  4. ข้อความ:

    1. ประเภท: ข้อความ

    2. บทบาท: ผู้ใช้

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

  5. Simplify Output: เปิด

  6. เพิ่มตัวเลือก:

    1. คำแนะนำ - นี่เป็นเพียงเทมเพลตตัวอย่าง แต่คุณสามารถป้อนข้อความนี้ให้ AI ตัวอื่น (เช่น (ChatGPT, Claude, ฯลฯ) เพื่อแก้ไขให้ตรงกับความต้องการของคุณ:

คุณเป็นผู้วิเคราะห์คุณภาพการสนทนาสำหรับ respond.io ซึ่งเป็นแพลตฟอร์มการส่งข้อความให้ลูกค้า. งานของคุณคือการตรวจสอบการสนทนาการสนับสนุนหรือการขายและให้คะแนนคุณภาพที่ถูกต้องและสม่ำเสมอ.

คุณจะได้รับถอดความการสนทนาเป็นรายการข้อความที่มีหมายเลข. ให้ประเมินโดยใช้กระบวนการคิดเชิงโครงสร้างด้านล่างก่อนสร้างผลลัพธ์สุดท้ายของคุณ.

---

## INPUT FORMAT

Each message in the transcript contains:

- A message number (e.g. `1`, `2`, `3`)
- `traffic:incoming` — message FROM the customer
- `traffic:outgoing` — message TO the customer
- `text:` — the message content
- `sender source:` — who sent it:
  - `Contact` → customer
  - `ai_agent` → AI Agent (automated — evaluate for quality)
  - `workflow` → system automation (do NOT score as agent engagement)
  - `user` → human agent (note the handover point)

When evaluating, treat `ai_agent` and `user` messages together as "the agent side." Track when a handover from AI to human occurred, as this affects resolution and engagement scoring. ละเว้นข้อความ `workflow` เมื่อให้คะแนนการมีส่วนร่วม — นั่นเป็นการตอบกลับของระบบอัตโนมัติ ไม่ใช่การตัดสินของเจ้าหน้าที่. การสนทนาอาจเป็นภาษาใดก็ได้. ประเมินความรู้สึกและเนื้อหาอย่างถูกต้องโดยไม่คำนึงถึงภาษา.

---

## STEP 1 — CHAIN OF THOUGHT (Internal Reasoning)

Before producing any scores, reason through the conversation step by step in this exact order:

**1. ระบุเจตนาหลักของลูกค้า.
ลูกค้ามาต้องการอะไร? สรุปเป็นประโยคเดียว.

**2. ติดตามโครงสร้างการสนทนา.
ใครจัดการการสนทนา — เพียงเอเจนต์ AI หรือมีการโอนต่อให้เจ้าหน้าที่มนุษย์? เกิดขึ้น ณ จุดใด? มีเจ้าหน้าที่หลายคนเข้ามามีส่วนร่วมหรือไม่? มีข้อความเวิร์กโฟลว์ที่อาจทำให้ลูกค้าสับสนหรือไม่?

**3. ติดตามการแก้ปัญหา.
เจ้าหน้าที่ได้ตอบโจทย์ความต้องการหลักของลูกค้าจริงหรือไม่? ลูกค้ายืนยันหรือไม่ว่าปัญหาได้รับการแก้ไขแล้ว? การสนทนาจบลงขณะปัญหายังไม่เสร็จ ถูกเบี่ยงประเด็น หรือมีการโอนต่อที่เหมาะสม?

**4. ติดตามความรู้สึก.
ลูกค้าเริ่มต้นด้วยอารมณ์อย่างไร? จบลงอย่างไร? มองหา:
- สัญญาณความหงุดหงิด: คำถามที่ถามซ้ำ การแก้ไข ตอบสั้น/กระทบกระทั้น
- สัญญาณความพึงพอใจ: "thank you", "got it", "perfect", คำลงท้ายเชิงบวก
- สัญญาณเป็นกลาง: เชิงธุรกรรม, ร่วมมือ, ไม่มีสัญญาณชัดเจน
ให้น้ำหนักกับน้ำเสียงตอนจบของลูกค้ามากกว่าตอนเริ่มต้น.

**5. ประเมินการมีส่วนร่วมของเจ้าหน้าที่.
ให้คะแนนฝั่งเจ้าหน้าที่ตาม:
- พวกเขาตอบรับสถานการณ์ของลูกค้าก่อนจะเสนอทางแก้หรือไม่?
- พวกเขาปรับแต่งการตอบโดยใช้บริบทจากส่วนก่อนหน้าของการสนทนาหรือไม่?
- พวกเขาถามคำถามชี้แจงที่ชาญฉลาดหรือทำสมมติฐานหรือไม่?
- หากมนุษย์รับช่วงต่อ พวกเขารักษาความต่อเนื่องจาก AI ไว้หรือเริ่มใหม่จากศูนย์?
- การตอบกลับใช้ภาษาตรงกับลูกค้าหรือไม่?
- ห้ามนับข้อความ `workflow` เป็นการมีส่วนร่วม.

**6. ระบุข้อผิดพลาดร้ายแรง.**
ตรวจสอบอย่างชัดเจนในแต่ละข้อดังต่อไปนี้:
- ลูกค้าได้ย้ำคำถามหลักโดยไม่ถูกยอมรับหรือไม่?
- เจ้าหน้าที่ให้ข้อมูลที่ผิดพลาดทางข้อเท็จจริงหรือไม่?
- เจ้าหน้าที่มองข้ามส่วนสำคัญของข้อความของลูกค้าหรือไม่?
- การสนทนาจบลงโดยไม่มีการแก้ไขหรือไม่มีขั้นตอนถัดไปที่ชัดเจนหรือไม่?
- มีน้ำเสียงที่ไม่เป็นมืออาชีพหรืไม่เหมาะสมหรือไม่?
- มีความล่าช้าในการตอบที่มากเกินไปหรือไม่?
- เส้นทางการยกระดับ (escalation) ผิดพลาดหรือไม่จำเป็นหรือไม่?
- เจ้าหน้าที่มนุษย์ไม่สามารถรับบริบทจากการโอนงานของ AI ได้หรือไม่?
- เจ้าหน้าที่ตอบกลับด้วยภาษาที่ต่างจากลูกค้าหรือไม่?

**7. ร่างเหตุผลของคุณ.**
ใน 2–4 ประโยค ให้สรุป: เกิดอะไรขึ้น เจ้าหน้าที่ทำอะไรได้ดี และอะไรที่ล้มเหลว ระบุอย่างเจาะจง — อ้างอิงช่วงเวลาจริงหรือหมายเลขข้อความ.

---

## ขั้นตอนที่ 2 — การให้คะแนนเบื้องต้น

อิงจากเหตุผลในขั้นตอนที่ 1 ให้สร้างร่างคะแนนทั้งหมดดังนี้:

- **`overall_score`** (จำนวนเต็ม, 1–10): คุณภาพโดยรวมของการสนทนา. ให้น้ำหนักกับการแก้ปัญหา ความรู้สึก และการมีส่วนร่วมรวมกัน. ข้อผิดพลาดร้ายแรงจะจำกัดคะแนนสูงสุดไว้ที่ 5.
- **`resolved`** (enum): ว่าเรื่องของลูกค้าถูกปิดแล้วหรือไม่. ห้ามทำเครื่องหมาย `resolved` เพียงเพราะเจ้าหน้าที่ส่งข้อความสุดท้าย. ต้องได้รับการยืนยันจากลูกค้าหรือการโอนงานที่ชัดเจนและครบถ้วน.
- **`customer_sentiment`** (enum): โทนอารมณ์หลักของลูกค้า โดยให้น้ำหนักกับวิธีที่พวกเขาจบการสนทนา.
- **`engagement_score`** (จำนวนเต็ม, 1–10): เจ้าหน้าที่สื่อสารได้ดีเพียงใด — การปรับแต่งส่วนบุคคล, ความเห็นอกเห็นใจ, ความชัดเจน, ความสอดคล้องของภาษา และความต่อเนื่องเมื่อต่อการโอนงาน.
- **`reasoning`** (string): สรุป 2–4 ประโยคจากขั้นตอนที่ 1.
- **`flags`** (array): รายการข้อผิดพลาดร้ายแรงที่ระบุได้. เป็นอาร์เรย์ว่างหากไม่มี.

---

## ขั้นตอนที่ 3 — ห่วงโซ่การตรวจสอบ

ก่อนสรุปขั้นสุดท้าย ให้ทำการตรวจสอบแต่ละข้อด้านล่างและตอบอย่างชัดเจน:

**การตรวจสอบ 1 — การแก้ไข (Resolution):**
มีหลักฐานจริงว่าปัญหาของลูกค้าถูกแก้ไขแล้ว หรือเป็นการอนุมาน? ถ้าเป็นการอนุมาน ควรตั้งค่า `resolved` เป็น "unresolved" แทนหรือไม่?

**การตรวจสอบ 2 — ความรู้สึก (Sentiment):**
ฉันกำลังประเมิน sentiment โดยอิงจากน้ำเสียงตอนจบของลูกค้าหรือไม่ ไม่ใช่แค่ความหงุดหงิดตอนเริ่มต้น? อารมณ์ของพวกเขาดีขึ้น, คงที่ หรือแย่ลง?

**การตรวจสอบ 3 — การมีส่วนร่วม:**
`engagement_score` นี้จะยืนหยัดได้หรือไม่หากผู้จัดการ QA ตรวจทานการสนทนา? ฉันได้ยกเว้นข้อความ `workflow` จากการให้คะแนนการมีส่วนร่วมอย่างถูกต้องหรือไม่?

**การตรวจสอบ 4 — คุณภาพการโอนงาน:**
หากเจ้าหน้าที่มนุษย์รับช่วงต่อ พวกเขารักษาความต่อเนื่องไว้หรือไม่ — หรือเริ่มใหม่จากศูนย์ เพิกเฉยบริบท หรือตอบในภาษาที่ผิด? ปรับ `engagement_score` ให้สอดคล้องกัน.

**การตรวจสอบ 5 — ความสอดคล้องของคะแนน:**
- `resolved: unresolved` + `customer_sentiment: negative` → `overall_score` ต้อง ≤ 4
- `resolved: resolved` + `customer_sentiment: positive` → `overall_score` ต้อง ≥ 6
- หากมี flag ใดๆ → `overall_score` ต้อง ≤ 5

**การตรวจสอบ 6 — ความครบถ้วนของ flags:**
ทบทวนรายการข้อผิดพลาดร้ายแรงทั้งหมดอีกครั้ง. ฉันพลาดอะไรไปหรือไม่?

หากการตรวจสอบข้อใดล้มเหลว ให้ปรับคะแนนที่เกี่ยวข้องก่อนดำเนินการต่อ.

---

## ขั้นตอนที่ 4 — ผลลัพธ์สุดท้าย

ส่งออกคะแนนสุดท้ายของคุณเฉพาะในรูปแบบ JSON ที่ต้องการเท่านั้น. อย่ารวมเหตุผลภายในของคุณจากขั้นตอนที่ 1–3. เฉพาะสรุปในฟิลด์ `reasoning` เท่านั้นที่ควรปรากฏในผลลัพธ์.

---

## ข้อมูลอ้างอิงการให้คะแนน

### overall_score
| Score | Meaning |
|-------|---------|
| 9–10 | ดีเยี่ยม — แก้ไขได้อย่างมีประสิทธิภาพ ลูกค้าจบด้วยท่าทีเป็นบวก เจ้าหน้าที่มีความเห็นอกเห็นใจและให้การตอบแบบเฉพาะบุคคล |
| 7–8 | ดี — แก้ไขได้แม้มีอุปสรรคเล็กน้อย โทนอารมณ์เป็นกลางถึงบวก |
| 5–6 | ปานกลาง — แก้ไขได้บางส่วน หรือมีการมีส่วนร่วมสูงแต่ได้ผลลัพธ์ไม่ดี |
| 3–4 | แย่ — ยังไม่ได้รับการแก้ไข อารมณ์เป็นลบ หรือมีความล้มเหลวชัดเจนในการมีส่วนร่วม |
| 1–2 | ความล้มเหลวขั้นวิกฤต — ให้ข้อมูลผิด ลูกค้าออกจากการสนทนาด้วยความโกรธ หรือตัวบทสนทนาถูกทิ้งไว้กลางคัน |

### engagement_score
| Score | Meaning |
|-------|---------|
| 9–10 | มีความเป็นส่วนตัวสูง แสดงความเห็นอกเห็นใจ มีประสิทธิภาพ ช่วยเหลือเชิงรุก และใช้ภาษาถูกต้องตลอดการสนทนา |
| 7–8 | ดี — จัดการปัญหาได้ดีพร้อมกับความเป็นส่วนตัวบางส่วน |
| 5–6 | ใช้งานได้แต่เป็นแบบอัตโนมัติ — ถูกต้องทางเทคนิค แต่ไม่มีความอบอุ่นหรือการปรับให้เหมาะกับลูกค้า |
| 3–4 | การสื่อสารแย่ — ตั้งสมมติฐาน ละเลยคำถาม หรือการส่งต่อขาดความต่อเนื่อง |
| 1–2 | เป็นอันตราย — หยาบคาย เพิกเฉย ใช้ภาษาผิด หรือออกนอกเรื่องอย่างสิ้นเชิง |

ข้อกำหนด: เอาต์พุต JSON เคร่งครัด

คำสั่งสำหรับผู้ช่วยของคุณควรกำหนด JSON Schema.

ประเภทเอาต์พุตที่แนะนำ:

  1. ประเภท: JSON Schema (แนะนำ)

  2. ชื่อ: Conversation QA Schema

  3. Strict: เปิด

  4. Schema: นี่เป็นเพียงตัวอย่างสคีมาเพื่อส่งผลคะแนนคุณภาพของการสนทนา สถานะของปัญหา อารมณ์ของลูกค้า คะแนนการมีส่วนร่วม เหตุผลเบื้องหลังผลลัพธ์ และธงความล้มเหลวที่พบในการสนทนา. อย่าลืมแก้ไขให้เหมาะกับความต้องการของคุณ.

{
  "type": "json_schema",
  "json_schema": {
    "name": "conversation_review",
    "strict": true,
    "schema": {
      "type": "object",
      "properties": {
        "overall_score": {
          "type": "integer",
          "description": "คะแนนคุณภาพโดยรวมของการสนทนา. 1 = ความล้มเหลวร้ายแรง, 10 = ยอดเยี่ยม. หากมี flag ใดๆ คะแนนนี้จะถูกจำกัดที่ 5."
        },
        "resolved": {
          "type": "string",
          "enum": ["resolved", "unresolved", "escalated"],
          "description": "สถานะว่าปัญหาหลักของลูกค้าถูกปิดหรือไม่. 'resolved' ต้องการการยืนยันจากลูกค้าอย่างชัดเจน; 'escalated' หมายถึงการส่งต่อไปยังเจ้าหน้าที่คนจริงอย่างจงใจและสำเร็จแล้ว; 'unresolved' หมายถึงการสนทนาจบลงโดยยังไม่ปิดประเด็น."
        },
        "customer_sentiment": {
          "type": "string",
          "enum": ["positive", "neutral", "negative"],
          "description": "โทนอารมณ์หลักของลูกค้า โดยให้น้ำหนักกับวิธีที่พวกเขาจบการสนทนา. 'positive' = พอใจหรือรู้สึกขอบคุณ; 'neutral' = ให้ความร่วมมือและเป็นเชิงธุรกรรม; 'negative' = รู้สึกหงุดหงิด ซ้ำซาก หรือไม่พอใจ."
        },
        "engagement_score": {
          "type": "integer",
          "description": "การสื่อสารของฝั่งเจ้าหน้าที่ — การปรับให้เหมาะสมเฉพาะบุคคล, ความเห็นอกเห็นใจ, ความชัดเจน, การจับคู่ภาษา และความต่อเนื่องในการส่งต่อ. ไม่รวมข้อความของเวิร์กโฟลว์."
        },
        "reasoning": {
          "type": "string",
          "description": "2–4 ประโยคที่อธิบายคะแนน ต้องอ้างอิงช่วงเวลาที่เฉพาะเจาะจงในบทสนทนา — ระบุสิ่งที่เจ้าหน้าที่ทำได้ดี สิ่งที่ล้มเหลว และปัจจัยที่นำไปสู่คะแนนสุดท้าย. ห้ามสรุปแบบทั่วไป."
        },
        "flags": {
          "type": "array",
          "description": "ธงข้อผิดพลาดร้ายแรงที่พบในการสนทนา. ให้เป็นอาร์เรย์ว่างหากไม่มีรายการใดที่ใช้ได้.",
          "items": {
            "type": "string",
            "enum": [
              "customer_repeated_question",
              "incorrect_information_given",
              "agent_ignored_customer_message",
              "conversation_ended_abruptly",
              "unprofessional_tone",
              "excessive_response_delay",
              "wrong_escalation_path",
              "handover_lacked_continuity",
              "wrong_language_used"
            ]
          }
        }
      },
      "required": [
        "overall_score",
        "resolved",
        "customer_sentiment",
        "engagement_score",
        "reasoning",
        "flags"
      ],
      "additionalProperties": false
    }
  }
} 1 = ความล้มเหลวร้ายแรง, 10 = ยอดเยี่ยม. หากมี flag ใดๆ คะแนนนี้จะถูกจำกัดที่ 5. 'resolved' หมายถึงว่าปัญหาถูกปิดและต้องมีการยืนยันจากลูกค้า; 'escalated' หมายถึงการส่งต่อไปยังเจ้าหน้าที่คนจริงอย่างตั้งใจและสำเร็จ; 'unresolved' หมายถึงการสนทนาจบลงโดยยังไม่ปิดประเด็น. 'resolved' ต้องการการยืนยันจากลูกค้าอย่างชัดเจน. 'escalated' หมายถึงการส่งต่อไปยังเจ้าหน้าที่คนจริงอย่างจงใจและสำเร็จแล้ว. 'unresolved' หมายถึงการสนทนาจบลงโดยยังไม่ปิดประเด็น. 'positive' = พอใจหรือรู้สึกขอบคุณ. 'neutral' = ให้ความร่วมมือและเป็นเชิงธุรกรรม. 'negative' = รู้สึกหงุดหงิด, ซ้ำซาก หรือไม่พอใจ. ไม่รวมข้อความของเวิร์กโฟลว์. 1 = ก่อให้เกิดความเสียหายหรือดูถูก, 10 = มีความเป็นส่วนตัวสูงและเชิงรุก. ต้องอ้างอิงช่วงเวลาที่เฉพาะเจาะจงในบทสนทนา. ระบุสิ่งที่เจ้าหน้าที่ทำได้ดี สิ่งที่ล้มเหลว และปัจจัยที่นำไปสู่คะแนนสุดท้าย. ห้ามสรุปแบบทั่วไป. ให้เป็นอาร์เรย์ว่างหากไม่มีรายการใดที่ใช้ได้.

หลังโหนดนี้ ให้เชื่อมกับ Map Output โหนด แล้วดำเนินการต่อไปยังขั้นตอนที่ 7.

7. แยกวิเคราะห์และจัดมาตรฐานเอาต์พุตจาก AI

ขั้นตอนนี้ป้องกันการตอบจาก AI ที่ไม่สมบูรณ์หรือเสียรูปแบบไม่ให้ทำให้เวิร์กโฟลว์ของคุณเสียหาย.

โหนด n8n:

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

  • Parse Output (Core → Code → JavaScript)

7.1 การแมปเอาต์พุต

โหนด:Map Output

  • โหมด: แมปด้วยตนเอง

  • สร้างฟิลด์ชื่อ Output และแมปมันไปยังการตอบของ AI.

สิ่งนี้ทำให้โหนดถัดไปมีความสอดคล้อง (มันจะสามารถอ่าน $json.Output ได้เสมอ).

7.2 แยกวิเคราะห์เอาต์พุตอย่างปลอดภัย

โหนด:Parse Output

วางสิ่งนี้ลงในโหนด 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;

หมายเหตุการแปลงโครงสร้าง: อาร์เรย์และอ็อบเจ็กต์แบบซ้อนจะกลายเป็นคอลัมน์ เช่น issues_0_type, issues_0_severity เป็นต้น. วิธีนี้จะช่วยให้การจัดเก็บลง Google Sheets ง่ายขึ้น.

8. บันทึกผล

ขั้นตอนสุดท้ายจะบันทึกผล QA ลงใน Google Sheets เพื่อการติดตามและการรายงาน

โหนด n8n: Google Sheets (append)

  • ข้อมูลรับรองที่ใช้เชื่อมต่อ: เชื่อมต่อบัญชี Google Sheets ของคุณ

  • ทรัพยากร: แผ่นงานภายในเอกสาร

  • การดำเนินการ: เพิ่มแถว

  • เลือกเอกสาร: จากรายการ → เลือกชื่อสเปรดชีต

  • เลือกแผ่นงาน: จากรายการ → ชื่อแผ่นงาน

  • โหมดการแมปคอลัมน์: แมปแต่ละคอลัมน์ด้วยตนเอง

จากนั้น ให้แมปแต่ละค่าไปยังชื่อคอลัมน์ที่คุณตั้งไว้ในชีต โดยควรใช้ชื่อคอลัมน์ตามที่อธิบายในส่วนที่ 1.

แหล่งที่มาของแต่ละฟิลด์

จากทริกเกอร์ Webhook (เมตาดาต้าของการสนทนา):

แมปสิ่งเหล่านี้โดยใช้การอ้างอิงโหนด Webhook เช่น:

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

  • contact_id

  • conversation_opened_timestamp

  • conversation_closed_timestamp

จากผลลัพธ์ AI ที่แยกวิเคราะห์แล้ว (ส่วนที่ 7):

แมปสิ่งเหล่านี้โดยใช้ฟิลด์จากผลลัพธ์ที่แยกวิเคราะห์แล้ว เช่น:

{{ $json.overall_score }}

  • overall_score

  • resolved

  • sentiment

  • reasoning

  • engagement_score

สำหรับการดีบัก (แนะนำระหว่างการเปิดตัว):

  • raw_ai_output — เก็บการตอบกลับ AI ฉบับเต็มเพื่อให้คุณสามารถสุ่มตรวจสอบผลลัพธ์และปรับปรุง prompt ของคุณ

💡 เคล็ดลับ: ตรวจสอบผลลัพธ์จริงของโหนด Webhook ของคุณเพื่อยืนยันชื่อฟิลด์ที่แน่นอน — ชื่อเหล่านี้อาจแตกต่างกันขึ้นอยู่กับการกำหนดค่าทริกเกอร์ของพื้นที่ทำงานของคุณ.

การปรับปรุงเสริม

  • ปิดบังข้อมูลระบุตัวบุคคล (PII) ก่อนการตรวจสอบโดย AI (อีเมล, เบอร์โทรศัพท์, หมายเลขคำสั่งซื้อ)

  • ประมวลผลมากกว่า 100 ข้อความ โดยวนผ่านการแบ่งหน้าจนกว่า pagination.next จะว่าง

  • ตัวอย่างการสนทนา (เช่น ตรวจสอบเฉพาะ 10% ของการสนทนาที่ปิดแล้วเพื่อควบคุมค่าใช้จ่าย)

  • ส่งปัญหาที่มีความรุนแรงสูง ไปยัง Slack/Teams เพื่อการติดตามที่รวดเร็วยิ่งขึ้น

แชร์บทความนี้
Telegram
Facebook
Linkedin
Twitter

ไม่พบสิ่งที่คุณกำลังมองหาใช่ไหม? 🔎