Mastering Individual Calls to LLMs
Roles of AI in software, prompt engineering, structured outputs, and stateless vs stateful interactions
Slides: Roles of AI in Software · Mastering Individual Calls
The plan for today
- Roles of AI in software — Understand the roles of AI in software: think before you implement
- Making good individual calls — Understand how to make "good" individual calls to AI
- Stateless vs stateful interactions — Understand nuances of stateless vs stateful interactions and context management
Learning objectives
By the end of this lesson, you will be able to:
- Identify the three roles AI plays in software systems and decide when each applies
- Construct effective API calls with well-designed system messages, user messages, and parameters
- Apply prompting techniques: few-shot, chain-of-thought, structured output, and prompt chaining
- Understand the difference between stateless and stateful interactions and how to manage context
Three roles for AI
Before writing any code, ask: what role does AI play in this system? The answer shapes everything — architecture, testing, risk management.
01 — AI as a Component
AI is embedded inside your solution — a model call in the pipeline. You design around its strengths and limitations: prompts, guardrails, evaluation, fallback logic.
02 — AI as a Coding Agent
AI helps you write, review, and refactor code. It accelerates development but the output is traditional software — deterministic, testable, fully under your control.
03 — AI as a User
AI consumes and produces information through your system. It calls your APIs, reads your docs, and acts on your data. You need to design for this new kind of consumer.
When do you need AI as a component?
| Category | Characteristics | Examples |
|---|---|---|
| No AI Fully specifiable |
Behavior is specified, reviewed, and approved. Outputs are deterministic and testable. Adding AI adds complexity without value. | Billing logic, form validation, API routing, access control |
| AI as a Convenience Easier in English |
The behavior can be fully written down as rules, but a prompt is simpler than writing code. English becomes the implementation language. Outputs are known — you can still test correctness. | Data format conversion, template filling, rule-based classification |
| AI as a System Component Hard to specify |
Behavior is too complex or contextual for rules. Hard to write conventional tests — no expected output. The value comes precisely from what's hard to specify. | Summarization, intent detection, creative generation, open-ended Q&A |
Ask yourself: 1. What are you building — how "reviewable" and fixed you want your output? 2. Who are you building for? If the answer to #2 is "humans", think harder.
Four challenges of working with LLMs
- Getting a single call right — How do I craft a prompt that reliably produces the output I need?
- Managing a conversation — How do I give the model the right context across turns without blowing up cost?
- Long-term memory — How do I remember a user across sessions — their facts, preferences, history?
- Enterprise / community knowledge — How do I connect knowledge across users while respecting privacy?
The building blocks
Every API call has three parts: the system message, the user message, and the parameters.
Your first API call
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4.1-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is the capital of France?"},
],
temperature=0.0,
)
print(response.choices[0].message.content)
# >>> The capital of France is Paris.
System vs. User messages
Two distinct roles that frame every API call.
System Message
Purpose: Defines the model's persona, tone, constraints, and output format.
Convention: You resend it every call, but keep it the same across turns. The API is stateless.
Tip: Put guardrails and formatting rules here so they can't be overridden by user input.
User Message
Purpose: The actual task, question, or input data for this specific request.
Changes: Every turn. Each new request or follow-up goes here.
Tip: Include few-shot examples, context, and delimiters in the user message for best results.
System = stable rules. User = variable input. Separate concerns cleanly.
System message examples
Same question, same model, same temperature — completely different tone and depth:
| System Message | User Message | Output |
|---|---|---|
| "You are a formal financial analyst. Be precise and use technical terminology." | "Explain what an API is." | An Application Programming Interface (API) is a formally defined contract specifying how software components exchange data and invoke operations... |
| "You are a friendly teacher explaining concepts to a 10-year-old. Use analogies." | "Explain what an API is." | Think of an API like a waiter in a restaurant! You tell the waiter what you want, they go to the kitchen, and bring back exactly what you asked for... |
UI vs API
With both, you have defaults and controls. But they vary — and you must make choices intentionally. New features come out every day.
| Capability | Chat UI | API |
|---|---|---|
| Tools | Product-managed | Developer-defined |
| Memory | Product-managed | Developer-managed (if you want) |
| Structured outputs | Best-effort | Enforced via JSON Schema |
The creativity dial
Low Temperature (0.0 – 0.3)
Behavior: Picks the most likely token almost every time. Output is almost consistent and repeatable.
Best for: Classification, data extraction.
High Temperature (0.7 – 1.5)
Behavior: Flattens the probability distribution, giving less-likely tokens a real chance. Output varies across runs.
Best for: Creative writing, brainstorming, dialogue, exploration, generating diverse options.
Temperature in action
Prompt: "Write a one-sentence description of the ocean."
| temp=0.0 | temp=0.7 | temp=1.5 |
|---|---|---|
| The ocean is a vast body of saltwater that covers over 70% of the Earth's surface. | The ocean is an immense, ever-shifting expanse of saltwater teeming with life. | Beneath turquoise veils the ocean hums ancient lullabies to forgotten shores. |
| Deterministic, same every time | Similar but varied | Wildly different |
Start at 0.0 for precision tasks, 0.7 for creative tasks. Adjust from there.
Other useful API parameters
Six knobs that control how the model generates its response:
| Parameter | What it does |
|---|---|
| top_p (Nucleus) | Alternative to temperature. Samples from the smallest set of tokens whose cumulative probability exceeds p. |
| max_tokens | Hard cap on response length. Prevents runaway generation and controls cost. |
| stop sequences | Strings that halt generation when encountered. Useful for structured outputs. |
| frequency_penalty | Penalizes tokens proportional to how often they've appeared. Reduces repetition in long-form text. |
| presence_penalty | Flat penalty for any token that has appeared at all. Encourages the model to explore new topics. |
Default values work for most cases. Tune one parameter at a time and measure the impact.
Reusable patterns for modern LLMs
1. Ask the model — iterate — "fine tune" the prompt. 2. Don't trust people with "key meta-insight slides" — just try stuff.
The meta-insight
- Calibrate specificity — Find the Goldilocks zone between brittle over-specification and vague under-specification. Anthropic calls this "the right altitude."
- Models follow literally — Claude 4.x does exactly what you ask — no more, no less. If you want thoroughness, ask for it. If you want brevity, specify it.
- Cut redundancy, not structure — Remove hedging and over-explanation. Keep output format, constraints, audience, and verification criteria.
A possible prompt skeleton
| ROLE | OBJECTIVE | CONTEXT | CONSTRAINTS | OUTPUT FORMAT | VERIFY |
|---|---|---|---|---|---|
| Expert stance (optional) | One-sentence goal | Facts, input data | Audience, length, tone | Exact structure | Quality gate |
Claude prefers XML tags (<context>, <instructions>). OpenAI works with both XML and LABEL: sections.
Direct instruction & role prompting
Direct Instruction
The baseline. Clear, literal instructions for simple tasks.
Summarize for a VP:
- exactly 5 bullets
- include 1 risk + 1 recommendation
Text: [paste]
If output drifts, add an output clamp.
Role Prompting
Shapes tone, vocabulary, depth. Does NOT improve factual correctness.
Act as a senior SRE.
Return:
1) Findings (major)
2) Missing evidence
3) Follow-ups (top 5)
Gemini takes personas very seriously — test for conflicts.
Scope & structure
Scope Discipline
Set explicit boundaries. "Fix this function" without scope may trigger a full refactor.
Implement EXACTLY this and nothing else. If ambiguous, pick simplest valid interpretation.
Structured Formatting
Delimiters prevent the model from confusing instructions with data. Separate instructions from data. Put long data first, instructions last.
| Format | Example | Best for |
|---|---|---|
| XML tags | <context>...</context> | Claude, OpenAI |
| LABEL: sections | ROLE: / CONSTRAINTS: | Quick UIs, Gemini 3 |
| Markdown | ## Instructions | General readability |
| Triple quotes | """content""" | Data vs instructions |
Few-shot prompting
Show the model what you want by providing examples in the prompt itself.
The technique
- Zero-shot: No examples. Relies entirely on the model's training.
- One-shot: One input–output pair shows the pattern.
- Few-shot (3–5): Multiple examples lock in format, tone, and edge cases.
Best practices
- Diverse examples: Cover typical cases AND edge cases.
- Consistent format: Use identical delimiters (Input: / Output:) in every example.
- Order matters: Place the most similar example last, closest to the actual task.
Few-shot is the single most reliable way to control output format without fine-tuning.
Chain of Thought (CoT)
Force the model to show its work before giving a final answer.
Why it works
LLMs generate tokens sequentially. When you force intermediate reasoning steps, each subsequent token is conditioned on better context.
Key insight: The model "thinks" by writing. No writing = no thinking. CoT gives the model scratch space.
CoT variants
- Zero-shot CoT: Add "Let's think step by step" to trigger reasoning.
- Structured CoT: Prescribe steps: "First analyze X, then evaluate Y, finally conclude Z."
- Few-shot CoT: Provide examples that include reasoning traces, not just answers.
CoT is most impactful for math, logic, multi-step reasoning, and complex decision-making tasks.
Long-context placement
Where you place instructions relative to data significantly affects quality.
| Poor Placement (maybe) | Good Placement (maybe) |
|---|---|
|
|
Long data first → instructions last → query at the very end.
Schema extraction & reasoning depth
Schema extraction
The biggest practical difference between UI and API.
UI: Best-Effort
Extract into JSON with exactly these keys:
party_name, jurisdiction,
effective_date, termination_summary
If unknown, set null.
No extra keys.
API: Enforced
OpenAI: text.format with strict: true → guaranteed shape
Anthropic: Prefilling + tool-use schemas → enforced structure
If you're building a pipeline that parses model output, always use API-level enforcement.
Reasoning depth
Trade off latency/cost vs. correctness. Not all tasks need deep reasoning.
| OpenAI | Anthropic | ||
|---|---|---|---|
| Knob | reasoning.effort(none/low/med/high) |
thinking.budget_tokens(extended thinking) |
Model selection (Flash vs Pro) |
| Best for | Granular effort tuning | Complex multi-step; interleaved thinking | Binary model choice |
From prompts to production
Quality patterns
Answer → Review → Re-answer
A single-call self-critique: Draft your answer, critique it for errors and gaps, then revise. All three steps in one API call — no orchestration needed.
Especially powerful for factual tasks, math, and any output that can be objectively verified.
Evaluation-First
Define success before proposing solutions. The prompting equivalent of test-driven development.
Before proposing, do this:
- Define success metrics
- Define baseline + thresholds
- Propose 3 interventions
- Predict impact + validation plan
Reflection loops
Multi-turn self-improvement: ask the LLM to evaluate and iterate on its own output.
The loop
- Call 1: Generate initial answer.
- Call 2: "Here is your previous answer. Rate it 1–10 on [criteria]. What would you improve?"
- Call 3: "Apply those improvements and produce the final version."
Repeat calls 2–3 until quality plateaus or budget is reached.
When to use
- High-stakes output: Legal drafts, medical summaries, customer-facing content.
- Latency-tolerant: Batch processing, async workflows where extra calls are acceptable.
Set a max iteration count (2–3 rounds). Diminishing returns kick in fast.
Prompt chaining
The bridge between "prompting" and "building an AI system."
Why chain? Verify intermediate results. Different models/reasoning per step. Clear debugging surface. Prevent corner-cutting.
Error handling & retries
Production LLM calls fail. Plan for it.
Common failures
- Rate limits (429): Back off exponentially. Use jitter to avoid thundering herds.
- Timeouts: Set aggressive timeouts (30–60s). Retry with the same prompt; non-determinism means a retry may succeed.
- Malformed output: JSON didn't parse? Retry once. If it fails again, fall back to a simpler prompt or model.
Robustness patterns
- Retry with backoff: 1s → 2s → 4s with random jitter. Max 3 retries for most use cases.
- Model fallback: If GPT-4o fails, fall back to GPT-4o-mini. Graceful degradation beats total failure.
- Validation layer: Always validate output before using it. Schema checks, length checks, safety checks.
Managing conversation state
The API is stateless: every call is independent. The model has no memory of previous interactions. To have a conversation, you manage the state.
Stateless agent
Each call is self-contained. Send a system message + user message, get a response. No history, no context. Simple, cheap, predictable.
# Every call starts fresh
response = client.chat.completions.create(
model="gpt-4.1-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is the capital of France?"},
],
)
Stateful agent
You maintain the conversation history and resend it with each call. The model sees the full context and can reference earlier turns.
# You manage the conversation history
history = [
{"role": "system", "content": "You are a helpful assistant."},
]
def chat(user_message):
history.append({"role": "user", "content": user_message})
response = client.chat.completions.create(
model="gpt-4.1-mini",
messages=history,
)
assistant_msg = response.choices[0].message.content
history.append({"role": "assistant", "content": assistant_msg})
return assistant_msg
More context = better responses, but also more tokens = more cost. You must decide what context is worth keeping and what to trim.
Context management strategies
- Sliding window: Keep only the last N messages. Simple but loses early context.
- Summarization: Periodically summarize the conversation and replace older messages with the summary.
- Selective injection: Only include messages relevant to the current query (requires retrieval logic).
Start simple. Escalate on failure. Measure everything.
Patterns persist across model updates; knobs don't.
Lab notebooks
lesson2_singlecall.ipynb— Single-call exercises: system messages, temperature, prompting techniqueslesson2_stateful.ipynb— Stateful agent: conversation history, context managementlesson2_exercises.ipynb— Practice exerciseslesson2_testing.ipynb— Testing and metrics for AI calls
Python scripts
1_stateless_agent.py— Basic stateless agent implementation2_stateful_agent.py— Stateful agent with conversation history3_agent_with_memory.py— Agent with persistent memory4_agent_with_long_term_memory.py— Agent with long-term memory storage