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 ChatAnthropic chat model with .invoke() and read the AIMessage it returns
  • Build conversations from SystemMessage and HumanMessage objects
  • Write reusable prompts with ChatPromptTemplate and {variables}
  • Turn a model reply into a clean string with StrOutputParser and 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 typesSystemMessage sets behavior, HumanMessage is the user’s turn.
  • ChatPromptTemplate — a reusable prompt with {variable} placeholders.
  • StrOutputParser — extracts the plain string from an AIMessage.
  • 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.