Cả Zalo OA lẫn Messenger đều theo mô hình webhook bất đồng bộ: user nhắn → nền tảng POST event 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 5 khối:
1. Webhook nhận tin — verify chữ ký/token của nền tảng; trả 200 ngay rồi xử lý async (đẩy vào queue) vì LLM mất vài giây trong khi nền tảng yêu cầu webhook phản hồi nhanh và sẽ retry khi timeout → xử lý phải idempotent theo message id để không trả lời trùng.
2. Session & context — nền tảng không giữ ngữ cảnh hộ bạn: lưu lịch sử theo khóa (channel, user_id) trong DB/Redis, đặt TTL phiên (ngừng nhắn một khoảng thì tính phiên mới); mỗi lượt ghép system prompt + lịch sử phiên + tin mới rồi gọi LLM. 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 policy của kênh.
3. Grounding bằng RAG — trả lời CSKH phải bám tài liệu sản phẩm/chính sách qua retrieval, kèm guardrail: không hứa ngoài chính sách, dùng structured output khi cần điều hướng nghiệp vụ (tra đơn, tạo ticket).
4. Handoff sang người — trigger: user yêu cầu gặp nhân viên, model tự nhận diện vượt phạm vi, chủ đề nhạy cảm (khiếu nại, hoàn tiền), hoặc lặp N lượt không giải quyết được. Khi handoff: tắt bot cho phiên đó bằng flag trong session, chuyển hội thoại kèm tóm tắt ngữ cảnh vào công cụ của agent người, báo cho user biết; bot chỉ bật lại khi agent đóng phiên. Thiếu cơ chế tắt bot là lỗi kinh điển — bot và người trả lời chen nhau.
5. Đa kênh — tách channel adapter (format tin, template, button, giới hạn mỗi nền tảng) khỏi lõi hội thoại LLM dùng chung, để thêm kênh mới không đụng logic chính.