Models · one task

LLM Fundamentals

A large language model reads tokens and writes the next token from probabilities it learned. Temperature, prompts, the context window, cost, and latency are the knobs you actually turn. The ideas are the same on Groq, OpenAI, Anthropic, or Gemini.

BeginnerWorks with any chat API

Hosted models and open weights

AnalogyA hosted model is a taxi: you pay per ride and you do not own the car. An open-weight model is a car you can drive yourself or rent from someone who already bought it.
Data flow
Prompt→API or local weights→Tokens in→Tokens out→Usage and text

Almost every current chat model is a transformer. It is trained to predict the next token, then tuned to follow instructions. The ideas here — tokens, temperature, messages, cost — do not change when you switch from Groq to OpenAI, Anthropic, or Gemini. The client library and the model name change.

Starter

messages = [
    {"role": "system", "content": "Reply with one short sentence."},
    {"role": "user", "content": "What is a token?"},
]
print("send messages, read text + usage from the response")
print(messages[1]["content"])

Example

gpt-oss on Groq, GPT on OpenAI, and Claude on Anthropic are three taxis. Your ticket-sorting prompt can ride in any of them.

Almost every current chat model is a transformer, the architecture from the 2017 paper Attention Is All You Need. It is trained to predict the next token, then tuned to follow instructions.

  • Hosted: the provider runs the weights. You send an API key. GPT, Claude, Gemini, and Groq-hosted models work this way.
  • Open-weight: the numbers are published. You can download them or call a host that already did. Llama, Qwen, and gpt-oss are in this family.

A practical free path used in many public labs is openai/gpt-oss-120b on Groq, with the groq Python client. Swap the client and the model string and the rest of this page still holds.

import os
from groq import Groq

client = Groq(api_key=os.environ["GROQ_API_KEY"])
reply = client.chat.completions.create(
    model="openai/gpt-oss-120b",
    temperature=0,
    messages=[
        {"role": "system", "content": "Reply with one short sentence."},
        {"role": "user", "content": "What is a token?"},
    ],
)
print(reply.choices[0].message.content)
print(reply.usage)

Count tokens before you guess

AnalogyTokens are the stamps on a letter. The post office charges by stamp, not by how pretty the handwriting is. A short policy pasted into every chat is a lot of stamps.
Data flow
Text→Tokenizer→Token ids→Count→Cost estimate

The generation loop assigns a score to every token in the vocabulary, turns scores into probabilities, and picks one. Output tokens are usually more expensive than input tokens, and they arrive one by one, so a long answer is both the bill and the wait. Count the system prompt once and multiply by daily calls.

Starter

text = "Refund order A-1001, the parcel arrived broken."
# Rough English guess: ~4 characters per token. Count for real before you budget.
guess = max(1, len(text) // 4)
print(text)
print("rough tokens:", guess)

Example

A 40-word refund policy looks cheap until it is pasted into 10,000 chats a day. That is the line item to shorten.

Billing and the context window are both in tokens. A rough English rule is 1 token ≈ 4 characters, and it is wrong often enough that you should count.

import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
text = "Refund order A-1001, the parcel arrived broken."
print(len(enc.encode(text)), enc.encode(text)[:8])

Use case: a support macro

A 40-word refund policy looks cheap. Pasted into every one of 10,000 chats a day, with the customer thread on top, it dominates the bill. Count the system prompt once, multiply by daily calls, and you know whether to shorten it.

Generation is a loop: the model assigns a score to every token in its vocabulary, turns scores into probabilities, picks one, appends it, and repeats. Streaming just shows you those tokens as they are chosen.

Temperature, top-p, length, seed

AnalogySampling is how the model breaks ties. Temperature 0 almost always takes the favorite. top_p ignores the rare nonsense tokens at the tail. A seed asks for the same path again, and not every host promises it.
Data flow
Scores→temperature / top_p→One token→Append→Repeat

Set max tokens so a rambling answer cannot eat the budget. Two runs at temperature 0.7 will not match. Judge a prompt by a score over many examples, not by one screenshot. A seed helps when you compare two prompts and want less noise.

Starter

def pick(task: str) -> dict:
    if task == "json":
        return {"temperature": 0, "max_tokens": 80}
    return {"temperature": 0.7, "max_tokens": 400}

print(pick("json"))

Example

Ticket JSON wants temperature 0 and a short cap. A marketing title wants a higher temperature and a cap so it stays one line.

KnobEffectSet it when
temperature 0Mostly the top token. Stable JSON and labels.Ticket sorting, extraction, SQL
temperature 0.7–1More variety.Drafting emails, titles, brainstorms
top_pOnly sample from the smallest set of tokens whose probabilities add up to p.You want variety without the rare nonsense tokens
max tokensHard stop on the answer length.You must leave budget, or stop a rambling model
seedAsks the provider to repeat a sampling path. Not a promise on every host.You are comparing two prompts and want less noise
Same code, different words. Unless temperature is 0, two runs will not match. Judge prompts by a score over many examples, not by one screenshot.

One task: sort a support ticket

AnalogyA prompt is a job description plus a form. The job description is the system message. The form is the shape of the answer. Few-shot means you also show two filled-in forms.
Data flow
System rules→One or two examples→New ticket→JSON label

Change one thing at a time: the instruction, then an example, then temperature. If accuracy goes up and tokens go down, keep it. If the prose sounds smarter and the score falls, revert. The same form works for a hospital inbox, an invoice extract, and a review tagged bug / feature / praise.

Starter

SYSTEM = (
    "Sort the ticket. Reply with JSON only: "
    '{"topic":"billing|shipping|other","priority":"low|medium|high"}'
)
ticket = "Charged twice for order A-1001"
print(SYSTEM)
print("USER:", ticket)

Example

“Charged twice” should become billing/high. “Where is parcel A17?” should become shipping/medium. If the second one comes back billing, add that example.

Vague instructions fail in public. This task makes the failure obvious: label a ticket as billing, shipping, or other, with a priority, as one JSON object.

SYSTEM = """You sort support tickets.
Return only JSON: {"topic":"billing|shipping|other","priority":"low|medium|high"}.
billing = payment, refund, invoice. shipping = delivery, tracking, damaged parcel.
If unsure, use other and low."""

def messages(text: str):
    return [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": text},
    ]

Few-shot means you also show examples. Two or three is enough. Put them in the user message or as earlier user/assistant pairs.

EXAMPLES = """
Ticket: Charged twice. → {"topic":"billing","priority":"high"}
Ticket: Where is parcel A17? → {"topic":"shipping","priority":"medium"}
Ticket: Do you sell gift cards? → {"topic":"other","priority":"low"}
"""

More prompt jobs with the same shape

  • Route a hospital inbox: appointment, prescription, emergency. Emergency must be high and must tell the user to call local emergency services, not wait on the bot.
  • Extract invoice fields: vendor, total, currency, date. Reject the run if currency is missing.
  • Classify a product review as bug, feature, or praise so a PM dashboard can count them.

Score the prompt, do not admire it

AnalogyAn eval is a small exam with an answer key. You do not grade the model by how impressive one answer feels. You grade how many questions it got right.
Data flow
Goldens→Your prompt→Predictions→Exact match→A percent you can track

Keep a handful of questions you never edit the prompt against. Otherwise the bot memorizes your quiz. Exact match is enough for labels. When wording can vary and still be right, you need a looser score, which is what RAGAS-style judges are for later.

Starter

goldens = [("Charged twice", "billing"), ("Where is my parcel?", "shipping")]
preds = ["billing", "billing"]  # second one is wrong
correct = sum(p == g[1] for p, g in zip(preds, goldens))
print(f"{correct}/{len(goldens)}")

Example

Three goldens that match support’s real macros will teach you more than thirty goldens you invented in one sitting.

GOLDENS = [
    ("Charged twice for order A-1001", "billing", "high"),
    ("Tracking has not moved in 6 days", "shipping", "medium"),
    ("What are your store hours?", "other", "low"),
]

def exact(pred, topic, priority):
    return pred.get("topic") == topic and pred.get("priority") == priority

# correct / len(GOLDENS) is the number you track when you edit the prompt

Change one thing at a time: the instruction, then an example, then temperature. If accuracy jumps from 70% to 90% and tokens drop, keep it. If the prose “sounds smarter” and the score falls, revert.

Context, cost, and latency

AnalogyCost is stamps. Latency is how long the customer stares at a spinner. The context window is the desk those stamps have to fit on.
Data flow
Input tokens→Output tokens→Price→Time→Cache if the question repeats

Leave room in the window for the answer. Cap max_tokens. Stream when a person is watching. Batch when nobody is. If the same question arrives during an outage, cache it for 30 seconds instead of calling the model on every refresh.

Starter

def dollars(in_tok, out_tok, in_price, out_price):
    return (in_tok * in_price + out_tok * out_price) / 1_000_000

print(round(dollars(400, 30, 0.15, 0.60) * 20_000, 2), "dollars for 20k calls")

Example

A status-page assistant that calls the model for “is checkout down?” on every reload will get rate-limited at the worst moment. A 30-second cache fixes that.

The context window must hold the system prompt, examples, the ticket, and the answer. A 128k window is not a reason to paste the wiki. Extra tokens add cost and delay, and they dilute the instruction.

# sketch: dollars ≈ (input_tokens * in_price + output_tokens * out_price) / 1_000_000
def estimate(in_tokens, out_tokens, in_price, out_price):
    return (in_tokens * in_price + out_tokens * out_price) / 1_000_000

Output tokens are often several times more expensive than input tokens, and they arrive one by one, so a long answer is both the bill and the wait. Cap max_tokens. Stream when a human is watching. Batch when nobody is.

Use case: a status page assistant

Users refresh during an outage. Cache the system prompt result for “is checkout down?” for 30 seconds instead of calling the model on every hit. Latency goes from a second to a cache read, and the provider stops rate-limiting you at the worst moment.

Practice

  • Count tokens in your system prompt with tiktoken or the provider’s usage field.
  • Run the same ticket at temperature 0 and 1. Compare.
  • Add one few-shot example that fixes a ticket your prompt gets wrong.
  • Compute daily cost for 20,000 calls if each call is 400 input and 30 output tokens.