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 alice@company.com or bob@example.org 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.