Inside AI Models
← All articles
LangChainAI Engineering

An Introduction to LangChain: Chains, RAG & Agents the v1 Way

Aug 24, 2026 · 9 min read

Share

If you've tried to build anything beyond a single prompt, you've hit the problem LangChain exists to solve: real LLM applications are plumbing. You call a model, but first you format a prompt from user input; you take the reply, but then you parse it, maybe feed it to a tool, maybe pull in documents to ground it, maybe remember what was said three turns ago. LangChain is the framework that gives all of that a common shape — and lets you swap the model underneath without rewriting the rest.

One warning before we start, because it will save you hours: LangChain changed a lot. Most tutorials ranking in search today still use the old LLMChain / create_react_agent patterns from the v0.1 era. Everything below targets the current v1 API (langchain-core 1.x, LangGraph 1.0). If a snippet you find imports LLMChain, it predates this article — the primitives here replace it.

The one idea: everything is a Runnable

Here's the mental model that makes LangChain click. Almost every building block — a prompt template, a chat model, an output parser, a retriever, a whole chain — implements the same interface, called a Runnable. Every Runnable has .invoke() (run it once), .stream() (stream the output), and .batch() (run many inputs). Because they all speak the same interface, you can connect them with a single operator: the pipe, |.

chain = prompt | model | parser

That line reads left to right: send the input through prompt, pass its output to model, pass that to parser. This is LCEL — the LangChain Expression Language — and it's the whole composition story in v1. Let's build one for real.

Setup and your first model call

Install the core package plus the provider integration you want. We'll use Claude:

pip install -U langchain langchain-anthropic
export ANTHROPIC_API_KEY="your-key"

The single most useful function to know is init_chat_model. It gives you one consistent way to spin up a model from any provider — you pass a "provider:model" string, and swapping providers later is a one-line change:

from langchain.chat_models import init_chat_model
 
model = init_chat_model("anthropic:claude-sonnet-5")
 
reply = model.invoke("Give me one sentence on what LangChain is.")
print(reply.content)

model.invoke() takes a string (or a list of messages) and returns an AIMessage; its text lives on .content. That's the atom. Now let's compose.

Your first chain

A chain is just Runnables piped together. The canonical starter is prompt → model → parser: a prompt template turns your variables into messages, the model answers, and a parser cleans the reply into a plain string.

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
 
prompt = ChatPromptTemplate.from_template(
    "Explain {topic} in one sentence, for a {level}."
)
 
chain = prompt | model | StrOutputParser()
 
print(chain.invoke({"topic": "backpropagation", "level": "beginner"}))

Notice what you didn't do: no glue code between steps, no manual message formatting, no digging .content out of the reply. The pipe handles the handoffs. And because the whole chain is itself a Runnable, chain.stream({...}) streams the answer token by token and chain.batch([...]) runs a list of inputs — for free, without changing the chain.

The reason the pipe works is that each step's output type matches the next step's input type. Tap through the stages below to see what flows where:

chain = prompt | model | parser
invoke({ })
str
dictpromptPromptValue

Fills your template variables and produces the messages to send.

Tap a step — see what type flows in and out. That matching is what the pipe (|) enforces.

That type-matching is the whole trick — and the most common source of confusion when a chain breaks, because a mismatch shows up as a cryptic error two steps later. When that happens, walk the pipe left to right and check what each stage actually returns.

Getting structured output

Plain strings are fine for prose, but often you want data — fields you can use in code. Instead of parsing the model's text yourself, describe the shape you want with a Pydantic model and let LangChain enforce it:

from pydantic import BaseModel, Field
 
class Movie(BaseModel):
    title: str = Field(description="The film's title")
    year: int = Field(description="Release year")
 
extractor = model.with_structured_output(Movie)
 
print(extractor.invoke("Blade Runner came out in 1982."))
# title='Blade Runner' year=1982

with_structured_output returns a new Runnable that hands you a typed Movie object instead of a message. Under the hood it uses the provider's native structured-output support, so you get validation, not vibes. This is the cleanest path from "the model said something" to "I have a value my program can use."

Grounding answers with retrieval (RAG)

Models don't know your data. Retrieval-Augmented Generation fixes that: before answering, you fetch the most relevant chunks of your own documents and hand them to the model as context. The moving parts are an embeddings model (turns text into vectors), a vector store (holds them), and a retriever (finds the closest matches).

One honest note up front: Anthropic doesn't offer an embeddings API, so pair Claude with an embeddings provider — OpenAI below, though Voyage or a local model work too. This mixing is a feature, not a workaround; it's exactly the provider-agnostic design LangChain is built for.

from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
 
store = InMemoryVectorStore.from_texts(
    [
        "LangChain composes LLM components with the pipe operator.",
        "In LCEL, prompt | model | parser is a single Runnable.",
        "LangGraph handles stateful, multi-step agent workflows.",
    ],
    embedding=OpenAIEmbeddings(),
)
retriever = store.as_retriever(search_kwargs={"k": 2})
 
prompt = ChatPromptTemplate.from_template(
    "Answer using only the context below.\n\n{context}\n\nQuestion: {question}"
)
 
def format_docs(docs):
    return "\n\n".join(d.page_content for d in docs)
 
rag = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)
 
print(rag.invoke("How do you compose components in LangChain?"))

Read the dict at the top as "build the prompt's inputs in parallel": question passes the user's query straight through, while context runs it through the retriever and formats the hits. Everything after the first | is the plain chain you already know. That's the payoff of the Runnable interface — retrieval slots into the same pipe as everything else.

Tools and agents

A chain runs a fixed path. An agent decides for itself — it can call tools (functions you expose), read the results, and loop until it's done. In v1, you turn a Python function into a tool with the @tool decorator (the docstring tells the model what it does), and assemble the agent with create_agent:

from langchain.tools import tool
from langchain.agents import create_agent
 
@tool
def word_count(text: str) -> int:
    """Return the number of words in the given text."""
    return len(text.split())
 
agent = create_agent(
    model="anthropic:claude-sonnet-5",
    tools=[word_count],
    system_prompt="You are precise. Use tools when they help.",
)
 
result = agent.invoke(
    {"messages": [{"role": "user", "content": "How many words are in 'the quick brown fox'?"}]}
)
print(result["messages"][-1].content)

The agent sees word_count, decides to call it, feeds "the quick brown fox", gets 4, and answers. You never wrote the "should I use a tool?" logic — that's the agent's job.

By default an agent forgets everything between calls. To give it memory across turns, add a checkpointer and invoke with a thread_id — the checkpointer persists the conversation for that thread:

from langgraph.checkpoint.memory import InMemorySaver
 
agent = create_agent(
    model="anthropic:claude-sonnet-5",
    tools=[word_count],
    checkpointer=InMemorySaver(),
)
 
config = {"configurable": {"thread_id": "user-1"}}
agent.invoke({"messages": [{"role": "user", "content": "My name is Alper."}]}, config)
followup = agent.invoke({"messages": [{"role": "user", "content": "What's my name?"}]}, config)
print(followup["messages"][-1].content)  # knows it's Alper

That InMemorySaver import is your first hint of the bigger picture: agents in v1 run on LangGraph. create_agent is a friendly prebuilt on top of LangGraph's state machine. The moment you need real branching, human-in-the-loop approval, or several agents coordinating, you graduate from create_agent to writing the graph yourself — but the concepts (state, tools, checkpointers) carry straight over.

This is also where retrieval is heading in v1: instead of the fixed RAG chain above, you increasingly give the agent a search tool and let it decide when to look things up. The chain is easier to reason about; the agent is more flexible. Both are worth knowing.

Seeing what happened: LangSmith

The first time an agent makes a surprising decision, print() stops being enough. LangSmith is LangChain's tracing tool: set two environment variables and every model call, tool call, and chain step is recorded as a visual trace you can inspect.

export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="your-key"

No code changes — the same chains and agents just start reporting. For anything you plan to run more than a few times, turn it on early; debugging LLM apps blind is the slowest way to work.

When to use LangChain — and when not to

LangChain earns its keep when your app has many moving parts or many providers: retrieval plus tools plus memory, or a codebase that needs to swap between Claude, GPT, and a local model without a rewrite. The common interface, the ready-made integrations, and LangSmith tracing genuinely save you time there.

It's the wrong tool when your app is one prompt and one response. If you're calling a single model and reading .content, the provider's own SDK is fewer layers, fewer abstractions, and easier to debug. Reaching for LangChain there adds indirection without buying you anything. A good rule: start with the raw SDK, and adopt LangChain the moment you find yourself hand-rolling the plumbing — chains, retrieval, tool loops — that it already standardizes.

Where to go next

You now have the whole spine: everything is a Runnable, you compose them with the pipe, and that one idea scales from a three-step chain to a RAG pipeline to a tool-using agent. From here, the highest-leverage next steps are LangGraph (when create_agent isn't enough and you need to design the control flow yourself) and LangSmith (so you can see what your chains actually do).

Build the smallest thing that's real — a chain that answers questions over five of your own documents — and grow it. That's the fastest way to make all of this stick.

views

Want to know when a new article drops?

Get an email whenever I publish something new. No spam, unsubscribe anytime.

Comments

Related articles