GenAI bills and bugs both start with the same unit: the token. Once you see prompts, context limits, and sampling knobs in token terms, pricing, truncation failures, and "why did it get weird?" stop feeling mystical.
What is a token? The model never reads "words." A tokenizer chops your string into discrete IDs from a fixed vocabulary. Those IDs fill a context window — a hard budget shared by system instructions, history, retrieved docs, tools, and the reply.
Why do sampling knobs matter? After the model scores the next token, temperature and top-p reshape which candidates you are willing to sample. Low and narrow → boring but stable. High and wide → creative but risky.
Tokenization is model-specific. Rough patterns for English:
Rule-of-thumb for planning (not billing truth): ~4 characters per token for English prose, or ~100 tokens ~= 75 words. Always measure with the real tokenizer in CI for anything cost-sensitive.
If the window is W tokens, everything the model can attend to in one call must fit in W. Overflow typically means:
Long RAG dumps are the classic way to spend the whole budget on noise. Prefer ranked snippets, summaries, and hard caps per source.
Let z_i be the logit (raw score) for vocabulary item i. Softmax turns logits into probabilities. Temperature T > 0 scales logits before softmax:
p_i = softmax(z_i / T)
| Temperature | Plain-English effect |
|---|---|
T → 0 (very small) |
Distribution collapses toward the top choice → greedy-like, stable |
T = 1 |
Use the model's native distribution |
T > 1 |
Flatten the distribution → more surprise, more nonsense risk |
Temperature does not add knowledge. It only changes how aggressively you explore the model's uncertainty.
Sort tokens by probability descending. Keep the smallest prefix whose cumulative probability is at least p. Sample only inside that nucleus.
top_p = 0.1 → tiny, high-confidence settop_p = 0.9 → broader, still cuts the long tailtop_p = 1.0 → effectively no nucleus cutoffTop-p adapts to the shape of the distribution: when the model is peaked, the nucleus is small; when it is flat, the nucleus grows.
| Task | Temperature | Top-p | Why |
|---|---|---|---|
| Extraction / JSON | 0–0.2 | 0.1–0.5 | Minimize format drift |
| Support answers | 0.2–0.4 | 0.5–0.8 | Stable tone, light variety |
| Code | 0.1–0.3 | 0.5–0.9 | Prefer correctness |
| Brainstorm | 0.7–1.0 | 0.9–0.95 | Explore alternatives |
Tune one primary diversity control first (often temperature or top-p), then the other.
Every call roughly costs price_in * prompt_tokens + price_out * completion_tokens. Output tokens are often more expensive and always more latency-sensitive because generation is sequential.
Approximate English token count without a real tokenizer — good for budgeting demos, bad for invoices:
def estimate_tokens(text: str) -> int:
# Rough: ~4 chars/token for English prose
return max(1, (len(text) + 3) // 4)
prompt = "System: be brief.\nUser: Explain tokens."
print(estimate_tokens(prompt))
Simulate a context packer that reserves output room:
def pack(parts: list[str], window: int, reserve_out: int) -> list[str]:
budget = window - reserve_out
kept = []
used = 0
for part in reversed(parts): # keep newest first
cost = estimate_tokens(part)
if used + cost > budget:
continue
kept.append(part)
used += cost
return list(reversed(kept))
history = ["old policy...", "mid turn...", "latest user question"]
print(pack(history, window=200, reserve_out=50))
Temperature on a toy logit vector (numpy-free):
import math
def softmax(xs):
m = max(xs)
exps = [math.exp(x - m) for x in xs]
s = sum(exps)
return [e / s for e in exps]
def with_temperature(logits, T: float):
return softmax([z / T for z in logits])
logits = [2.0, 1.0, 0.1] # pretend vocabulary of 3
print([round(p, 3) for p in with_temperature(logits, 0.5)]) # peakier
print([round(p, 3) for p in with_temperature(logits, 1.5)]) # flatter
Nucleus filter:
def nucleus(probs: list[float], p: float) -> list[int]:
order = sorted(range(len(probs)), key=lambda i: probs[i], reverse=True)
kept, cum = [], 0.0
for i in order:
kept.append(i)
cum += probs[i]
if cum >= p:
break
return kept
probs = [0.5, 0.3, 0.15, 0.05]
print(nucleus(probs, 0.8)) # indices covering >= 0.8 mass
Tokens meter cost and memory; the context window is a shared budget; temperature and top-p reshape sampling — use them deliberately for stability vs diversity.