AI for Builders

A crash course in the concepts, patterns, and practical knowledge you need to build an AI-powered MVP.

Derrick Bozkurt
Speaker & Mentor

What Actually Happens When You Use AI

Built on the Transformer architecture (2017).
Every AI tool you've used runs this pipeline:

1. Tokenize

Text → tokens → vectors

→

2. Attention

Each token weighs all others

→

3. Compute

Next token probabilities

→

4. Sample

Pick one token, append

→

5. Repeat

Loop until done

The Reality

No stored facts — only learned patterns

Context window = temporary memory only

Same input can give different outputs

It simulates language, not retrieves truth.

For Builders

Plausible ≠ correct (hallucination)

Small prompt changes → big output shifts

You shape behavior through structure

You're programming behavior, not querying knowledge.

LLMs are stateless — memory must be built around them.

The Context Window

Think of it as a desk.
You can only have so many papers on it at once.
The model can only see what's on the desk.

200K
Claude
128K
GPT-4o
1M+
Gemini
~750
Words / 1K tokens

The Unifying Insight

Every architecture pattern we'll learn today — RAG, memory, tool use — exists to manage what goes on this desk. That's the whole game.

Tokenization: The Currency of AI

How Text Becomes Tokens

// A token is NOT a word. It can be:
// - a whole word    - part of a word
// - punctuation     - a space + word

// "Tokenization is surprisingly tricky!"
["Token", "ization", " is",
 " surprising", "ly",
 " tricky", "!"]  // 7 tokens

// Common suffixes (-ing, -tion, -ly) = 1 token
// Rare words get split into pieces:
"ChatGPT"  → ["Chat", "G", "PT"]  // 3 tokens
"🏠"       → ["🏠"]             // 1 token
"$450,000" → ["$", "450", ",", "000"] // 4 tokens

Why You Should Care

  • Cost — you pay per token, both input and output
  • Limits — context window is measured in tokens
  • Speed — more tokens = slower response
  • Multilingual — non-English text uses more tokens per word

The Cost Reality

100 API calls with Claude Sonnet costs ~$0.30. Your entire hackathon's API usage will probably cost less than lunch. Don't let cost anxiety slow you down.

The API Call: It's Just HTTP

Strip away every framework and abstraction.
This is all it is.

import anthropic                              # pip install anthropic

client = anthropic.Anthropic()                 # uses ANTHROPIC_API_KEY env var

response = client.messages.create(             # send request → get response
    model='claude-sonnet-4-20250514',
    max_tokens=1024,
    messages=[
        {'role': 'user', 'content': 'What is machine learning?'}
    ]
)

print(response.content[0].text)               # extract text from response

Five lines of real code. Everything else — LangChain, LlamaIndex, frameworks — is a wrapper around this.

Prompt Engineering

Give the model distinct sections.
Each one constrains its output differently.

The Prompt — one effective pattern

System message
# Your code builds this template once
# {vars} are injected at runtime — dates, user data, docs

ROLE: You are a senior backend engineer
who writes clean, production-ready Python.

CONTEXT: Stack: FastAPI + SQLAlchemy, PostgreSQL 15.
Today's date: {date}
User timezone: {timezone}
Order model: id, user_id, total, status, created_at

CONSTRAINTS: Max 50 results, support pagination,
sort by created_at DESC, return empty list if none,
use typed Pydantic response models.

FORMAT: Respond with only Python code. No explanation.
both sent together on each API call
User message
# This changes every turn — the actual request

I need an endpoint that returns a user's
most recent orders. Something like
GET /users/{user_id}/orders

Why Each Section Matters

Role

Sets expertise level and voice. "Senior backend engineer" produces different code than "helpful assistant."

Context

This can be your stack, your data, your user — whatever your code injects at runtime.

Task

One clear instruction. Ambiguity here is the #1 cause of bad outputs.

Constraints

Prevents over-engineering and scope creep. The model will add features you didn't ask for without these.

Format

Controls output shape. "Only code, no explanation" eliminates preamble you'd have to strip.

How the API Call Works

The left card shows how these sections map to the actual API call. Both are sent together on every request:

System message — the model's job description. The template stays the same, but your code injects variables at runtime — current date, user preferences, retrieved documents, etc. LLMs are stateless, so you re-send it on every call.

User message — the work ticket. This is the only part that changes per turn.

This split is a guideline. If context varies per request (e.g., different data), move it to the user message.

You can call an LLM and prompt it well.

But what about YOUR data?

The model only knows what's in the context window.
It hasn't read your docs, your database, or your product.
How do you give it that knowledge?

This is where embeddings, RAG, and memory come in.

Embeddings: Coordinates in Meaning Space

Turn text into numbers so "similar" things are nearby.
Like GPS coordinates, but for meaning.

dimension 1 dimension 2 "dog ran fast" "puppy sprinted" "the hound dashed" "stock market crashed" "economy in recession" "bake a chocolate cake" "recipe for brownies"

"dog ran fast"
↔ "puppy sprinted quickly"

0.92

cosine similarity

"bake a chocolate cake"
↔ "recipe for brownies"

0.89

cosine similarity

"dog ran fast"
↔ "stock market crashed"

0.11

cosine similarity

RAG: Retrieval-Augmented Generation

Don't train the model on your data.
Just retrieve the relevant parts and put them in the prompt.

1. Query

User asks a question

user input

→

2. Embed

Convert query to vector

model

→

3. Search

Find similar chunks

deterministic

→

4. Augment

Inject into prompt

deterministic

→

5. Generate

LLM answers with context

LLM

When RAG Works

  • Q&A over documents, knowledge bases, product data
  • Data changes frequently (no retraining needed)
  • You need the model to cite sources

When RAG Fails

  • Bad chunking retrieves the wrong passage
  • Answer sounds right but pulls from irrelevant context
  • RAG quality is an Information Retrieval (IR) problem, not an LLM problem

RAG quality is a search problem, not an AI problem. Bad answers? Fix your retrieval first.

Structured Output: Making AI Predictable

The bridge between "cool demo" and "reliable feature."

Free Text Response

The product has a rating of about 4.5 stars.
Most users liked it. Some complained about
the weight. Price is $299.
  • Output varies every time
  • No consistent structure to parse
  • Your code has to guess the format

JSON Structured Output

{
  "rating": 4.5,
  "pros": ["battery life", "display"],
  "cons": ["weight"],
  "price": 299,
  "recommendation": "buy"
}
  • Predictable shape every time
  • Parse with json.loads()
  • Feed directly into your UI or DB

Memory: Making AI Remember

LLMs are stateless. Every API call starts fresh.
If you want a conversation, YOU send the history.

1. Buffer 2. Summary 3. Vector Recall 4. Structured

Tool Use: Letting AI Do Things

The model requests actions. Your code executes them.
The model never runs code directly.

1. Define the Tool

1. Give each tool a name and description
2. Define what inputs it accepts (JSON schema)
3. The model reads this to understand what's available

2. Pass Tools to the API

1. Send your tools list alongside the user message
2. Model decides if a tool is needed
3. If yes: returns tool name + inputs
4. Your code executes the actual function

User

"Weather in Dallas?"

→

Model

Requests get_weather

→

Your Code

Runs the function

→

Model

Formats the response

The model requests. You execute. That's the safety boundary.

Model Selection: The Tradeoff Triangle

Quality

Best reasoning, most capable. Slower, costs more.

Claude OpusGPT-4o

Speed

Fast responses, good enough for most tasks. Great for real-time UX.

Claude SonnetGPT-4o mini

Cost

Cheapest per token. Good for high-volume, simpler tasks.

Claude HaikuGemini Flash

Use Different Models for Different Tasks

Your summarizer doesn't need the same model as your reasoner. A well-prompted smaller model often beats a poorly-prompted frontier model. Mix and match.

AI Product vs. AI Feature

This changes your architecture, UI, error handling,
and demo strategy.

AI IS the Product

The entire UX is the AI interaction. The AI is front and center.

  • AI chatbot / assistant
  • Content generation tool
  • AI tutor or coach

Focus: prompt quality, conversation design, response speed

AI is a Feature

A traditional app enhanced with AI capabilities. AI works behind the scenes.

  • Dashboard with AI-generated insights
  • App with smart search / recommendations
  • Workflow tool with AI automation

Focus: integration quality, graceful fallbacks, UX consistency

Decide which one you're building before writing code. It shapes every decision that follows.

AI System Design Patterns

One primary pattern per project.
Extend only when needed.

Pick one core pattern

Answer questions about data

RAG + Chat

Q&A over docs, knowledge bases

Have a conversation

Chat + Memory

Assistants, tutors, support bots

Generate content

Prompting + Structured Output

Copy, summaries, reports, code

+ Layer extensions as needed

Tool Use

Let the AI take actions

Multimodal

Analyze images / docs

Agents (advanced)

Multi-step automation

You know enough to start.

You don't need to understand everything — you need to understand enough to make good decisions and build something real.

Quick Recap

LLMs predict tokens. The context window is your constraint. Prompting is your highest-ROI skill. RAG gives the model your data. Tools let it take actions. Test with 15 golden examples. Start simple. Ship it working.

Slack QR Code

Join the Slack

Connect with your team, mentors, and the community.

promptengineersai.slack.com
Speaker Notes
1 / 16
Press P to open presenter mode

How Tokenization Works

Text is broken into small units (tokens) and converted into vectors the model can process.

1. Split text into tokens
"The cat sat" → ["The", "cat", "sat"]
2. Convert tokens → IDs → vectors
Each token maps to a number, then to a high-dimensional vector
3. Position is encoded
Order matters: "cat sat" ≠ "sat cat"

Text → numbers. Everything after this is vector operations.

How Attention Works

Each token computes a relevance score against every other token, assigns weights, and blends their information.

1. "cat" compares itself to "The", "sat", "on"   → similarity scores
2. Scores become weights: [0.1, 0.8, 0.1]   → "80% attention on 'sat'"
3. New representation = weighted blend of all tokens

What matters? This is how context flows between tokens.

How Compute Works

The model transforms token representations into next-token probabilities through stacked layers.

1. Apply transformer layers
Attention + feedforward layers repeatedly refine representations
2. Build contextual understanding
Each token now encodes meaning from the entire sequence
3. Output probabilities
Final layer produces likelihoods for every possible next token

Transform meaning. "Reasoning" emerges from repeated transformations, not logic rules.

How Sampling Works

The model selects the next token from a probability distribution.

1. Start with probabilities
["sat": 0.6, "ran": 0.3, "slept": 0.1]
2. Apply sampling strategy
Greedy → pick highest  |  Temperature → add randomness  |  Top-k → limit choices
3. Choose and append
One token is selected and added to the sequence

Choose next step. Same input can produce different outputs — behavior is controlled, not fixed.

The Generation Loop

The entire pipeline runs once per token. A 500-word response is ~700 iterations of this loop.

1. New token is appended to input sequence
2. Attention + Compute + Sample runs again
3. Stop when model produces an end-of-sequence token

This is why longer responses cost more (more tokens = more compute) and why streaming works token-by-token.

# define each tool as a dict — model reads these to
# know what it can request (e.g. weather_tool, search_tool)

weather_tool = {
  'name': 'get_weather',          # what the model calls it
  'description': 'Get current weather', # helps model decide when to use it
  'input_schema': {                # what inputs it accepts
    'type': 'object',
    'properties': {
      'city': {'type': 'string'}    # one required input: city name
    }
  }
}
# Step 1: send tools + user message to the model
response = client.messages.create(
    model='claude-sonnet-4-20250514',
    max_tokens=1024,
    tools=[weather_tool, search_tool],
    messages=[{'role': 'user',
      'content': 'Weather in Dallas?'}]
)

# Step 2: model responds with a tool request
# response contains: name='get_weather', input={'city': 'Dallas'}

# Step 3: YOUR code executes the actual function
result = get_weather_from_api('Dallas')  # you write this

# Step 4: send the result back to the model
# model formats it into a natural language response
history = [] # empty list — you manage state

# each turn: add user msg, send all, save response
history.append({'role': 'user', ...})
resp = client.messages.create(
    model='claude-sonnet-4-20250514',
    max_tokens=1024,
    messages=history # grows: [user, asst, user, asst, ...]
)
history.append({'role': 'assistant', ...})
# when history gets long, ask LLM to compress it
summary = client.messages.create(
    model='claude-sonnet-4-20250514',
    max_tokens=1024,
    system='Summarize this conversation',
    messages=old_messages
).content[0].text # returns summary string → cache it

# future calls use summary instead of full history
resp = client.messages.create(
    model='claude-sonnet-4-20250514',
    max_tokens=1024,
    system=summary, # compressed past
    messages=recent_messages # raw recent
)
# at ingest: embed each turn, store in vector db
for msg in history:
    vector = embed(msg['content'])
    vector_db.store(vector, msg)

# embed query, find top 5 matches → relevant
relevant = vector_db.query(
    embed(user_input), top_k=5
)
resp = client.messages.create(
    model='claude-sonnet-4-20250514',
    max_tokens=1024,
    messages=relevant + [{'role': 'user', ...}]
)
schema = {'preferences': [], 'goals': [], 'decisions': []}

# ask LLM to extract facts matching schema
facts = client.messages.create(
    model='claude-sonnet-4-20250514',
    max_tokens=1024,
    system=f'Extract facts as JSON: {schema}',
    messages=recent
).content[0].text
db.save(json.loads(facts)) # JSON string → dict → database

# later: pull specific facts back into prompt
prefs = db.query('preferences')
resp = client.messages.create(
    model='claude-sonnet-4-20250514',
    max_tokens=1024,
    system=f'User prefs: {prefs}',
    messages=[{'role': 'user', ...}]
)
import anthropic

client = anthropic.Anthropic()
response = client.messages.create(
    model='claude-sonnet-4-20250514',
    max_tokens=1024,
    messages=[{'role': 'user',
      'content': 'Review wireless earbuds'}]
)
print(response.content[0].text)
import anthropic, json, re

client = anthropic.Anthropic()
response = client.messages.create(
    model='claude-sonnet-4-20250514',
    max_tokens=1024,
    system='Respond ONLY with valid JSON.',
    messages=[{'role': 'user',
      'content': 'Review wireless earbuds'}]
)

raw = response.content[0].text
clean = re.sub(r'^[^{]*', '',
              re.sub(r'[^}]*$', '', raw))
data = json.loads(clean)
print(json.dumps(data, indent=2))

1. Query

The user asks a natural language question. This is the only human input in the pipeline.

"How do I reset my password?"

This raw text gets passed to the embedding model in the next step.

2. Embed

The query is converted into a vector — same embedding process as your pre-stored docs.

"How do I reset my password?"
→ [0.39, -0.71, 0.12, ...]

Your docs were embedded the same way at ingest time. Now both are in the same vector space.

4. Augment (Deterministic)

Take the top matching chunks and inject them into the prompt as context. This is literally string concatenation.

# build the prompt
context = top_matches[:3]
prompt = f'Given: {context}\n\nAnswer: {query}'

RAG is just: stuff relevant docs into the prompt. That's the whole trick.

5. Generate (LLM)

The LLM receives the augmented prompt and generates an answer grounded in the retrieved context.

resp = client.messages.create(
    model='claude-sonnet-4-20250514',
    max_tokens=1024,
    system=context,  # retrieved docs
    messages=[{'role': 'user', ...}]
)

This is the only probabilistic step. Everything before it was deterministic math.

1. Plan

The model receives available tools + conversation history and predicts the next step. Tools can be functions, APIs — or even other agent loops (sub-agents) that run their own cycle internally.

resp = client.messages.create(
    model='claude-sonnet-4-20250514',
    max_tokens=1024,
    tools=[search_tool, calc_tool],
    messages=history
)

# resp.content contains blocks:
# TextBlock → final answer (done)
# ToolUseBlock → tool request (continue)

At its core: next-token prediction over the tool menu + context. Advanced systems add evaluators, routers, and sub-agents — but this loop is always the foundation.

2. Act

Extract the tool request from the response. Your code executes it — the model never runs code, it only generates a structured request.

# extract tool call from response
tool_block = next(b for b in resp.content
                if b.type == 'tool_use')

# your code runs the actual function
result = run_tool(
    tool_block.name,  # e.g. 'search'
    tool_block.input  # e.g. {'query': 'revenue Q3'}
)

The safety boundary: the model requests, you execute. Same pattern as single tool use.

3. Observe

Append both the assistant's tool request and the tool result to history. The model sees this on the next call — it has no live awareness. In advanced systems, evaluators (separate LLM calls) can grade the result before continuing.

# 1. record what the model requested
history.append({'role': 'assistant',
    'content': resp.content})

# 2. record the tool result
history.append({'role': 'user',
    'content': [{'type': 'tool_result',
      'tool_use_id': tool_block.id,
      'content': result}]})

# optional: evaluator grades before looping
# grade = evaluate(result)

Both messages required: the assistant's request + the tool's response. Evaluators and sub-agents plug in here as additional LLM calls or nested loops.

4. Loop

The entire agent is a while loop. If the response contains a tool_use block, keep going. If the model returns only text, it's done.

while True:
    resp = client.messages.create(
        model='...', tools=tools, messages=history)

    tool_calls = [b for b in resp.content
                 if b.type == 'tool_use']

    if not tool_calls:  # text only → done
        break

    # act
    history.append({'role': 'assistant',
        'content': resp.content})
    for tool in tool_calls:
        result = run_tool(tool.name, tool.input)
        history.append({'role': 'user',
            'content': [{'type': 'tool_result',
              'tool_use_id': tool.id,
              'content': result}]}) # observe

That's the entire agent. A while loop, tool extraction, execution, and history updates. Everything else is a wrapper around this.

← → Space · F = fullscreen · P = presenter