Ứng dụng chat với AI
Hiểu đề
Yêu cầu chức năng
- Người dùng có thể gửi tin nhắn và thấy câu trả lời hiện dần từng chữ (streaming)
- Người dùng có thể dừng câu trả lời đang sinh hoặc yêu cầu sinh lại
- Người dùng có thể xem danh sách hội thoại, mở lại một hội thoại cũ và hỏi tiếp với đúng ngữ cảnh
- Người dùng gói Free và Plus có hạn mức khác nhau và được báo rõ khi hết hạn mức
Yêu cầu phi chức năng
- Token đầu tiên hiện ra p95 < 1,5 s khi hệ thống chưa quá tải
- Tốc độ sinh ≥ 20 token/s cho mỗi câu trả lời đang stream
- Mất mạng giữa chừng không mất câu trả lời: nối lại stream được trong 10 phút
- API sẵn sàng 99,9%; khi GPU quá tải thì ưu tiên gói trả phí, gói Free nhận 429 kèm Retry-After
- Mở lại hội thoại cũ p95 < 300 ms, lịch sử không mất
Ngoài phạm vi: Huấn luyện và fine-tune mô hình; Tải file, phân tích ảnh, sinh ảnh, giọng nói; Plugin, công cụ gọi web, agent nhiều bước; Kiểm duyệt nội dung chi tiết (chỉ nhắc là một bước trong luồng).
Thực thể và API
| Method | Endpoint | Ghi chú |
|---|---|---|
| POST | /v1/conversations | Tạo hội thoại mới, trả conversationId. |
| GET | /v1/conversations?cursor= | Danh sách hội thoại theo updated_at giảm dần, phân trang bằng cursor. |
| GET | /v1/conversations/{id}/messages?before={seq} | Lịch sử tin nhắn, tải dần từ mới về cũ. |
| POST | /v1/conversations/{id}/messages | Gửi tin: body {content, model, clientMessageId}. Response là text/event-stream, mỗi event có id tăng dần. Trả 429 nếu hết hạn mức. |
| GET | /v1/generations/{id}/stream | Nối lại stream đang chạy, header Last-Event-ID để chỉ nhận phần còn thiếu. |
| POST | /v1/generations/{id}/cancel | Dừng sinh. GPU nhả slot ngay, phần đã sinh vẫn được lưu. |
Thiết kế tổng thể
Đi lần lượt từng yêu cầu chức năng. Mục tiêu ở đây là chạy đúng, việc tối ưu để dành cho phần đào sâu.
1) Người dùng có thể gửi tin nhắn và thấy câu trả lời hiện dần
Chat API không giữ trạng thái: nó kiểm tra hạn mức, ghép ngữ cảnh, đẩy yêu cầu vào bộ điều phối GPU rồi đọc token từ một stream trung gian để gửi xuống client bằng SSE. Vì token nằm ở stream trung gian theo generation_id, instance nào cũng phục vụ được việc nối lại.
- 1Client gửi POST /messages, Chat API lưu tin của user và tạo Generation QUEUED
- 2Chat API trừ trước hạn mức ước lượng (max_tokens) trong Redis, hết hạn mức thì trả 429
- 3Chat API ghép ngữ cảnh (system prompt + tóm tắt + các tin gần nhất) và gửi tới bộ điều phối
- 4Bộ điều phối xếp yêu cầu vào hàng đợi theo gói, GPU sinh token theo continuous batching
- 5GPU worker ghi từng cụm token vào Redis Stream gen:{id}; Chat API đọc và đẩy xuống client qua SSE
- 6Xong thì lưu tin assistant, ghi số token thật vào UsageLedger và hoàn lại phần hạn mức trừ dư
Nguồn: Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · bài "System Design: The Distributed Messaging Queue", mục "Motivation" (hàng đợi hấp thụ đột biến, priority queue) ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "Rate Limiter Algorithms", mục "Token bucket algorithm" ; Redis in Action ↗ · ch.6 · §6.5 Pull messaging (tr. 140–146) ; Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản
2) Người dùng có thể dừng hoặc sinh lại câu trả lời
Dừng là việc quan trọng về chi phí: mỗi giây GPU tiếp tục sinh cho người đã bỏ đi là tiền mất. Lệnh dừng đi qua cờ trong Redis mà GPU worker kiểm tra giữa các bước sinh.
- 1Client gọi POST /generations/{id}/cancel hoặc đóng tab (server phát hiện SSE bị ngắt quá 30 s)
- 2Chat API đặt cờ cancel:{id} trong Redis
- 3GPU worker thấy cờ ở bước sinh kế tiếp, bỏ yêu cầu khỏi batch, giải phóng KV cache
- 4Phần đã sinh được lưu với trạng thái CANCELLED; sinh lại tạo Message mới cùng parent_id
Nguồn: Redis in Action ↗ · ch.3 · §3.7 Other commands › "Expiring keys" (tr. 61–62) ; Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản
3) Người dùng có thể mở lại hội thoại cũ và hỏi tiếp
Lịch sử đọc theo conversation_id và seq nên chọn store khóa theo hội thoại. Khi hỏi tiếp, không gửi toàn bộ lịch sử cho mô hình mà chỉ gửi phần vừa đủ trong giới hạn ngữ cảnh.
- 1Client gọi GET /conversations để lấy danh sách, GET /messages?before= để tải lịch sử
- 2Store tin nhắn đọc theo partition conversation_id, sắp theo seq
- 3Khi user hỏi tiếp, Chat API lấy summary + N tin gần nhất cho đến khi chạm ngân sách token
- 4Worker nền cập nhật summary khi phần lịch sử ngoài cửa sổ vượt ngưỡng
Nguồn: Grokking Modern System Design · ch.10 Key-value Store ↗ · bài "Design of a Key-value Store", mục "Data type" và "API design" ; Grokking Modern System Design · ch.9 Databases ↗ · bài "Data Partitioning", mục "Hash-based sharding" ; Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản
Hạ tầng theo quy mô
Cùng một sản phẩm, ba mức người dùng. Chọn từng mức để xem sơ đồ, phép tính và cấu hình server thay đổi thế nào.
Sản phẩm mới, gọi API mô hình của nhà cung cấp và trả tiền theo token. Chat API trên Cloud Run stream SSE, một Postgres lưu user, hội thoại, tin nhắn và hạn mức.
Ước lượng
- Tin nhắn: 10.000 DAU × 10 tin / 86.400 ≈ 1,2 req/s, đỉnh ×5 ≈ 6 req/s
- Giả định mỗi lượt: 2.000 token vào, 400 token ra, sinh 30 token/s → 400 / 30 ≈ 13 s; stream đồng thời ≈ 6 × 13 ≈ 80
- Giá API giả định 0,5 USD/1M token vào, 1,5 USD/1M token ra: 2.000 × 0,5 / 10^6 + 400 × 1,5 / 10^6 = 0,001 + 0,0006 = 0,0016 USD/lượt
- Chi phí token: 100.000 lượt/ngày × 30 × 0,0016 ≈ 4.800 USD/tháng
- Nếu tự host: 6 × 600 token tương đương = 3.600 /s → ceil(3.600 / 1.050) = 4 GPU × 730 h × 2,5 USD ≈ 7.300 USD/tháng và rảnh phần lớn thời gian
- Lưu trữ: 100.000 lượt × 2,5 KB ≈ 250 MB/ngày ≈ 91 GB/năm
Nguồn con số: Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · bài "Examples of Resource Estimation", mục "Number of servers required", "Storage requirements" ; Quy tắc ước lượng của khoá học · /hoc/chon-server (500 req/s mỗi vCPU, chạy 60% CPU, Redis ×1,3) ; Chưa có nguồn trong thư viện: thư viện không có sách về LLM/GPU; thông lượng GPU, giá GPU-giờ, giá token API, token/s và kích thước KV cache đều là giả định của khoá học, cần đo thực tế
Bảng hạ tầng
| Thành phần | Chọn | Số lượng | Cấu hình | Vì sao |
|---|---|---|---|---|
| Chat API | Node.js (NestJS), Cloud Run, bật streaming response | 2 | 2 vCPU · 4 GB RAM | 6 req/s và 80 stream chỉ cần 1 instance, chạy 2 để dự phòng; timeout request nâng lên 5 phút. |
| Mô hình | API LLM của nhà cung cấp (OpenAI, Anthropic, Google...) | 1 | Trả theo token, stream sẵn | 4 GPU tự host đắt hơn và phần lớn thời gian ngồi không; API không cần vận hành. |
| Database | PostgreSQL managed | 1 | 2 vCPU · 8 GB · 200 GB SSD | 91 GB/năm, index (conversation_id, seq); bảng usage đếm hạn mức theo ngày. |
| Đăng nhập | Auth0 / Firebase Auth / Clerk | 1 | Dịch vụ bên ngoài | Không tự làm phần đăng nhập ở giai đoạn đầu. |
| Thanh toán gói | Stripe | 1 | Dịch vụ bên ngoài, webhook | Webhook cập nhật gói của user trong Postgres. |
- Điểm xuất phát: một Chat API stream thẳng từ API của nhà cung cấp mô hình
- Mọi dữ liệu nằm trong một Postgres, hạn mức đếm bằng bảng usage theo ngày
- Chưa có GPU riêng, chi phí biến đổi theo số token
Hóa đơn token tăng tuyến tính theo người dùng và nhà cung cấp giới hạn số token mỗi phút của tài khoản. Lên 1 triệu DAU thì vừa đắt vừa bị giới hạn, cần cân nhắc tự host và hạn mức chính xác theo token.
Cấu hình và chi phí là ước lượng để học và trình bày trong phỏng vấn. Xem cách tính ở bài Chọn server.
So sánh nhanh ba mức
| Thành phần | Mức 1 | Mức 2 | Mức 3 |
|---|---|---|---|
| Người dùng | 10.000 DAU | 1 triệu DAU | 50 triệu DAU |
| Chi phí | ≈ 4.000–6.000 USD/tháng (ước lượng, gần hết là tiền token) | ≈ 230.000–380.000 USD/tháng (ước lượng, GPU chiếm trên 90%, giá giả định 2,5 USD/GPU-giờ) | ≈ 5–9 triệu USD/tháng (ước lượng, giả định 2 USD/GPU-giờ thuê dài hạn; GPU chiếm trên 90%) |
| Chat API | Node.js (NestJS), Cloud Run, bật streaming response 2 × 2 vCPU · 4 GB RAM | Node.js, container autoscale 2–4 (autoscale) × 2 vCPU · 4 GB RAM | Node.js trên Kubernetes, 3 region 76 pod (HPA theo số kết nối) × 2 vCPU · 4 GB mỗi pod |
| Mô hình | API LLM của nhà cung cấp (OpenAI, Anthropic, Google...) 1 × Trả theo token, stream sẵn | Chưa cần | Chưa cần |
| Database | PostgreSQL managed 1 × 2 vCPU · 8 GB · 200 GB SSD | PostgreSQL managed (HA) 1 primary + 1 replica × 4 vCPU · 16 GB · 200 GB SSD | PostgreSQL HA, đọc qua replica 1 primary + 2 replica × 16 vCPU · 64 GB · 1 TB SSD |
| Đăng nhập | Auth0 / Firebase Auth / Clerk 1 × Dịch vụ bên ngoài | Chưa cần | Chưa cần |
| Thanh toán gói | Stripe 1 × Dịch vụ bên ngoài, webhook | Chưa cần | Chưa cần |
| Load balancer | Chưa cần | ALB / Cloud Load Balancing 1 × Idle timeout 300 s cho SSE | Chưa cần |
| Bộ điều phối | Chưa cần | Service nhỏ (Go/Node) giữ hàng đợi Plus/Free 3 × 2 vCPU · 4 GB RAM | Chưa cần |
| Cụm GPU | Chưa cần | vLLM, mô hình open-weight ~70B, continuous batching 70–200 GPU (9–25 node 8×H100) × 8 × H100 80 GB mỗi node | vLLM/TensorRT-LLM, pool lớn + pool nhỏ, prefix cache ≈ 3.000 + 1.050 GPU (≈ 507 node) × 8 × H100 80 GB mỗi node |
| Redis | Chưa cần | ElastiCache / Memorystore 1 primary + 1 replica × 2 vCPU · 8 GB | Chưa cần |
| Tin nhắn | Chưa cần | DynamoDB, PK conversation_id, SK seq 1 bảng × On-demand | ScyllaDB / Cassandra, PK conversation_id, CK seq Cluster 30+ node, RF 3 × 16 vCPU · 64 GB · NVMe mỗi node |
| Global LB + WAF | Chưa cần | Chưa cần | Cloudflare / Global Load Balancer 1 × Định tuyến theo region gần nhất, chặn bot |
| Router + điều phối | Chưa cần | Chưa cần | Service phân loại câu hỏi + hàng đợi ưu tiên 12 pod × 4 vCPU · 8 GB mỗi pod |
| Redis Cluster | Chưa cần | Chưa cần | ElastiCache Redis Cluster mỗi region 16 shard × (1 primary + 1 replica) × 4 vCPU · 16 GB mỗi node |
| Kafka + worker | Chưa cần | Chưa cần | MSK / Confluent, worker Node.js 6 broker, 20 pod worker × Topic usage, conversation-updated |
| Vector DB | Chưa cần | Chưa cần | pgvector shard / Qdrant / OpenSearch k-NN Cluster theo region × Index theo user_id |
Đào sâu
Mỗi câu hỏi đi từ cách đơn giản đến cách người phỏng vấn mong đợi ở cấp senior.
1) Stream token tới client thế nào khi API là stateless và nhiều instance?
API gọi mô hình, đợi 10-20 s cho đủ câu trả lời rồi trả JSON. Dễ làm nhưng người dùng nhìn màn hình trống rất lâu, và load balancer hay proxy có thể cắt request vì timeout.
Đánh đổi: Đơn giản nhất nhưng trải nghiệm kém, không đạt yêu cầu token đầu < 1,5 s.
Nguồn: Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản ; Grokking Modern System Design · ch.4 Non-functional System Characteristics ↗ · bài "Availability" (độ trễ và tính sẵn sàng là yêu cầu phi chức năng)
API mở stream gRPC tới worker và chuyển tiếp từng token xuống client qua SSE. SSE là HTTP thường, một chiều, đi qua proxy dễ, trình duyệt có sẵn EventSource. Chú ý backpressure: client chậm thì API phải đệm có giới hạn chứ không đọc vô hạn từ worker.
Đánh đổi: Kết nối bị gắn vào một instance API: instance chết hoặc client đổi mạng là mất phần đang sinh, phải sinh lại và tốn GPU lần nữa.
Nguồn: Grokking Modern System Design · ch.3 Abstractions ↗ · bài "Network Abstractions: Remote Procedure Calls" (RPC từ API tới worker) ; Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản
Worker ghi cụm token (khoảng 250 ms một lần) vào Redis Stream gen:{id} với TTL 10 phút, mỗi entry có id tăng dần. Instance API nào cũng XREAD từ stream đó và gửi xuống client với id của SSE event. Mất kết nối thì client gọi /stream kèm Last-Event-ID và nhận đúng phần thiếu, GPU không phải sinh lại. Gom token theo cụm giúp giảm số lần ghi Redis và số gói tin xuống client.
Đánh đổi: Thêm một bước trung gian (vài ms trễ) và Redis phải chịu tải ghi tỉ lệ với số stream đồng thời; ở quy mô lớn cần Redis Cluster.
Nguồn: Redis in Action ↗ · ch.6 · §6.5 Pull messaging (list/pub-sub làm kênh nhận) ; Redis in Action ↗ · ch.3 · §3.6 Publish/subscribe (tr. 54–57) ; Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản
2) Cần bao nhiêu GPU, và xếp hàng, gom batch yêu cầu thế nào cho hiệu quả?
Mỗi bước sinh token phải đọc toàn bộ trọng số mô hình từ bộ nhớ GPU, nên chạy một yêu cầu thì GPU chủ yếu chờ bộ nhớ. Giả định một H100 chỉ ra khoảng 30-40 token/s cho một luồng, tức là phải có hàng nghìn GPU cho vài trăm req/s.
Đánh đổi: Dễ hiểu nhưng lãng phí gần hết năng lực GPU, chi phí mỗi request cao gấp hàng chục lần.
Nguồn: Chưa có nguồn trong thư viện: thư viện không có sách về LLM/GPU; thông lượng GPU, giá GPU-giờ, giá token API, token/s và kích thước KV cache đều là giả định của khoá học, cần đo thực tế ; Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · bài "Put Back-of-the-envelope Numbers in Perspective", mục "Requests estimation" (công thức số server từ RPS)
Đợi đủ 16-32 yêu cầu (hoặc hết 50 ms) rồi chạy chung một batch, một lần đọc trọng số phục vụ nhiều luồng. Thông lượng tăng nhiều lần.
Đánh đổi: Câu trả lời ngắn phải đợi câu dài nhất trong batch xong mới nhường chỗ, GPU lại bị bỏ trống và độ trễ token đầu tăng.
⚠ Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản
Dùng server như vLLM: sau mỗi bước sinh, yêu cầu xong rời batch và yêu cầu mới vào ngay, KV cache cấp phát theo trang để không phí bộ nhớ. Giả định để tính: một H100 chạy mô hình ~70B ra ≈ 1.500 token tương đương/s tổng, với 1 token tương đương = 1 token ra hoặc 10 token vào (prefill rẻ hơn nhiều). Số GPU = tải đỉnh / (1.500 × 0,7). Phía trước là bộ điều phối có hàng đợi riêng cho Plus và Free, chỉ đưa yêu cầu vào GPU khi còn chỗ KV cache, đo độ dài hàng đợi để autoscale và cắt tải gói Free trước.
Đánh đổi: Một GPU mới cần vài phút để tải trọng số nên autoscale chậm; phải giữ sẵn dung lượng nền và chấp nhận trả 429 cho gói Free trong những phút đỉnh.
Nguồn: Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · bài "System Design: The Distributed Messaging Queue", mục "Motivation" (Priority queue: mỗi mức ưu tiên một queue, Rate limiting hấp thụ đột biến) ; Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · bài "Design Considerations", mục "Execution cap" và "Prioritization" ; Chưa có nguồn trong thư viện: thư viện không có sách về LLM/GPU; thông lượng GPU, giá GPU-giờ, giá token API, token/s và kích thước KV cache đều là giả định của khoá học, cần đo thực tế
3) Lưu hội thoại và lấy ngữ cảnh cho câu hỏi tiếp theo thế nào?
Mỗi tin mới lại gửi cả hội thoại. Hội thoại 50 lượt có thể dài 30.000 token, vừa chậm token đầu vừa tốn tiền, và sẽ vượt giới hạn ngữ cảnh của mô hình.
Đánh đổi: Chi phí mỗi lượt tăng theo độ dài hội thoại, tổng chi phí một hội thoại tăng theo bình phương số lượt.
Nguồn: Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "System Design: The Rate Limiter", mục "Why do we need a rate limiter?" (không nêu gì về cửa sổ ngữ cảnh) ; Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản
Lưu Message với khóa (conversation_id, seq), đọc ngược từ mới về cũ đến khi đủ ngân sách, ví dụ 4.000 token. Ở mức nhỏ dùng Postgres với index (conversation_id, seq); khi dữ liệu lên hàng TB thì chuyển sang DynamoDB hoặc Cassandra với partition key là conversation_id.
Đánh đổi: Rẻ và dự đoán được chi phí, nhưng mô hình quên hẳn những gì nói ở đầu hội thoại.
Nguồn: Grokking Modern System Design · ch.9 Databases ↗ · bài "Data Partitioning", mục "Key-range based sharding" ; Microservices Patterns ↗ (.zip) · ch.7 · §7.4 Implementing a CQRS view with AWS DynamoDB, mục "Data modeling and query design with DynamoDB" (tr. 244–249) ; Grokking Modern System Design · ch.9 ↗ · bài "Types of Databases", mục "Key-value database"
Ngữ cảnh = system prompt + summary của phần cũ + vài đoạn cũ liên quan nhất (tìm bằng vector của câu hỏi) + các tin gần nhất. Worker nền cập nhật summary và embedding sau mỗi vài lượt, không nằm trên đường trả lời. Giữ phần đầu prompt ổn định để GPU tái dùng KV cache của prefix, giảm thời gian prefill. Tin nhắn cũ hơn 90 ngày có thể chuyển sang storage rẻ và nạp lại khi mở.
Đánh đổi: Nhiều thành phần hơn (worker tóm tắt, vector DB) và tóm tắt có thể làm mất chi tiết; cần đo chất lượng câu trả lời chứ không chỉ đo độ trễ.
Nguồn: Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · bài "System Design: The Distributed Messaging Queue", mục "Messaging queue use cases" (Recommender systems: xử lý tốn thời gian đưa ra ngoài đường trả lời bằng queue) ; Chưa có nguồn trong thư viện: vector database không có sách; xem bài /hoc/vector-database của khoá (cũng đánh dấu chưa có nguồn) ; Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản
4) Giới hạn theo gói và kiểm soát chi phí mỗi request thế nào?
Giới hạn 20 request/phút cho mọi người. Nhưng một request 'tóm tắt tài liệu này' có thể tốn gấp 50 lần request 'xin chào', nên đếm request không phản ánh chi phí GPU.
Đánh đổi: Dễ làm nhưng vài người dùng nặng có thể ngốn phần lớn GPU mà vẫn trong hạn mức.
Nguồn: Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "System Design: The Rate Limiter", mục "Why do we need a rate limiter?" ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "Requirements of a Rate Limiter's Design", mục "Types of throttling"
Mỗi user có bucket tính theo token tương đương mỗi phút và hạn mức theo ngày, giá trị tùy gói (ví dụ Free 50 tin/ngày, Plus cao hơn nhiều). Dùng script Lua trên Redis để kiểm tra và trừ nguyên tử. Trả header còn lại bao nhiêu để client hiển thị.
Đánh đổi: Chưa biết trước câu trả lời dài bao nhiêu nên chỉ trừ được sau khi xong; nhiều request song song có thể vượt hạn mức.
Nguồn: Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "Rate Limiter Algorithms", mục "Token bucket algorithm" (tham số, ưu nhược điểm) ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "Design of a Rate Limiter", mục "Race condition" (kiểm tra và trừ phải nguyên tử) ; Redis in Action ↗ · ch.11 · §11.2 Rewriting locks and semaphores with Lua (tr. 255–260) ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "Design of a Rate Limiter", mục "High-level design" (luật theo loại)
Trước khi sinh, trừ trước token vào + max_tokens; xong thì trả lại phần dư theo số token thật và ghi UsageLedger qua sự kiện. Mỗi request có chi phí tính được: (token tương đương / thông lượng GPU) × giá GPU-giây. Với giả định ở bài này một lượt 600 token tương đương tốn ≈ 0,57 GPU-giây ≈ 0,0004 USD. Để giảm chi phí: định tuyến câu hỏi dễ và gói Free sang mô hình nhỏ, cache prefix, giới hạn max_tokens theo gói, và dừng sinh ngay khi client bỏ đi.
Đánh đổi: Phải có bộ phân loại để định tuyến, chất lượng có thể giảm cho câu hỏi bị đánh giá sai là dễ; sổ quyết toán cần idempotent để không trừ hai lần.
Nguồn: Microservices Patterns ↗ (.zip) · ch.3 · §3.3.6 Handling duplicate messages (sổ quyết toán idempotent, tr. 95–97) ; Chưa có nguồn trong thư viện: thư viện không có sách về LLM/GPU; thông lượng GPU, giá GPU-giờ, giá token API, token/s và kích thước KV cache đều là giả định của khoá học, cần đo thực tế
Trong phỏng vấn
- “Tài nguyên đắt nhất là GPU, nên tôi tính kích thước theo token mỗi giây, không theo request mỗi giây.”
- “Tôi stream bằng SSE; token đi qua Redis Stream theo generation_id nên API stateless và client nối lại được.”
- “Trên GPU tôi dùng continuous batching, phía trước có hàng đợi ưu tiên theo gói để cắt tải Free trước.”
- “Hạn mức tính theo token: trừ trước theo max_tokens, quyết toán theo số token thật khi sinh xong.”
- Tính số máy theo req/s như API thường và quên rằng mỗi request chiếm GPU hơn 10 giây.
- Gửi toàn bộ lịch sử hội thoại cho mô hình mỗi lượt.
- Để load balancer cắt SSE ở timeout mặc định 60 s.
- Không dừng sinh khi client bỏ đi, GPU vẫn chạy cho người không còn đọc.
Đọc thêm
Sách trong thư viện của bạn. Số chương có thể lệch nhẹ so với bản PDF.