Tất cả bài viết Ảnh bìa: Token của LLM không phải là một từ
15 tháng 7, 2026 · 23 phút đọc

Token của LLM không phải là một từ

Token được tạo ra thế nào, đếm ra sao, chiếm context window ở đâu và vì sao token/giây chưa đủ để nói một LLM nhanh. Một bản đồ thực dụng từ tokenizer tới chi phí và hiệu năng inference.

LLMTokenizationContext WindowInference

Bạn dán một file JSON dài vào chatbot, hỏi đúng một câu ngắn rồi nhận lỗi “context length exceeded”. Lần khác, bạn gửi một đoạn tiếng Việt trông chẳng dài hơn bao nhiêu nhưng hóa đơn API lại nhảy vọt. Đến lúc benchmark hai model, bên A quảng cáo 120 token/giây, bên B chỉ 70 token/giây, vậy mà người dùng bên B lại thấy câu trả lời xuất hiện sớm hơn.

Ba tình huống ấy cùng xoay quanh token, nhưng không phải cùng một bài toán. Token vừa là đơn vị model đọc và viết, vừa là đơn vị chia context window, tính tiền, cấp phát KV cache và đo tốc độ inference. Nếu coi token đơn giản là “một từ”, gần như mọi phép tính phía sau đều lệch.

Tôi muốn dựng lại toàn bộ đường đi ấy từ đầu: text biến thành số như thế nào, model sinh token kế tiếp ra sao, context window thực sự chứa những gì, rồi TTFT và token/giây đang đo đoạn nào của trải nghiệm.

Luồng text đi qua tokenizer, context, prefill và decode

Model không đọc chữ, nó đọc token ID

LLM không nhận trực tiếp chuỗi ký tự Xin chào. Trước khi text chạm tới model, một tokenizer biến nó thành một dãy token; mỗi token lại ánh xạ tới một số nguyên gọi là token ID. Model nhận các ID đó, tra embedding tương ứng, rồi xử lý các vector số.

flowchart LR
    A[Text thô] --> B[Normalize<br/>nếu tokenizer có]
    B --> C[Pre-tokenize]
    C --> D[Chia thành subword / byte]
    D --> E[Token IDs]
    E --> F[Embeddings]
    F --> G[Transformer]
    G --> H[Logits cho token kế tiếp]

Token là một phần tử trong vocabulary của tokenizer. Nó có thể là cả từ, một phần của từ, dấu cách đi cùng một cụm chữ, dấu câu, một byte, hoặc token đặc biệt không tương ứng với chữ nào người dùng nhìn thấy. Vocabulary là tập các token mà model biết; token ID chỉ là vị trí định danh của token trong tập ấy.

Điều này dẫn tới một quy tắc quan trọng: token ID không có ý nghĩa chung giữa các model. ID 1234 ở tokenizer A có thể là ing, sang tokenizer B lại là một mẩu byte hoàn toàn khác. Ngay cả hai model cùng họ cũng chưa chắc dùng cùng vocabulary hoặc cùng quy tắc tách.

Vì sao không tách theo từ hoặc ký tự?

Tách theo từ nghe tự nhiên, nhưng vocabulary sẽ phình rất lớn và vẫn vướng tên riêng, typo, code, từ mới. Tách theo từng ký tự thì bao phủ được nhiều text hơn, đổi lại chuỗi đầu vào dài, model phải ghép quá nhiều bước mới nhận ra một đơn vị có nghĩa.

Phần lớn LLM dùng một điểm cân bằng: subword tokenization. Từ hoặc cụm thường gặp có thể nằm gọn trong một token; từ hiếm được ghép từ những mảnh vocabulary đã biết. Cách này giữ vocabulary ở kích thước hữu hạn mà chuỗi vẫn ngắn hơn đáng kể so với tách từng ký tự. Nó làm giảm mạnh bài toán out-of-vocabulary (OOV); với tokenizer có byte-level hoặc byte fallback, text bất kỳ còn có thể được biểu diễn mà không cần dồn cả từ lạ vào một token [UNK].

Công trình của Sennrich, Haddow và Birch năm 2016 đưa Byte Pair Encoding (BPE) vào bài toán dịch máy để biểu diễn từ hiếm bằng các đơn vị con thay vì một vocabulary từ đóng.1 SentencePiece sau đó cho phép huấn luyện tokenizer subword trực tiếp trên câu thô, không bắt buộc text phải được tách từ trước.2

Các họ tokenizer bạn thường gặp gồm:

HọCách nghĩ gần đúngĐiểm đáng nhớ
BPEBắt đầu từ đơn vị nhỏ, lặp lại việc gộp cặp xuất hiện cùng nhau thường xuyênHọc vocabulary theo hướng từ dưới lên
WordPieceChọn phép gộp bằng một score có xét tần suất của cặp so với từng mảnh, không chỉ đếm cặp thôKý pháp ## thường đánh dấu mảnh tiếp nối trong một từ
UnigramBắt đầu bằng vocabulary lớn rồi lần lượt loại các mảnh ít đóng góp nhất vào likelihoodHọc vocabulary theo hướng từ trên xuống và chấm điểm nhiều cách phân đoạn
Byte-level BPELàm việc trên byte rồi học cách gộpBao phủ được text bất kỳ mà không cần một token “không biết” cho mỗi ký tự lạ

Ví dụ, giả sử các tokenizer đều gặp từ tokenization nhưng dùng vocabulary và tham số đã học khác nhau:

Trường hợp giả địnhKết quả có thể cóVì sao
WordPiece A["token", "##ization"]Vocabulary có token và mảnh nối tiếp ##ization.
WordPiece B["to", "##ken", "##iza", "##tion"]Vocabulary không có hai mảnh dài ở trên nên phải ghép từ các mảnh ngắn hơn.
BPE["token", "iz", "ation"]Lịch sử merge đã tạo ra token, ization, nhưng chưa gộp toàn bộ ization thành một token.
Unigram["token", "ization"]Trong các cách hợp lệ như ["to", "ken", "ization"] hay ["token", "iz", "ation"], cách này có tổng điểm xác suất cao nhất.

Dấu ## là quy ước của WordPiece để báo một mảnh đang nối tiếp từ phía trước; BPE và Unigram không mặc định dùng ký hiệu này. Các kết quả trên dùng vocabulary giả định để minh họa cơ chế, không phải output cố định của bất kỳ model cụ thể nào.

Một tokenizer hoàn chỉnh còn có thể normalize Unicode, tách sơ bộ, thêm special token và decode ID trở lại text. Tài liệu Hugging Face mô tả pipeline này thành bốn khâu: normalizer, pre-tokenizer, tokenization model và post-processor.3 Vì vậy, hai chuỗi nhìn gần giống nhau vẫn có thể ra số token khác nếu khác dạng Unicode, dấu cách, xuống dòng hoặc tokenizer.

Tiếng Việt, code và emoji thường làm phép ước lượng bằng từ sai hẳn

Tiếng Việt dùng dấu và khoảng trắng giữa các âm tiết; code có identifier dài, dấu ngoặc và indentation; emoji có thể gồm nhiều code point ghép lại. Tokenizer được huấn luyện trên corpus nào, vocabulary lớn bao nhiêu và xử lý byte ra sao sẽ quyết định mức “nén” của từng loại text.

README chính thức của tiktoken viết rằng, tính trung bình trong thực tế, mỗi token tương ứng khoảng bốn byte.4 Byte không phải ký tự: một ký tự Unicode có thể chiếm nhiều byte, còn ranh giới token cũng không bắt buộc trùng ranh giới ký tự. Vì vậy, con số trên chỉ mô tả mức nén trung bình của tokenizer này; không được đổi nó thành quy tắc “một token bằng bốn ký tự” rồi áp dụng cho mọi ngôn ngữ và model. Với tiếng Việt, JSON, base64, UUID, stack trace hoặc emoji, sai số có thể đủ lớn để làm hỏng một context budget vốn đã sát trần.

Muốn biết chính xác, hãy đếm bằng đúng tokenizer của đúng model.

import tiktoken

encoding = tiktoken.encoding_for_model("gpt-4o")  # thay bằng model bạn đang dùng
text = "Xin chào! Hàm get_user_by_id() đang trả về 404."

token_ids = encoding.encode(text)
print(len(token_ids))
print(token_ids)
print([encoding.decode_single_token_bytes(t) for t in token_ids])

Nếu provider không công khai tokenizer hoặc model xử lý thêm token nội bộ, số đếm local chỉ là ước lượng. Usage do API trả về sau request mới là số dùng để đối soát chi phí.

Special token là cú pháp điều khiển, không phải chữ trang trí

Tokenizer thường dành một số ID cho special token: bắt đầu hoặc kết thúc chuỗi, phân cách message, đánh dấu role, padding, tool call, ảnh, audio hay phần cần điền vào giữa. Chúng có thể không hiện trong prompt mà bạn nhìn thấy, nhưng vẫn tồn tại trong chuỗi model nhận.

Đây là lý do len(tokenizer.encode(user_text)) chưa chắc bằng input_tokens trên hóa đơn. Một request chat thường còn có system instruction, role marker, schema của tool, wrapper do SDK hoặc provider thêm, tool output từ lượt trước và metadata đa phương thức. Một ảnh cũng có thể được quy đổi thành token hoặc một đơn vị tính tương tự theo policy riêng của model; đừng lấy kích thước file JPEG để suy ra token text.

Special token cũng là ranh giới bảo mật. Đừng tự nối chuỗi giả role hoặc giả delimiter rồi hy vọng model hiểu giống protocol chính thức. Hãy dùng message structure và API field mà provider hỗ trợ; tokenizer của tiktoken mặc định còn kiểm tra special token không được phép để tránh vô tình coi một chuỗi điều khiển là token đặc biệt.4

LLM sinh từng token kế tiếp như thế nào?

Sau khi nhận chuỗi input, model tạo một phân phối xác suất trên toàn vocabulary cho vị trí kế tiếp. Trước softmax, các điểm số thô được gọi là logits. Decoder chọn một token từ phân phối đó, nối token vừa chọn vào chuỗi, rồi lặp lại.

flowchart LR
    C[Context hiện tại] --> M[Model forward pass]
    M --> L[Logits trên vocabulary]
    L --> S[Sampling / chọn token]
    S --> T[Token kế tiếp]
    T --> C
    T --> Q{EOS hoặc chạm giới hạn?}
    Q -- chưa --> M
    Q -- rồi --> O[Trả output]

Các tham số generation can thiệp ở bước chọn token:

  • temperature thấp làm phân phối sắc hơn và thường giảm tính ngẫu nhiên, nhưng không bảo đảm output đúng.
  • top_p chỉ lấy phần nhỏ nhất của vocabulary có tổng xác suất đạt ngưỡng rồi sample trong phần đó.
  • max_output_tokens chặn số token model được phép sinh, không phải lời hứa rằng model sẽ dùng hết.
  • stop yêu cầu dừng khi gặp chuỗi chỉ định; token kết thúc tự nhiên thường được gọi là EOS, viết tắt của end-of-sequence.
  • logprobs, nếu API hỗ trợ, cho biết xác suất log của token đã chọn và đôi khi cả các ứng viên gần nhất.

Token được sinh tuần tự theo kiểu autoregressive: token sau phụ thuộc chuỗi trước. Paper về speculative decoding của Leviathan, Kalman và Matias diễn đạt đúng nút thắt này: sinh K token theo cách thông thường cần K lượt chạy nối tiếp; kỹ thuật speculative decoding dùng một model nhỏ draft nhiều token để model đích kiểm tra song song, nhưng vẫn giữ phân phối đầu ra của model đích trong thiết lập thuật toán của họ.9

Một chi tiết dễ gây nhầm: token stream không đồng nghĩa với character stream. Có token chỉ decode an toàn khi ghép với token kế tiếp vì biên token có thể cắt qua byte của một ký tự UTF-8. SDK tốt sẽ xử lý phần ghép này; nếu tự viết streamer ở tầng byte, đừng decode từng token một cách mù quáng.

Input token, output token và những con số trên hóa đơn

Tên field thay đổi theo API, nhưng ba nhóm sau xuất hiện rất thường xuyên:

Thuật ngữNó tính gì?Dễ nhầm với
Input / prompt tokensMọi token model phải đọc cho requestChỉ riêng message mới nhất của user
Output / completion tokensToken model sinh raSố từ trong câu trả lời
Cached input tokensPrefix đã được hệ thống nhận ra và tái sử dụng theo chính sách cacheToken miễn phí hoặc không chiếm context

Cached input vẫn thuộc context, nhưng có thể được tính với đơn giá riêng. Tương tự, một số reasoning model có token suy luận không hiện nguyên văn cho người dùng nhưng vẫn được báo trong usage hoặc bị tính vào output budget theo tài liệu của model. Khi viết bộ đo chi phí, đừng hard-code một schema chung cho mọi API.

Nếu bảng giá niêm yết theo một triệu token, phép tính cơ bản là:

Tuncached=TinputTcachedC=1106(TuncachedPinput+TcachedPcached+ToutputPoutput)\begin{aligned} T_{\text{uncached}} &= T_{\text{input}} - T_{\text{cached}} \\ C &= \frac{1}{10^6}\left( \begin{aligned} &T_{\text{uncached}}P_{\text{input}} \\ &+ T_{\text{cached}}P_{\text{cached}} \\ &+ T_{\text{output}}P_{\text{output}} \end{aligned} \right) \end{aligned}

Trong đó TT là số token, PP là đơn giá trên một triệu token và CC là chi phí của request. Batch, cache, priority tier hoặc khu vực có thể có bảng giá khác. Hãy lưu cả raw usage lẫn bảng giá có version; nếu chỉ lưu một con số tiền cuối cùng, bạn sẽ khó giải thích hóa đơn khi giá thay đổi.

Cached token giảm tiền input, không làm context ngắn đi

Nếu nhiều request lặp lại một prefix dài — system instruction, tool schema, few-shot example hoặc tài liệu nền — model phải prefill lại cùng một đoạn input. Prompt caching hoặc prefix caching cho phép serving stack tái sử dụng trạng thái đã tính của prefix đó, thay vì tính lại từ đầu. Model vẫn decode một output mới; cơ chế này khác response cache, nơi hệ thống trả lại nguyên câu trả lời đã lưu.1011

flowchart LR
    A[Prefix ổn định<br/>system · tools · tài liệu] --> B{Cache hit?}
    B -- Không --> C[Prefill và lưu prefix]
    B -- Có --> D[Tái sử dụng prefix]
    C --> E[Prefill input mới<br/>và decode output mới]
    D --> E

Điểm đáng tiền: với workload có prefix dài và lặp lại nhiều lần, cached input có thể là phần tiết kiệm lớn nhất trên hóa đơn. Mỗi token cache hit chuyển từ đơn giá input thường sang đơn giá cached input:

Scache=Tcached(PinputPcached)106S_{\text{cache}} = \frac{T_{\text{cached}} \left(P_{\text{input}} - P_{\text{cached}}\right)}{10^6}

Công thức trên giả định giá được niêm yết theo một triệu token. Mức giảm cụ thể phụ thuộc provider và model. Một số provider còn tách cache writecache read: lần đầu tạo cache có thể có giá bằng hoặc cao hơn input thường, còn các lần đọc sau mới rẻ. Vì vậy cache chỉ sinh lời khi prefix được tái sử dụng đủ nhiều; đừng nhìn mỗi cache hit rate mà bỏ qua chi phí ghi cache.12

Muốn tăng tỷ lệ cache hit, hãy giữ prefix ổn định và đẩy dữ liệu thay đổi về cuối prompt:

  • đặt system instruction, tool schema, few-shot example và tài liệu dùng chung ở đầu;
  • đặt message mới, timestamp, request ID và dữ liệu riêng của user ở cuối;
  • giữ thứ tự tool và cách serialize JSON ổn định giữa các request;
  • tránh sửa một message cũ ở đầu history nếu không cần thiết, vì thay đổi sớm có thể làm mất cache cho toàn bộ phần sau.

Dashboard nên ghi ít nhất cached_input_tokens, cache_write_tokenscache_read_tokens nếu provider trả các field đó, cùng ba chỉ số dẫn xuất:

Hcache=TcachedTinputScache=Tcached(PinputPcached)106ΔTTTFT=TTTFT, missTTTFT, hit\begin{aligned} H_{\text{cache}} &= \frac{T_{\text{cached}}}{T_{\text{input}}} \\ S_{\text{cache}} &= \frac{T_{\text{cached}} \left(P_{\text{input}} - P_{\text{cached}}\right)}{10^6} \\ \Delta T_{\text{TTFT}} &= T_{\text{TTFT, miss}} - T_{\text{TTFT, hit}} \end{aligned}

Ba dòng lần lượt là cache hit ratio, số tiền cache tiết kiệm và phần TTFT giảm được khi cache hit.

Cached input vẫn chiếm context window và không làm giảm số output token phải trả. Cache giảm chi phí input và thời gian prefill; nó không biến context thành miễn phí, không mở rộng context limit và không sửa được một prompt vốn chứa quá nhiều dữ liệu thừa.

Context window là chiếc khung chung cho cả request

Context window là số token tối đa model có thể xét trong một lần inference. Nó không phải bộ nhớ dài hạn và cũng không chỉ là “độ dài prompt”. Trong một ứng dụng chat hoặc agent, chiếc khung này có thể chứa:

  • system và developer instructions;
  • lịch sử hội thoại còn được giữ;
  • message mới nhất;
  • tài liệu RAG được truy xuất;
  • mô tả cùng JSON schema của tool;
  • output của tool;
  • token ảnh hoặc audio theo cách model quy đổi;
  • phần output đã sinh ở thời điểm hiện tại.

Một cách lập ngân sách an toàn là:

TlimitTsystem+Ttools+Thistory+TRAG+Tcurrent+Treserved output+Toverhead\begin{aligned} T_{\text{limit}} \geq{}& T_{\text{system}} + T_{\text{tools}} + T_{\text{history}} + T_{\text{RAG}} \\ &+ T_{\text{current}} + T_{\text{reserved output}} + T_{\text{overhead}} \end{aligned}

Nhiều API cho phép đặt max_output_tokens, nhưng model còn có thể có trần output riêng. Có nơi input và output chia chung context limit; có nơi tài liệu sản phẩm trình bày thêm giới hạn riêng. Luôn đọc contract của đúng model thay vì suy từ model cùng họ.

flowchart LR
    subgraph W[Context window cố định]
      S[System] --> H[History]
      H --> R[RAG / tools]
      R --> U[User input]
      U --> O[Output đang sinh]
      O --> F[Khoảng dự phòng]
    end

Nếu tổng vượt trần, SDK hoặc server có thể báo lỗi, cắt message cũ, truncate tài liệu hoặc giảm chỗ cho output. Cách nào cũng có hệ quả. Cắt đầu hội thoại có thể làm rơi instruction; cắt cuối tài liệu có thể bỏ mất đáp án; dành quá ít output khiến câu trả lời dừng giữa chừng.

Context dài không có nghĩa model dùng đều mọi vị trí

Một model chấp nhận được chuỗi dài chỉ chứng minh request đi qua được cổng đầu vào. Nó chưa chứng minh model truy xuất và suy luận tốt trên mọi token trong chiếc khung ấy. Liu và cộng sự quan sát trên các bài multi-document QA và key-value retrieval rằng hiệu năng của nhiều model được thử thường cao hơn khi thông tin liên quan nằm ở đầu hoặc cuối, rồi giảm khi thông tin nằm giữa context dài.6

Kết quả đó không nên bị biến thành tuyên bố rằng mọi model luôn “quên phần giữa”. Nó cho thấy context length công bố và effective context trong một tác vụ cụ thể là hai thứ cần benchmark riêng. Với hệ thống thật, tôi sẽ làm các test thay đổi vị trí bằng chứng, lượng nhiễu, độ dài output và số lượt hội thoại thay vì chỉ thử một prompt nhồi đầy tới trần.

Nhồi thêm context có bốn cái giá

Context dài thường kéo theo bốn loại chi phí:

  • Tiền: input token tăng, kéo theo chi phí API nếu provider tính theo lượng token xử lý.
  • Latency: prefill phải xử lý prompt dài hơn. Với full self-attention trong kiến trúc Transformer nguyên bản, chi phí tính toán mỗi layer có một thành phần tăng theo bình phương chiều dài chuỗi; các kiến trúc và kernel hiện đại có thể thay đổi chi tiết này, nhưng context dài vẫn không miễn phí.5
  • Bộ nhớ: với attention giữ toàn bộ lịch sử, KV cache tăng theo số token trong lúc request còn sống. Paper PagedAttention chỉ ra KV cache của từng request lớn, tăng giảm động và nếu quản lý kém sẽ lãng phí bộ nhớ vì phân mảnh hoặc sao chép; vLLM được xây để quản lý phần này theo kiểu paging.7
  • Chất lượng: tài liệu gần đúng nhưng không cần thiết có thể che lấp instruction và bằng chứng tốt.

Vì vậy, tối ưu context không đơn giản là cắt cho ngắn. Việc đúng hơn là giữ đúng token: instruction có thứ tự ưu tiên rõ, tài liệu RAG đã rerank và loại trùng, tool output được rút gọn, lịch sử cũ được tóm tắt có kiểm chứng, cùng một khoảng dự phòng đủ cho câu trả lời.

Output token/giây thường phản ánh pha decode

Một request LLM thường có hai pha tính toán dễ phân biệt:

  1. Prefill: model đọc toàn bộ input, tạo trạng thái attention và KV cache. Các token prompt có thể được xử lý song song ở mức độ lớn.
  2. Decode: model sinh token kế tiếp, cập nhật cache, rồi lặp. Pha này có quan hệ tuần tự giữa các token.

Vì thế, khi một benchmark nói model sinh 80 output token/giây, con số đó thường phản ánh pha decode và không cho biết người dùng phải nhìn màn hình trống bao lâu trước token đầu tiên. Cụm “token/giây” đứng một mình vẫn có thể chỉ prompt throughput, generation throughput hoặc tổng throughput của server, nên phải đọc đúng định nghĩa của benchmark. Tài liệu metrics của vLLM tách rõ TTFT, inter-token latency, end-to-end latency, thời gian prefill, thời gian decode và throughput token ở cấp server.8

MetricCông thức gần đúngNó trả lời câu gì?
TTFTt_first_token - t_arrivalBao lâu người dùng thấy phản hồi bắt đầu?
ITLkhoảng cách giữa hai token liên tiếpStream có đều hay bị khựng?
TPOTtrung bình thời gian cho mỗi output token sau token đầuNhịp decode trung bình là bao nhiêu?
Decode token/s1 / TPOT hoặc số token chia thời gian decodeMột request đang sinh nhanh tới đâu?
E2E latencyt_last_token - t_arrivalRequest hoàn tất sau bao lâu?
Throughputtổng request hoặc token hoàn tất trên một đơn vị thời gianCả server gánh được bao nhiêu tải?

Với N output token, một phép ước lượng hữu ích là:

TE2ETTTFT+(N1)TTPOTT_{\text{E2E}} \approx T_{\text{TTFT}} + (N - 1)T_{\text{TPOT}}

Công thức này dùng cách định nghĩa TTFT từ lúc request đến hệ thống, nên queue time đã nằm bên trong TTFT. Nó chỉ để tạo trực giác; network buffering, moderation, tool call, retry, scheduler, tokenization ở client và preemption có thể làm timeline thật phức tạp hơn.

Vì sao hai con số token/giây không so trực tiếp được?

Token không phải đơn vị nội dung cố định. Nếu tokenizer A dùng ít token hơn tokenizer B cho cùng một đoạn tiếng Việt, A có thể báo token/giây thấp hơn nhưng vẫn hoàn tất cùng câu sớm hơn. Benchmark còn thay đổi theo:

  • model và mức quantization;
  • GPU, memory bandwidth, tensor parallelism và serving engine;
  • độ dài input, độ dài output và kích thước batch;
  • số request chạy đồng thời;
  • sampling, structured output, speculative decoding;
  • prefix cache hit hay miss;
  • cách tính có bỏ token đầu tiên, queue time và network hay không.

Muốn benchmark tử tế, hãy cố định workload rồi báo ít nhất p50/p95 của TTFT, TPOT hoặc ITL, E2E latency, output token/s mỗi request, tổng throughput của server, error rate và chất lượng output. Với sản phẩm chat, p95 TTFT cùng độ đều của stream thường gần trải nghiệm người dùng hơn một con số token/giây đẹp ở chế độ một request, prompt ngắn.

Một checklist dùng token mà không tự gài mình

Khi tích hợp API:

  1. Đếm trước bằng tokenizer đúng model nếu có, nhưng lấy usage từ server làm số đối soát.
  2. Tính cả system prompt, tool schema, history, RAG và tool output; đừng chỉ đếm user message.
  3. Dành reserved_output trước khi nhồi tài liệu vào context.
  4. Ghi log input, cached input, output và reasoning token theo đúng schema provider, kèm model version.
  5. Đặt hard limit cho input người dùng, số document, kích thước tool output và số vòng agent để tránh đốt token không kiểm soát.
  6. Khi truncate, cắt theo cấu trúc message hoặc document; đừng chém chuỗi byte giữa một token, JSON object hay code block.
  7. Benchmark bằng dữ liệu thật của ngôn ngữ và định dạng bạn phục vụ, nhất là tiếng Việt, code và JSON.

Khi tối ưu chất lượng:

  • đặt instruction quan trọng ở vị trí rõ ràng và tránh lặp mâu thuẫn;
  • chọn kết quả retrieval theo relevance rồi loại trùng, không mặc định top-k càng lớn càng tốt;
  • tóm tắt lịch sử nhưng giữ đường dẫn về dữ liệu gốc khi cần kiểm chứng;
  • kiểm tra bằng chứng ở nhiều vị trí trong context dài;
  • nếu output bị cụt, kiểm tra finish reason và output cap trước khi trách model “quên”.

Khi tối ưu tốc độ:

  • giảm prompt rác để hạ prefill và TTFT;
  • tận dụng prefix caching cho phần đầu lặp lại nếu serving stack hỗ trợ;
  • stream output để người dùng thấy token đầu tiên sớm;
  • đo riêng queue, prefill và decode trước khi đổi GPU hoặc model;
  • tối ưu cho concurrency mục tiêu, không chỉ benchmark một request cô lập.

Token là đơn vị giao nhau của nhiều tầng

Token không phải “từ đã được mã hóa”. Nó là quyết định thiết kế nằm giữa ngôn ngữ, vocabulary và kiến trúc model. Từ quyết định đó kéo theo độ dài chuỗi, phần nào lọt vào context, bao nhiêu KV cache phải giữ, request tốn bao nhiêu tiền và output xuất hiện với nhịp nào.

Một dashboard chỉ có total_tokens không đủ để chẩn đoán chi phí hay latency. Tối thiểu, hãy ghi:

  • Usage: input_tokens, cached_input_tokens, output_tokensreasoning_tokens nếu provider có trả;
  • Latency: queue time, TTFT, TPOT hoặc ITL và E2E latency, ít nhất ở p50 và p95;
  • Serving: prompt token/s, generation token/s, số request đang chạy, số request đang chờ và cache hit rate;
  • Kết quả: model và version, finish_reason, retry_count, error_rate và chi phí tính theo bảng giá có version.

API thường không cho biết phần input nào đến từ system instruction, history, RAG hay tool output. Muốn có breakdown đó, ứng dụng phải tự đếm và gắn nhãn trước khi gửi request. Khi p95 TTFT tăng mà TPOT vẫn ổn, hãy nhìn queue và prefill; khi input token phình lên, hãy kiểm tra history, tài liệu RAG và tool output.

Đếm token là bước đầu. Gắn token với đúng nguồn, pha xử lý và kết quả mới giúp ta khoanh đúng tầng gây ra vấn đề.

Tài liệu tham khảo

  1. Sennrich, R., Haddow, B., Birch, A. (2016). Neural Machine Translation of Rare Words with Subword Units. ACL 2016. ACL Anthology: P16-1162
  2. Kudo, T., Richardson, J. (2018). SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing. arXiv:1808.06226
  3. Hugging Face. Tokenizers: Components. huggingface.co/docs/tokenizers
  4. OpenAI. tiktoken. github.com/openai/tiktoken
  5. Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, L., Polosukhin, I. (2017). Attention Is All You Need. arXiv:1706.03762
  6. Liu, N. F., Lin, K., Hewitt, J., Paranjape, A., Bevilacqua, M., Petroni, F., Liang, P. (2023). Lost in the Middle: How Language Models Use Long Contexts. arXiv:2307.03172
  7. Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H., Stoica, I. (2023). Efficient Memory Management for Large Language Model Serving with PagedAttention. SOSP 2023. arXiv:2309.06180
  8. vLLM Project. Metrics. docs.vllm.ai
  9. Leviathan, Y., Kalman, M., Matias, Y. (2023). Fast Inference from Transformers via Speculative Decoding. ICML 2023. arXiv:2211.17192
  10. OpenAI. Prompt Caching in the API. openai.com
  11. vLLM Project. Automatic Prefix Caching. docs.vllm.ai
  12. Anthropic. Pricing: Prompt caching. docs.anthropic.com