AI SWE Prep
0/10in AI/ML Engineering
AI/ML Engineering

The AI Engineer's Mental Model

Where the AI SWE sits between research and product, the shape of an LLM call, and the vocabulary the rest of the module builds on.

~15 minLesson 35 of 60
Your progressNot started

The “AI Software Engineer” role is not about training foundation models from scratch. It’s about building reliable software around models that already exist — calling them, grounding them, evaluating them, and shipping features that happen to be powered by them. That reframing changes everything about what you need to know.

So before the fancy techniques, get the core object right: a call to a large language model (LLM) is, at heart, an HTTP request that takes text in and streams text out. Everything else is detail — but the details are where the engineering lives.

Tokens: the unit of everything

Models don’t see characters or words; they see tokens — subword chunks. The word “unbelievable” might be three tokens (un, believ, able). Roughly, one token ≈ 4 characters ≈ ¾ of a word in English.

Tokens matter because they are the unit of three things you constantly manage:

  • Cost — you pay per input token and (usually more) per output token.
  • Latency — output is generated one token at a time, so longer answers take longer, largely independent of how hard the question is.
  • Limits — every model has a maximum context window (input + output tokens it can consider at once). Exceed it and the request fails.

The context window is your working memory

The context window is the single most important constraint in applied AI. The model has no memory between calls — it knows only what you put in the prompt this time. A “conversation” is an illusion you maintain by resending the relevant history on every request.

This is why so much AI engineering is really context engineering: deciding what to put in the window, in what order, within a fixed token budget. Retrieval (next lessons), summarization of old turns, and prompt templates are all tactics for spending that budget well.

Anatomy of a request

A typical chat request has three ingredients:

  1. System prompt — durable instructions and persona (“You are a support agent. Answer only from the provided policy.”).
  2. Messages — the conversation so far, as alternating user/assistant turns.
  3. Parameters — knobs like temperature (higher = more random/creative, lower = more deterministic) and max_tokens (a cap on the output length).
resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": "Answer only from the context. If unsure, say so."},
        {"role": "user", "content": question},
    ],
    temperature=0.2,
)
What actually happens on one LLM call
  1. 1
    Your appsystem + messages + params
  2. 2
    Tokenizetext → subword token IDs
  3. 3
    Modelpredict next-token distribution
  4. 4
    Sampletemperature / top-p pick a token
  5. 5
    Detokenizetokens → streamed text
Stateless: nothing here is remembered between calls — you resend the context every time.

Determinism is not guaranteed

The same prompt can produce different answers. That non-determinism is a feature for creative work and a hazard for anything you need to test. Lowering temperature reduces variation but never fully eliminates it, which is exactly why evaluation (a later lesson) becomes a first-class engineering concern rather than an afterthought.

The mental model to carry forward

An LLM is a stateless, probabilistic, token-limited function from text to text. Your job as an AI engineer is to build deterministic, reliable systems around that unreliable core — supplying it the right context, constraining its outputs, and measuring its quality.

Hold that sentence in mind. Embeddings, retrieval, serving, and evaluation are all just ways of managing one of those four properties.

Pattern recognition

Variations

Worked problems

DesignEasy
  • System thinking
  • LLM call

Map a feature to responsibilities

A PM says: “Add an AI assistant that answers questions about a user’s invoices and can explain charges.” Name the engineering responsibilities hidden inside that sentence.

Approach. Decompose the feature into context, model call, product boundary, quality measurement, and failure handling. Do not start with a prompt.

Show solution

A solid design checklist:

  1. Identity and authorization — the assistant can only access invoices the current user may see.
  2. Data access — fetch invoice rows, line items, payments, and plan metadata.
  3. Context assembly — include only the relevant invoices and definitions.
  4. Prompt contract — answer from provided data and say when data is missing.
  5. Output format — return answer plus invoice IDs used as citations.
  6. Evals — test charge explanations, refunds, taxes, and missing data cases.
  7. Guardrails — no legal/tax advice beyond the invoice facts.
  8. Observability — log model, prompt version, token counts, citations, and validation failures.
def invoice_context(user_id, invoice_id, db):
    invoice = db.get_invoice_for_user(user_id=user_id, invoice_id=invoice_id)
    if invoice is None:
        raise PermissionError("invoice not found for user")
    return {
        "invoice_id": invoice.id,
        "line_items": invoice.line_items,
        "currency": invoice.currency,
    }

Complexity. O(1) to name the responsibilities; data fetch complexity depends on the billing schema. The key is that the model is only one box in the feature.

DrillMedium
  • Messages
  • State

Sketch the request envelope

Design the message envelope for a stateless chat assistant that must answer from a user profile, recent conversation summary, and retrieved docs. Keep it small and explicit.

Approach. Put durable behavior in the system message. Put runtime state and retrieved context in delimited user content. Do not rely on the model remembering previous calls.

Show solution
def build_messages(profile, conversation_summary, docs, question):
    context = "\n\n".join(
        f"<doc id='{doc['id']}'>\n{doc['text']}\n</doc>" for doc in docs
    )
    return [
        {
            "role": "system",
            "content": (
                "You are a product assistant. Answer only from supplied profile, "
                "conversation summary, and docs. Cite doc IDs when using docs."
            ),
        },
        {
            "role": "user",
            "content": (
                f"<profile>\n{profile}\n</profile>\n\n"
                f"<summary>\n{conversation_summary}\n</summary>\n\n"
                f"<docs>\n{context}\n</docs>\n\n"
                f"Question: {question}"
            ),
        },
    ]

This envelope makes state explicit. If the next request needs memory, the caller must send a new summary or retrieve previous facts again.

Complexity. O(d + h) in the size of retrieved docs and history summary. The budgeting decision is what to omit when the window fills.

DrillMedium
  • Failure mode
  • Context

Spot the statelessness bug

A chat app sends only the latest user message to the model because “the provider has the conversation ID.” Users complain that the assistant forgets earlier constraints. Diagnose the bug and fix the request flow.

Approach. Treat the model call as stateless unless the API explicitly manages state for you. The application should own what context is resent.

Show solution

The bug is assuming server-side memory that the request does not actually provide. A robust flow stores conversation turns, summarizes old ones, and sends the relevant state every time.

def next_chat_messages(store, conversation_id, new_user_message):
    history = store.recent_turns(conversation_id, limit=8)
    summary = store.summary(conversation_id)
    messages = [{"role": "system", "content": "Be concise and follow user constraints."}]
    if summary:
        messages.append({"role": "user", "content": f"Conversation summary:\n{summary}"})
    messages.extend(history)
    messages.append({"role": "user", "content": new_user_message})
    return messages

If history exceeds the token budget, summarize or retrieve relevant turns instead of dropping constraints silently.

Complexity. O(t) in the number of turns included. The product invariant is simple: any fact the model must use must be present in this request.

The pitfall to avoid

Your progressNot started