Ask a state-of-the-art language model how many times the letter "r" appears in "strawberry" and there's a good chance it confidently answers two. It's a famous, oddly durable failure, and the usual reaction is to conclude the model can't reason. That's the wrong lesson. The model gets it wrong because, in a very literal sense, it never sees the word "strawberry" as a sequence of letters at all. To understand why, you have to understand the very first thing that happens to your text before a model ever touches it: tokenization.
Models don't see text — they see token IDs
An LLM does not operate on characters or words. The first step in every pipeline is to chop the input text into chunks called tokens, and then map each token to an integer ID that indexes into the model's embedding matrix. Everything the model computes on is those integer IDs and the vectors they point to — never the raw characters.
A token can be a whole common word, a fragment of a longer word, a piece of punctuation, or in the limit a single character. The set of all possible tokens is the model's vocabulary, fixed at training time. So when you type "strawberry," the model doesn't receive ten letters; it receives a small handful of token IDs, each an opaque index. The spelling is gone before the first layer runs.
How byte-pair encoding builds the vocabulary
The dominant method for building that vocabulary is Byte-Pair Encoding (BPE), used in models from GPT-2 through GPT-4 and Llama 3. It's a compression idea repurposed for language, and the training algorithm is simple:
- Start with a base vocabulary of the 256 possible byte values as the first 256 single-character tokens.
- Scan the training corpus and find the most frequent adjacent pair of tokens.
- Merge that pair into a single new token, assign it the next ID, and record the merge.
- Repeat until you hit a target vocabulary size.
For GPT-2, that target is 50,257 tokens. Encoding new text later just replays the learned merges: split the text into bytes, then repeatedly find and merge adjacent pairs that appear in the learned merge table, until no more merges apply. The frequent pairs that got merged early — common letter sequences, prefixes, whole words — become single tokens; rare sequences stay split into smaller pieces.
# Illustrative (tiktoken)
import tiktoken
enc = tiktoken.get_encoding("gpt2")
ids = enc.encode("strawberry")
# Returns a few subword token IDs — NOT ten character codes.
# The model reasons over these opaque integers, not the letters.Why this is a good trade-off
BPE lands in a useful middle ground between two bad extremes. Tokenizing by whole words gives a huge, brittle vocabulary that breaks on any unseen word. Tokenizing by individual characters keeps the vocabulary tiny but makes sequences painfully long. Subword tokenization gets the best of both: frequent words become a single efficient token, while rare or novel words gracefully decompose into familiar pieces, so nothing is ever truly out-of-vocabulary.
The efficiency shows up in the numbers. A common rule of thumb for English is roughly 1.3 tokens per word, or about 4 characters per token — which is why "This is some text" compresses to just four tokens rather than seventeen characters. This is also why your API bill and context window are measured in tokens, not words: tokens are the real unit the model consumes, and the ratio shifts for code, punctuation-heavy text, or non-English languages.
Back to the strawberry
Now the failure makes sense. "Strawberry" arrives at the model as a couple of subword tokens — something like "straw" and "berry" — each a single opaque integer ID. The model has no built-in view of the characters inside a token. To count the r's, it would have to have separately learned the exact letter spelling of every token in its 50,000-plus vocabulary and then sum across them. Nothing in the training objective directly teaches that, so it approximates and often misses.
This is the root of a whole family of quirks: counting letters, reversing strings, splitting words by character, and even some digit-level arithmetic are all hard for the same reason — they demand character-level access to something the model only ever saw as whole-chunk IDs. It isn't a reasoning gap so much as an information gap baked in before inference begins. The practical fix is equally telling: give the model the characters explicitly ("s-t-r-a-w-b-e-r-r-y") or let it use a tool like code execution, and the problem evaporates, because now the individual letters are actually in front of it.
Key Takeaways
- LLMs operate on token IDs, not characters — the spelling of a word is gone before the first layer.
- BPE builds the vocabulary by starting from 256 bytes and iteratively merging the most frequent adjacent pair up to a target size (GPT-2: 50,257 tokens).
- Subword tokenization balances vocabulary size against sequence length, so common words are one token and rare words split into pieces.
- English averages roughly 1.3 tokens per word (~4 characters per token) — the reason context windows and pricing are in tokens.
- Character-level tasks like counting letters fail because that information isn't visible inside a token; spelling it out or using a tool fixes it.
