Cả Zalo OA lẫn Messenger đều theo mô hình webhook bất đồng bộ: người dùng nhắn → nền tảng POST sự kiện về endpoint của bạn → bạn xử lý → gọi API của nền tảng để gửi tin trả lời. Không phải request/response đồng bộ, nên kiến trúc xoay quanh năm khối.
1. Nhận webhook — xác thực chữ ký của nền tảng, rồi trả 200 ngay và đẩy việc vào hàng đợi. Lý do: LLM mất vài giây trong khi nền tảng đòi phản hồi nhanh và sẽ retry khi timeout — nên phần xử lý phải idempotent theo message id, nếu không người dùng sẽ nhận hai tin giống nhau.
2. Phiên và ngữ cảnh — nền tảng không giữ ngữ cảnh hộ bạn. Lưu lịch sử theo khoá (kênh, user_id) trong Redis hoặc DB, đặt TTL để một khoảng im lặng đủ dài thì tính là phiên mới. Mỗi lượt ghép system prompt + lịch sử phiên + tin mới rồi mới gọi model. Lưu ý mỗi nền tảng có cửa sổ thời gian được phép nhắn lại và quy định riêng cho tin chủ động — thiết kế luồng theo đúng chính sách của kênh, đây là chỗ hay bị chặn khi lên production.
3. Bám tài liệu bằng RAG — câu trả lời chăm sóc khách hàng phải dựa trên tài liệu sản phẩm và chính sách thật, kèm guardrail không hứa ngoài chính sách. Việc mang tính nghiệp vụ (tra đơn, tạo ticket) thì dùng đầu ra có cấu trúc thay vì để model tự do viết.
4. Bàn giao sang người — kích hoạt khi khách yêu cầu gặp nhân viên, khi model tự nhận thấy vượt phạm vi, khi chạm chủ đề nhạy cảm (khiếu nại, hoàn tiền), hoặc khi đã vài lượt mà không giải quyết được.
Khi bàn giao, việc quan trọng nhất là tắt bot cho đúng phiên đó bằng một cờ trong session, chuyển hội thoại kèm tóm tắt ngữ cảnh sang công cụ của nhân viên, và báo cho khách biết. Bot chỉ bật lại khi nhân viên đóng phiên. Thiếu cơ chế tắt bot là lỗi thường gặp — bot và người cùng trả lời một khách, chen nhau ngay trong một cuộc hội thoại.
5. Nhiều kênh — tách adapter theo kênh (định dạng tin, template, nút bấm, giới hạn riêng của từng nền tảng) khỏi phần lõi hội thoại dùng chung. Nhờ vậy thêm kênh mới không phải đụng vào logic chính.