Lesson 2 - LangChain Basics
Welcome to LangChain Basics
In the last lesson you saw what LangChain offers from a distance and ran one small chain to prove it works. Now you’ll slow down and meet the pieces one at a time. Every LangChain application — including the RAG pipelines and agents in the lessons ahead — is built from the same four primitives: a chat model, messages, a prompt template, and an output parser. Once you can hold each one in your hand and see exactly what it takes in and gives back, composing them with the LCEL pipe becomes obvious rather than magic.
This lesson is hands-on. Every code block below was run against Claude, and the output you see is the real reply.
By the end of this lesson, you will be able to:
- Call the
ChatAnthropicchat model with.invoke()and read theAIMessageit returns - Build conversations from
SystemMessageandHumanMessageobjects - Write reusable prompts with
ChatPromptTemplateand{variables} - Turn a model reply into a clean string with
StrOutputParserand compose everything into an LCEL chain
We’re on LangChain 1.x with the claude-haiku-4-5 model. Let’s begin.
The Chat Model and the AIMessage
The chat model is the engine. In LangChain, the wrapper for Claude is ChatAnthropic. You create one, then call .invoke() with your input. The model reads your Anthropic API key from the ANTHROPIC_API_KEY environment variable automatically — you never pass it in code.
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(model="claude-haiku-4-5", max_tokens=100)
resp = model.invoke("In one short sentence, what is LangChain?")
print(type(resp).__name__)
print(resp.content)AIMessage
LangChain is a framework for building applications with large language models by
chaining together various components like prompts, models, and memory.Two things to notice. First, .invoke() is the call you’ll use on every LangChain component — the model, a prompt, a parser, a whole chain. Learn it once and it applies everywhere. Second, you don’t get back a bare string. You get an AIMessage object, and the text lives on its .content attribute. That object carries more than the text: it also records which model answered and how many tokens the call used.
print(resp.response_metadata["model"])
print(resp.usage_metadata)claude-haiku-4-5-20251001
{'input_tokens': 19, 'output_tokens': 32, 'total_tokens': 51, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}}That metadata is why the reply is an object and not a plain string — it’s the same usage and model information the raw SDK returns, just attached to a tidy message type.
A plain string is fine for a one-off question, but real conversations have roles: a system instruction that sets behavior, and a human turn with the actual request. LangChain models these as message objects. Pass a list of them to .invoke():
from langchain_core.messages import HumanMessage, SystemMessage
resp = model.invoke([
SystemMessage("You are a terse assistant. Answer in one sentence."),
HumanMessage("What is a vector database?"),
])
print(resp.content)A vector database stores and retrieves high-dimensional numerical vectors
(embeddings) that represent semantic meaning, enabling fast similarity searches
useful for AI applications like recommendation systems and semantic search.The SystemMessage shapes the assistant’s behavior — here, “be terse, one sentence” — and the HumanMessage is the user’s turn. You can stack as many turns as you like; this list of messages is the standard shape of a conversation throughout LangChain.
Prompt Templates: Reusable Prompts with Variables
Hard-coding the question is fine once, but you usually want the same prompt with different inputs. That’s what a ChatPromptTemplate is for: a prompt with {variable} placeholders that you fill in at call time. It produces the message list for you, so you write the structure once and reuse it.
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "You are a terse assistant. Answer in one sentence."),
("human", "Explain {topic} to a beginner."),
])
filled = prompt.invoke({"topic": "tokenization"})
for m in filled.messages:
print(type(m).__name__, "->", m.content)SystemMessage -> You are a terse assistant. Answer in one sentence.
HumanMessage -> Explain tokenization to a beginner.Notice that the template is just another component with an .invoke() method. You give it a dictionary, {"topic": "tokenization"}, and it hands back the exact SystemMessage/HumanMessage list you built by hand a moment ago — the {topic} slot filled in. Each tuple is a (role, text) pair, where the role is "system" or "human", and any {name} in the text becomes a variable you supply later.
For a single-message prompt you can skip the list entirely with from_template:
quick = ChatPromptTemplate.from_template("Give one practical use of {tool}.")
print(quick.invoke({"tool": "an embedding model"}).messages[0].content)Give one practical use of an embedding model.The point of the template is reuse: define the wording once, then feed it any topic. That’s what makes prompts maintainable instead of scattered string formatting.
Output Parsers and the LCEL Chain
So far the model hands back an AIMessage, and you reach into .content to get the text. An output parser does that extraction for you. The simplest one, StrOutputParser, takes an AIMessage and returns just its text as a clean string — no .content access needed.
The real payoff comes when you connect the three pieces with the LCEL | (pipe) operator. Each component feeds its output to the next, left to right: the template fills the prompt, the prompt goes to the model, and the model’s reply flows into the parser.
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
model = ChatAnthropic(model="claude-haiku-4-5", max_tokens=100)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a terse assistant. Answer in one sentence."),
("human", "Explain {topic} to a beginner."),
])
chain = prompt | model | StrOutputParser()
out = chain.invoke({"topic": "tokenization"})
print(isinstance(out, str))
print(out)True
Tokenization is breaking text into smaller pieces (called tokens) like words or
subwords so computers can process and understand language.The whole chain behaves like one component: you call .invoke() on it with the template’s variables, and you get a plain string back — no AIMessage, no .content. Read the pipe literally: {"topic": "tokenization"} fills the prompt, the filled prompt calls the model, and StrOutputParser() turns the AIMessage into text. Swapping any link — a different prompt, a JSON parser, another model — is a one-line change, because every link speaks the same .invoke() interface.
Because a chain is just another .invoke()-able component, it also supports .batch() to run many inputs at once:
results = chain.batch([{"topic": "an API"}, {"topic": "a database index"}])
for r in results:
print("-", r)- An API is a set of rules that lets different programs talk to each other and share information, like how a restaurant menu lets you order food without needing to know how the kitchen works.
- A database index is like a book's index—it lets you quickly find data by a specific column instead of searching through every single row.One interface is what makes them compose
The model, the prompt template, the parser, and the full chain all expose the same .invoke() method (and .batch(), and .stream()). That shared interface is the whole trick behind the | pipe: because each piece accepts and returns predictably, you can snap them together in any order. When you build RAG and agents later, you’re snapping together these same kinds of components.
Practice Exercises
Exercise 1: Read the AIMessage
After resp = model.invoke("Name one LangChain component."), what type is resp, where is the text, and how would you find out how many output tokens the call used?
Hint
resp is an AIMessage. The text is on resp.content. The token counts are on resp.usage_metadata — read resp.usage_metadata["output_tokens"].
Exercise 2: Write a template
Write a ChatPromptTemplate.from_messages with a system role telling the model to answer like a tutor, and a human role that takes two variables: {level} and {concept}. What dictionary would you pass to .invoke() to fill it?
Hint
Use ("human", "Explain {concept} to a {level} learner."). Fill it with a dict that has a key for every variable: {"concept": "embeddings", "level": "beginner"}. Missing a key raises an error.
Exercise 3: Build the chain
Compose your template from Exercise 2 with the model and StrOutputParser() into a chain, then call it. What type comes out, and which line would you change to get the raw AIMessage instead of a string?
Hint
chain = prompt | model | StrOutputParser(), then chain.invoke({"concept": ..., "level": ...}) returns a plain string. Drop | StrOutputParser() (use prompt | model) and the chain returns the AIMessage instead.
Summary
LangChain gives you four core building blocks and one way to connect them. The chat model (ChatAnthropic) is called with .invoke() and returns an AIMessage whose text is on .content and whose usage_metadata records token counts. Conversations are built from SystemMessage and HumanMessage objects. A ChatPromptTemplate holds a reusable prompt with {variables} you fill at call time, and a StrOutputParser turns the model’s reply into a clean string. The LCEL | pipe snaps these into a chain that you call with one .invoke() — and because every component shares that interface, swapping any piece is a one-line change.
Key Concepts
ChatAnthropic— the chat model wrapper for Claude; reads the API key from the environment.AIMessage— the model’s reply object; text on.content, token counts on.usage_metadata.- Message types —
SystemMessagesets behavior,HumanMessageis the user’s turn. ChatPromptTemplate— a reusable prompt with{variable}placeholders.StrOutputParser— extracts the plain string from anAIMessage.- LCEL pipe (
|) — composes components into a chain because they all share.invoke().
Why This Matters
These four primitives are not just an intro exercise — they are the literal parts every LangChain program is assembled from. A RAG pipeline is a prompt template, a retriever, a model, and a parser piped together; an agent is built from these same message and model components plus tools. By understanding exactly what each piece takes in and hands back, you’ll read and debug real LangChain code with confidence instead of guessing at what the framework is doing for you.
Next Steps
Continue to Lesson 3 - RAG with LangChain
Use these building blocks to load documents, retrieve relevant context, and answer questions with a real RAG chain on Claude.
Back to Module Overview
Return to the LangChain & LangGraph module overview
Continue Building Your Skills
You can now call a chat model, read its AIMessage, write reusable prompt templates, parse outputs to clean strings, and pipe it all into a chain. These are the exact parts the next lesson assembles into a retrieval-augmented chain — so you’ll recognize every piece as you go.