We use cookies to enhance your experience on the site
CodeWorlds

LangChain - the chain that ties it all together

You can already call a model and write a decent prompt. But a real application needs more than that: load a template, send it to the model, parse the answer, remember the conversation. LangChain is a framework for building applications with LLMs - it ties those steps into one flow, and it does it with a single, surprisingly simple idea you will meet in a moment. It is not a database, not a programming language and certainly not a cryptocurrency - just a Python library that arranges the pieces you already know.

1pip install langchain langchain-openai langchain-anthropic

Three packages: the core library plus one connector per provider. You only install the connectors for the models you actually intend to call - here OpenAI and Anthropic, so you can compare them side by side on the same Safari questions.

Building block: the model

Let us start with the smallest building block - the model itself. In LangChain every provider exposes the same

invoke
method, so switching models means changing one line: swap
ChatOpenAI
for
ChatAnthropic
and everything downstream stays exactly as it was.

1from langchain_openai import ChatOpenAI
2from langchain_core.messages import HumanMessage, SystemMessage
3
4llm = ChatOpenAI(model="gpt-5", temperature=0.7)
5
6messages = [
7    SystemMessage(content="You are a Safari expert."),
8    HumanMessage(content="Tell me about elephants.")
9]
10print(llm.invoke(messages).content)

Read the import line carefully -

from langchain_openai import ChatOpenAI
. The package name always follows
from
, the class you want follows
import
, and that order never changes. Then look at
invoke
: it is the shared "run this" of every LangChain building block. In a moment you will see that a single model and an entire composed chain are started in exactly the same way. That uniformity is the foundation the whole library rests on.

Building block: the prompt template

The second building block is a template with gaps - your prompt from the previous lesson, but reusable. Instead of gluing strings together by hand, you leave

{variables}
in the text and let LangChain fill them in later.

1from langchain_core.prompts import ChatPromptTemplate
2
3prompt = ChatPromptTemplate.from_messages([
4    ("system", "You are an expert in {field}."),
5    ("human", "Tell me about {topic}.")
6])

The gaps

{field}
and
{topic}
are filled only when you run the chain, so one template can serve hundreds of questions.
ChatPromptTemplate
is simply the chat flavour of a PromptTemplate: it produces a list of system and human messages instead of one flat string. And because it is a LangChain building block like any other, it also has
invoke
- which means it can be joined to the model. That is exactly where the main idea of the library comes in.

The big idea: the | operator (pipe)

Here is the heart of LangChain, and it has a name: LCEL, the LangChain Expression Language. Despite the word "language" it is not a new programming language to learn, not a file format and not a network protocol - it is simply a way to chain LangChain components using the

|
operator. You read
prompt | llm | parser
as "fill the prompt, send it to the model, process the answer", exactly like a pipe: whatever flows out of one block flows straight into the next.

1from langchain_core.output_parsers import StrOutputParser
2
3chain = prompt | llm | StrOutputParser()
4
5result = chain.invoke({"field": "Safari", "topic": "lions"})
6print(result)

It is the same

invoke
you used on a lone model - but now it runs the whole chain at once. Data flows in as a dictionary of gap values, travels through the prompt template, the LLM and the output parser, and flows out as finished text.
StrOutputParser
at the end pulls the plain string out of the response so you never have to write
.content
again. Remember this shape - a PromptTemplate, an LLM and an OutputParser connected by the
|
operator - because it returns in every example that follows.

The same chain, a stronger parser: output as an object

Since the parser is just the last block in the pipe, swapping it is enough to get a ready-made object instead of raw text. You describe the structure you expect with a class, and LangChain makes sure the model respects that format.

1from langchain_core.pydantic_v1 import BaseModel, Field
2from langchain_core.output_parsers import JsonOutputParser
3
4class AnimalInfo(BaseModel):
5    name: str = Field(description="Animal name")
6    habitat: str = Field(description="Habitat")
7    population: int = Field(description="Population")
8
9parser = JsonOutputParser(pydantic_object=AnimalInfo)
10
11prompt = ChatPromptTemplate.from_messages([
12    ("human", "{format_instructions}\n\nAnimal: {animal}")
13])
14chain = prompt | llm | parser
15
16result = chain.invoke({
17    "animal": "African elephant",
18    "format_instructions": parser.get_format_instructions()
19})
20print(result)  # {'name': 'African elephant', 'habitat': ..., 'population': ...}

Still the very same

prompt | llm | parser
chain - only the final block changed. The
AnimalInfo
class describes the fields you expect, and adding one more line such as
diet: str
is enough for the model to start returning that too. Meanwhile
get_format_instructions
injects an automatic "return JSON in this shape" instruction into the prompt. The result arrives as a plain Python dictionary, ready to use in code immediately - no manual parsing, no hunting for braces inside a wall of prose.

Adding memory to the chain

By default the model remembers nothing between calls - every

invoke
starts from zero. To make it hold a real conversation, you wrap the chain in one more block: the one that records the history.

1from langchain_community.chat_message_histories import ChatMessageHistory
2from langchain_core.runnables.history import RunnableWithMessageHistory
3
4history = ChatMessageHistory()
5
6chain_with_history = RunnableWithMessageHistory(
7    chain,
8    lambda session_id: history,
9    input_messages_key="input",
10    history_messages_key="history"
11)
12
13chain_with_history.invoke(
14    {"input": "I am on a Safari in Kenya"},
15    config={"configurable": {"session_id": "abc123"}}
16)
17response2 = chain_with_history.invoke(
18    {"input": "What animals can I see here?"},
19    config={"configurable": {"session_id": "abc123"}}
20)

RunnableWithMessageHistory
wraps an existing chain and prepends the earlier messages on every turn. That is why the model knows that "here" means Kenya in the second question, even though you never repeated the country. The
session_id
keeps separate conversations apart, so two travellers sharing the same server never end up reading each other's history.

Remember one character from this lesson:

|
. LangChain is a framework for building applications with LLMs, and it works in blocks - a PromptTemplate, an LLM, an OutputParser, a memory wrapper - glued into a pipe by that one operator and launched by that one
invoke
. Once you see the pattern, every new feature is just another block to snap onto the chain.

Go to CodeWorlds