TungDaDev's Blog

How LLMs work? LLM thật ra hoạt động thế nào

Computer generated human brain
Published on
/18 mins read/

Bạn gõ vào ChatGPT, Claude hay Gemini: "Viết giúp mình hàm Java kiểm tra số nguyên tố". Vài giây sau có ngay code chạy được, giải thích đâu ra đấy. Cảm giác như bên kia màn hình có ai đó hiểu mình thật.

Nhưng tháo cái máy đó ra xem thì chẳng thấy chữ "hiểu" nằm ở đâu cả. Bên trong chỉ có một đống ma trận số to khủng khiếp, và một vòng lặp cứ chạy đi chạy lại đúng một phép tính: với đoạn chữ này thì chữ tiếp theo nhiều khả năng là gì.

Mình sẽ đi qua cái máy đó từng phần một. Bạn chưa học machine learning ngày nào vẫn theo được, còn ai làm AI rồi thì coi như ôn lại.


# tất cả chỉ là đoán chữ tiếp theo

Bàn phím điện thoại có tính năng gợi ý chữ: gõ "chúc mừng" là nó nhảy ra "sinh nhật", "năm mới". Đó cũng là một model đoán chữ tiếp theo, có điều nhỏ và ngây ngô.

LLM (Large Language Model) làm đúng việc ấy, chỉ là ở một đẳng cấp khác. Đưa cho nó câu:

Thủ đô của Việt Nam là

Nó không trả về một chữ duy nhất. Nó chấm điểm mọi token nó biết (thường vài chục nghìn tới vài trăm nghìn token) rồi đổi thành xác suất:

" Hà"      0.91
" thành"   0.03
" một"     0.02
" TP"      0.01
...        ...

Hệ thống bốc một token, thường là cái xác suất cao nhưng có pha chút ngẫu nhiên, nối vào câu rồi hỏi lại: "Thủ đô của Việt Nam là Hà" thì tiếp theo là gì? Lần này " Nội" thắng áp đảo. Cứ thế lặp lại tới khi model sinh ra token kết thúc thì dừng.

Kiểu sinh chữ cái sau dựa vào cái trước như vậy gọi là autoregressive. Chữ trong khung chat hiện ra từ từ cũng vì thế: model đang nặn ra từng token một thật, chứ không phải hiệu ứng cho đẹp.

Thế đoán chữ thôi mà sao nó code được, giải toán được, dịch được? Mấu chốt nằm ở chỗ muốn đoán chữ tiếp theo cho giỏi trên cả cái Internet thì model buộc phải nắm được ngữ pháp, sự kiện, logic, cách code chạy... vì biết mấy thứ đó thì đoán mới trúng. Cứ thử nghĩ mà xem, muốn đoán chính xác dòng tiếp theo của một file Java thì ít nhiều phải hiểu Java đã.


# token: model không đọc chữ giống mình

Model không làm việc với chữ cái hay từ, mà với token. Một token có thể là nguyên một từ, nửa từ, một dấu câu, thậm chí một khoảng trắng.

Khâu cắt chữ thành token do tokenizer đảm nhận. Đa số LLM bây giờ dùng biến thể của BPE (Byte Pair Encoding): xuất phát từ từng byte, cặp nào hay đi cùng nhau thì gộp lại thành token mới, gộp mãi tới khi đủ số token cần thiết.

Ví dụ cho dễ hình dung (mỗi tokenizer cắt mỗi kiểu):

"Hello world"            -> ["Hello", " world"]                       2 token
"unbelievable"           -> ["un", "believ", "able"]                  3 token
"Xin chào các bạn"       -> ["X", "in", " ch", "ào", " các", " bạn"]  6 token

Nhìn ví dụ cuối là thấy thiệt thòi của tiếng Việt. Tokenizer được dựng chủ yếu từ dữ liệu tiếng Anh nên chữ có dấu hay bị băm vụn, cùng một ý mà viết tiếng Việt thường tốn token hơn. Mà API tính tiền theo token, "context window 128K" cũng tính theo token chứ không theo chữ, nên viết tiếng Việt vừa tốn tiền hơn vừa chiếm chỗ trong context nhiều hơn. Mấy model đời mới có tokenizer đa ngôn ngữ tốt hơn nên khoảng cách đang hẹp dần.

Tokenizer cũng là thủ phạm của câu đố kinh điển "chữ strawberry có mấy chữ r" mà model hay trả lời sai. Nó có nhìn thấy từng chữ cái đâu, nó chỉ thấy mấy mảnh kiểu ["str", "aw", "berry"] thôi.

Cắt xong, mỗi token được gán một con số ID. Từ đây trở đi, câu của bạn với model chỉ còn là một dãy số nguyên kiểu [9906, 1917, 0].


# embedding: biến con số thành ý nghĩa

Con số 9906 tự nó chẳng nói lên gì. Nên bước tiếp theo là đổi mỗi token thành một vector, tức một dãy vài nghìn số thực:

" mèo"  -> [ 0.12, -0.83,  0.44, ...,  0.07]   (ví dụ 4096 chiều)
" chó"  -> [ 0.10, -0.79,  0.51, ...,  0.02]
" xe"   -> [-0.66,  0.31, -0.12, ...,  0.90]

Chẳng ai ngồi điền tay mấy con số này, model tự học ra trong lúc train. Và chúng có một tính chất rất hay: token nghĩa gần nhau thì vector cũng nằm gần nhau. "mèo" ở gần "chó" hơn hẳn so với "xe". Thậm chí hướng đi trong không gian vector cũng mang nghĩa, ví dụ kinh điển:

vector("vua") - vector("đàn ông") + vector("phụ nữ")  ≈  vector("nữ hoàng")

Bạn cứ hình dung mỗi token là một điểm trong một căn phòng hàng nghìn chiều, mỗi chiều ứng với một khía cạnh nào đó: đực hay cái, con vật hay đồ vật, trang trọng hay suồng sã... Thực tế các chiều không rạch ròi được như vậy, nhưng nghĩ thế là đủ dùng.

Còn một chuyện nữa là thứ tự, vì "chó cắn người" với "người cắn chó" khác nhau một trời một vực. Thông tin vị trí được trộn thẳng vào vector. Model hiện đại hay dùng RoPE (Rotary Position Embedding): xoay vector đi một góc tuỳ theo vị trí, nhờ vậy model biết token nào đứng trước, cách nhau bao xa.


# attention: nhìn trước ngó sau để hiểu ngữ cảnh

Đây là trái tim của LLM, cũng là lý do bài báo "Attention Is All You Need" năm 2017 nổi như cồn.

Cái khó là một từ mang nghĩa khác nhau tuỳ hoàn cảnh:

"Mình ngồi bên bờ sông, nhìn con cá bơi."
"Mình gửi tiền ở ngân hàng bên kia đường."

Cùng một chữ "bên" mà mỗi câu gắn với một thứ khác nhau. Model cần cách để mỗi token liếc sang các token khác và lấy thông tin từ những đứa liên quan tới mình. Cơ chế đó gọi là attention.

Q, K, V

Từ embedding của mình, mỗi token sinh ra ba vector. Query (Q) là câu hỏi "mình đang cần thông tin gì?". Key (K) là tấm biển "mình có thông tin về cái này". Value (V) là phần thông tin thật sự đưa ra cho ai cần.

Giống như đi chợ vậy. Mỗi token cầm danh sách cần mua (Query) đi một vòng, ngó biển hiệu từng sạp (Key). Sạp nào khớp nhiều thì ghé lâu, mua nhiều (Value), sạp nào không liên quan thì lướt qua. Điểm khớp được quy về tỷ lệ phần trăm cộng lại bằng 1, rồi token gom hàng của các sạp theo đúng tỷ lệ đó.

Viết thành công thức thì gọn thế này:

Attention(Q, K, V) = softmax( Q · Kᵀ / √d ) · V

Q · Kᵀ là điểm khớp giữa mọi cặp token. Chia cho √d (căn của số chiều) để con số không phình quá to, train cho ổn định. softmax đổi điểm thành tỷ lệ phần trăm, còn nhân với V là bước gom thông tin theo tỷ lệ ấy.

Lấy câu "Con mèo ngồi trên tấm thảm vì nó mệt" làm ví dụ. Tới token "nó", attention sẽ dồn phần lớn trọng số vào "mèo" chứ không phải "thảm", nên vector của "nó" được nạp thêm thông tin "đây là con mèo".

không được nhìn trộm tương lai

Nhiệm vụ là đoán token tiếp theo, nên mỗi token chỉ được nhìn những token đứng trước nó. Cho nhìn cả phía sau thì thành ra chép bài. Phần tương lai bị che đi bằng causal mask:

             Con   mèo   ngồi   trên
Con           x
mèo           x     x
ngồi          x     x     x
trên          x     x     x     x

nhiều head, nhiều góc nhìn

Một lượt attention chỉ học được một kiểu quan hệ. Thế nên model chạy song song nhiều "head", mỗi head một bộ Q/K/V riêng: head này chuyên nối đại từ với danh từ, head kia để ý chủ ngữ với động từ, head nọ bám cú pháp code... rồi ghép kết quả lại.

Model hiện đại còn dùng thêm GQA (Grouped-Query Attention), cho nhiều Query head dùng chung một bộ Key/Value để đỡ tốn bộ nhớ khi chạy. Chuyện bộ nhớ này mình kể kỹ ở bài chuyện gì xảy ra khi bạn nhấn Enter.


# transformer: chồng hàng chục tầng lên nhau

Attention mới là một mảnh. Một transformer block gồm bốn thứ. Đầu tiên là attention để các token trao đổi thông tin với nhau. Sau đó tới feed-forward network (FFN), nơi từng token tự xử lý riêng mớ thông tin vừa gom được, và cũng là chỗ chứa phần lớn tham số, phần lớn "kiến thức" của model. Xen giữa có residual connection (cộng đầu vào vào đầu ra để thông tin cũ không rơi rớt, gradient chảy được qua mạng rất sâu) và normalization (thường là RMSNorm) giữ các con số trong khoảng ổn định.

LLM là hàng chục block như vậy xếp chồng. Model cỡ 7–8B thường có khoảng 32 tầng, model to hơn có thể lên 80–100 tầng hoặc hơn.

Qua mỗi tầng, vector của từng token lại được bồi thêm một ít. Mấy tầng đầu thường lo chuyện nông như ngữ pháp, chính tả, còn các tầng giữa và cuối mới đụng tới ý nghĩa, quan hệ, suy luận. Tới cuối cùng, vector của token cuối được chiếu ra thành một điểm số (logit) cho từng token trong từ điển, softmax một phát là ra bảng xác suất bạn thấy ở đầu bài.

MoE: to xác mà chạy nhẹ

Dạo này hay gặp những cái tên kiểu "30B-A3B" hay "26B, 3.8B active". Đó là Mixture of Experts (MoE). Thay vì một FFN to đùng, mỗi block có nhiều FFN nhỏ gọi là expert, và một bộ định tuyến (router) chọn vài expert cho mỗi token.

Giống một phòng khám đa khoa có ba chục bác sĩ, nhưng mỗi bệnh nhân chỉ gặp hai, ba người đúng chuyên khoa. Phòng khám vẫn phải đủ chỗ cho cả ba chục người (tổng tham số quyết định model biết bao nhiêu và tốn bao nhiêu bộ nhớ), nhưng thời gian khám chỉ tính theo mấy người bạn gặp (tham số active quyết định tốc độ).

Nhờ vậy MoE cho chất lượng gần model to mà chạy nhanh gần bằng model nhỏ. Có điều RAM/VRAM thì vẫn phải đủ chứa toàn bộ. Phần lớn model mạnh năm 2026 (DeepSeek, Qwen, Mistral, gpt-oss...) đều đi theo đường này.


# model được nuôi lớn ra sao

Lúc mới khởi tạo, hàng tỷ tham số chỉ là số ngẫu nhiên, model đoán bậy hoàn toàn. Để biến nó thành một trợ lý ra hồn thường phải qua ba chặng.

pre-training: đọc gần hết Internet

Chặng đầu là cho model đọc một núi văn bản: web, sách, code, bài báo khoa học... Model đời mới thường được train trên hàng chục nghìn tỷ token.

Cách học thì đơn giản tới bất ngờ. Lấy một đoạn văn thật, che token tiếp theo đi, bắt model đoán. Đoán xong đem so với đáp án, tính xem sai bao nhiêu (con số đó gọi là loss, thường dùng cross-entropy). Rồi dùng backpropagation tính xem từng tham số nên nhích lên hay xuống bao nhiêu để lần sau bớt sai, và nhích một chút (gradient descent). Cứ thế lặp lại hàng nghìn tỷ lần.

Không ai dạy nó ngữ pháp hay lịch sử. Nó tự rút ra trong quá trình cố đoán cho trúng. Có điều chặng này tốn kém kinh khủng: hàng nghìn tới hàng chục nghìn GPU chạy liên tục nhiều tuần, nhiều tháng.

Xong chặng này ta có base model, viết tiếp văn bản rất giỏi nhưng chưa biết "trả lời". Hỏi nó "Thủ đô nước Pháp là gì?" có khi nó viết tiếp ra cả một loạt câu trắc nghiệm, vì trên mạng câu đó hay nằm trong đề thi.

SFT: dạy cách làm trợ lý

Chặng hai là Supervised Fine-Tuning: cho model học tiếp trên những cuộc hội thoại mẫu do người viết hoặc tuyển chọn kỹ. Số lượng ít hơn pre-training rất nhiều nhưng chất lượng cao. Học xong, model hiểu cái khuôn "người hỏi, mình đáp".

RLHF, DPO và học bằng phần thưởng

Có những thứ khó viết thành đáp án mẫu, kiểu câu trả lời nào hữu ích hơn hay câu nào lịch sự hơn. Với mấy thứ đó, cách làm phổ biến là RLHF (Reinforcement Learning from Human Feedback): cho model sinh vài câu trả lời, người chấm xem câu nào hay hơn, dạy một reward model bắt chước cách chấm đó, rồi dùng reinforcement learning để model sinh ra câu được điểm cao. DPO và các biến thể thì học thẳng từ từng cặp "câu tốt, câu dở", khỏi cần reward model riêng nên rẻ và gọn hơn.

Với toán và code thì còn một ngả nữa. Đáp án ở đây kiểm tra tự động được (chạy test, so kết quả), nên model được thưởng mỗi lần giải đúng. Các reasoning model học "nghĩ trước khi nói" chính bằng cách này: viết ra một chuỗi suy luận dài rồi mới chốt câu trả lời. Chuyện này mình có viết riêng ở bài DeepSeek R1 và reasoning model.


# vì sao nói trơn tru mà vẫn bịa

Nắm được cơ chế ở trên rồi thì nhiều cái tính của LLM bỗng trở nên dễ hiểu.

Trước hết là chuyện bịa (hallucination). Model được tối ưu để sinh ra văn bản nghe hợp lý, chứ không phải văn bản đúng sự thật. Phần lớn thời gian hai cái này trùng nhau, nên ta ít để ý. Nhưng khi model không biết, nó vẫn nặn ra thứ nghe xuôi tai nhất: một hàm không tồn tại, một bài báo khoa học chẳng có thật, một con số trông rất tự tin. Post-training giúp giảm bớt bằng cách dạy model biết nói "mình không chắc", nhưng không xoá hẳn được.

Kiến thức của model cũng có hạn sử dụng. Nó chỉ biết những gì nằm trong dữ liệu train, tính tới một mốc gọi là knowledge cutoff. Muốn nó biết chuyện mới hay tài liệu nội bộ công ty thì phải nhét thông tin vào prompt, qua RAG, search hoặc tool calling (xem thêm RAG hay fine-tuning).

Model cũng chẳng nhớ gì giữa các lần gọi. Bạn thấy nó "nhớ" cuộc trò chuyện là vì ứng dụng gửi lại toàn bộ lịch sử chat mỗi lần bạn hỏi. Tính năng "memory" của các app thực chất là ghi chú lưu ở ngoài rồi chèn lại vào prompt.

Hỏi cùng một câu mà mỗi lần trả lời một kiểu là do bước chọn token có pha ngẫu nhiên (temperature, top-p). Hạ temperature về 0 thì ổn định hơn nhiều, nhưng vẫn có thể lệch chút ít vì cách GPU tính toán song song.

Còn mấy việc tưởng dễ như đếm chữ cái, đảo ngược chuỗi, so 9.11 với 9.9 mà model vẫn làm hỏng thì phần lớn là do cách token bị cắt, chứ không phải nó ngốc.


# con số "7B" nói lên điều gì

"Llama 8B", "Qwen 27B", "gpt-oss 120B": con số đó là số tham số, tức số lượng con số model học được. Mỗi tham số cần chỗ để chứa, và bảng dưới cho bạn ước lượng nhanh:

độ chính xácbyte / tham sốmodel 8B cần khoảngmodel 27B cần khoảng
FP16 / BF16216 GB54 GB
8-bit18 GB27 GB
4-bit~0.5–0.64–5 GB14–17 GB

Đó mới là chỗ chứa trọng số, chưa tính bộ nhớ cho context (KV cache) lúc chạy.

Kỹ thuật nén số từ 16-bit xuống 8-bit, 4-bit gọi là quantization. Mất một chút chất lượng, đổi lại model 27B nhét vừa một card 24GB, model 8B chạy ngon trên laptop. Chạy LLM ở nhà được là nhờ nó, mình viết riêng trong bài các model local đáng chạy nhất 2026.

Còn model càng to có càng giỏi không? Đúng, nhưng chỉ một phần. Các nghiên cứu về scaling law cho thấy chất lượng tăng khá đều khi tăng cùng lúc số tham số, lượng dữ liệu và lượng tính toán. Có điều dữ liệu sạch và cách train khéo cũng quan trọng chẳng kém, nên model 27B năm 2026 hoàn toàn có thể ăn đứt model 70B của vài năm trước.


# tự tay xem model đoán chữ

Đọc mãi không bằng tự chạy. Đoạn Python dưới đây tải một model nhỏ về và in ra 5 token có xác suất cao nhất cho câu bạn đưa vào.

pip install torch transformers
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
 
model_name = "Qwen/Qwen2.5-0.5B"  # model nhỏ, chạy được trên CPU
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
 
text = "Thủ đô của Việt Nam là"
inputs = tokenizer(text, return_tensors="pt")
 
print("Token:", [tokenizer.decode(t) for t in inputs["input_ids"][0]])
 
with torch.no_grad():
    logits = model(**inputs).logits  # [batch, số token, kích thước từ điển]
 
probs = torch.softmax(logits[0, -1], dim=-1)  # chỉ lấy vị trí cuối cùng
top = torch.topk(probs, k=5)
 
for p, idx in zip(top.values, top.indices):
    print(f"{tokenizer.decode(idx)!r:>12}  {p.item():.3f}")

Chạy xong bạn sẽ để ý hai thứ. Dòng Token: cho thấy câu tiếng Việt bị băm thành mấy mảnh khá kỳ cục. Và model không hề "biết" đáp án là "Hà Nội", nó chỉ cho " Hà" một xác suất rất cao thôi. Toàn bộ cái mà ta gọi là thông minh của LLM nằm ở chỗ phân bổ xác suất ấy cho trúng.

Đổi câu thành một đoạn code dở dang kiểu public static void main( rồi xem nó đoán gì, khá vui đấy.


# gom lại

Gõ một câu vào, câu đó bị tokenizer cắt thành token và đổi ra số ID. Mỗi ID thành một vector mang nghĩa, được trộn thêm thông tin vị trí. Vector đi qua hàng chục tầng transformer, ở mỗi tầng attention cho token nhìn các token phía trước còn FFN xử lý tiếp. Tới cuối, model ra bảng xác suất cho token kế tiếp, chọn một cái, nối vào rồi chạy vòng mới. Còn để có được bộ tham số biết làm những việc đó thì phải qua pre-training để học đoán chữ, SFT để học làm trợ lý, và RL để học cư xử với suy luận.

Bài tiếp theo trong chuỗi: chuyện gì xảy ra bên dưới khi bạn prompt xong và nhấn Enter. Lần này mình đi theo một request, từ lúc nó rời trình duyệt tới khi chữ đầu tiên hiện lên.

Tài liệu tham khảo:


Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.