Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 25 — Backend for AI app — Stack patterns

Building AI-Powered Products Bài 25/60

Mở đầu — vì sao bài này quan trọng

Nếu bạn đã đi qua các bài về RAG, embeddings, guardrails hay latency & cost, thì đến bài này chúng ta trả lời một câu hỏi rất thực tế: "Vậy toàn bộ những thứ đó ráp lại thành một backend chạy được như thế nào?"

Đây là chỗ nhiều nhóm sản phẩm AI ở Việt Nam mắc kẹt. Họ demo được một notebook Python gọi OpenAI, thấy chạy ngon, rồi tưởng rằng chỉ cần "đưa lên server là xong". Nhưng khi có 500 người dùng thật, họ mới phát hiện: request bị nghẽn vì gọi LLM đồng bộ, chi phí phình ra vì không cache, người dùng thấy màn hình đứng hình 8 giây vì không streaming, và mỗi khi OpenAI bị rate-limit thì cả sản phẩm sập.

Backend của một sản phẩm AI khác backend của một web app CRUD truyền thống ở vài điểm cốt lõi: nó phải làm việc với các lời gọi mạng chậm và đắt (LLM API), phải xử lý dữ liệu dạng vector, phải chịu được sự bất định (LLM có thể lỗi, chậm, hoặc trả về rác), và phải tối ưu cho trải nghiệm streaming. Bài này cho bạn một bản đồ kiến trúc backend và các stack pattern đã được kiểm chứng, để bạn không phải trả học phí bằng những sự cố production.

Lưu ý phạm vi: bài này tập trung vào cấu trúc backend và các mẫu tổ chức stack. Chi tiết về RAG pipeline, embeddings, hay tối ưu latency/cost đã có bài riêng — ở đây ta nhìn tổng thể cách các mảnh ghép nối lại.

Khái niệm cốt lõi

Một backend AI điển hình gồm bốn khối chính. Hãy hiểu vai trò từng khối trước khi bàn cách ráp.

1. API Server — cửa ngõ nhận request

Đây là lớp mà client (web, mobile) nói chuyện trực tiếp. Nó nhận request người dùng, xác thực (auth), kiểm tra quota, gọi các dịch vụ bên trong, rồi trả kết quả — thường dưới dạng stream thay vì một cục JSON.

Điểm khác biệt lớn nhất so với API thường: các endpoint AI chạy lâu (long-running). Một request chat có thể mất 3–15 giây. Nếu backend của bạn dùng mô hình đồng bộ, mỗi worker bị "giữ" suốt thời gian đó, và bạn hết worker rất nhanh. Vì vậy stack AI ưu tiên async I/O (Python: FastAPI + async; Node: mặc định async) và streaming response (SSE — Server-Sent Events, hoặc WebSocket).

2. LLM Service — bộ não

Đây là nơi gọi model. Có ba lựa chọn:

  • API bên ngoài: OpenAI, Anthropic (Claude), Google (Gemini). Bạn không quản hạ tầng, trả tiền theo token. Nhanh để bắt đầu.
  • Self-host model mở: Llama, Qwen, Mistral chạy trên GPU của bạn (qua vLLM, Ollama, TGI). Kiểm soát nhiều hơn, phù hợp khi có yêu cầu dữ liệu nhạy cảm hoặc khối lượng lớn.
  • Kết hợp (hybrid): dùng API cho tác vụ khó, self-host cho tác vụ đơn giản, phổ biến.
Quan trọng: đừng gọi LLM trực tiếp rải rác khắp code. Hãy bọc nó sau một lớp trừu tượng — một "LLM gateway" nội bộ. Lớp này lo: chọn model, retry khi lỗi, fallback sang nhà cung cấp khác, đếm token, log, và cache. Đây là mẫu quan trọng nhất của cả bài.

3. Vector DB — trí nhớ cho RAG

Nơi lưu embeddings để truy hồi ngữ nghĩa. Các lựa chọn phổ biến: pgvector (extension của PostgreSQL — gọn nếu bạn đã dùng Postgres), Qdrant, Weaviate, Pinecone (managed), Milvus. Với đa số sản phẩm giai đoạn đầu ở Việt Nam, lời khuyên của tôi là bắt đầu bằng pgvector: một database duy nhất, ít thứ phải vận hành, đủ tốt tới hàng triệu vector.

4. Data & State layer — dữ liệu, hàng đợi, cache

Khối thứ tư mà dàn ý gốc bỏ ngỏ, nhưng cực kỳ quan trọng, gồm ba thành phần:

  • Database quan hệ (PostgreSQL): lưu người dùng, hội thoại, tin nhắn, cấu hình, log chi phí.
  • Cache (Redis): cache câu trả lời LLM, lưu session, đếm rate-limit, lưu prompt template.
  • Queue / background worker (Redis Queue, Celery, BullMQ, hoặc AWS SQS): đẩy các tác vụ nặng và chậm ra khỏi đường request chính. Ví dụ: khi người dùng upload 200 trang PDF để index vào RAG, bạn không để họ chờ; bạn nhận file, trả về "đang xử lý", và một worker nền lo việc chunking + embedding.

Sơ đồ luồng tổng quát

Client → API Server (auth, quota, stream) → LLM Gateway → {LLM API hoặc self-host} và song song truy vấn Vector DB + Postgres. Các tác vụ nặng (index tài liệu, phân tích batch) được đẩy vào Queue cho Worker xử lý nền. Redis nằm cắt ngang để cache và đếm rate-limit.

Ghi nhớ ba nguyên tắc thiết kế: async mọi lời gọi mạng, tách việc nặng ra background, và mọi lời gọi LLM đi qua một gateway chung.

Tình huống thực tế

Ví dụ 1 — Chatbot CSKH của một sàn TMĐT tại TP.HCM

Một công ty thương mại điện tử tầm trung (giả định tên "ChợViệt", ~40.000 người dùng hoạt động/tháng) xây chatbot trả lời về đơn hàng và chính sách đổi trả.

Bản đầu tiên của họ rất ngây thơ: một endpoint Flask đồng bộ, mỗi request gọi thẳng OpenAI, và nhúng cả bước embedding tài liệu chính sách ngay trong request. Kết quả: giờ cao điểm buổi tối, server chỉ chịu được ~15 request đồng thời rồi nghẽn; p95 latency lên tới 22 giây; và họ đốt hết credit vì mỗi câu hỏi giống nhau ("phí ship bao nhiêu?") đều gọi lại LLM.

Họ tái cấu trúc theo bốn khối: chuyển sang FastAPI async với SSE streaming (người dùng thấy chữ chạy ra ngay sau ~800ms); đưa toàn bộ việc index tài liệu chính sách vào worker nền chạy một lần, lưu vào pgvector; và thêm Redis cache cho các câu hỏi phổ biến kèm một lớp semantic cache đơn giản (nếu câu hỏi gần giống câu đã có trong 24h thì trả lại luôn). Kết quả sau hai tuần: chi phí LLM giảm ~45% nhờ cache hit ~38%, p95 xuống dưới 4 giây, và cùng một server chịu được hơn 200 request đồng thời.

Bài học: đừng nhét việc nặng và việc chậm vào đường request. Tách index sang worker, cache câu lặp lại, và streaming để che latency.

Ví dụ 2 — Startup fintech và ràng buộc dữ liệu nhạy cảm

Một startup fintech ở Hà Nội xây trợ lý phân tích giao dịch cho khách hàng doanh nghiệp. Vấn đề: dữ liệu giao dịch tài chính nhạy cảm, và họ lo ngại việc gửi thẳng dữ liệu khách hàng ra API nước ngoài dưới góc độ tuân thủ (chủ đề compliance có bài riêng, ở đây ta chỉ nói phần kiến trúc).

Họ chọn stack hybrid: một model mở (Qwen 32B) self-host trên GPU thuê tại một nhà cung cấp cloud trong khu vực, dùng vLLM để phục vụ, lo phần đọc và tóm tắt dữ liệu giao dịch thô. Với các tác vụ suy luận phức tạp hơn nhưng đã được ẩn danh hóa, họ mới gọi ra API ngoài. Điểm mấu chốt: nhờ đã bọc mọi thứ sau LLM gateway ngay từ đầu, việc định tuyến "câu này chạy nội bộ, câu kia gọi API" chỉ là một quyết định trong gateway, không phải sửa khắp codebase.

Bài học: lớp trừu tượng LLM gateway cho bạn quyền tự do định tuyến. Ràng buộc dữ liệu và chi phí sẽ thay đổi theo thời gian — kiến trúc phải cho phép đổi model mà không đập lại backend.

Ví dụ 3 — Tính năng "tóm tắt tài liệu" gắn thêm vào SaaS có sẵn

Một công ty SaaS quản lý văn bản (giả định "DocFlow", đã có sẵn backend Node.js + PostgreSQL) muốn thêm tính năng AI tóm tắt và hỏi đáp tài liệu. Họ không muốn dựng một hệ thống riêng biệt.

Lựa chọn của họ rất thực dụng và đáng học: giữ nguyên Postgres, chỉ bật extension pgvector để lưu embeddings ngay trong database hiện có; dùng BullMQ trên Redis (Redis họ đã có sẵn cho cache) làm queue để xử lý tài liệu nền; và viết một module llm-service nhỏ trong codebase Node bọc lời gọi Anthropic. Không thêm Vector DB riêng, không thêm ngôn ngữ mới, không thêm dịch vụ mới phải vận hành. Tổng thời gian đưa lên production: khoảng 3 tuần.

Bài học: với tính năng AI gắn thêm, hãy tái dùng hạ tầng sẵn có trước khi mang về công nghệ mới. pgvector + queue trên Redis có sẵn thường là con đường ngắn nhất tới production.

Hướng dẫn từng bước

Đây là lộ trình dựng backend AI từ số 0 tới production, theo thứ tự tôi khuyên làm.

Bước 1 — Chọn ngôn ngữ/framework theo đội hình. Python (FastAPI) nếu đội mạnh về AI/ML và cần hệ sinh thái thư viện. Node.js (Fastify/Express, hoặc Next.js API routes) nếu đội full-stack JS và muốn ráp nhanh với frontend. Cả hai đều tốt — điều quan trọng là async và streaming phải được hỗ trợ tốt.

Bước 2 — Dựng lớp LLM Gateway trước tiên. Trước cả khi viết business logic, hãy tạo một module llm/ với một hàm thống nhất, ví dụ generate(messages, options). Bên trong nó: chọn provider, gắn timeout, retry với backoff, fallback provider dự phòng, đếm token, và ghi log. Toàn bộ phần còn lại của app chỉ gọi hàm này, không bao giờ gọi SDK nhà cung cấp trực tiếp.

Bước 3 — Thiết kế database schema. Tối thiểu cần: users, conversations, messages (kèm cột lưu token và chi phí), documents, và bảng lưu vector (với pgvector là một cột vector trong bảng chunks). Ghi lại chi phí từng lời gọi ngay từ ngày đầu — sau này bạn sẽ cần nó để phân tích unit economics.

Bước 4 — Bật streaming end-to-end. Endpoint chat trả về SSE. API server nhận stream token từ LLM gateway và đẩy thẳng xuống client. Đây là thứ tạo cảm giác "nhanh" dù model thực ra không nhanh hơn.

Bước 5 — Thêm queue cho việc nặng. Bất kỳ tác vụ nào tốn hơn ~2 giây và không cần kết quả tức thì (index tài liệu, xử lý batch, gọi model phân tích dài) đều đẩy vào queue. Endpoint chỉ nhận việc và trả job id; client hỏi trạng thái sau hoặc nhận thông báo qua webhook/WebSocket.

Bước 6 — Thêm cache và rate-limit bằng Redis. Cache câu trả lời cho prompt lặp lại; rate-limit theo user để chống lạm dụng và bảo vệ chi phí. Semantic cache (so khớp gần đúng qua embedding) là tầng nâng cao đáng thêm khi lưu lượng lớn.

Bước 7 — Gắn observability. Log mọi request LLM: model, prompt (hoặc hash), token vào/ra, latency, chi phí, trạng thái thành công/lỗi. Không có lớp này, bạn sẽ "mù" khi production trục trặc. Công cụ như Langfuse hay Helicone giúp việc này, hoặc tự log vào Postgres cho đơn giản.

Bước 8 — Chuẩn bị chịu lỗi. Timeout hợp lý, retry có giới hạn, circuit breaker khi provider chập chờn, và fallback provider. LLM API sẽ lỗi và chậm — hãy coi đó là điều bình thường và thiết kế cho nó.

Lỗi thường gặp & mẹo

Lỗi: gọi LLM đồng bộ, làm nghẽn worker. Đây là lỗi số một. Một request treo 10 giây giữ chết một worker. Luôn dùng async I/O cho mọi lời gọi mạng.

Lỗi: nhét việc index/embedding vào đường request. Người dùng upload file 100 trang và ngồi chờ 40 giây. Sai. Đẩy vào queue, trả về ngay, xử lý nền.

Lỗi: gọi thẳng SDK OpenAI khắp code. Khi bạn muốn đổi sang Claude, thêm fallback, hay đổi giá — bạn phải sửa 30 chỗ. Bọc sau một gateway từ ngày đầu.

Lỗi: không log token và chi phí. Đến cuối tháng nhận hóa đơn sốc mà không biết endpoint nào ngốn tiền. Ghi lại chi phí per-request ngay từ đầu.

Lỗi: quên rate-limit. Một người dùng (hoặc bot) có thể spam và đốt sạch ngân sách LLM trong vài phút. Rate-limit theo user là hàng rào bắt buộc.

Lỗi: mang Vector DB "xịn" quá sớm. Chưa có 1 triệu vector đã dựng Pinecone/Milvus riêng, tăng gánh vận hành vô ích. pgvector đủ dùng rất lâu.

Mẹo — bắt đầu monolith, tách service khi cần. Đừng vội chia microservices. Một backend gọn gàng với module rõ ràng (api/, llm/, rag/, workers/) chạy tốt tới quy mô kha khá. Tách service chỉ khi có lý do vận hành thực sự.

Mẹo — thiết kế cho "graceful degradation". Nếu vector DB chậm, vẫn trả lời được (không có ngữ cảnh). Nếu provider chính lỗi, fallback provider phụ. Sản phẩm AI tốt là sản phẩm hiếm khi sập hoàn toàn.

Mẹo — tách config prompt ra khỏi code. Prompt template nên nằm ở nơi dễ sửa (file config hoặc DB), để chỉnh mà không cần deploy lại.

Bài tập thực hành

  • Vẽ sơ đồ kiến trúc. Chọn một ý tưởng sản phẩm AI (ví dụ: trợ lý trả lời câu hỏi nội quy công ty). Vẽ đủ bốn khối: API server, LLM service, Vector DB, và data/state layer (DB + cache + queue). Đánh dấu đâu là luồng streaming, đâu là việc chạy nền.
  • Thiết kế LLM gateway. Viết pseudo-code (hoặc code thật) cho hàm generate(messages, options) xử lý: timeout, retry với backoff, fallback từ provider A sang B, và đếm token. Đây là trái tim của backend.
  • Quyết định build-vs-reuse. Với sản phẩm ở bài tập 1, liệt kê: sẽ dùng API ngoài hay self-host (và vì sao), sẽ dùng pgvector hay Vector DB riêng, sẽ tái dùng Postgres/Redis có sẵn hay không. Viết một câu lý do cho mỗi lựa chọn.
  • Xác định việc cần đẩy ra queue. Liệt kê mọi tác vụ trong sản phẩm tốn hơn 2 giây. Với mỗi cái, ghi rõ: client sẽ biết kết quả bằng cách nào (polling, webhook, hay WebSocket)?
  • Nâng cao — thiết kế schema log chi phí. Thiết kế bảng lưu mỗi lời gọi LLM kèm model, token vào/ra, chi phí, latency. Viết một truy vấn trả lời: "Endpoint nào tốn nhiều tiền nhất tháng này?"

Tóm tắt

  • Backend AI khác backend truyền thống ở chỗ phải làm việc với lời gọi chậm, đắt, bất định, và tối ưu cho streaming.
  • Bốn khối cốt lõi: API server (async, streaming), LLM service (API / self-host / hybrid), Vector DB (bắt đầu với pgvector), và data/state layer (Postgres + Redis + queue).
  • Mẫu quan trọng nhất: bọc mọi lời gọi LLM sau một gateway chung để lo retry, fallback, đếm token, cache, log — cho bạn tự do đổi model mà không đập lại backend.
  • Ba nguyên tắc vàng: async mọi lời gọi mạng, tách việc nặng ra background queue, cache và rate-limit để bảo vệ chi phí.
  • Thực dụng trước, phức tạp sau: bắt đầu monolith gọn gàng, tái dùng hạ tầng sẵn có, và chỉ tách service/nâng cấp Vector DB khi quy mô thực sự đòi hỏi.
Backend vững không làm sản phẩm AI của bạn thông minh hơn — nhưng backend yếu chắc chắn sẽ giết một sản phẩm thông minh trước khi nó kịp chứng minh giá trị. Hãy đầu tư đúng chỗ ngay từ đầu.