Chatbot xử lý tin nhắn ngắt quãng hiệu quả với Redis và n8n

Viewed 10

Chia sẻ của bạn Little Excel

Bạn đã bao giờ cảm thấy khó chịu khi chatbot cứ liên tục ngắt lời bạn, trả lời từng mẩu tin nhắn rời rạc mà không đợi bạn diễn đạt hết ý? Đây là một vấn đề phổ biến khiến trải nghiệm người dùng với chatbot trở nên kém thân thiện. May mắn thay, có một giải pháp hiệu quả để giúp chatbot "thông minh" và "kiên nhẫn" hơn trong việc xử lý những tin nhắn ngắt quãng này: sử dụng Redis kết hợp với N8N. Bài viết này sẽ hướng dẫn bạn chi tiết cách triển khai kỹ thuật này.

Tại sao Chatbot cần xử lý tin nhắn ngắt quãng?

Trong giao tiếp thực tế, chúng ta thường không gõ một câu hoàn chỉnh ngay lập tức. Thay vào đó, nhiều người có thói quen nhắn tin thành nhiều đoạn ngắn, ví dụ:

  • "Chào bạn"
  • "cho mình hỏi"
  • "cách làm luồng N8N"
  • "kết nối Telegram"
  • "như thế nào?"

Nếu chatbot của bạn được lập trình theo kiểu truyền thống, nó sẽ phản hồi ngay sau mỗi tin nhắn nhận được. Điều này dẫn đến một cuộc hội thoại đứt đoạn, chatbot không nắm bắt được toàn bộ yêu cầu của người dùng, gây khó hiểu và làm giảm hiệu quả tương tác.

Việc chatbot xử lý tin nhắn ngắt quãng một cách thông minh sẽ mang lại những lợi ích sau:

  • Cải thiện trải nghiệm người dùng: Người dùng cảm thấy thoải mái hơn khi có thể nhắn tin theo thói quen tự nhiên của mình mà không sợ bị ngắt lời.
  • Tăng độ chính xác của chatbot: Chatbot có đủ thông tin để hiểu rõ ý định của người dùng, từ đó đưa ra phản hồi chính xác và hữu ích hơn.
  • Tạo ấn tượng chuyên nghiệp: Một chatbot "biết lắng nghe" sẽ tạo thiện cảm và sự tin cậy cho người dùng.

Vậy làm thế nào để chatbot có thể "gom" các tin nhắn rời rạc này lại thành một yêu cầu hoàn chỉnh trước khi xử lý? Câu trả lời nằm ở Redis.

Giải pháp tối ưu: Sử dụng Redis để gom tin nhắn cho Chatbot

Redis (Remote Dictionary Server) là một hệ quản trị cơ sở dữ liệu key-value mã nguồn mở, tốc độ cao, thường được sử dụng làm bộ đệm (cache), message broker, hoặc cơ sở dữ liệu. Điểm mạnh của Redis là khả năng lưu trữ dữ liệu trực tiếp trên RAM, giúp tốc độ truy xuất cực kỳ nhanh chóng.

Trong trường hợp này, chúng ta sẽ sử dụng Redis như một "bộ đệm thông minh" để tạm thời lưu trữ các tin nhắn của người dùng. Ý tưởng cốt lõi như sau:

  1. Nhận tin nhắn: Khi người dùng gửi một tin nhắn, thay vì xử lý ngay, chatbot sẽ lưu tin nhắn này vào Redis. Mỗi người dùng sẽ có một "khay" lưu trữ riêng biệt, thường được xác định bằng PSID (Page-Scoped ID trên Facebook Messenger) hoặc một định danh người dùng duy nhất khác.
  2. Đặt thời gian chờ (Timeout): Sau khi lưu tin nhắn, chatbot sẽ khởi động một "đồng hồ đếm ngược" (timeout). Ví dụ, 10-15 giây.
  3. Xử lý tin nhắn mới:
    • Nếu trong khoảng thời gian chờ này, người dùng gửi thêm tin nhắn mới, chatbot sẽ tiếp tục lưu tin nhắn đó vào "khay" của họ và reset lại đồng hồ đếm ngược.
    • Quá trình này lặp lại cho đến khi người dùng ngừng nhắn tin trong một khoảng thời gian nhất định (bằng thời gian timeout đã đặt).
  4. Gom và xử lý: Khi đồng hồ đếm ngược kết thúc (timeout), chatbot sẽ lấy toàn bộ các tin nhắn đã được lưu trong "khay" của người dùng đó, ghép chúng lại thành một yêu cầu hoàn chỉnh. Lúc này, yêu cầu hoàn chỉnh mới được gửi đến bộ não AI của chatbot để xử lý và phản hồi.

Bằng cách này, chatbot xử lý tin nhắn ngắt quãng một cách mượt mà, đợi người dùng trình bày xong ý tưởng rồi mới phản hồi, tương tự như cách con người giao tiếp.

Hướng dẫn chi tiết cài đặt và cấu hình Redis cho Chatbot N8N

Để triển khai giải pháp này, chúng ta cần cài đặt Redis. Một cách phổ biến và tiện lợi là sử dụng Docker. Nếu bạn chưa quen với Docker, Portainer là một giao diện quản lý Docker rất trực quan.

Cài đặt Redis với Docker và Portainer

  1. Chuẩn bị Docker và Portainer: Đảm bảo bạn đã cài đặt Docker và Portainer trên máy chủ của mình.
  2. Tạo Container Redis mới trong Portainer:
    • Đăng nhập vào Portainer.
    • Trong mục "Containers", chọn "Add container".
    • Name: Đặt tên cho container, ví dụ: my-redis-service.
    • Image: Nhập redis:latest (hoặc một phiên bản cụ thể như redis:7.0).
    • Port mapping: Click "Publish a new network port".
      • host: 6379
      • container: 6379
        (Đây là port mặc định của Redis. Bạn có thể đổi port host nếu muốn).
  3. Cấu hình Volumes (để lưu trữ dữ liệu và cấu hình):
    • Chuyển xuống tab "Volumes".
    • Volume 1 (Lưu trữ dữ liệu):
      • Click "Map additional volume".
      • container: /data
      • host: Chọn "Bind" và nhập đường dẫn trên máy host của bạn để lưu dữ liệu Redis, ví dụ: /opt/redis/data. Hoặc bạn có thể chọn "Volume" và tạo một Docker volume mới tên là redis-data.
    • Volume 2 (Lưu file cấu hình - quan trọng để đặt mật khẩu):
      • Click "Map additional volume".
      • container: /usr/local/etc/redis/redis.conf (Đây là đường dẫn mà Redis sẽ tìm file config khi khởi động với command phù hợp).
      • host: Chọn "Bind" và nhập đường dẫn trên máy host tới file redis.conf của bạn, ví dụ: /opt/redis/config/redis.conf.
  4. Tạo file cấu hình redis.conf trên máy Host:
    • Trên máy chủ của bạn, tạo thư mục /opt/redis/config (hoặc đường dẫn bạn đã chọn ở trên).
    • Trong thư mục đó, tạo file redis.conf với nội dung sau để đặt mật khẩu:
      requirepass YOUR_STRONG_PASSWORD_HERE
      
      Thay YOUR_STRONG_PASSWORD_HERE bằng một mật khẩu mạnh bạn chọn.
  5. Cấu hình Command để Redis sử dụng file config:
    • Chuyển xuống tab "Command & logging".
    • Trong mục "Command", nhập:
      redis-server /usr/local/etc/redis/redis.conf
      
      Lệnh này yêu cầu Redis server khởi động và sử dụng file cấu hình tại đường dẫn trong container mà bạn đã map volume.
  6. Cấu hình Restart Policy:
    • Chuyển xuống tab "Restart policy".
    • Chọn Unless stopped để container tự khởi động lại nếu có lỗi, trừ khi bạn chủ động dừng nó.
  7. Deploy Container:
    • Click nút "Deploy the container".

Sau khi hoàn tất, bạn đã có một instance Redis đang chạy và được bảo vệ bằng mật khẩu.

Xây dựng luồng N8N thông minh với Redis để xử lý tin nhắn ngắt quãng

Bây giờ, chúng ta sẽ xây dựng luồng N8N để kết nối với Redis và thực hiện logic gom tin nhắn.

Chuẩn bị thư viện cho Node Code trong N8N

Luồng N8N sẽ sử dụng một Node "Code" để viết mã JavaScript tùy chỉnh. Node này cần sử dụng hai thư viện: redis (để tương tác với Redis) và axios (để gửi yêu cầu HTTP sau khi gom tin nhắn).

  1. Cài đặt thư viện:
    • Nếu bạn chạy N8N qua Docker, bạn cần truy cập vào terminal của container N8N.
    • Nếu bạn cài N8N trực tiếp trên máy, mở terminal tại thư mục cài đặt N8N (thường là thư mục .n8n trong thư mục home của user nếu cài đặt qua npm, hoặc thư mục global nếu cài global).
    • Chạy lệnh sau để cài đặt:
      npm install redis axios
      
  2. Cho phép N8N sử dụng thư viện ngoài:
    • Bạn cần khai báo biến môi trường NODE_FUNCTION_ALLOW_EXTERNAL để Node "Code" của N8N có thể require các thư viện này.
    • Nếu dùng Docker Compose, thêm vào phần environment của service N8N trong file docker-compose.yml:
      environment:
        - NODE_FUNCTION_ALLOW_EXTERNAL=redis,axios
      
    • Nếu chạy N8N bằng lệnh khác, hãy thiết lập biến môi trường này trước khi khởi động N8N.
    • Khởi động lại N8N để áp dụng thay đổi.

Logic trong Node "Code" của N8N

Tạo một workflow mới trong N8N. Thêm một Node "Webhook" làm điểm bắt đầu và một Node "Code".

Node Webhook (Trigger):

  • Đây sẽ là Webhook URL mới mà bạn cấu hình trên Facebook Developer Portal (hoặc nền tảng nhắn tin khác) để nhận tin nhắn từ người dùng.
  • Chọn phương thức POST.

Node Code (Xử lý chính):
Nội dung JavaScript trong Node Code sẽ như sau:

// Biến toàn cục để lưu trữ các timeout timers, key là PSID
const timeouts = {};

// --- CẤU HÌNH ---
const REDIS_HOST = 'your_redis_host_ip_or_docker_service_name'; // Ví dụ: 'localhost' hoặc tên service 'my-redis-service' nếu N8N và Redis cùng Docker network
const REDIS_PORT = 6379;
const REDIS_PASSWORD = 'YOUR_STRONG_PASSWORD_HERE'; // Mật khẩu bạn đặt cho Redis
const REDIS_DATABASE_INDEX = 0; // Mặc định là 0 (từ 0-15)
const DEBOUNCE_TIME_MS = 15000; // Thời gian chờ (ms), ví dụ 15 giây
const TARGET_WEBHOOK_URL = 'YOUR_N8N_AI_PROCESSING_WEBHOOK_URL'; // Webhook của luồng N8N xử lý AI chính

// --- KHỞI TẠO REDIS CLIENT ---
const redis = require('redis');
const client = redis.createClient({
  socket: {
    host: REDIS_HOST,
    port: REDIS_PORT,
  },
  password: REDIS_PASSWORD,
  database: REDIS_DATABASE_INDEX,
});

client.on('error', (err) => console.error('Redis Client Error', err));
await client.connect(); // Kết nối tới Redis

// --- HÀM XỬ LÝ ---

/**
 * Hàm xử lý tin nhắn sau khi timeout.
 * Lấy tất cả tin nhắn từ Redis, gửi đến webhook mục tiêu, rồi xóa key.
 */
async function processBufferedMessages(psid) {
  try {
    console.log(`Processing messages for PSID: ${psid} after timeout.`);
    const messages = await client.lRange(psid, 0, -1); // Lấy tất cả tin nhắn trong list

    if (messages && messages.length > 0) {
      const fullConversation = messages.join(' '); // Ghép các tin nhắn lại
      const payload = {
        sender: { id: psid },
        page: { id: items[0].json.body.entry[0].id }, // Lấy page ID nếu cần
        message: { text: fullConversation },
        originalMessages: messages, // Gửi cả mảng tin nhắn gốc nếu muốn
      };

      // Gửi payload đến webhook xử lý AI
      const axios = require('axios');
      await axios.post(TARGET_WEBHOOK_URL, payload);
      console.log(`Sent buffered messages for PSID: ${psid} to target webhook.`);

      // Xóa key khỏi Redis sau khi xử lý
      await client.del(psid);
      console.log(`Deleted key for PSID: ${psid} from Redis.`);
    }
  } catch (error) {
    console.error(`Error processing messages for PSID ${psid}:`, error);
  } finally {
    delete timeouts[psid]; // Xóa timer khỏi danh sách
  }
}

/**
 * Hàm thêm tin nhắn vào Redis và quản lý debounce.
 */
async function addMessageAndDebounce(psid, messageText) {
  try {
    console.log(`Adding message for PSID: ${psid}: "${messageText}"`);
    // Lưu tin nhắn vào cuối list trong Redis
    // LTRIM/RPUSH để giới hạn số lượng tin nhắn nếu cần, ví dụ: client.lPush + client.lTrim
    await client.rPush(psid, messageText); 
    // Có thể đặt thời gian hết hạn cho key nếu muốn dọn dẹp tự động: await client.expire(psid, DEBOUNCE_TIME_MS / 1000 + 3600); // Thêm 1 giờ

    // Xóa timeout cũ nếu có
    if (timeouts[psid]) {
      clearTimeout(timeouts[psid]);
      console.log(`Cleared existing timeout for PSID: ${psid}`);
    }

    // Đặt timeout mới
    timeouts[psid] = setTimeout(() => {
      processBufferedMessages(psid);
    }, DEBOUNCE_TIME_MS);
    console.log(`Set new timeout for PSID: ${psid} - ${DEBOUNCE_TIME_MS}ms`);

  } catch (error) {
    console.error(`Error adding message for PSID ${psid}:`, error);
  }
}

// --- LUỒNG CHÍNH ---
// Lấy dữ liệu từ Webhook (ví dụ cho Facebook Messenger)
// Cần điều chỉnh tùy theo cấu trúc payload của nền tảng bạn dùng
const incomingData = items[0].json.body; 

if (incomingData.object === 'page' && incomingData.entry && incomingData.entry.length > 0) {
  for (const entry of incomingData.entry) {
    if (entry.messaging && entry.messaging.length > 0) {
      for (const event of entry.messaging) {
        if (event.message && event.message.text && !event.message.is_echo) { // Chỉ xử lý tin nhắn text và không phải echo
          const senderId = event.sender.id;
          const messageText = event.message.text;
          
          await addMessageAndDebounce(senderId, messageText);
        }
      }
    }
  }
}

// Node Code không cần trả về gì cụ thể ở đây, vì nó chỉ trigger việc xử lý bất đồng bộ
return [{ json: { status: "Message received and being processed." } }];

Giải thích Node Code:

  1. Cấu hình: Khai báo các thông tin kết nối Redis, thời gian chờ (DEBOUNCE_TIME_MS), và URL của Webhook N8N sẽ xử lý logic AI chính (TARGET_WEBHOOK_URL).
  2. Khởi tạo Redis Client: Tạo đối tượng client để kết nối và tương tác với Redis server.
  3. timeouts object: Lưu trữ các ID của setTimeout cho mỗi người dùng, để có thể clearTimeout khi có tin nhắn mới.
  4. addMessageAndDebounce(psid, messageText):
    • Nhận psid (ID người gửi) và messageText (nội dung tin nhắn).
    • Sử dụng client.rPush(psid, messageText) để thêm tin nhắn vào cuối một danh sách (list) trong Redis, với psid là key của danh sách đó.
    • Kiểm tra xem có timeout nào đang chạy cho psid này không. Nếu có, dùng clearTimeout để hủy nó.
    • Tạo một setTimeout mới. Sau khoảng thời gian DEBOUNCE_TIME_MS, hàm processBufferedMessages(psid) sẽ được gọi.
  5. processBufferedMessages(psid):
    • Được gọi khi timeout kết thúc.
    • Dùng client.lRange(psid, 0, -1) để lấy tất cả tin nhắn trong danh sách có key là psid.
    • Nếu có tin nhắn, ghép chúng lại thành một chuỗi (fullConversation).
    • Tạo payload chứa senderId, pageId (nếu cần), và fullConversation.
    • Sử dụng axios.post để gửi payload này đến TARGET_WEBHOOK_URL (luồng N8N xử lý AI của bạn).
    • Dùng client.del(psid) để xóa danh sách tin nhắn khỏi Redis, giải phóng bộ nhớ và chuẩn bị cho lần tương tác tiếp theo.
    • Xóa timer khỏi timeouts.
  6. Luồng chính:
    • Lấy dữ liệu từ Node Webhook trigger (items[0].json.body). Cấu trúc này dành cho Facebook Messenger, bạn cần điều chỉnh nếu dùng nền tảng khác.
    • Lặp qua các sự kiện tin nhắn, lấy senderIdmessageText.
    • Gọi addMessageAndDebounce để xử lý mỗi tin nhắn.

Lưu ý quan trọng:

  • Đảm bảo rằng N8N và Redis có thể "thấy" nhau qua mạng. Nếu cả hai chạy trên cùng một Docker network, bạn có thể dùng tên service của Redis (ví dụ: my-redis-service) làm REDIS_HOST. Nếu không, hãy dùng địa chỉ IP của máy host Redis.
  • TARGET_WEBHOOK_URL là URL của một luồng N8N khác - luồng mà trước đây bạn dùng để kết nối trực tiếp với Facebook và xử lý AI.

Tích hợp luồng Redis vào hệ thống Chatbot hiện có và kiểm tra

Sau khi đã xây dựng xong luồng N8N với Redis, bạn cần tích hợp nó vào hệ thống chatbot hiện tại:

  1. Cập nhật Webhook trên Facebook Developer:
    • Truy cập trang Developer của Facebook, vào phần Webhooks cho ứng dụng Messenger của bạn.
    • Thay thế URL Webhook cũ bằng URL của Node Webhook trong luồng N8N mới (luồng chứa Node Code Redis).
  2. Điều chỉnh luồng N8N xử lý AI (luồng cũ):
    • Node Webhook trigger của luồng này giờ đây sẽ nhận dữ liệu từ Node Code Redis (thông qua TARGET_WEBHOOK_URL).
    • Dữ liệu đầu vào sẽ có dạng payload mà Node Code Redis đã gửi, ví dụ:
      {
        "sender": { "id": "USER_PSID" },
        "page": { "id": "PAGE_ID" },
        "message": { "text": "Chào bạn cho mình hỏi cách làm luồng N8N kết nối Telegram như thế nào?" },
        "originalMessages": ["Chào bạn", "cho mình hỏi", "cách làm luồng N8N", "kết nối Telegram", "như thế nào?"]
      }
      
    • Bạn sẽ lấy message.text (là chuỗi đã được gom) để đưa vào các bước xử lý AI tiếp theo (ví dụ: gọi OpenAI, lấy lịch sử trò chuyện, v.v.). Phần logic này về cơ bản không thay đổi so với trước đây.
    • Video gốc có đề cập đến một Node Code phụ trong luồng AI để nối mảng tin nhắn. Tuy nhiên, với logic code Redis ở trên, tin nhắn đã được nối sẵn (fullConversation), nên bạn có thể không cần bước này nữa, hoặc bạn có thể tùy chỉnh để Node Code Redis gửi mảng originalMessages và bạn xử lý nối mảng ở luồng AI nếu muốn.

Kiểm tra:
Gửi nhiều tin nhắn ngắt quãng tới chatbot của bạn. Quan sát xem chatbot có "chờ" một khoảng thời gian rồi mới phản hồi bằng một câu trả lời dựa trên toàn bộ ý bạn đã nhập hay không. Kiểm tra log của N8N và Redis (nếu cần) để gỡ lỗi.

Kết luận

Sử dụng Redis để làm bộ đệm và gom tin nhắn là một kỹ thuật mạnh mẽ giúp chatbot xử lý tin nhắn ngắt quãng một cách thông minh và thân thiện hơn. Bằng cách triển khai luồng N8N kết hợp với Redis như đã hướng dẫn, bạn có thể cải thiện đáng kể trải nghiệm người dùng và nâng cao hiệu quả của chatbot.

Mặc dù đoạn code và cấu hình có vẻ phức tạp ban đầu, nhưng lợi ích mà nó mang lại là rất lớn. Hãy thử nghiệm và điều chỉnh thời gian DEBOUNCE_TIME_MS cho phù hợp với đối tượng người dùng của bạn. Chúc bạn thành công trong việc xây dựng những chatbot ngày càng "thấu hiểu" hơn!

0 Answers