Kiến trúc Generative UI mang lại một chân trời mới về trải nghiệm người dùng, nhưng nó lại là cơn ác mộng tồi tệ nhất của đội ngũ QA (Kiểm thử) và DevOps.

Làm thế nào để bạn viết một kịch bản kiểm thử tự động (E2E Test) cho một giao diện mà bạn không biết trước AI sẽ sinh ra nội dung gì? Và làm thế nào để đảm bảo hệ thống không bị “đốt tiền” API khi hàng ngàn user hỏi cùng một câu hỏi?

6.1. Rào cản Non-deterministic trong E2E Testing

Trong các ứng dụng truyền thống (Deterministic), một kịch bản test bằng Cypress hoặc Playwright thường trông như sau:

  1. Nhập “Hà Nội” vào ô Search.
  2. Bấm “Tìm kiếm”.
  3. expect(page.locator('.weather-title')).toHaveText('Thời tiết Hà Nội').

Tuy nhiên, LLM có tính chất phi xác định (Non-deterministic). Cùng một câu lệnh, hôm nay nó trả về {"title": "Thời tiết Hà Nội"}, ngày mai nó có thể trả về {"title": "Bản tin thời tiết tại Thủ đô Hà Nội"}. Bài test tĩnh của bạn sẽ fail liên tục (Flaky tests).

Giải pháp 1: Tách biệt hoàn toàn AI và UI khi Test

Nguyên tắc vàng: Không bao giờ gọi API LLM thật trong các bài E2E Test của UI. Bạn phải Mock (giả lập) WebSocket Server, trả về một chuỗi JSON cứng (Ví dụ: gọi thẳng Component Registry với Payload giả). Điều này chứng minh rằng: “Miễn là AI trả về JSON đúng chuẩn A2UI, Frontend của tôi chắc chắn sẽ render đúng.” Việc test độ thông minh của AI phải được đẩy sang một layer khác (LLM Evaluation), tách biệt với UI Testing.

Lưu ý (Dành cho Full Integration Test): Nếu QA bắt buộc phải test toàn bộ luồng đi qua cả Backend Agent, hãy sử dụng kỹ thuật VCR / Cassette Recording (như thư viện pollyjs). Lần chạy test đầu tiên sẽ gọi LLM thật và “ghi âm” (record) lại JSON response. Các lần chạy test CI/CD sau đó sẽ tự động “phát lại” (replay) cục JSON đó để giữ tính Deterministic.

Giải pháp 2: Property-Based Testing

Thay vì kiểm tra một đoạn text cụ thể (Exact Match), hãy kiểm tra “Thuộc tính” (Properties) của Component.

  • Sai: expect(page).toHaveText("Chuyển 500k cho user B")
  • Đúng:
    • expect(page.locator('form[data-testid="transfer-form"]')).toBeVisible()
    • expect(page.locator('input[name="amount"]').inputValue()).toBeGreaterThan(0)
    • expect(page.locator('button[type="submit"]')).toBeEnabled()

Bằng cách test Sự hiện diện của cấu trúc thay vì text cụ thể, bài test của bạn sẽ sống sót qua mọi sự thay đổi ngôn từ của AI.

6.2. Semantic Caching tại Edge (Vùng biên)

Một vấn đề lớn khác là chi phí và độ trễ. Nếu có 1.000 user cùng gõ “Hướng dẫn đổi mật khẩu”, việc gọi OpenAI API 1.000 lần là một sự lãng phí kinh khủng cả về tiền bạc lẫn thời gian chờ đợi (latency).

Bộ nhớ đệm ngữ nghĩa (Semantic Cache) là gì?

Cache truyền thống dựa trên Exact Match (Khớp chính xác chuỗi string). Nếu user A gõ "Đổi mật khẩu", và user B gõ "Làm sao để thay đổi password", Cache truyền thống sẽ miss (trượt).

Semantic Caching giải quyết điều này bằng Vector Database:

  1. User nhập câu hỏi.
  2. Mã hóa (Embed) câu hỏi thành một Vector.
  3. So sánh khoảng cách Vector với các câu hỏi trong Cache.
  4. "Đổi mật khẩu""Làm sao để thay đổi password" có độ tương đồng hình học rất cao (Similarity > 0.95).
  5. Cache HIT!

Kiến trúc Edge Caching

Để tối ưu hóa độ trễ xuống mức mili-giây, Semantic Cache không nên đặt ở Backend Server, mà nên đặt ở Vùng biên (Edge) — ví dụ như Cloudflare Workers kết hợp với Vectorize (Vector Database của Cloudflare).

  1. Request của user chạm đến Cloudflare Worker ở trạm gần nhất (ví dụ: POP Singapore).
  2. Worker nhúng prompt thành vector và query Cache.
  3. Nếu HIT, Worker trả ngay lập tức cấu trúc JSON A2UI về cho Client. Latency < 50ms. Không tốn tiền gọi OpenAI.
  4. Chỉ khi MISS, request mới được đẩy về cụm Kubernetes Backend để AI Agent xử lý.

🔗 Bước tiếp theo: Bạn đã nắm trong tay mọi lý thuyết kiến trúc từ UI, State, Security đến Caching. Đã đến lúc bắt tay vào viết code. Trong phần cuối cùng của Series, chúng xuất xem xét cấu trúc thư mục của một Repo Mẫu (Boilerplate) và chiến lược đưa nó vào dự án cũ: Phần 7 — Reference Repository & Chiến Lược Migration.