Khuyến nghị khách hàng nên rà soát các cuộc hội thoại do AI xử lý sau khi bật AI Agent, nhưng QA thủ công trở nên quá tốn thời gian khi mở rộng quy mô. Hướng dẫn này trình bày cách tự động hóa QA cuộc hội thoại bằng n8n + mô hình AI, để mọi cuộc hội thoại đã đóng đều được rà soát tự động và ghi vào Google Sheets để báo cáo.
Tóm lại, workflow n8n này được kích hoạt mỗi khi một cuộc hội thoại được đóng. Khi được kích hoạt, nó gọi List Messages API để lấy 50 tin nhắn gần nhất (tuỳ chọn lấy đến 100 bằng phân trang), rồi chuyển bản ghi hội thoại cho mô hình AI để xử lý QA — như phân tích cảm xúc, chấm điểm tương tác và kiểm tra tình trạng giải quyết. Kết quả được lưu trong Google Sheet để bạn dễ dàng tra cứu bất kỳ cuộc hội thoại nào theo ID liên hệ.
Bạn sẽ xây dựng
Một quy trình tự động gồm:
Kích hoạt khi một cuộc hội thoại được đóng
Truy xuất 50 tin nhắn gần nhất bằng List Messages API (tuỳ chọn tới 100 với phân trang)
Làm sạch và định dạng tin nhắn thành bản ghi hội thoại
Gửi bản ghi cho mô hình AI để đánh giá QA
Phân tích đầu ra AI một cách an toàn (chỉ theo hợp đồng JSON)
Ghi kết quả vào Google Sheets (một hàng cho mỗi cuộc hội thoại)
Yêu cầu
Công cụ
Nền tảng tự động hóa: n8n
Respond.io Developer API — điểm cuối List Messages
AI API — OpenAI, Anthropic, Gemini, hoặc bất kỳ nhà cung cấp nào được n8n hỗ trợ
Báo cáo / lưu trữ — Google Sheets (hoặc công cụ lưu trữ bạn chọn)
Thông tin xác thực
Khóa Respond.io Developer API
Khóa API của nhà cung cấp AI (OpenAI, Anthropic, Gemini, v.v.)
Thông tin xác thực Google Sheets trong n8n (OAuth hoặc tài khoản dịch vụ)
Trước khi bắt đầu
1) Tạo một Google Sheet để lưu kết quả QA
Tạo một bảng tính và thêm một sheet (ví dụ QA_Results) với các tiêu đề sau:
timestampcontact_idopened_atchanneloverall_scoreresolvedcustomer_sentimentreasoningengagement_score
2) Xác nhận các trường payload của trigger
Từ đầu ra Conversation closed trigger, xác định chính xác tên các trường cho:
ID cuộc hội thoại
ID liên hệ
Dấu thời gian mở cuộc hội thoại
Dấu thời gian đóng cuộc hội thoại (tuỳ chọn)
Tên trường thay đổi theo từng triển khai. Trong các node code bên dưới, bạn sẽ thấy các placeholder như
trigger.conversationId. Cập nhật các mapping đó để khớp với đầu ra trigger của bạn.
Hướng dẫn từng bước
1. Kích hoạt: Conversation Closed
Khi một cuộc hội thoại được đóng trên respond.io, chúng ta muốn tự động kích hoạt workflow n8n. Điều này đảm bảo mọi cuộc hội thoại hoàn tất đều được kiểm tra QA — không cần rà soát thủ công.
n8n node:Conversation closed trigger
Trong n8n, thêm node trigger respond.io.
Chọn Conversation Closed.
Kết nối khóa Respond.io Developer API của bạn. Tìm hiểu cách thiết lập trong n8n với hướng dẫn tích hợp của chúng tôi.
Kích hoạt này đảm bảo bạn chỉ rà soát cuộc hội thoại sau khi chúng đã kết thúc.
2. Lấy tin nhắn cuộc hội thoại
Ở bước này, bạn sẽ lấy 50 tin nhắn đến và đi gần nhất bằng List Messages API. Đây thường là đủ để thực hiện một kiểm tra QA kỹ lưỡng. Nếu cần thêm ngữ cảnh, bạn có thể lấy thêm 50 tin nhắn (tối đa 100 tổng) bằng phân trang.
Các node n8n:
get 1st 50 messages(HTTP Request)Is there a second page?(IF) — tuỳ chọnget 2nd 50 messages(HTTP Request) — tuỳ chọn
2.1 Lấy 50 tin nhắn đầu tiên
Node:get 1st 50 messages
Chọn Core > HTTP Request
Method:
GETURL: endpoint List Messages của bạn
Ví dụ URL (thay bằng URL cơ sở API thực tế của bạn + định dạng endpoint):
https://api.respond.io/v2/contact/{{identifier}}/message/list
Trong đó identifier cần được thay bằng một trong các định dạng sau: id:<contactID>, phone:+<countryCodeAndPhone>, hoặc email:<contactEmail>
Bật Send Query Parameters > Using Fields Below, rồi thêm:
Tên:
limitGiá trị:
50
Nếu API của bạn hỗ trợ, hãy đặt sắp xếp từ cũ → mới. Ví dụ: sort=asc.
Bật Send Headers > Using Fields Below, rồi thêm:
Tên:
AcceptGiá trị:
application/json
Tên:
AuthorizationGiá trị:
Bearer <your_respond_api_key>
Nếu phản hồi List Messages của bạn có
pagination.next, bạn có thể dùng trực tiếp để lấy trang thứ hai.
2.2 Kiểm tra và lấy nhiều hơn 50 tin nhắn (Tuỳ chọn)
Nếu 50 tin nhắn không đủ cho nhu cầu QA của bạn, bạn có thể lấy trang thứ hai gồm 50 tin nhắn. Thêm một node IF để kiểm tra xem còn tin nhắn hay không.
Node:Is there a second page? (IF)
Giá trị bên trái (Biểu thức):
{{ $json.pagination.next }}Toán tử: không rỗng
Nếu đúng, tức là còn trang tin nhắn để lấy. Nếu sai, workflow tiếp tục mà không lấy thêm — điều này ngăn workflow lỗi khi không có trang thứ hai. Node Merge phía sau sẽ đợi cả hai nhánh hoàn thành, nên workflow vẫn chạy mượt mà dù có hay không có trang thứ hai.
2.3 Lấy 50 tin nhắn tiếp theo (Tuỳ chọn)
Nếu node IF trả đúng (tồn tại trang thứ hai), hãy lấy lô tin nhắn tiếp theo.
Node:get 2nd 50 messages
Thêm một HTTP Request node nữa.
Thiết lập:
Method:
GETURL:
{{ $json.pagination.next }}
Thêm các header giống trước:
Accept:
application/jsonAuthorization:
Bearer <your_respond_api_key>
3. Gộp các trang tin nhắn (Tuỳ chọn)
Bước này chỉ cần khi bạn lấy hơn 50 tin nhắn. Nếu bạn chọn không lấy trang thứ hai, có thể bỏ node này và kết nối trực tiếp
get 1st 50 messagestới bước tiếp theo (Clean + keep messages since last open).
Node Merge kết hợp 50 tin nhắn đầu với 50 tin nhắn thứ hai thành một danh sách duy nhất. Nếu không có node này, workflow không thể xử lý hai phản hồi API riêng biệt cùng lúc.
n8n node:Merge
Thêm Flow > Merge.
Đặt Mode thành Append.
Đặt Number of Inputs:
2Kết nối:
Input 1:
get 1st 50 messagesInput 2:
get 2nd 50 messages
4. Làm sạch và lọc tin nhắn
Phản hồi API vẫn thô và chứa nhiều thông tin thừa mà AI không cần. Bước này sẽ làm sạch — loại bỏ tin nhắn cũ, chuẩn hoá nhãn người gửi và cấu trúc lại thành danh sách đơn giản sẵn sàng cho xử lý AI. Bạn chỉ cần thêm Code node và dán JavaScript bên dưới.
n8n node:Clean + keep messages since last convo open
Để làm điều này, Thêm node → Chọn Core → Code → Chọn Code bằng JavaScript.
Đoạn mã này sẽ:
Kết hợp tin nhắn từ cả hai trang
Lọc tin nhắn gửi trước khi cuộc hội thoại mở
Sắp xếp tin nhắn từ cũ → mới
Chuẩn hoá người gửi thành nhãn nhất quán:
Contact,AI Agent,Human Agent,WorkflowThêm số chỉ mục tin nhắn
Trả về một danh sách sạch, có cấu trúc cho bước tạo bản ghi hội thoại
Ghi chú: Ví dụ này lọc bằng
messageIdso với thời điểm mở được chuyển sang microgiây. Nếu API của bạn cung cấp timestampcreatedAt, hãy lọc bằngcreatedAtthay vìmessageId. Đảm bảoconversation_open_timestampcủa bạn bao gồm múi giờ. Nếu không, hãy đặt nó về múi giờ không gian làm việc của bạn trước khi phân tích.
Dán đoạn này vào 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,
},
},
},
];Hãy chắc chắn đặt Mode = Run Once for All Items và Language = JavaScript.
Đoạn script trên chỉ mang tính tham khảo. Workspace của bạn có thể trả về các trường hoặc cấu trúc khác nhau. Để xây dựng logic phân tích JSON phù hợp với cài đặt của bạn, sao chép đầu ra từ node trước, dán vào công cụ AI (ví dụ ChatGPT hoặc Claude) cùng với script tham khảo ở trên, và mô tả định dạng đầu ra bạn cần. AI sau đó có thể điều chỉnh script để khớp dữ liệu thực tế của bạn.
5. Xây dựng bản ghi hội thoại
Bước này chuyển danh sách tin nhắn đã làm sạch thành bản ghi hội thoại kiểu Markdown để xử lý bởi AI. Bản ghi có cấu trúc giúp AI hiểu tốt hơn và giảm hiện tượng tạo thông tin sai (hallucination).
n8n node:Build markdown transcript (Core → Code → JavaScript)
Dán đoạn này vào 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;Hãy chắc chắn đặt Mode = Run Once for All Items và Language = JavaScript.
6. Rà soát cuộc hội thoại dựa trên AI
Node này gửi bản ghi cho mô hình AI (ví dụ OpenAI 5.4) và trả về một phản hồi JSON có cấu trúc.
Cách thiết lập
Chọn thông tin xác thực nhà cung cấp AI của bạn. Trong ví dụ này, chúng ta sẽ dùng OpenAI.
Tài nguyên: Message a Model
Hoạt động: Message an Assistant
Tin nhắn:
Loại: Văn bản
Vai trò: Người dùng
Lệnh:
{{ $json.transcript }}
Đơn giản hóa kết quả: Bật
Thêm tùy chọn:
Instructions - Đây chỉ là mẫu ví dụ, nhưng bạn có thể nhập nó vào một AI khác (ví dụ: ChatGPT, Claude, v.v.) để chỉnh sửa theo nhu cầu của bạn:
Bạn là một chuyên viên phân tích chất lượng cuộc hội thoại cho respond.io, nền tảng nhắn tin chăm sóc khách hàng. Công việc của bạn là rà soát các cuộc hội thoại hỗ trợ hoặc bán hàng và đưa ra điểm chất lượng chính xác, nhất quán.
Bạn sẽ nhận được bản ghi hội thoại dưới dạng danh sách có số thứ tự các tin nhắn. Đánh giá theo quy trình suy luận có cấu trúc bên dưới trước khi đưa ra kết quả cuối cùng.
---
## INPUT FORMAT
Mỗi tin nhắn trong bản ghi chứa:
- Một số tin nhắn (ví dụ `1`, `2`, `3`)
- `traffic:incoming` — tin nhắn TỪ khách hàng
- `traffic:outgoing` — tin nhắn ĐẾN khách hàng
- `text:` — nội dung tin nhắn
- `sender source:` — ai gửi:
- `Contact` → khách hàng
- `ai_agent` → Trợ lý AI (tự động — đánh giá chất lượng)
- `workflow` → automation hệ thống (KHÔNG chấm điểm là tương tác của nhân viên)
- `user` → nhân viên (ghi nhận điểm chuyển giao)
Khi đánh giá, xem `ai_agent` và `user` cùng là phía "nhân viên". Theo dõi khi nào có chuyển giao từ AI sang con người, vì điều này ảnh hưởng tới điểm giải quyết và điểm tương tác. Bỏ qua các tin nhắn `workflow` khi chấm điểm tương tác — đó là phản hồi hệ thống tự động, không phải quyết định của con người. Cuộc hội thoại có thể bằng bất kỳ ngôn ngữ nào. Đánh giá cảm xúc và nội dung chính xác bất kể ngôn ngữ.
---
## BƯỚC 1 — CHAIN OF THOUGHT (Suy luận nội bộ)
Trước khi đưa ra điểm, hãy suy luận qua cuộc hội thoại theo thứ tự chính xác sau: Xác định ý định cốt lõi của khách hàng.**
Khách hàng đến để làm gì? Nêu nó trong một câu.
**2. Theo dõi cấu trúc cuộc hội thoại.**
Ai xử lý cuộc hội thoại — chỉ Trợ lý AI hay có chuyển giao sang con người? Vào thời điểm nào? Có nhiều nhân viên (con người) tham gia không? Có tin nhắn workflow nào có thể làm khách hàng bối rối không?
**3. Theo dõi độ giải quyết.**
Agent có thực sự giải quyết ý định cốt lõi của khách hàng không? Khách hàng có xác nhận vấn đề được giải quyết không? Cuộc hội thoại có kết thúc khi vấn đề còn dang dở, bị tránh né, hoặc với một chuyển giao phù hợp không?
**4. Theo dõi cảm xúc.**
Khách hàng bắt đầu với tâm trạng thế nào? Kết thúc thế nào? Tìm các dấu hiệu:
- Dấu hiệu bực bội: câu hỏi lặp lại, sửa lỗi, trả lời ngắn/gắt
- Dấu hiệu hài lòng: "cảm ơn", "hiểu rồi", "tuyệt vời", lời chào tích cực
- Dấu hiệu trung tính: giao dịch, hợp tác, không tín hiệu mạnh
Ưu tiên trọng số cho tâm trạng khi kết thúc hơn khi bắt đầu.
**5. Đánh giá mức độ tương tác của nhân viên.**
Chấm phía nhân viên về:
- Họ có ghi nhận tình huống của khách trước khi đề xuất giải pháp không?
- Họ có cá nhân hoá phản hồi bằng ngữ cảnh từ các tin nhắn trước không?
- Họ có hỏi câu hỏi làm rõ thông minh, hay đưa ra giả định?
- Nếu có con người tiếp quản, họ có tiếp tục mạch từ AI — hay bắt đầu lại từ đầu?
- Các phản hồi có cùng ngôn ngữ với khách hàng không?
- KHÔNG tính các tin nhắn `workflow` vào điểm tương tác.
**6. Xác định lỗi nghiêm trọng.**
Kiểm tra từng mục sau:
- Khách hàng có lặp lại câu hỏi chính mà không được xác nhận không?
- Nhân viên có cung cấp thông tin sai sự thật không?
- Nhân viên có phớt lờ phần quan trọng trong tin nhắn của khách hàng không?
- Cuộc hội thoại có kết thúc mà không có giải pháp hay hướng tiếp theo rõ ràng không?
- Có giọng điệu thiếu chuyên nghiệp hoặc không phù hợp không?
- Có độ trễ phản hồi quá mức không?
- Quy trình chuyển tiếp có sai hoặc không cần thiết không?
- Nhân viên (con người) có không nắm được ngữ cảnh sau khi AI chuyển giao không?
- Nhân viên có trả lời bằng ngôn ngữ khác với khách hàng không?
**7. Soạn lập luận của bạn.**
Trong 2–4 câu, tóm tắt: đã xảy ra gì, nhân viên đã làm tốt điều gì, và điều gì đã thất bại. Cụ thể — tham chiếu đến các khoảnh khắc hoặc số tin nhắn cụ thể.
---
## BƯỚC 2 — CHẤM ĐIỂM BAN ĐẦU
Dựa trên suy luận Bước 1, tạo bản nháp tất cả điểm:
- **`overall_score`** (số nguyên, 1–10): Điểm chất lượng tổng thể của cuộc hội thoại. Cân nhắc trọng số của khả năng giải quyết, cảm xúc và mức độ tương tác cùng nhau. Lỗi nghiêm trọng tự động giới hạn điểm tối đa là 5.
- **`resolved`** (enum): Liệu vấn đề của khách hàng đã được đóng hay chưa. KHÔNG đánh dấu `resolved` chỉ vì nhân viên gửi tin nhắn cuối cùng. Cần có xác nhận của khách hàng hoặc chuyển giao đã được hoàn tất rõ ràng.
- **`customer_sentiment`** (enum): Xu hướng cảm xúc chủ đạo của khách hàng, ưu tiên cách họ kết thúc cuộc hội thoại.
- **`engagement_score`** (integer, 1–10): Mức độ giao tiếp của nhân viên — cá nhân hoá, đồng cảm, rõ ràng, khớp ngôn ngữ và duy trì tính liên tục khi chuyển giao.
- **`reasoning`** (string): Bản tóm tắt 2–4 câu từ Bước 1.
- **`flags`** (array): Danh sách lỗi nghiêm trọng đã xác định. Mảng rỗng nếu không có.
---
## BƯỚC 3 — CHUỖI XÁC MINH
Trước khi hoàn tất, chạy từng kiểm tra sau và trả lời rõ ràng: Nếu suy luận, có nên để `resolved` = "unresolved" không?
**Kiểm tra 2 — Cảm xúc:**
Tôi có đang đánh giá cảm xúc dựa trên giọng điệu kết thúc của khách hàng, không chỉ dựa trên sự bực bội ban đầu không? Tâm trạng của họ đã cải thiện, giữ nguyên hay tệ đi?
**Kiểm tra 3 — Tương tác:**
`engagement_score` này có đứng vững nếu QA manager rà soát không? Tôi đã loại trừ đúng các tin nhắn `workflow` khỏi điểm tương tác chứ?
**Kiểm tra 4 — Chất lượng chuyển giao:**
Nếu con người tiếp quản, họ có giữ mạch — hay bắt đầu lại, bỏ qua ngữ cảnh, hoặc trả lời bằng ngôn ngữ sai? Điều chỉnh `engagement_score` tương ứng.
**Kiểm tra 5 — Tính nhất quán điểm:**
- `resolved: unresolved` + `customer_sentiment: negative` → `overall_score` phải ≤ 4
- `resolved: resolved` + `customer_sentiment: positive` → `overall_score` phải ≥ 6
- Có bất kỳ flag nào → `overall_score` phải ≤ 5
**Kiểm tra 6 — Độ đầy đủ của flags:**
Rà soát lại danh sách lỗi nghiêm trọng. Tôi có bỏ sót gì không?
Nếu bất kỳ kiểm tra nào thất bại, sửa lại điểm liên quan trước khi tiếp tục.
---
## BƯỚC 4 — KẾT QUẢ CUỐI
Xuất điểm cuối cùng ở định dạng JSON yêu cầu. Không đưa ra suy luận nội bộ Bước 1–3. Chỉ phần tóm tắt của trường `reasoning` được xuất trong đầu ra.
---
## THAM CHIẾU CHẤM ĐIỂM
### overall_score
| Điểm | Ý nghĩa |
|-------|---------|
| 9–10 | Xuất sắc — giải quyết hiệu quả, khách hàng kết thúc tích cực, nhân viên thể hiện đồng cảm và cá nhân hóa |
| 7–8 | Tốt — đã giải quyết với vài khúc mắc nhỏ, cảm xúc trung tính đến tích cực |
| 5–6 | Trung bình — giải quyết một phần, hoặc tương tác cao nhưng kết quả kém |
| 3–4 | Kém — chưa giải quyết, cảm xúc tiêu cực, hoặc có lỗi rõ ràng trong tương tác |
| 1–2 | Thất bại nghiêm trọng — cung cấp thông tin sai, khách hàng rời đi giận dữ, hoặc cuộc hội thoại bị bỏ dở |
### engagement_score
| Điểm | Ý nghĩa |
|-------|---------|
| 9–10 | Rất cá nhân hóa, đồng cảm, hiệu quả, chủ động hỗ trợ, sử dụng ngôn ngữ đúng |
| 7–8 | Ổn — giải quyết vấn đề tốt với phần nào đó được cá nhân hóa |
| 5–6 | Vận hành nhưng máy móc — kỹ thuật đúng nhưng thiếu sự ấm áp hoặc cá nhân hóa |
| 3–4 | Giao tiếp kém — đưa ra giả định, bỏ qua câu hỏi, hoặc chuyển giao thiếu liên tục |
| 1–2 | Có hại — thô lỗ, phớt lờ, dùng sai ngôn ngữ, hoặc hoàn toàn lạc đề |Yêu cầu: xuất JSON nghiêm ngặt
Hướng dẫn cho trợ lý của bạn nên yêu cầu một JSON Schema.
Định dạng đầu ra được đề xuất:
Loại: JSON Schema (khuyến nghị)
Tên: Conversation QA Schema
Strict: Bật
Schema: Đây cũng chỉ là một schema ví dụ để xuất điểm chất lượng của cuộc hội thoại, trạng thái vấn đề, cảm xúc khách hàng, điểm tương tác, lý do đằng sau kết quả và các cờ lỗi của cuộc hội thoại. Hãy chỉnh sửa để phù hợp với nhu cầu của bạn.
{
"type" : "json_schema",
"json_schema" : {
"name" : "conversation_review",
"strict" : true,
"schema" : {
"type" : "object",
"properties" : {
"overall_score" : {
"type" : "integer",
"description" : "Điểm chất lượng tổng thể của cuộc hội thoại. 1 = thất bại nghiêm trọng, 10 = xuất sắc. Nếu có bất kỳ flag nào, điểm này tối đa là 5."
},
"resolved" : {
"type" : "string",
"enum" : ["resolved", "unresolved", "escalated"],
"description" : "Liệu vấn đề cốt lõi của khách hàng đã được đóng hay chưa. 'resolved' yêu cầu xác nhận rõ ràng từ khách hàng; 'escalated' nghĩa là chuyển giao có chủ ý và thành công cho nhân viên hỗ trợ; 'unresolved' nghĩa là cuộc hội thoại kết thúc mà vấn đề chưa được giải quyết."
},
"customer_sentiment" : {
"type" : "string",
"enum" : ["positive", "neutral", "negative"],
"description" : "Tâm trạng chủ đạo của khách hàng, được cân nhắc theo cách họ kết thúc cuộc hội thoại. 'positive' = hài lòng hoặc biết ơn; 'neutral' = hợp tác và mang tính giao dịch; 'negative' = thất vọng, lặp đi lặp lại hoặc cảm thấy không hài lòng."
},
"engagement_score" : {
"type" : "integer",
"description" : "Mức độ tốt trong giao tiếp của phía nhân viên — cá nhân hóa, đồng cảm, rõ ràng, phù hợp ngôn ngữ và tính liên tục khi chuyển giao. Không bao gồm các tin nhắn `workflow`."
},
"reasoning" : {
"type" : "string",
"description" : "2–4 câu giải thích các điểm đã chấm. Phải tham chiếu các khoảnh khắc cụ thể trong cuộc hội thoại; nêu rõ nhân viên đã làm tốt gì, điều gì thất bại, và những yếu tố dẫn đến điểm cuối cùng; không được tổng quát hóa."
},
"flags" : {
"type" : "array",
"description" : "Các cờ thất bại nghiêm trọng được xác định trong cuộc hội thoại. Mảng rỗng nếu không có trường hợp nào áp dụng.",
"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 = thất bại nghiêm trọng, 10 = xuất sắc. Nếu có bất kỳ flag nào, điểm tối đa là 5. 'resolved' yêu cầu xác nhận rõ ràng từ khách hàng. 'escalated' nghĩa là chuyển giao có chủ ý và thành công cho nhân viên hỗ trợ. 'unresolved' nghĩa là cuộc hội thoại kết thúc mà vấn đề chưa được giải quyết. 'positive' = hài lòng hoặc biết ơn. 'neutral' = hợp tác và mang tính giao dịch. 'negative' = thất vọng, lặp đi lặp lại, hoặc cảm thấy không hài lòng. Không bao gồm các tin nhắn `workflow`. 1 = gây hại hoặc coi thường, 10 = rất cá nhân hóa và chủ động. Phải tham chiếu các thời điểm cụ thể trong cuộc hội thoại. Nêu rõ nhân viên đã làm tốt những gì, điều gì thất bại, và những yếu tố dẫn đến điểm cuối cùng. Không được tổng quát hóa. Mảng rỗng nếu không có trường hợp nào áp dụng.Sau node này, kết nối nó với một Map Output node và tiếp tục đến bước 7.
7. Phân tích và chuẩn hóa đầu ra AI
Bước này ngăn các phản hồi AI bị lỗi hoặc không đầy đủ làm hỏng workflow của bạn.
Các node n8n:
Map Output(Data Transformation → Edit Fields / Set)Parse Output(Core → Code → JavaScript)
7.1 Map Output
Node:Map Output
Chế độ: Ánh xạ thủ công
Tạo một trường tên là
Outputvà ánh xạ nó tới phản hồi AI.
Điều này giúp node tiếp theo nhất quán (nó luôn có thể đọc $json.Output).
7.2 Parse Output một cách an toàn
Node:Parse Output
Dán đoạn này vào 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;
Lưu ý về flattening: các mảng và đối tượng lồng nhau sẽ trở thành các cột như issues_0_type, issues_0_severity, v.v. Điều này giúp việc lưu trữ vào Google Sheets dễ dàng hơn.
8. Lưu kết quả
Bước cuối cùng lưu kết quả QA vào Google Sheets để theo dõi và báo cáo.
n8n node: Google Sheets (append)
Thông tin xác thực để kết nối: Kết nối tài khoản Google Sheets của bạn
Tài nguyên: Sheet Within Document
Thao tác: Append Row
Chọn tài liệu: Từ danh sách → Chọn tên bảng tính
Chọn sheet: Từ danh sách → Tên worksheet
Chế độ ánh xạ cột: Ánh xạ từng cột thủ công
Sau đó, ánh xạ từng giá trị vào tên cột bạn đã thiết lập trong sheet, tốt nhất là theo tên cột đã mô tả ở Phần 1.
Nơi mỗi trường đến từ
Từ trigger Webhook (siêu dữ liệu cuộc hội thoại):
Ánh xạ những trường này theo tham chiếu node Webhook, ví dụ:
{{ $('Webhook').first().json.body.contact_id }}
contact_idconversation_opened_timestampconversation_closed_timestamp
Từ kết quả AI đã phân tích (Mục 7):
Ánh xạ chúng theo các trường đầu ra đã phân tích, ví dụ:
{{ $json.overall_score }}
overall_scoreresolvedsentimentreasoningengagement_score
Để gỡ lỗi (khuyến nghị trong giai đoạn triển khai):
raw_ai_output— lưu toàn bộ phản hồi của AI để bạn có thể kiểm tra ngẫu nhiên kết quả và tinh chỉnh prompt của mình
💡 Mẹo: Kiểm tra kết quả thực tế của node Webhook để xác nhận tên trường chính xác — chúng có thể khác nhau tùy vào cấu hình trình kích hoạt của không gian làm việc.
Các cải tiến tùy chọn
Ẩn PII trước khi AI đánh giá (email, số điện thoại, mã đơn hàng)
Xử lý hơn 100 tin nhắn bằng cách lặp qua phân trang cho đến khi
pagination.nexttrốngMẫu cuộc hội thoại (ví dụ: chỉ xem xét 10% các cuộc hội thoại đã đóng để kiểm soát chi phí)
Chuyển các vấn đề mức độ nghiêm trọng cao tới Slack/Teams để theo dõi nhanh hơn