01 · LLM dưới nắp capo — token, attention, KV cache, sampling

Bài đầu của series "AI agent mức senior". Mục tiêu không phải để bạn tự train model, mà để mọi quyết định kỹ thuật phía sau (chọn model, ước chi phí, thiết kế prompt, xử lý lỗi, giải thích vì sao agent "ngáo") đều suy ra được từ cơ chế, thay vì học thuộc mẹo. Interviewer mức senior hay hỏi kiểu: "vì sao cùng prompt, temperature=0 mà vẫn ra khác nhau?", "vì sao context 1M token mà vẫn quên?", "vì sao tiếng Việt đắt hơn?" — cả ba đều trả lời được từ bài này.

Đã verify gì, chưa verify gì. Lab chạy thật trên máy lab (Python 3.11, numpy 2.4.6, tokenizers 0.23.2, CPU Xeon 4 core): đếm token bằng tokenizer Claude đời cũ, phân phối sampling, float không kết hợp, độ phức tạp KV cache trên một decoder đồ chơi. Kiến trúc thật của Claude không public — mọi con số về model thật (RAM KV cache) dùng cấu hình public của Llama 3.1 để minh hoạ. Môi trường lab không có Context7 MCP và không có API key: thông tin API/giá lấy từ tài liệu tham chiếu Claude API đóng gói kèm công cụ (bản cache 2026-09-25) và mã nguồn SDK đã cài; nguồn ngoài đã tra lại bằng web search ngày 2026-09-30. Chỗ nào không chạy được thì ghi "chưa verify".

Câu trả lời 60 giây

"LLM là một hàm: nhận chuỗi token, trả về phân phối xác suất cho token kế tiếp; sinh văn bản là gọi hàm đó lặp lại, mỗi lần nối token vừa chọn vào đầu vào. Từ một abstraction đó suy ra gần hết hành vi: model luôn sinh ra thứ nghe hợp lý kể cả khi không biết (hallucination), tính tiền và giới hạn theo token, không có trí nhớ giữa hai lần gọi, và không tất định vì sampling cộng với số học float trên GPU phụ thuộc batch. Về chi phí, phần đọc prompt (prefill) chạy song song nên nhanh, phần sinh (decode) tuần tự và bị giới hạn bởi băng thông bộ nhớ nên chậm — vì vậy output đắt hơn input 5 lần. Với tiếng Việt có dấu, tokenizer cắt vụn byte UTF-8 nên cùng một câu tốn gấp ~3 lần token so với tiếng Anh — tôi đo được 40 vs 12 token. Tôi coi LLM như một dependency xác suất: mọi output phải được validate như input từ người dùng."

Thuật ngữ dùng trong bài

Thuật ngữNghĩa ngắn
tokenĐơn vị chữ model đọc/viết, do tokenizer cắt ra (một từ, một mẩu từ, hoặc vài byte).
BPE (Byte Pair Encoding)Thuật toán tạo bảng token: bắt đầu từ byte, gộp dần các cặp hay đi cùng nhau thành token mới.
logitsĐiểm thô model gán cho từng token trong bảng từ vựng trước khi đổi thành xác suất bằng softmax.
attentionCơ chế cho mỗi token "nhìn" các token trước nó và lấy thông tin có trọng số.
KV cacheBộ nhớ lưu Key/Value của các token đã xử lý để không phải tính lại ở mỗi bước sinh.
prefill / decodePrefill: xử lý toàn bộ prompt một lượt. Decode: sinh từng token một.
TTFT (time to first token)Thời gian từ lúc gửi request tới lúc nhận token đầu tiên — người dùng cảm nhận độ "nhanh" qua con số này.
temperature / top-pTham số sampling: làm phẳng/nhọn phân phối; cắt đuôi phân phối theo xác suất cộng dồn.
context windowSố token tối đa (input + output) trong một lần gọi.
thinking / effortToken "suy nghĩ" model sinh ra trước câu trả lời; effort là núm chỉnh độ kỹ (và chi phí) trên model Claude đời mới.

1. Bài toán gốc và bối cảnh ra đời

Bài toán cổ điển của xử lý ngôn ngữ: dự đoán từ tiếp theo (language modeling). Nó "tự giám sát" — văn bản nào trên internet cũng là dữ liệu huấn luyện miễn phí, không cần người gán nhãn. Vấn đề là các giải pháp trước đó không scale:

Thế hệGiới hạnCái mới giải quyết
n-gram (thập niên 1990–2000)Chỉ nhìn 3–5 từ trước; bảng đếm nổ tổ hợp—
RNN / LSTM (2014–2017)Xử lý tuần tự từng từ → không train song song được trên GPU; quên thông tin xaNhớ được ngữ cảnh dài hơn n-gram
Transformer (Vaswani et al., Google, 2017 — "Attention Is All You Need")Chi phí attention tăng theo bình phương độ dàiBỏ tuần tự khi train: mọi vị trí tính song song → tận dụng GPU → scale được lên hàng trăm tỉ tham số
GPT-3 (OpenAI, 2020)Chỉ "nối chữ", không biết làm theo chỉ dẫnScale lớn sinh ra in-context learning: đưa vài ví dụ trong prompt là model bắt chước được
Instruction tuning + RLHF (InstructGPT 2022, ChatGPT 11/2022; Anthropic dùng thêm Constitutional AI)Tốn công gán nhãn sở thích con ngườiModel trả lời như trợ lý, làm theo chỉ dẫn, từ chối yêu cầu nguy hiểm
Reasoning / thinking model (2024–nay)Tốn thêm token suy nghĩ → chậm, đắtHuấn luyện bằng RL trên bài có đáp án kiểm được → giỏi toán, code, kế hoạch nhiều bước

Từ bối cảnh đó suy ra LLM được tối ưu cho: sinh văn bản trôi chảy và hợp lý theo thống kê của dữ liệu; nhận dạng và bắt chước pattern (định dạng, giọng văn, cấu trúc code); tổng hợp kiến thức phổ biến; làm theo chỉ dẫn bằng ngôn ngữ tự nhiên.

Không tối ưu cho: đúng tuyệt đối (mục tiêu train là "hợp lý", không phải "đúng"); tính toán chính xác (số bị cắt thành token rời rạc); biết mình không biết; tất định; kiến thức sau ngày cắt dữ liệu (knowledge cutoff); dữ liệu riêng của công ty bạn. Đây chính là danh sách "khi nào KHÔNG dùng LLM" — học thuộc danh sách này trước danh sách tính năng.

2. Mô hình tư duy cốt lõi

LLM = hàm f(token₁…tokenₙ) → P(tokenₙ₊₁), gọi lặp (autoregressive). Không có "hiểu", không có "nhớ", không có "tra cứu" nằm ngoài hàm này — mọi thứ phải đi qua chuỗi token đầu vào.
flowchart LR
  P["Prompt (chữ)"] --> T["Tokenizer: chữ -> id"]
  T --> M["Transformer: n token -> logits cho ~100k token ứng viên"]
  M --> S["Sampling: chọn 1 token theo xác suất"]
  S --> O["Token mới"]
  O -->|"nối vào đầu vào, lặp lại"| M
  S -->|"gặp token kết thúc / max_tokens"| E["Dừng: stop_reason"]

Gần như mọi tính chất bạn gặp khi làm sản phẩm là hệ quả của abstraction này:

Hệ quảVì sao (suy từ abstraction)Ý nghĩa khi thiết kế
HallucinationHàm luôn trả về phân phối — luôn có token "hợp lý nhất" để chọn, kể cả khi không có thông tin đúngCần grounding (đưa dữ liệu vào context), cho phép "không biết", validate output
Stateless — không nhớ giữa 2 lần gọiĐầu vào duy nhất là chuỗi token của lần gọi đó"Trí nhớ" của chatbot là do bạn gửi lại lịch sử mỗi lần → chi phí tăng theo độ dài hội thoại
Tính tiền và giới hạn theo tokenMỗi token đi qua toàn bộ mạng một lầnƯớc chi phí = token × đơn giá; tiếng Việt đắt hơn (mục 3)
Output đắt và chậm hơn inputInput xử lý song song (prefill); output sinh tuần tự từng token (decode)Giới hạn max_tokens, bắt model trả lời gọn, stream để giảm cảm giác chờ
Streaming tự nhiênToken sinh ra lần lượtTTFT là metric UX quan trọng hơn tổng thời gian
Nhạy với cách viết promptPrompt khác → phân phối khácPrompt là code: version, review, eval trước khi đổi
Không tất địnhSampling ngẫu nhiên + số học float phụ thuộc batchKhông dùng "chạy lại ra y hệt" làm giả định; test bằng thống kê, không bằng so chuỗi
"Tool use" chỉ là sinh chữ đúng định dạngModel không chạy được gì — nó sinh ra JSON mô tả lời gọi hàmCode của bạn thực thi và chịu trách nhiệm quyền hạn (bài 02, 08)

3. Internals và quyết định thiết kế

3.1 Tokenization — vì sao tiếng Việt đắt gấp ~3 lần

Tokenizer hiện đại dùng byte-level BPE: bắt đầu từ 256 byte, rồi gộp dần các cặp xuất hiện nhiều nhất trong dữ liệu huấn luyện thành token mới cho tới khi đủ ~65k–200k token. Chữ tiếng Anh xuất hiện nhiều trong dữ liệu nên cả từ refund thành 1 token. Chữ Việt có dấu mã hoá UTF-8 thành 2–3 byte (ề = 3 byte) và ít xuất hiện hơn nên bị cắt vụn tới mức byte.

Lab đo bằng tokenizer Claude đời cũ (65 000 token, bundle trong gói anthropic==0.30.0):

vocab size: 65000
loại            ký tự  token  ký tự/token
EN                 58     12         4.83
VI có dấu          62     40         1.55
VI không dấu       62     20         3.10
JSON               60     20         3.00
PHP                93     23         4.04
Số                 34     12         2.83
  'đơn hàng' -> ['Äij', 'Æ', '¡', 'n', 'Ġh', 'Ãł', 'ng']
 'hoàn tiền' -> ['ho', 'Ãł', 'n', 'Ġti', 'á»', 'ģ', 'n']
    'refund' -> ['refund']
    'Nguyễn' -> ['N', 'guy', 'á»', 'ħ', 'n']
    'nguyen' -> ['ng', 'uy', 'en']
   ' 350000' -> ['Ġ35', '0000']

Đọc kết quả: cùng một ý, bản tiếng Việt có dấu tốn 40 token so với 12 (3,3 lần); bỏ dấu còn 20. Các ký tự lạ như Ãł là cách tokenizer hiển thị từng byte UTF-8 bị tách rời. Số 350000 bị cắt thành 35 + 0000 — đây là lý do LLM yếu so sánh/tính số: nó không "thấy" con số như một giá trị.

Chưa verify: tokenizer của model Claude hiện tại không public, nên tỉ lệ chính xác sẽ khác. Theo tài liệu Anthropic, tokenizer từ Opus 4.7 trở đi sinh ~1–1,35 lần số token so với đời trước trên cùng văn bản. Muốn con số thật cho prompt của mình, gọi endpoint POST /v1/messages/count_tokens — đừng dùng tiktoken (tokenizer của OpenAI) để ước lượng cho Claude.

Đánh đổi của thiết kế: bảng từ vựng lớn hơn → câu ngắn hơn (rẻ, nhanh, context chứa được nhiều hơn) nhưng lớp embedding và lớp output to ra (tốn RAM, chậm mỗi bước). Byte-level đảm bảo mọi chuỗi đều mã hoá được (không có "từ lạ"), đổi lại ngôn ngữ ít dữ liệu bị cắt vụn. Hệ quả thực tế cho team Việt Nam: system prompt nên viết bằng tiếng Anh nếu dài và cố định (rẻ hơn ~2–3 lần), còn phần giao tiếp với khách giữ tiếng Việt.

3.2 Transformer và attention

flowchart TB
  I["id token"] --> E["Embedding: id -> vector d chiều"]
  E --> B1["Block 1: Attention + MLP"]
  B1 --> B2["Block 2 ..."]
  B2 --> BN["Block L (hàng chục đến hơn 100 block)"]
  BN --> H["Lớp output: vector -> logits trên toàn bộ vocab"]
  H --> SM["softmax -> xác suất"]

Mỗi block có hai phần. Attention: mỗi token tạo ra Query (tôi đang tìm gì), Key (tôi chứa gì), Value (thông tin của tôi); điểm giống nhau giữa Query của token hiện tại và Key của mọi token trước quyết định lấy bao nhiêu Value của từng token. MLP: mạng hai lớp xử lý từng token riêng lẻ — phần lớn "kiến thức" được cho là nằm ở đây.

Đánh đổi cốt lõi: attention cho mọi token nhìn mọi token trước → chi phí tính O(n²) theo độ dài n. Đó là cái giá để train song song và bắt được quan hệ xa. Các biến thể như FlashAttention (tính lại thay vì lưu, tối ưu bộ nhớ GPU), GQA — grouped-query attention (nhiều head Query dùng chung Key/Value để giảm KV cache), sliding window, đều là cách gỡ bớt cái giá này.

3.3 Prefill vs decode, và KV cache

Khi sinh token thứ n+1, model cần Key/Value của n token trước. Không cache thì mỗi bước phải tính lại toàn bộ — tổng chi phí tăng rất nhanh. Lab dùng một decoder đồ chơi 1 layer (numpy, d=256):

 n token  không cache (s)  có KV cache (s)   tỉ lệ
     250            0.223            0.036     6.2x
     500            0.791            0.089     8.9x
    1000            2.372            0.352     6.7x
    2000            8.813            1.218     7.2x

Hai điều rút ra: (1) KV cache nhanh hơn ~7 lần ngay cả trên model đồ chơi; (2) kể cả có cache, thời gian vẫn tăng ~4 lần khi độ dài tăng gấp đôi (0,35s → 1,22s) vì mỗi token mới vẫn phải attention qua toàn bộ context. Context dài không miễn phí.

Cái giá của KV cache là RAM GPU. Công thức: 2 × số layer × số KV head × head_dim × byte/phần tử × số token. Với cấu hình public:

Llama-3.1-8B  (32 layer, 8 kv head, dim 128, bf16): 128 KiB/token -> 128k token = 15.6 GiB / 1 request
Llama-3.1-70B (80 layer, 8 kv head, dim 128, bf16): 320 KiB/token -> 128k token = 39.1 GiB / 1 request

Một request 128k token trên model 70B chiếm ~39 GiB chỉ riêng KV cache — gần nửa một GPU 80 GB. Đây là lý do nhà cung cấp tính giá cao hơn / giới hạn chặt hơn cho context dài, và là nền tảng kinh tế của prompt caching: nếu phần đầu prompt giống hệt request trước, nhà cung cấp dùng lại KV đã tính thay vì prefill lại → rẻ hơn ~10–20 lần cho phần đó (bài 03).

PhaLàm gìBị giới hạn bởiQuyết định metric
PrefillXử lý toàn bộ prompt song song, tạo KV cacheSức tính GPU (compute-bound)TTFT — prompt càng dài TTFT càng lớn
DecodeSinh từng token, mỗi bước đọc lại toàn bộ trọng số + KV cacheBăng thông bộ nhớ (memory-bound)Output tokens/giây

Hệ quả kinh tế: vì decode tuần tự và "phí" GPU, output đắt gấp 5 lần input trên mọi model Claude hiện tại (Opus 5.5: $4 vs $20 / 1M token). Nhà cung cấp gộp nhiều request vào một batch (continuous batching) để tận dụng GPU khi decode — và chính việc batch này gây ra không tất định (3.5).

3.4 Sampling — temperature, top-p

Model trả về logits; softmax đổi thành xác suất; sampling chọn một token. Lab với 6 token ứng viên:

token     :   hoàn     đổi     huỷ    kiểm     xin   không
T=0.2     :  0.817   0.182   0.000   0.000   0.000   0.000   entropy=0.69 bit
T=0.7     :  0.551   0.359   0.049   0.032   0.009   0.001   entropy=1.45 bit
T=1.0     :  0.470   0.348   0.086   0.064   0.026   0.007   entropy=1.79 bit
T=1.5     :  0.383   0.314   0.123   0.101   0.055   0.023   entropy=2.12 bit
T=1,top_p=0.9:  0.520   0.385   0.095   0.000   0.000   0.000

1000 lần sample ở T=1.0 — đếm token đầu tiên được chọn:
   {'hoàn': 473, 'đổi': 346, 'huỷ': 88, 'kiểm': 61, 'xin': 24, 'không': 8}

Temperature thấp làm phân phối nhọn (ít đa dạng, dễ lặp); cao làm phẳng (sáng tạo hơn, dễ "lạc"). Top-p cắt đuôi: chỉ giữ nhóm token chiếm 90% xác suất. Ở T=1, cứ 1000 lần thì 8 lần model chọn "không" — token xác suất 0,7%. Trong một câu trả lời 500 token, những lựa chọn hiếm như vậy chắc chắn xảy ra ở đâu đó, và mỗi token sai kéo lệch mọi token sau.

Thay đổi quan trọng trên model mới: theo tài liệu Anthropic, Claude Opus 5.5 / Sonnet 5.5 / Opus 4.7+ không nhận temperature, top_p, top_k khác mặc định (trả 400). Núm chỉnh thay thế là output_config.effort (low → max). Code cũ có temperature: 0 "cho chắc" sẽ vỡ khi nâng model. Chưa verify bằng API thật — lấy từ bảng tương thích model trong tài liệu tham chiếu.

3.5 Vì sao temperature = 0 vẫn không tất định

Temperature 0 = luôn chọn token điểm cao nhất (greedy). Nhưng logits được tính bằng hàng tỉ phép cộng float trên GPU, và phép cộng float không có tính kết hợp: (a+b)+c ≠ a+(b+c). Lab cộng cùng 4096 số float32 theo ba thứ tự:

Float không kết hợp: cùng các số, cộng khác thứ tự -> kết quả khác
   tuần tự xuôi : -80132.992188
   tuần tự ngược: -80133.039062
   chia 64 block: -80133.046875

Hai token gần hoà: argmax đổi khi logit lệch 1e-6 (do batch/kernel khác nhau)
   batch A logits: [5.000001  5.0000005] -> greedy chọn hoàn
   batch B logits: [5.000001  5.0000014] -> greedy chọn đổi

Trên server, request của bạn bị gộp chung batch với request người khác; batch size đổi theo tải → GPU kernel chia khối tính khác nhau → logits lệch ở chữ số thứ 6–7 → khi hai token gần hoà, greedy chọn khác → mọi token sau khác theo. Thinking Machines Lab (9/2025, "Defeating Nondeterminism in LLM Inference") chạy cùng prompt 1000 lần ở temperature 0 ra 80 kết quả khác nhau; phải viết lại kernel "batch-invariant" mới ra 1000/1000 giống nhau. API thương mại không cam kết điều này. Kết luận thiết kế: không cache theo giả định "cùng input → cùng output", không viết test so chuỗi chính xác, không dùng LLM cho việc cần kiểm toán tái lập (ví dụ tính phí).

3.6 Các giai đoạn huấn luyện và "thinking"

Giai đoạnDữ liệuTạo ra năng lựcTạo ra tật
PretrainingHàng chục nghìn tỉ token văn bản/codeKiến thức, ngôn ngữ, patternKnowledge cutoff; thiên kiến của dữ liệu internet
SFT (supervised fine-tuning)Cặp hỏi–đáp mẫuĐịnh dạng trợ lý, làm theo chỉ dẫnBắt chước giọng tự tin của câu trả lời mẫu
RLHF / RLAIF / Constitutional AIĐánh giá sở thích (người hoặc model chấm theo "hiến pháp")Hữu ích, an toàn, lịch sựSycophancy — xu nịnh, đồng ý với người dùng vì người chấm thích thế
RL trên bài kiểm được (reasoning)Toán, code có test, task có đáp ánSuy luận nhiều bước, dùng tool, tự sửaTốn token suy nghĩ; đôi khi "lách" tiêu chí chấm (reward hacking — vd sửa test cho pass)

Kalai và cộng sự (OpenAI, 9/2025, "Why Language Models Hallucinate") chỉ ra một nguyên nhân gốc của hallucination: các bộ chấm điểm nhị phân đúng/sai thưởng cho việc đoán và phạt câu "tôi không biết" — model được tối ưu để làm bài thi giỏi, và đoán bừa có điểm kỳ vọng cao hơn bỏ trống. Hệ quả cho bạn: prompt phải cho phép rõ ràng trả lời "không đủ thông tin", và eval của bạn phải không phạt câu trả lời đó (bài 07).

Thinking trên Claude đời mới là adaptive: model tự quyết suy nghĩ bao nhiêu; bạn chỉnh qua effort. Token suy nghĩ tính tiền như output dù nội dung bị ẩn (mặc định display: "omitted"). Đánh đổi: chất lượng trên việc khó tăng rõ, đổi lại TTFT dài và hoá đơn khó đoán hơn.

4. Đảm bảo và giới hạn

Không có khung lý thuyết kiểu CAP cho LLM; khung hữu ích hơn là coi LLM như một dịch vụ mạng không đáng tin + một hàm xác suất. Những gì được đảm bảo và không:

Khía cạnhĐảm bảoKhông đảm bảo
Định dạngVới structured outputs / strict: true: JSON khớp schema (grammar-constrained decoding)Giá trị trong JSON đúng sự thật; tự do văn bản thì không đảm bảo gì
Nội dung—Đúng sự thật, nhất quán giữa các lần gọi, nhất quán giữa các phiên bản model
Tất định—Cùng input → cùng output (kể cả temperature 0)
Độ dàiKhông vượt max_tokens; bị cắt thì stop_reason = "max_tokens"Câu trả lời hoàn chỉnh trong giới hạn
ContextNhận tới giới hạn cửa sổ (1M token với Opus/Sonnet đời mới, 200k với Haiku 4.5)Dùng đều tốt mọi vị trí trong context (context rot — bài 03)
Kiến thứcTới knowledge cutoffSự kiện sau cutoff, dữ liệu nội bộ của bạn
Độ trễ—SLA cứng; mô hình gần đúng: latency ≈ TTFT(độ dài prompt) + số token output ÷ tốc độ decode

Bảng giá tham khảo (USD / 1M token, bản cache 2026-09-25 — kiểm tra lại trước khi phỏng vấn):

ModelContextInputOutputGhi chú
Claude Fable 5.11M$10$50Mạnh nhất, cho việc dài và khó
Claude Opus 5.51M$4$20Mặc định khuyến nghị; cache read $0,20
Claude Sonnet 5.51M$2$10Cân bằng tốc độ/chất lượng
Claude Haiku 4.5200k$1$5Rẻ, nhanh; phân loại, trích xuất

Ước lượng nhanh cho một chatbot CSKH: prompt 3k token + trả lời 300 token trên Opus 5.5 ≈ 3000×4/1e6 + 300×20/1e6 = $0,012 + $0,006 = $0,018/lượt (chưa tính cache). 100k lượt/tháng ≈ $1 800. (Số liệu minh hoạ.)

5. Failure modes

LỗiBiểu hiệnHệ quảPhát hiệnXử lý
HallucinationBịa chính sách, mã đơn, tên API không tồn tạiSai dữ liệu, rủi ro pháp lý (Air Canada)Eval đối chiếu nguồn; citation; kiểm tra chéo với DBGrounding (RAG/tool), cho phép "không biết", trích dẫn nguồn
Cắt cụtstop_reason = "max_tokens", JSON dở dangParse lỗi, tool nhận input thiếuLuôn kiểm stop_reasonTăng max_tokens / stream; không chạy tool khi bị cắt
Refusalstop_reason = "refusal"Không có câu trả lờiĐếm tỉ lệ refusal theo routeFallback model (tham số fallbacks), chỉnh prompt, chuyển người
Không tất địnhCùng câu hỏi, lúc đúng lúc saiBug không tái hiện đượcChạy eval nhiều lần, đo pass^kThiết kế chịu được biến thiên; validate output
SycophancyKhách nói "shop hứa hoàn 100%" → bot đồng ýCam kết saiEval với câu hỏi mồiChính sách là dữ liệu từ tool, không phải trí nhớ model
Trôi phiên bản modelNâng model, output đổi định dạng / lời lẽRegression im lặngEval chạy trước khi đổi modelGhim model id; canary; structured outputs
Quá tải nhà cung cấpHTTP 529 overloaded_error, 429Chậm/lỗi hàng loạtTỉ lệ lỗi theo mãRetry có jitter, circuit breaker, fallback (bài 09)
Chi phí token bất ngờTiếng Việt, JSON dài, lịch sử dàiHoá đơn gấp 3Log usage mỗi requestĐo bằng count_tokens; rút gọn; cache

Lab tái hiện: nguồn gốc không tất định (mục 3.5) và chi phí tokenization (3.1) chạy thật ở trên. Hallucination, refusal, sycophancy cần model thật — chưa verify trong môi trường lab; phần xử lý stop_reason, retry 529 được tái hiện bằng SDK thật + mock server ở bài 02 và bài 09.

6. Vận hành trong production

Metric bắt buộc

Tham số ảnh hưởng lớn

Tham sốẢnh hưởngLời khuyên
modelChất lượng, giá (chênh 10 lần giữa Haiku và Fable), tốc độGhim id chính xác; đổi qua eval
output_config.effortĐộ kỹ + số token thinkingĐặt tường minh (Opus 5.5 mặc định medium, không phải high); đo theo route
max_tokensTrần output; quá thấp → cắt cụtPhân loại ~256; chat ~16k không stream; dài thì stream
Độ dài promptTTFT, chi phí, context rotĐo bằng count_tokens; ưu tiên cache

Nâng cấp model

Model mới thường có breaking change ở API, không chỉ ở hành vi. Ví dụ Opus 5.5 so với Opus 5: không tắt được thinking, tool_choice ép buộc (any/tool) trả 400, effort mặc định hạ xuống medium. Quy trình: đọc migration guide → chạy eval trên model mới → canary 5% traffic → so metric (chất lượng, chi phí, latency) → chuyển hết. Ghi ngày ngừng hỗ trợ (deprecation) của model đang dùng vào lịch team.

Gánh nặng cho team

Dùng API thì không ai phải trực GPU, nhưng cần người hiểu: đọc usage để debug chi phí, đọc trace để debug chất lượng, và biết phân biệt "lỗi của mình" (prompt, context) với "lỗi của nhà cung cấp" (529). Lúc 3h sáng, câu hỏi đầu tiên luôn là: status page của nhà cung cấp có sự cố không, tỉ lệ 529 có tăng không, có ai vừa deploy prompt mới không.

7. So sánh và ma trận quyết định

TrụcAPI model đóng (Claude, GPT, Gemini)Open-weight tự host (Llama, Qwen, DeepSeek qua vLLM)Model nhỏ fine-tune riêngKhông dùng LLM (rule, ML cổ điển)
Chất lượng việc khóCao nhấtKhá → cao (model lớn cần nhiều GPU)Tốt trong phạm vi hẹp đã trainChỉ cho bài toán xác định rõ
Chi phí ở traffic thấpThấp (trả theo token)Cao (GPU chạy 24/7)Trung bìnhRất thấp
Chi phí ở traffic rất caoCó thể rất caoCó thể rẻ hơn nếu tận dụng GPU tốtRẻRẻ nhất
Độ phức tạp vận hànhThấpCao (GPU, batching, KV cache, nâng cấp)Cao (pipeline train + serve)Thấp
Dữ liệu rời khỏi công tyCó (cần DPA, ZDR nếu nhạy cảm)KhôngKhôngKhông
Tất định / kiểm toánKhôngLàm được nếu kiểm soát kernelLàm đượcCó
Tuyển ngườiDễ (backend dev học được)Khó (ML infra)Khó (ML engineer)Dễ
Độ trưởng thành hệ sinh tháiCao, đổi nhanh (breaking change mỗi thế hệ)Cao, đổi nhanhTrung bìnhRất cao

Dùng LLM qua API khi: đầu vào là ngôn ngữ tự nhiên đa dạng, chấp nhận được sai số và có cách kiểm (người duyệt, test, đối chiếu DB), khối lượng vừa phải, cần ra sản phẩm nhanh. Tránh khi: kết quả phải tất định/kiểm toán được (tính tiền, thuế), bài toán giải được bằng rule hay SQL, độ trễ yêu cầu dưới ~200ms, hoặc dữ liệu không được phép rời hạ tầng mà không có hợp đồng phù hợp. Tự host chỉ đáng khi traffic lớn và ổn định, hoặc ràng buộc dữ liệu bắt buộc.

8. Góc nhìn tech lead: tác động tổ chức

Viết theo dạng chép được vào ADR:

9. Anti-patterns và bài học thực tế

Điều tài liệu chính thức không nói nhưng người làm lâu đều biết: hầu hết "model ngu" là do context kém (thiếu dữ liệu, thừa nhiễu, chỉ dẫn mâu thuẫn), không phải do model. Trước khi đổi sang model đắt hơn, in nguyên context đã gửi ra và đọc nó như thể bạn là model.

10. Câu hỏi phụ interviewer hay hỏi tiếp

Vì sao output đắt hơn input?
Input xử lý song song một lượt (prefill, compute-bound); output sinh tuần tự từng token, mỗi bước đọc lại trọng số và KV cache (decode, memory-bound) — tốn thời gian GPU hơn nhiều trên mỗi token.
Context 1M token thì cần RAG làm gì nữa?
Vì chi phí (trả tiền mọi token mỗi lần gọi), độ trễ (prefill lâu), và chất lượng giảm khi context dài và nhiễu. RAG/tool chọn đúng vài nghìn token cần thiết. Context dài hữu ích khi tài liệu cần đọc liền mạch và dùng lại được nhờ cache.
Làm sao để output tất định?
Qua API thương mại thì không đảm bảo được. Chỉ có thể: structured outputs cho định dạng, cache kết quả theo input của mình, và thiết kế để hệ thống đúng dù output biến thiên. Tất định thật cần tự host với kernel batch-invariant.
Hallucination có "sửa" được không?
Giảm được, không triệt tiêu: grounding bằng dữ liệu, cho phép trả lời "không biết", yêu cầu trích dẫn và kiểm trích dẫn, eval không phạt "không biết", tách bước tạo và bước kiểm.
Vì sao LLM đếm chữ cái hoặc so sánh số sai?
Model thấy token, không thấy ký tự hay giá trị số. "strawberry" là 1–3 token, số lớn bị cắt thành mảnh.
Thinking/reasoning khác gì prompt "hãy suy nghĩ từng bước"?
Cùng ý tưởng (cho model thêm token để tính toán trước khi trả lời), nhưng model reasoning được huấn luyện bằng RL để dùng token đó hiệu quả, và API tách phần thinking ra khỏi câu trả lời. Chi phí: token thinking tính như output.
Khi nào chọn model nhỏ (Haiku) thay vì lớn?
Việc hẹp, có tiêu chí rõ (phân loại, trích xuất), khối lượng lớn, cần nhanh — và eval chứng minh chất lượng đủ. Thử model lớn ở effort thấp trước: thường ngang model nhỏ về chi phí mà ổn định hơn, và chỉ một namespace cache.

Phụ lục: lab đã chạy

Môi trường: Ubuntu 24.04, Python 3.11.15, numpy 2.4.6, tokenizers 0.23.2, CPU Intel Xeon 2.1 GHz 4 core. Mã nguồn trong repo: labs/ai/py/, output gốc trong labs/ai/out/01-*.txt.

cd labs/ai/py
python3 -m venv .venv && . .venv/bin/activate && pip install numpy tokenizers

# 1. Lấy tokenizer Claude đời cũ (bundle trong SDK Python bản cũ)
pip download --no-deps anthropic==0.30.0 -d /tmp/dl
unzip -j /tmp/dl/anthropic-0.30.0-*.whl anthropic/tokenizer.json -d /tmp/dl
python tokenizer_vi_en.py /tmp/dl/tokenizer.json

# 2. Temperature, top-p, float không kết hợp
python sampling.py

# 3. KV cache trên decoder đồ chơi (mất ~20 giây)
python kv_cache.py

Chưa verify trong lab: tokenizer của Claude hiện tại (không public); hành vi hallucination/refusal của model thật (cần API key); việc model mới trả 400 với temperature (lấy từ tài liệu tham chiếu API). Nguồn ngoài: Vaswani et al. 2017; Thinking Machines Lab, "Defeating Nondeterminism in LLM Inference" (9/2025); Kalai et al., "Why Language Models Hallucinate" (OpenAI, 9/2025); Moffatt v. Air Canada, 2024 BCCRT 149; model card Llama 3.1 (cấu hình layer/head).

Bài tiếp: 02 · Tool use và agent loop.