LangChain: Complete Guide from Beginner to Advanced
AI/ML25 min read

LangChain: Complete Guide from Beginner to Advanced

Vighnesh Salunkhe

Vighnesh Salunkhe

Full Stack Developer

Published

June 11, 2026

LangChain: Complete Guide from Beginner to Advanced

LangChain: Complete Guide from Beginner to Advanced

Large Language Models are powerful in isolation — but they become transformative when you can connect them to tools, data, memory, and logic. That is exactly what LangChain does.

LangChain is an open-source framework that lets you build LLM-powered applications by composing reusable components: prompts, chains, memory, agents, and tools. It abstracts the plumbing so you can focus on what your application needs to do.

This guide takes you from zero to production-ready. We start with the fundamentals, build intuition through working code, and progress to advanced patterns like RAG pipelines, autonomous agents, and streaming deployments.

"LLMs are the reasoning engine. LangChain is the orchestration layer that connects them to the real world."


Prerequisites and Setup

Before writing a single line of LangChain code, get your environment ready.

text
# Create a virtual environment (strongly recommended)
python -m venv langchain-env
source langchain-env/bin/activate  # Windows: langchain-env\Scripts\activate

# Install core packages
pip install langchain langchain-openai langchain-community python-dotenv

# For later sections (RAG, vector stores)
pip install langchain-chroma tiktoken pypdf faiss-cpu

# Verify installation
python -c "import langchain; print(langchain.__version__)"
text
# .env file — never hardcode API keys
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
TAVILY_API_KEY=tvly-...
text
# config.py — load environment variables at startup
from dotenv import load_dotenv
import os

load_dotenv()

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if not OPENAI_API_KEY:
    raise ValueError("OPENAI_API_KEY is not set in environment variables")
LangChain supports many LLM providers: OpenAI, Anthropic, Google Gemini, Mistral, Ollama (local), Hugging Face, and more. The concepts in this guide apply universally — only the provider class changes.

Part 1: Core Concepts

1.1 Language Models — The Foundation

LangChain wraps any LLM behind a consistent interface. You swap providers by changing one import.

text
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage

# Initialize the model
llm = ChatOpenAI(
    model="gpt-4o",
    temperature=0.7,       # 0 = deterministic, 1 = creative
    max_tokens=1024,
    timeout=30,
    max_retries=2,
)

# Simple invocation
response = llm.invoke("Explain quantum computing in one paragraph.")
print(response.content)

# Multi-turn conversation using message history
messages = [
    SystemMessage(content="You are a senior Python developer. Be concise and practical."),
    HumanMessage(content="What is the difference between a list and a tuple?"),
    AIMessage(content="Lists are mutable sequences; tuples are immutable. Use tuples for fixed data."),
    HumanMessage(content="When would you actually choose a tuple over a list in production?"),
]
response = llm.invoke(messages)
print(response.content)
text
# Switching to Anthropic Claude — zero other changes needed
from langchain_anthropic import ChatAnthropic

llm = ChatAnthropic(model="claude-3-5-sonnet-20241022", temperature=0)
response = llm.invoke("What is the CAP theorem?")
print(response.content)
text
# Using a local model via Ollama (fully offline, free)
from langchain_community.llms import Ollama

llm = Ollama(model="llama3.2", temperature=0)
# Pull the model first: ollama pull llama3.2
response = llm.invoke("Summarize the SOLID principles.")
print(response)

1.2 Prompt Templates — Structured Inputs

Raw strings are fragile. Prompt templates are reusable, parameterized, and composable.

text
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate

# Simple string prompt template
template = PromptTemplate(
    input_variables=["topic", "level"],
    template="Explain {topic} to someone with {level} programming experience. Be practical.",
)

prompt = template.format(topic="async/await", level="beginner")
print(prompt)
# Output: Explain async/await to someone with beginner programming experience. Be practical.
text
# Chat prompt template — for chat models (more powerful)
chat_template = ChatPromptTemplate.from_messages([
    ("system", "You are a {persona}. Respond in {language}."),
    ("human", "{user_question}"),
])

# Format with variables
messages = chat_template.format_messages(
    persona="senior DevOps engineer",
    language="English",
    user_question="What is blue-green deployment?",
)

response = llm.invoke(messages)
print(response.content)
text
# Few-shot prompt template — teach the model by example
from langchain_core.prompts import FewShotChatMessagePromptTemplate

examples = [
    {"input": "What is Docker?", "output": "Docker is a platform for packaging applications into containers — isolated environments that run consistently anywhere."},
    {"input": "What is Kubernetes?", "output": "Kubernetes is an orchestration system that automates deploying, scaling, and managing containerized applications across clusters."},
]

example_prompt = ChatPromptTemplate.from_messages([
    ("human", "{input}"),
    ("ai", "{output}"),
])

few_shot_prompt = FewShotChatMessagePromptTemplate(
    example_prompt=example_prompt,
    examples=examples,
)

final_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a cloud infrastructure expert. Give concise, one-sentence definitions."),
    few_shot_prompt,
    ("human", "{input}"),
])

chain = final_prompt | llm
response = chain.invoke({"input": "What is Terraform?"})
print(response.content)

1.3 Output Parsers — Structured Responses

LLMs return text. Output parsers transform that text into structured Python objects.

text
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field
from typing import List

# String parser — simplest, strips metadata
str_parser = StrOutputParser()
chain = llm | str_parser
result = chain.invoke("What is REST?")
print(result)  # str, not AIMessage

# JSON output parser with Pydantic schema
class TechExplanation(BaseModel):
    concept: str = Field(description="The technical concept being explained")
    definition: str = Field(description="Clear, one-paragraph definition")
    use_cases: List[str] = Field(description="3-5 real-world use cases")
    difficulty: str = Field(description="Beginner, Intermediate, or Advanced")
    related_concepts: List[str] = Field(description="Related concepts to learn next")

parser = JsonOutputParser(pydantic_object=TechExplanation)

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a technical educator. Always respond in valid JSON matching the schema exactly."),
    ("human", "Explain: {concept}\n\n{format_instructions}"),
])

chain = prompt | llm | parser
result = chain.invoke({
    "concept": "WebSockets",
    "format_instructions": parser.get_format_instructions(),
})

print(f"Concept: {result['concept']}")
print(f"Difficulty: {result['difficulty']}")
print(f"Use cases: {', '.join(result['use_cases'])}")
text
# Pydantic output parser — type-safe structured output
from langchain.output_parsers import PydanticOutputParser

class CodeReview(BaseModel):
    summary: str = Field(description="One-sentence summary of the code")
    issues: List[str] = Field(description="List of bugs or code quality issues found")
    suggestions: List[str] = Field(description="Actionable improvement suggestions")
    score: int = Field(description="Code quality score from 1 to 10")

parser = PydanticOutputParser(pydantic_object=CodeReview)

prompt = ChatPromptTemplate.from_template(
    "Review this Python code:\n\n```python\n{code}\n```\n\n{format_instructions}"
)

chain = prompt | llm | parser

review: CodeReview = chain.invoke({
    "code": "def add(a,b):\n    return a+b",
    "format_instructions": parser.get_format_instructions(),
})

print(f"Score: {review.score}/10")
print(f"Issues: {review.issues}")
Pydantic parsers give you type safety and IDE autocomplete on LLM responses. This is essential for production applications where you need predictable structured data.

Part 2: Chains — The LangChain Expression Language (LCEL)

Chains are the heart of LangChain. The LangChain Expression Language (LCEL) lets you compose chains using the pipe

text
|
operator — the same pattern as Unix pipes.

2.1 Basic LCEL Chains

text
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

llm = ChatOpenAI(model="gpt-4o-mini")

# The simplest chain: prompt | model | parser
chain = (
    ChatPromptTemplate.from_template("Tell me a fact about {topic}.")
    | llm
    | StrOutputParser()
)

# Invoke synchronously
result = chain.invoke({"topic": "black holes"})
print(result)

# Invoke asynchronously
import asyncio
async def main():
    result = await chain.ainvoke({"topic": "quantum entanglement"})
    print(result)

asyncio.run(main())

# Batch multiple inputs in parallel
results = chain.batch([
    {"topic": "Python"},
    {"topic": "Rust"},
    {"topic": "Go"},
])
for r in results:
    print(r)

2.2 Sequential Chains

text
# Chain of chains — output of one feeds into the next
from langchain_core.runnables import RunnablePassthrough

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# Step 1: Generate a technical concept
concept_chain = (
    ChatPromptTemplate.from_template("Name one important concept in {field} in 3 words or less.")
    | llm
    | StrOutputParser()
)

# Step 2: Explain the concept that was generated
explanation_chain = (
    ChatPromptTemplate.from_template("Explain '{concept}' with a concrete code example in Python.")
    | llm
    | StrOutputParser()
)

# Sequential pipeline: field → concept → explanation
full_chain = (
    {"concept": concept_chain, "field": RunnablePassthrough()}
    | explanation_chain
)

result = full_chain.invoke("distributed systems")
print(result)

2.3 Parallel Chains with RunnableParallel

text
from langchain_core.runnables import RunnableParallel

llm = ChatOpenAI(model="gpt-4o-mini")

# Run multiple chains on the same input simultaneously
analysis_chain = RunnableParallel(
    pros=ChatPromptTemplate.from_template("List 3 pros of {technology} as bullet points.")
         | llm | StrOutputParser(),

    cons=ChatPromptTemplate.from_template("List 3 cons of {technology} as bullet points.")
         | llm | StrOutputParser(),

    alternatives=ChatPromptTemplate.from_template("Name 3 alternatives to {technology}, one sentence each.")
                 | llm | StrOutputParser(),
)

result = analysis_chain.invoke({"technology": "MongoDB"})
print("PROS:\n", result["pros"])
print("\nCONS:\n", result["cons"])
print("\nALTERNATIVES:\n", result["alternatives"])

2.4 Branching with RunnableBranch

text
from langchain_core.runnables import RunnableBranch

# Route to different chains based on input content
code_chain = (
    ChatPromptTemplate.from_template("Explain this code step by step: {input}")
    | llm | StrOutputParser()
)

concept_chain = (
    ChatPromptTemplate.from_template("Explain this concept with an analogy: {input}")
    | llm | StrOutputParser()
)

default_chain = (
    ChatPromptTemplate.from_template("Answer this question clearly: {input}")
    | llm | StrOutputParser()
)

branch = RunnableBranch(
    (lambda x: "def " in x["input"] or "class " in x["input"], code_chain),
    (lambda x: any(w in x["input"].lower() for w in ["what is", "explain", "define"]), concept_chain),
    default_chain,
)

# Each is routed to the right chain automatically
print(branch.invoke({"input": "def fibonacci(n): return n if n <= 1 else fibonacci(n-1)+fibonacci(n-2)"}))
print(branch.invoke({"input": "What is a neural network?"}))
print(branch.invoke({"input": "Should I use Redis or Memcached?"}))

Part 3: Memory — Stateful Conversations

LLMs are stateless by default. Every call is isolated. Memory gives them context across turns.

3.1 In-Memory Conversation History

text
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

llm = ChatOpenAI(model="gpt-4o-mini")

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful coding assistant. Be concise."),
    ("placeholder", "{chat_history}"),  # This gets replaced with message history
    ("human", "{input}"),
])

chain = prompt | llm | StrOutputParser()

# Store per-session histories
store: dict[str, InMemoryChatMessageHistory] = {}

def get_session_history(session_id: str) -> InMemoryChatMessageHistory:
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]

# Wrap chain with memory management
chain_with_memory = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)

config = {"configurable": {"session_id": "user-123"}}

# Turn 1
response = chain_with_memory.invoke({"input": "My name is Vighnesh."}, config=config)
print(response)  # "Hi Vighnesh! How can I help you today?"

# Turn 2 — model remembers the name from turn 1
response = chain_with_memory.invoke({"input": "What's my name?"}, config=config)
print(response)  # "Your name is Vighnesh."

# Different session — separate memory
config2 = {"configurable": {"session_id": "user-456"}}
response = chain_with_memory.invoke({"input": "What's my name?"}, config=config2)
print(response)  # "I don't know your name yet. What is it?"

3.2 Persistent Memory with Redis

text
from langchain_community.chat_message_histories import RedisChatMessageHistory

def get_redis_session_history(session_id: str) -> RedisChatMessageHistory:
    return RedisChatMessageHistory(
        session_id=session_id,
        url="redis://localhost:6379",
        ttl=3600,  # Auto-expire sessions after 1 hour
    )

chain_with_redis = RunnableWithMessageHistory(
    chain,
    get_redis_session_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)
# Now conversation history survives server restarts and scales across instances

3.3 Summarization Memory for Long Conversations

text
from langchain.memory import ConversationSummaryBufferMemory

# Keeps recent messages in full, summarizes older ones
# Prevents context window overflow in long sessions
memory = ConversationSummaryBufferMemory(
    llm=ChatOpenAI(model="gpt-4o-mini"),
    max_token_limit=500,         # Once exceeded, older messages are summarized
    return_messages=True,
    memory_key="chat_history",
)

# Add messages
memory.save_context(
    {"input": "I'm building a SaaS product for code review automation."},
    {"output": "That's a great idea! What tech stack are you considering?"},
)
memory.save_context(
    {"input": "Thinking about FastAPI backend and React frontend."},
    {"output": "Good choice. FastAPI is excellent for ML-heavy backends."},
)

# Retrieve with automatic summarization of old messages
history = memory.load_memory_variables({})
print(history["chat_history"])
In-memory storage is lost when your server restarts. Always use persistent storage (Redis, PostgreSQL, DynamoDB) for production applications. LangChain supports all of these through
text
langchain-community
.

Part 4: Document Loaders and Text Splitters

Most real-world LLM apps need to work with external data — PDFs, websites, databases, code repositories. LangChain provides loaders and splitters for all of these.

4.1 Document Loaders

text
from langchain_community.document_loaders import (
    PyPDFLoader,
    WebBaseLoader,
    TextLoader,
    DirectoryLoader,
    CSVLoader,
    GitLoader,
)

# Load a PDF
loader = PyPDFLoader("documents/research-paper.pdf")
pages = loader.load()  # Returns list of Document objects
print(f"Loaded {len(pages)} pages")
print(pages[0].page_content[:500])       # The text
print(pages[0].metadata)                  # {"source": "research-paper.pdf", "page": 0}

# Load from a URL
loader = WebBaseLoader("https://docs.python.org/3/library/asyncio.html")
docs = loader.load()
print(docs[0].page_content[:300])

# Load all .txt files in a directory
loader = DirectoryLoader("./docs", glob="**/*.txt", loader_cls=TextLoader)
docs = loader.load()
print(f"Loaded {len(docs)} documents")

# Load CSV as structured documents
loader = CSVLoader("data/customers.csv", source_column="id")
docs = loader.load()
print(docs[0].page_content)  # Each row becomes a Document

# Load from a Git repository
loader = GitLoader(
    repo_path="./my-repo",
    branch="main",
    file_filter=lambda path: path.endswith(".py"),
)
code_docs = loader.load()
print(f"Loaded {len(code_docs)} Python files")

4.2 Text Splitters

LLMs have context window limits. Large documents must be split into chunks before embedding.

text
from langchain.text_splitter import (
    RecursiveCharacterTextSplitter,
    TokenTextSplitter,
    MarkdownHeaderTextSplitter,
)

# Recursive splitter — the best default choice
# Tries to split on paragraphs → sentences → words → characters
splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,         # Max characters per chunk
    chunk_overlap=200,       # Overlap to preserve context across chunks
    separators=["\n\n", "\n", ". ", " ", ""],
)

with open("long-document.txt") as f:
    text = f.read()

chunks = splitter.create_documents([text])
print(f"Split into {len(chunks)} chunks")
print(f"First chunk: {chunks[0].page_content[:200]}")

# Token-based splitter — more accurate for LLM context windows
token_splitter = TokenTextSplitter(
    chunk_size=512,     # Tokens, not characters
    chunk_overlap=50,
    encoding_name="cl100k_base",  # OpenAI's encoding
)

# Markdown-aware splitter — preserves document structure
md_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=[
        ("#", "h1"),
        ("##", "h2"),
        ("###", "h3"),
    ],
    strip_headers=False,
)

with open("README.md") as f:
    markdown_text = f.read()

md_chunks = md_splitter.split_text(markdown_text)
# Each chunk includes the header hierarchy in metadata
for chunk in md_chunks[:3]:
    print(chunk.metadata)    # {"h1": "Getting Started", "h2": "Installation"}
    print(chunk.page_content[:100])
    print("---")
text
chunk_overlap
is critical. Without overlap, context at the boundaries of chunks gets lost. A 20% overlap of your chunk size is a good starting point. For code, use
text
Language.PYTHON
with the
text
RecursiveCharacterTextSplitter
— it splits on function and class boundaries instead of arbitrary character counts.

Part 5: Embeddings and Vector Stores

This is where LangChain unlocks semantic search — finding documents by meaning, not just keywords.

5.1 Embeddings

text
from langchain_openai import OpenAIEmbeddings
from langchain_community.embeddings import HuggingFaceEmbeddings

# OpenAI embeddings — best quality, costs money
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")

# Generate embedding for a single text
vector = embeddings.embed_query("What is machine learning?")
print(f"Embedding dimension: {len(vector)}")  # 3072 for large model

# Generate embeddings for a batch of documents
docs = [
    "Python is a high-level programming language.",
    "JavaScript runs in the browser and on Node.js.",
    "Rust guarantees memory safety without a garbage collector.",
]
vectors = embeddings.embed_documents(docs)
print(f"Generated {len(vectors)} vectors")

# HuggingFace embeddings — free, runs locally
hf_embeddings = HuggingFaceEmbeddings(
    model_name="sentence-transformers/all-MiniLM-L6-v2",
    model_kwargs={"device": "cpu"},
)
vector = hf_embeddings.embed_query("semantic similarity search")
print(f"HuggingFace embedding dimension: {len(vector)}")  # 384

5.2 Vector Stores

text
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# Full pipeline: load → split → embed → store
loader = PyPDFLoader("documents/technical-docs.pdf")
raw_docs = loader.load()

splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = splitter.split_documents(raw_docs)

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# Create persistent vector store from documents
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db",   # Persist to disk
    collection_name="technical_docs",
)

print(f"Stored {vectorstore._collection.count()} vectors")
text
# Load existing vector store and query it
vectorstore = Chroma(
    persist_directory="./chroma_db",
    embedding_function=embeddings,
    collection_name="technical_docs",
)

# Similarity search — returns most relevant chunks
results = vectorstore.similarity_search(
    query="How do I configure authentication?",
    k=4,  # Return top 4 results
)
for doc in results:
    print(f"Source: {doc.metadata['source']}, Page: {doc.metadata.get('page', 'N/A')}")
    print(doc.page_content[:200])
    print("---")

# Similarity search with relevance scores
results_with_scores = vectorstore.similarity_search_with_relevance_scores(
    query="authentication configuration",
    k=4,
    score_threshold=0.7,  # Only return results above this similarity score
)
for doc, score in results_with_scores:
    print(f"Score: {score:.3f} | {doc.page_content[:100]}")

Part 6: Retrieval-Augmented Generation (RAG)

RAG is the most widely deployed LangChain pattern in production. It lets you ground LLM responses in your own documents — eliminating hallucination and keeping answers up to date.

6.1 Basic RAG Pipeline

text
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

llm = ChatOpenAI(model="gpt-4o", temperature=0)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)

# Create a retriever from the vector store
retriever = vectorstore.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 5},
)

# RAG prompt — instructs the model to use only provided context
rag_prompt = ChatPromptTemplate.from_messages([
    ("system", """You are a helpful assistant that answers questions based ONLY on the provided context.
If the answer is not in the context, say "I don't have enough information to answer that."
Never make up information.

Context:
{context}"""),
    ("human", "{question}"),
])

def format_docs(docs):
    """Join retrieved document chunks into a single context string."""
    return "\n\n---\n\n".join(
        f"[Source: {doc.metadata.get('source', 'unknown')}]\n{doc.page_content}"
        for doc in docs
    )

# Full RAG chain
rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | rag_prompt
    | llm
    | StrOutputParser()
)

answer = rag_chain.invoke("How do I set up authentication in this project?")
print(answer)

6.2 Advanced RAG with Source Citations

text
from langchain_core.runnables import RunnableParallel

# Return both the answer AND the source documents
rag_chain_with_sources = RunnableParallel(
    {
        "answer": rag_chain,
        "sources": retriever,
    }
)

result = rag_chain_with_sources.invoke("What are the API rate limits?")

print("Answer:", result["answer"])
print("\nSources:")
for i, doc in enumerate(result["sources"], 1):
    source = doc.metadata.get("source", "unknown")
    page = doc.metadata.get("page", "N/A")
    print(f"  [{i}] {source} (page {page})")

6.3 Conversational RAG

text
from langchain.chains import create_history_aware_retriever, create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

llm = ChatOpenAI(model="gpt-4o-mini")

# Step 1: Rephrase follow-up questions into standalone queries
# e.g., "What about its rate limits?" → "What are the OpenAI API rate limits?"
contextualize_q_prompt = ChatPromptTemplate.from_messages([
    ("system", """Given the chat history and the latest user question,
rephrase the question to be standalone (no references to chat history).
Return ONLY the rephrased question, nothing else."""),
    ("placeholder", "{chat_history}"),
    ("human", "{input}"),
])

history_aware_retriever = create_history_aware_retriever(
    llm, retriever, contextualize_q_prompt
)

# Step 2: Answer using retrieved context
qa_prompt = ChatPromptTemplate.from_messages([
    ("system", """Answer the question based only on the following context.
If you don't know, say so — don't make up answers.

Context:
{context}"""),
    ("placeholder", "{chat_history}"),
    ("human", "{input}"),
])

question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)

# Wrap with memory
store = {}
def get_session_history(session_id: str):
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]

conversational_rag = RunnableWithMessageHistory(
    rag_chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="chat_history",
    output_messages_key="answer",
)

config = {"configurable": {"session_id": "rag-session-1"}}

# Multi-turn Q&A grounded in your documents
r1 = conversational_rag.invoke({"input": "What authentication methods does this API support?"}, config=config)
print("Turn 1:", r1["answer"])

r2 = conversational_rag.invoke({"input": "How do I implement the second one?"}, config=config)
print("Turn 2:", r2["answer"])  # "It" is correctly resolved to the right auth method
Vector database and embeddings architecture
Vector database and embeddings architecture

Part 7: Tools and Agents

Agents take LLMs beyond question-answering. They give LLMs the ability to reason, decide which tools to use, call those tools, observe results, and iterate — autonomously.

7.1 Built-in Tools

text
from langchain_community.tools import TavilySearchResults, WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
from langchain.tools import tool

# Tavily web search (requires TAVILY_API_KEY)
search_tool = TavilySearchResults(
    max_results=3,
    search_depth="advanced",
    include_raw_content=True,
)

results = search_tool.invoke("LangChain latest version 2026")
for r in results:
    print(r["title"])
    print(r["url"])
    print(r["content"][:200])
    print("---")

# Wikipedia search
wiki = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper(top_k_results=2))
result = wiki.invoke("transformer architecture neural networks")
print(result[:500])

7.2 Custom Tools

text
from langchain.tools import tool
from pydantic import BaseModel, Field
import requests

# Simple decorator-based tool
@tool
def get_current_weather(city: str) -> str:
    """Get the current weather for a given city. Use this when the user asks about weather."""
    # In production, call a real weather API
    return f"The current temperature in {city} is 22°C with partly cloudy skies."

@tool
def calculate_compound_interest(
    principal: float,
    rate: float,
    time_years: int,
    compounding_frequency: int = 12,
) -> str:
    """
    Calculate compound interest.
    
    Args:
        principal: Initial investment amount in USD
        rate: Annual interest rate as a decimal (e.g., 0.05 for 5%)
        time_years: Investment period in years
        compounding_frequency: Times interest compounds per year (default: monthly=12)
    """
    amount = principal * (1 + rate / compounding_frequency) ** (compounding_frequency * time_years)
    interest = amount - principal
    return f"Principal: ${principal:,.2f}, Final Amount: ${amount:,.2f}, Interest Earned: ${interest:,.2f}"

# Structured input tool with Pydantic schema
class DatabaseQueryInput(BaseModel):
    table: str = Field(description="Database table name to query")
    condition: str = Field(description="SQL WHERE clause condition")
    limit: int = Field(default=10, description="Maximum number of rows to return")

@tool(args_schema=DatabaseQueryInput)
def query_database(table: str, condition: str, limit: int = 10) -> str:
    """Query the internal database for records matching specific conditions."""
    # In production, this connects to your actual database
    return f"SELECT * FROM {table} WHERE {condition} LIMIT {limit} — returned 3 rows: [mock data]"

# Test tools independently
print(get_current_weather.invoke({"city": "Tokyo"}))
print(calculate_compound_interest.invoke({
    "principal": 10000,
    "rate": 0.07,
    "time_years": 10,
}))

7.3 ReAct Agents — Reasoning + Acting

text
from langchain.agents import create_react_agent, AgentExecutor
from langchain import hub

llm = ChatOpenAI(model="gpt-4o", temperature=0)

tools = [
    TavilySearchResults(max_results=3),
    get_current_weather,
    calculate_compound_interest,
    query_database,
]

# Pull the ReAct prompt from LangChain Hub
# Thought → Action → Observation → Thought → ... → Final Answer
react_prompt = hub.pull("hwchase17/react")

agent = create_react_agent(llm=llm, tools=tools, prompt=react_prompt)

agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,         # Print the reasoning trace
    max_iterations=10,    # Prevent infinite loops
    handle_parsing_errors=True,
    return_intermediate_steps=True,
)

result = agent_executor.invoke({
    "input": "What is the weather in London right now, and if I invest $5000 at 6% for 5 years with monthly compounding, how much will I have?"
})

print("\nFinal Answer:", result["output"])
print("\nSteps taken:", len(result["intermediate_steps"]))
text
from langchain.agents import create_openai_tools_agent

# More reliable than ReAct for GPT-4o — uses native function calling
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
    ("system", """You are a helpful assistant with access to tools.
Use tools when needed. Think step by step before acting.
Always provide a clear final answer after using tools."""),
    ("placeholder", "{chat_history}"),
    ("human", "{input}"),
    MessagesPlaceholder("agent_scratchpad"),
])

agent = create_openai_tools_agent(llm=llm, tools=tools, prompt=prompt)

executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    max_iterations=8,
    return_intermediate_steps=True,
)

# Multi-step reasoning task
result = executor.invoke({
    "input": "Search for the latest news about AI regulation in 2026, then summarize the key points.",
    "chat_history": [],
})

print(result["output"])

Part 8: Streaming — Real-Time Responses

Streaming delivers tokens as they are generated, making your app feel instant instead of making users wait.

text
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

llm = ChatOpenAI(model="gpt-4o", streaming=True)

chain = (
    ChatPromptTemplate.from_template("Write a detailed explanation of {topic}.")
    | llm
    | StrOutputParser()
)

# Stream tokens as they arrive
print("Streaming response:")
for chunk in chain.stream({"topic": "how transformers work in deep learning"}):
    print(chunk, end="", flush=True)
print()  # Newline at end

# Async streaming — use in FastAPI or async frameworks
import asyncio

async def stream_async():
    async for chunk in chain.astream({"topic": "the future of edge computing"}):
        print(chunk, end="", flush=True)

asyncio.run(stream_async())
text
# Streaming with agents — stream each step as it happens
async def stream_agent():
    async for event in executor.astream_events(
        {"input": "What are the top 3 AI startups in 2026?", "chat_history": []},
        version="v2",
    ):
        kind = event["event"]
        if kind == "on_chat_model_stream":
            chunk = event["data"]["chunk"].content
            if chunk:
                print(chunk, end="", flush=True)
        elif kind == "on_tool_start":
            print(f"\n[Using tool: {event['name']}]\n")
        elif kind == "on_tool_end":
            print(f"[Tool complete]\n")

asyncio.run(stream_agent())

Part 9: Advanced Patterns

9.1 Multi-Agent Systems

text
from langchain_openai import ChatOpenAI
from langchain.agents import create_openai_tools_agent, AgentExecutor
from langchain.tools import tool
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

llm = ChatOpenAI(model="gpt-4o", temperature=0)

# Specialized sub-agents as tools
@tool
def research_agent_tool(query: str) -> str:
    """Research and gather information on a given topic using web search."""
    search = TavilySearchResults(max_results=5)
    results = search.invoke(query)
    return "\n".join(r["content"] for r in results)

@tool
def code_agent_tool(task: str) -> str:
    """Generate and review Python code for a given programming task."""
    code_llm = ChatOpenAI(model="gpt-4o", temperature=0)
    code_prompt = ChatPromptTemplate.from_template(
        "Write clean, production-ready Python code for: {task}\n"
        "Include docstrings, type hints, and error handling."
    )
    chain = code_prompt | code_llm | StrOutputParser()
    return chain.invoke({"task": task})

@tool
def review_agent_tool(code: str) -> str:
    """Review Python code for bugs, security issues, and best practices."""
    review_llm = ChatOpenAI(model="gpt-4o", temperature=0)
    review_prompt = ChatPromptTemplate.from_template(
        "Review this Python code for bugs, security issues, and best practices:\n\n```python\n{code}\n```"
    )
    chain = review_prompt | review_llm | StrOutputParser()
    return chain.invoke({"code": code})

# Orchestrator agent — delegates to specialists
orchestrator_tools = [research_agent_tool, code_agent_tool, review_agent_tool]

orchestrator_prompt = ChatPromptTemplate.from_messages([
    ("system", """You are an AI project orchestrator. You coordinate specialist agents:
- research_agent_tool: For gathering information from the web
- code_agent_tool: For writing Python code  
- review_agent_tool: For reviewing and critiquing code
Break complex tasks down and delegate to the right specialist."""),
    ("human", "{input}"),
    MessagesPlaceholder("agent_scratchpad"),
])

orchestrator = create_openai_tools_agent(llm, orchestrator_tools, orchestrator_prompt)
orchestrator_executor = AgentExecutor(agent=orchestrator, tools=orchestrator_tools, verbose=True)

result = orchestrator_executor.invoke({
    "input": "Research the best Python libraries for building REST APIs in 2026, then write a basic FastAPI example, and review it for issues."
})
print(result["output"])

9.2 LangGraph — Stateful Agent Workflows

LangGraph extends LangChain for complex, stateful agent workflows with cycles, branching, and human-in-the-loop.

text
# pip install langgraph
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from typing import TypedDict, List

llm = ChatOpenAI(model="gpt-4o-mini")

# Define the state that flows through the graph
class AgentState(TypedDict):
    question: str
    research: str
    draft: str
    feedback: str
    final_answer: str
    iteration: int

# Define nodes (each is a function that transforms state)
def research_node(state: AgentState) -> AgentState:
    """Search for relevant information."""
    search = TavilySearchResults(max_results=3)
    results = search.invoke(state["question"])
    research = "\n".join(r["content"][:300] for r in results)
    return {**state, "research": research}

def draft_node(state: AgentState) -> AgentState:
    """Write a draft answer based on research."""
    prompt = f"""Based on this research, write a comprehensive answer to: {state['question']}

Research:
{state['research']}

{'Previous feedback to address: ' + state['feedback'] if state['feedback'] else ''}"""
    
    draft = llm.invoke(prompt).content
    return {**state, "draft": draft, "iteration": state["iteration"] + 1}

def review_node(state: AgentState) -> AgentState:
    """Review the draft and provide feedback."""
    prompt = f"""Review this draft answer for accuracy, completeness, and clarity.
If it's good enough (score 8+/10), respond with "APPROVED".
Otherwise provide specific feedback.

Draft: {state['draft']}"""
    
    feedback = llm.invoke(prompt).content
    return {**state, "feedback": feedback}

def finalize_node(state: AgentState) -> AgentState:
    return {**state, "final_answer": state["draft"]}

def should_revise(state: AgentState) -> str:
    """Routing function: approve or request revision."""
    if "APPROVED" in state["feedback"] or state["iteration"] >= 3:
        return "finalize"
    return "draft"  # Revise

# Build the graph
workflow = StateGraph(AgentState)

workflow.add_node("research", research_node)
workflow.add_node("draft", draft_node)
workflow.add_node("review", review_node)
workflow.add_node("finalize", finalize_node)

workflow.set_entry_point("research")
workflow.add_edge("research", "draft")
workflow.add_edge("draft", "review")
workflow.add_conditional_edges("review", should_revise, {"draft": "draft", "finalize": "finalize"})
workflow.add_edge("finalize", END)

app = workflow.compile()

# Run the self-improving workflow
result = app.invoke({
    "question": "What are the most important AI safety considerations for 2026?",
    "research": "",
    "draft": "",
    "feedback": "",
    "final_answer": "",
    "iteration": 0,
})

print(f"Completed in {result['iteration']} iteration(s)")
print(result["final_answer"])

9.3 Evaluation and Testing

text
from langchain.evaluation import load_evaluator
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o", temperature=0)

# QA correctness evaluator
evaluator = load_evaluator("qa", llm=llm)

eval_result = evaluator.evaluate_strings(
    prediction="LangChain is a framework for building LLM-powered applications.",
    input="What is LangChain?",
    reference="LangChain is an open-source framework for building applications using large language models.",
)
print(f"Score: {eval_result['score']}")
print(f"Reasoning: {eval_result['reasoning']}")

# Criteria evaluator — check for specific qualities
criteria_evaluator = load_evaluator("criteria", llm=llm, criteria={
    "correctness": "Is the answer factually correct?",
    "conciseness": "Is the answer appropriately concise?",
    "helpfulness": "Is the answer genuinely helpful to the user?",
})

result = criteria_evaluator.evaluate_strings(
    prediction="Python is a programming language created in 1991 by Guido van Rossum.",
    input="Tell me about Python.",
)
print(result)

Part 10: Production Deployment

10.1 LangServe — Deploy Chains as REST APIs

text
# pip install langserve[all] fastapi uvicorn

from fastapi import FastAPI
from langserve import add_routes
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

app = FastAPI(
    title="LangChain API",
    description="LLM-powered API built with LangChain and LangServe",
    version="1.0.0",
)

llm = ChatOpenAI(model="gpt-4o-mini")

# Chain 1: Code explainer
code_chain = (
    ChatPromptTemplate.from_template("Explain this code clearly:\n\n```\n{code}\n```")
    | llm
    | StrOutputParser()
)
add_routes(app, code_chain, path="/explain-code")

# Chain 2: RAG endpoint
rag_chain = rag_chain  # From Part 6
add_routes(app, rag_chain, path="/ask-docs")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

# Now you get automatic REST endpoints:
# POST /explain-code/invoke
# POST /explain-code/stream
# POST /explain-code/batch
# GET  /explain-code/playground  ← interactive UI

10.2 LangSmith — Observability and Tracing

text
import os

# Enable LangSmith tracing — just set env vars
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "your-langsmith-api-key"
os.environ["LANGCHAIN_PROJECT"] = "my-production-app"

# All LangChain calls are now automatically traced
# View traces at: https://smith.langchain.com

# Every chain invocation gets:
# - Full input/output logs
# - Token usage and costs
# - Latency per step
# - Error traces with full context
# - Side-by-side comparison of different model versions

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOpenAI(model="gpt-4o-mini")
chain = ChatPromptTemplate.from_template("Explain {topic}") | llm | StrOutputParser()

# This invocation is automatically traced in LangSmith
result = chain.invoke({"topic": "neural networks"})

10.3 Caching for Cost Reduction

text
from langchain_community.cache import InMemoryCache, RedisCache
from langchain.globals import set_llm_cache
import langchain

# In-memory cache — great for development
set_llm_cache(InMemoryCache())

# Redis cache — production-grade, shared across instances
from redis import Redis
set_llm_cache(RedisCache(redis_=Redis(host="localhost", port=6379)))

llm = ChatOpenAI(model="gpt-4o-mini")

# First call — hits the API, stores result in cache
result1 = llm.invoke("What is the capital of France?")
print("First call (API):", result1.content)

# Second identical call — returned from cache instantly, no API cost
result2 = llm.invoke("What is the capital of France?")
print("Second call (cache):", result2.content)

# For high-traffic apps, semantic caching avoids near-duplicate calls
from langchain_community.cache import GPTCache
# GPTCache uses embeddings to find semantically similar cached responses
# "What is the capital of France?" hits the cache for "Capital city of France?"
Production Checklist:
  • Use LangSmith for tracing and monitoring from day one
  • Implement LLM response caching to reduce costs 60-80%
  • Set
    text
    max_retries
    and
    text
    timeout
    on all LLM clients
  • Use
    text
    max_iterations
    on all agents to prevent infinite loops
  • Store all conversation history in a persistent backend (Redis, Postgres)
  • Never expose raw LLM errors to end users — wrap with try/catch

Architecture Patterns at a Glance

PatternWhen to UseKey Components
Simple ChainSingle-step LLM tasksPrompt → LLM → Parser
Sequential ChainMulti-step pipelinesChain of chains with LCEL
RAGQ&A over your documentsLoader → Splitter → VectorStore → Retriever
Conversational RAGMulti-turn doc chatbotsHistory-aware retriever + memory
ReAct AgentOpen-ended tool useLLM + tools + reasoning loop
LangGraphComplex stateful workflowsNodes + edges + conditional routing
Multi-AgentSpecialized task delegationOrchestrator + specialist agents

What to Learn Next

LangChain is a deep ecosystem. Here is where to go after mastering these fundamentals:

  • LangGraph — Build complex agent workflows with cycles, human-in-the-loop, and long-running tasks
  • LangSmith — Set up evals, A/B testing between models, and production monitoring dashboards
  • Advanced RAG — Hybrid search (dense + sparse), reranking with Cohere, HyDE query transformation
  • Fine-tuning — When RAG is not enough, fine-tune with your domain data using OpenAI or Hugging Face
  • Multi-modal — Vision agents that process images, PDFs with layout, and audio transcriptions

The patterns in this guide — chains, memory, tools, agents, RAG — are the foundation. Everything else is built on top of them. Once these click, the entire LLM application space opens up.

#LangChain#Python#LLM#RAG#AI Agents#OpenAI
Vighnesh Salunkhe
Written by

Vighnesh Salunkhe

"Passionate about building scalable web applications and exploring the intersection of AI and human creativity."

Join the Conversation

Share your thoughts or ask a question

Share this article