// BLOG — 2026-08-26

P1.2 — Một app AI gần như miễn phí: khi "đọc" thay vì "sinh" đưa chi phí LLM về gần 0

Case study một app Kinh Dịch + lịch âm dùng deterministic engine và cache theo ngày để đưa số request LLM cho toàn bộ người dùng từ N lần/ngày xuống còn 1 lần/ngày.

<!-- MỨC GIẤU:

  • LỘ: câu chuyện / vấn đề / quyết định thiết kế (deterministic, AI chỉ đọc không sinh) / hiệu quả cache (đưa request LLM về ~1/ngày) / bảng request trước & sau / trade-off. Đây là "khái niệm + kết quả" — an toàn chia sẻ.
  • CHE CHI TIẾT: không nêu tên model chính xác, không nêu cache key/cấu trúc cache/giá trị rate-limit/hạn mức thật, không nêu prompt, không nêu code/config/path thật.
  • CODE TẦNG: các code block là pseudo-code MINH HOẠ mình tự viết để dễ hiểu cơ chế — KHÔNG phải source triển khai. Vivi kiểm tra chú thích "không phải nguồn triển khai".
  • Kỹ thuật nguồn (cách dựng thành phần workflow, tham số chính xác) được GIẤU — chỉ nói chung.

-->

# Một app AI gần như miễn phí: khi "đọc" thay vì "sinh" đưa chi phí LLM về gần 0

Bài này nằm trong chuỗi mổ xẻ kỹ thuật tại zeodavu.com/blog. Mình ghi lại một case study mình thật sự đang làm: một app Kinh Dịch + lịch âm. Điểm đáng nói không phải model, mà là một quyết định thiết kế khiến chi phí LLM không tăng theo số người dùng. Không có recipe để sao chép; chỉ có cấu trúc, con số, và cái giá phải trả.


Vấn đề mở đầu rất quen: một app gọi LLM mỗi request thì chi phí tăng tuyến tính theo số user. Nếu ngày nào cũng có nghìn người mở ra, mà mỗi lần mở là một lần gọi model, thì tháng nào cũng là một khoản đi vô.

App mình đang làm thuộc nhóm dễ dính bẫy này. Nó phục vụ một nhu cầu lặp lại và giống nhau mỗi ngày. Và cái "giống nhau" đó hoá ra lại là chìa khoá.


Bản ngây thơ: gọi model mỗi lần người dùng mở

Khi mới nghĩ về luồng này, cách dễ nhất là: người dùng mở app → tính ra kết quả → gọi model luận giải → trả về. Về mặt hiểu, nó sạch. Về mặt chi phí, nó là một cái máy in tiền.

```python

# pseudo-code minh hoạ — KHÔNG phải nguồn triển khai

# Bản "ngây thơ": mỗi request = 1 lần gọi model def handle_user_open(user, today): hexa = compute_daily_oracle(today) # deterministic, luôn giống nhau reading = llm_interpret(hexa) # <-- đắt: mỗi user 1 lần return reading ```

Vấn đề nằm ở dòng thứ ba. hexa thì ai mở hôm nay cũng nhận về một giá trị (nó tính từ ngày, không tính từ người). Nhưng reading lại bị gọi riêng cho từng user. Kết quả: cùng một đoạn văn luận giải, được sinh lại N lần mỗi ngày.

Đó là phí tổn không cần thiết. Và mình nhận ra điều đó sớm hơn là muộn.


Hai quyết định thiết kế đáng giá hơn model

1. Để máy "đọc", không để máy "sinh"

Điểm mấu chốt: mình không để model sinh ra kết quả (quẻ, ý nghĩa, can chi). Mọi phần "tử vi - dịch số" được tính bằng code thuần — hoàn toàn xác định (deterministic). Model chỉ có một việc duy nhất: đọc kết quả đã có sẵn và viết thành lời luận giải.

```python

# pseudo-code minh hoạ — KHÔNG phải nguồn triển khai

# AI chỉ đọc, không sinh: input là dữ kiện đã tính trước (deterministic) def predict(input): hexa = deterministic_engine(input) # không có AI ở đây return hexa # luôn cho cùng input -> cùng output ```

Hệ quả ngay lập tức: kết quả đưa vào model là có thể dự đoán được và lặp lại. Một khi nó lặp lại, nó cache được.

2. Cache theo ngày, không theo người

Vì kết quả cốt lõi giống nhau cho mọi người trong cùng một ngày, mình cache một lần / ngày — key theo ngày, không key theo user. Lần đầu tiên có người mở, gọi model một lần và giữ lại. Mọi lần mở sau đó trong ngày đều đọc thẳng từ cache, không chạm model.

```python

# pseudo-code minh hoạ — KHÔNG phải nguồn triển khai

# Bản tối ưu: cache theo ngày -> 1 request model/ngày cho TOÀN BỘ user CACHE = {}

def handle_user_open(user, today): if today in CACHE: return CACHE[today] # gần như miễn phí hexa = compute_daily_oracle(today) reading = llm_interpret(hexa) # chỉ gọi đúng 1 lần/ngày CACHE[today] = reading return reading ```

Đây là phần "đắt" của bài viết: một request model duy nhất phục vụ toàn bộ số user trong ngày. Càng nhiều người mở, mức tiết kiệm càng lớn — và nó rất dễ chứng minh.


Bảng số liệu trước & sau

Số liệu dưới đây tính theo số request LLM (không phải tiền — vì bản thân số request mới là đại lượng ta kiểm soát được, và nó nằm trong hạn mức miễn phí nếu giữ ở mức này).

| Kịch bản (user/ngày) | Bản ngây thơ: gọi model mỗi lần mở | Bản cache theo ngày |

|---|---|---|

| 100 | 100 request/ngày | 1 request/ngày |

| 1.000 | 1.000 request/ngày | 1 request/ngày |

| 10.000 | 10.000 request/ngày | 1 request/ngày |

Điểm mù mà nhiều người bỏ qua nằm ở chỗ: cột phải không đổi. Dù user tăng gấp trăm lần, số request LLM của luồng này vẫn là 1/ngày. Đó chính là lý do chi phí "gần như" không tăng theo quy mô.

Phần lớn app "AI" không đắt vì model. Đắt vì cùng một câu trả lời bị sinh lại N lần cho N người. Giết được chỗ lặp đó, chi phí co về gần 0.


Cái giá phải trả cho việc này

Cache không phải miễn phí — nó đổi một thứ này lấy một thứ khác.

Đánh đổi 1 — thông tin theo thời gian thực bị hy sinh. Vì cache theo ngày, mọi người cùng nhận một kết quả. Điều này ổn với nhu cầu "coi ngày xem quẻ", nhưng nếu bài toán cần cá nhân hoá hoặc cập nhật theo từng khoảnh khắc, cache theo ngày là sai thiết kế.

Đánh đổi 2 — chỉ phần lặp lại được cache. Phần không lặp lại (mỗi người một câu hỏi riêng) vẫn phải gọi model từng lần. Ở đây mình xử lý bằng một giới hạn tần suất đơn giản theo IP để chặn lạm dụng, chứ không cache. Vậy nên "gần 0" chỉ đúng cho luồng dùng chung, không phải luồng cá nhân.

Đánh đổi 3 — cache phải hợp lệ theo ngày. Nếu không cẩn thận, cache dính ngày cũ sẽ trả lời sai. Quy tắc đơn giản: key theo ngày, và xoá/bỏ qua khi đổi ngày. Nghe dễ, nhưng đây là nơi dễ sinh bug nhất nếu để cache sống quá đà.


Kết luận

Điểm đáng lấy đi từ bài này không phải "cache thì rẻ". Mà là: trước khi tối ưu, hãy xem câu trả lời của bạn có bị lặp lại hay không. Nếu có, can thiệp ở tầng dữ liệu (giết chỗ lặp) luôn rẻ hơn nhiều so với tối ưu ở tầng model.

Ba điều mình rút ra:

  • Tách "sinh" khỏi "đọc". Để model sinh là nơi phí tổn bùng nổ nhất. Đưa phần mang tính xác định về code thuần, để model chỉ lo phần cần ngôn ngữ.
  • Cache theo thuộc tính của câu hỏi, không theo người. Nếu nhiều user hỏi cùng một thứ, cache theo "thứ đó".
  • Chỉ cache cái lặp lại được. Phần cá nhân hoá thì đừng cố cache — hãy giới hạn và chấp nhận trả phí cho nó.

Bài này cố ý không nêu tên model, cache key, giá trị hạn mức, hay prompt chính xác. Mỗi code block là minh hoạ cơ chế, không phải source triển khai. Điều đáng giữ là tư duy tách tầng, không phải đoạn code.

Toàn bộ số liệu và thành quả mình ghi lại ở zeodavu.com/blog. Chính app này mình đang đưa lên khu apps.


Một phần của chuỗi mổ xẻ kỹ thuật tại zeodavu.com/blog. Nội dung là bài học mở, không phải blueprint để sao chép — thứ đáng giữ là cấu trúc và trải nghiệm, không phải danh sách công cụ.

Nếu thấy hữu ích, theo dõi [zeodavu.com](/blog) — mỗi bài là một thứ mình thật sự dùng, vấp, và sửa lại.