Skip to main content
LLM Engineering beginner Lesson 2 of 12

Introduction to LLM Engineering

Understand how large language models work, learn the OpenAI API, and write your first production LLM application.

What Is an LLM?

A large language model (LLM) is a neural network trained on massive text corpora to predict the next token given a sequence of tokens. Through scale and instruction tuning, these models develop the ability to follow instructions, reason, write code, summarize documents, and hold conversations.

The key models in production today:

  • Claude (Anthropic) — claude-opus-4-8, claude-sonnet-4-6, claude-haiku-4-5
  • GPT-4o (OpenAI) — strong general capability, multimodal
  • Gemini (Google) — very large context windows
  • Llama 3 (Meta) — open weights, self-hostable

How LLM APIs Work

Every LLM API follows the same core pattern: send a list of messages, receive a generated response.

Request:
  messages: [
    { role: "system",    content: "You are a helpful assistant." },
    { role: "user",      content: "Explain how a hash map works." },
  ]

Response:
  { role: "assistant",   content: "A hash map stores key-value pairs..." }

The system message sets the model’s persona and constraints. The user/assistant exchange is the conversation history.

Setting Up the Anthropic SDK

pip install anthropic
export ANTHROPIC_API_KEY="sk-ant-..."
import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY from environment

message = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "What is the capital of France?"}
    ]
)

print(message.content[0].text)   # "The capital of France is Paris."
print(f"Input tokens:  {message.usage.input_tokens}")
print(f"Output tokens: {message.usage.output_tokens}")

System Prompts and Temperature

import anthropic

client = anthropic.Anthropic()

# System prompt sets persona and constraints
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=2048,
    system="""You are a senior Python engineer at a fintech company.
When reviewing code:
- Be concise and direct
- Focus on security, correctness, and performance
- Suggest specific improvements with code examples
- Flag potential race conditions or data loss scenarios""",
    messages=[
        {"role": "user", "content": """Review this function:

def transfer_funds(from_account, to_account, amount):
    balance = db.get_balance(from_account)
    if balance >= amount:
        db.update_balance(from_account, balance - amount)
        db.update_balance(to_account, db.get_balance(to_account) + amount)
    return True
"""}
    ],
)
print(response.content[0].text)

Multi-Turn Conversations

import anthropic
from typing import List

client = anthropic.Anthropic()

def chat(messages: List[dict], user_input: str) -> tuple[str, List[dict]]:
    """Send a message, return the response and updated history."""
    messages.append({"role": "user", "content": user_input})

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system="You are a helpful Python tutor. Keep explanations concise with code examples.",
        messages=messages,
    )

    assistant_reply = response.content[0].text
    messages.append({"role": "assistant", "content": assistant_reply})
    return assistant_reply, messages


# Simulate a conversation
history = []
questions = [
    "What is a decorator in Python?",
    "Show me a real-world example using what you just explained.",
    "How would I unit test the decorator from your example?",
]

for question in questions:
    print(f"\nUser: {question}")
    reply, history = chat(history, question)
    print(f"Assistant: {reply[:200]}...")  # truncate for display

Streaming Responses

import anthropic

client = anthropic.Anthropic()

# Stream: print tokens as they arrive instead of waiting for the full response
with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "Explain gradient descent in 3 paragraphs."}
    ],
) as stream:
    for text_chunk in stream.text_stream:
        print(text_chunk, end="", flush=True)  # print without newline, flush buffer

final_message = stream.get_final_message()
print(f"\n\nTotal tokens: {final_message.usage.input_tokens + final_message.usage.output_tokens}")

Structured Outputs

import anthropic
import json

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    system="""Extract information from the user's text and return ONLY valid JSON 
matching this schema:
{
  "company": string,
  "revenue_usd_millions": number | null,
  "employee_count": number | null,
  "founded_year": number | null,
  "headquarters": string | null
}""",
    messages=[{
        "role": "user",
        "content": """Stripe was founded in 2010 by Patrick and John Collison. 
The company is headquartered in San Francisco and had revenue of approximately 
$14 billion in 2023. They employ around 8,000 people worldwide."""
    }]
)

data = json.loads(response.content[0].text)
print(json.dumps(data, indent=2))
# {
#   "company": "Stripe",
#   "revenue_usd_millions": 14000,
#   "employee_count": 8000,
#   "founded_year": 2010,
#   "headquarters": "San Francisco"
# }

Token Counting and Cost Estimation

import anthropic

client = anthropic.Anthropic()

messages = [{"role": "user", "content": "Summarize the history of machine learning."}]

# Count tokens before sending (no API cost for counting)
token_count = client.messages.count_tokens(
    model="claude-sonnet-4-6",
    messages=messages,
)
print(f"Input tokens: {token_count.input_tokens}")

# Claude Sonnet 4.6 pricing (verify current prices at anthropic.com)
INPUT_COST_PER_MILLION  = 3.00   # $3 per 1M input tokens
OUTPUT_COST_PER_MILLION = 15.00  # $15 per 1M output tokens

estimated_input_cost = token_count.input_tokens * INPUT_COST_PER_MILLION / 1_000_000
print(f"Estimated input cost: ${estimated_input_cost:.6f}")

Common Patterns

import anthropic
from functools import lru_cache

client = anthropic.Anthropic()

def llm_call(
    prompt: str,
    system: str = "You are a helpful assistant.",
    model: str = "claude-haiku-4-5-20251001",  # fast/cheap for simple tasks
    max_tokens: int = 512,
) -> str:
    """Minimal wrapper for one-shot LLM calls."""
    response = client.messages.create(
        model=model,
        max_tokens=max_tokens,
        system=system,
        messages=[{"role": "user", "content": prompt}],
    )
    return response.content[0].text


# Classification
def classify_sentiment(text: str) -> str:
    return llm_call(
        prompt=f"Classify the sentiment as POSITIVE, NEGATIVE, or NEUTRAL: {text}",
        system="Reply with only one word: POSITIVE, NEGATIVE, or NEUTRAL.",
    ).strip()


# Summarization
def summarize(text: str, max_words: int = 50) -> str:
    return llm_call(
        prompt=f"Summarize in under {max_words} words:\n\n{text}",
    )


# Entity extraction
def extract_emails(text: str) -> list[str]:
    import json
    result = llm_call(
        prompt=f"Extract all email addresses from this text as a JSON array: {text}",
        system="Return only a valid JSON array of strings, nothing else.",
    )
    return json.loads(result)


print(classify_sentiment("The product arrived on time and works perfectly!"))
print(extract_emails("Contact [email protected] or [email protected] for help."))

Frequently Asked Questions

What is the difference between completion and chat completion?
Completion takes a raw text prompt and generates a continuation. Chat completion takes a list of messages (system, user, assistant) and generates the next assistant response. All modern LLM APIs use chat completion — it gives you explicit control over the conversation structure and system instructions.
What are tokens and why do they matter?
Tokens are the units LLMs process — roughly 4 characters or 0.75 words in English. A 1,000-word document is ~1,333 tokens. Tokens matter because they determine cost (pricing is per token), latency (more tokens = slower), and context limits (GPT-4o supports 128k tokens). Always estimate token counts before designing a system.