LangChain Tutorial (v1): Models, LCEL, Tools, Agents and RAG in Python
A hands-on LangChain v1 tutorial in Python: call chat models with init_chat_model, build LCEL chains, get structured output, create tools and agents with create_agent, add memory and build a small RAG pipeline.
Prerequisites
- •Python 3.10 or newer
- •An API key for a model provider such as OpenAI (or a local model via Ollama)
- •Basic Python, including functions, type hints and classes
What You'll Learn
- Call and stream any provider's chat model through init_chat_model
- Compose prompt, model and parser into an LCEL chain
- Return validated structured output with Pydantic
- Build a tool-calling agent with create_agent and give it memory
- Build a minimal retrieval-augmented generation pipeline with embeddings and a vector store
LangChain is the most widely used open source framework for building applications on top of large language models. Since version 1.0, its centre is a simple, production-ready agent built on LangGraph, a standard interface for chat models from every major provider, and composable building blocks for prompts, tools, retrieval and structured output. This LangChain tutorial builds up from a single model call to a tool-using agent with memory and a small retrieval (RAG) pipeline, using current LangChain v1 style in Python.
What changed in LangChain v1
If you learned LangChain from older posts, a few things are different:
Step 1: install LangChain
python -m venv .venv
source .venv/bin/activate
pip install -U langchain langchain-openai langchain-text-splitters
export OPENAI_API_KEY="sk-proj-your-key-here"Provider integrations live in their own packages: langchain-openai, langchain-anthropic, langchain-google-genai, langchain-ollama and many more. This tutorial uses OpenAI, but every example works with another provider by changing the model string. If you do not have a key yet, follow the OpenAI API Python tutorial first.
Step 2: call a chat model
init_chat_model creates a chat model from a provider:model string, so switching providers is a one-line change:
import os
from langchain.chat_models import init_chat_model
MODEL = os.environ.get("LC_MODEL", "openai:gpt-6-luna")
model = init_chat_model(MODEL, temperature=0)
reply = model.invoke("In one sentence, what is LangChain?")
print(reply.content)
for chunk in model.stream("Give me three tips for writing good prompts."):
print(chunk.content, end="", flush=True)invoke returns an AIMessage with the text in content plus metadata such as token usage, and stream yields message chunks as they are generated. You can also pass a list of messages, for example a system message and a user message as dictionaries with role and content keys. To use a local model instead, install langchain-ollama and call init_chat_model("qwen3:8b", model_provider="ollama"); see the run an LLM locally tutorial.
Step 3: prompt templates and LCEL chains
For a fixed pipeline, prompt then model then output parser, LCEL's pipe operator composes the pieces into one runnable:
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_messages([
("system", "You are a technical writer. Explain things to a {audience}."),
("user", "Explain {topic} in under 80 words."),
])
chain = prompt | model | StrOutputParser()
print(chain.invoke({"audience": "junior developer", "topic": "database indexes"}))
results = chain.batch([
{"audience": "product manager", "topic": "Kubernetes"},
{"audience": "designer", "topic": "REST APIs"},
])Every runnable supports invoke, stream, batch and their async versions (ainvoke, astream, abatch), so the same chain works in scripts and async web servers. batch runs the inputs in parallel.
Step 4: structured output
Asking for JSON in the prompt is fragile. with_structured_output binds a schema to the model and returns validated objects:
from pydantic import BaseModel, Field
class BugReport(BaseModel):
title: str = Field(description="Short summary of the bug")
severity: str = Field(description="low, medium, high or critical")
steps: list[str] = Field(description="Steps to reproduce")
extractor = model.with_structured_output(BugReport)
report = extractor.invoke(
"The app crashes with a 500 error whenever I upload a PNG larger than 10 MB "
"on the profile page. Started after yesterday's deploy. Blocks all users."
)
print(report.title, report.severity)
print(report.steps)Under the hood LangChain uses the provider's native structured output or tool-calling support, so the result is a BugReport instance rather than a string you must parse.
Step 5: give the model tools
Tools are Python functions the model can decide to call. The @tool decorator turns a typed function with a docstring into a tool; the docstring and type hints become the description and schema the model sees:
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
fake = {"paris": "18C and cloudy", "tokyo": "24C and sunny"}
return fake.get(city.lower(), "No data for that city.")
@tool
def convert_currency(amount: float, rate: float) -> float:
"""Convert an amount using an exchange rate."""
return round(amount * rate, 2)Write clear docstrings: the model chooses tools based on them, so vague descriptions lead to wrong or missing calls.
Step 6: build an agent with create_agent
An agent runs a loop: the model reads the conversation, optionally calls tools, sees the results and repeats until it can answer. create_agent wires this up:
from langchain.agents import create_agent
agent = create_agent(
model=MODEL,
tools=[get_weather, convert_currency],
system_prompt="You are a helpful travel assistant. Use tools when they help.",
)
result = agent.invoke({
"messages": [{"role": "user", "content": "What's the weather in Tokyo, and what is 120 USD at a rate of 0.92?"}]
})
print(result["messages"][-1].content)The result contains the full message history, including the tool calls and tool results, so you can inspect what the agent did. To watch the loop step by step, stream it:
for step in agent.stream(
{"messages": [{"role": "user", "content": "Weather in Paris?"}]},
stream_mode="updates",
):
print(step)Each update shows which node ran, the model or the tools, and what it produced. create_agent also accepts a response_format schema if you want the agent's final answer as structured data, and a middleware list for cross-cutting behaviour such as conversation summarisation, PII redaction or requiring human approval before sensitive tools run.
Step 7: add memory to the agent
By default each invoke is independent. Add a checkpointer and a thread id, and the agent remembers earlier turns in that thread:
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model=MODEL,
tools=[get_weather],
checkpointer=InMemorySaver(),
)
config = {"configurable": {"thread_id": "user-42"}}
agent.invoke({"messages": [{"role": "user", "content": "Hi, I'm flying to Paris on Friday."}]}, config)
answer = agent.invoke({"messages": [{"role": "user", "content": "What's the weather where I'm going?"}]}, config)
print(answer["messages"][-1].content)InMemorySaver is for development. In production, use a persistent checkpointer such as the Postgres or SQLite savers from LangGraph, so conversations survive restarts and can be shared across server instances. Long threads grow expensive, which is exactly what summarisation middleware addresses.
Step 8: a minimal RAG pipeline
Retrieval-augmented generation lets the model answer from your own documents. Split documents into chunks, embed them into a vector store, and expose search as a tool:
from langchain_core.documents import Document
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
handbook = [
Document(page_content="Employees get 25 days of paid leave per year. Unused days roll over up to 5 days."),
Document(page_content="Expense claims must be submitted within 30 days with a receipt attached."),
Document(page_content="Remote work is allowed up to three days per week with manager approval."),
]
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(handbook)
vector_store = InMemoryVectorStore(OpenAIEmbeddings(model="text-embedding-3-small"))
vector_store.add_documents(chunks)
@tool
def search_handbook(query: str) -> str:
"""Search the employee handbook and return the most relevant passages."""
docs = vector_store.similarity_search(query, k=2)
return "\n\n".join(d.page_content for d in docs)
rag_agent = create_agent(
model=MODEL,
tools=[search_handbook],
system_prompt="Answer HR questions using the handbook search tool. Say so if the handbook does not cover it.",
)
reply = rag_agent.invoke({"messages": [{"role": "user", "content": "How many leave days can I carry over?"}]})
print(reply["messages"][-1].content)For real documents, load files with a document loader (PDF, HTML, Markdown and many more are available in langchain-community and partner packages) and replace InMemoryVectorStore with a persistent vector database such as pgvector, Qdrant, Chroma or Pinecone. Retrieval as a tool lets the agent decide when to search and run several searches for multi-part questions; for a fixed retrieve-then-answer flow, a simple LCEL chain also works.
Step 9: debug and evaluate with LangSmith
Agents are hard to debug from print statements alone. LangSmith, LangChain's hosted observability platform, records every model call, tool call, prompt and token count as a trace. Set two environment variables and traces appear automatically:
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY="your-langsmith-key"It is optional and has a free tier; open source alternatives such as Langfuse also integrate with LangChain.
When to use LangChain, LangGraph or something else
To go further, read about agentic AI and the Model Context Protocol, which LangChain can use to load tools from MCP servers through the langchain-mcp-adapters package.

