LangChain Complete Guide — Build LLM Apps with Python in 2025

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Introduction

Why This Matters

LangChain is the most widely adopted framework for building LLM-powered applications in Python. It abstracts away the complexity of managing prompt templates, conversation memory, tool use, and multi-step reasoning — letting you focus on product logic rather than plumbing. As of 2025, LangChain has over 90,000 GitHub stars and powers production applications at thousands of companies.

The framework introduced LCEL (LangChain Expression Language) as its primary API — a declarative, composable approach to building pipelines that is now the standard way to write LangChain code. Whether you are building a customer support bot, a coding assistant, or an autonomous research agent, LangChain provides the building blocks.

Understanding LangChain deeply means you can swap models, debug chains, integrate vector stores, and build agents that use real-world tools — all without rewriting your application from scratch.

Installation and Setup

pip install langchain langchain-openai langchain-community langchain-core python-dotenv

Configure API keys via environment variables:

import os
from dotenv import load_dotenv
 
load_dotenv()  # loads from .env file
# OPENAI_API_KEY=sk-... in your .env

Core Concepts

LangChain is built around five abstractions:

  • Models: Wrappers around LLM providers (OpenAI, Anthropic, Cohere, local models via Ollama)
  • Prompts: Reusable templates that format inputs before sending to the model
  • Chains: Sequences of operations composed using the pipe (|) operator in LCEL
  • Memory: Mechanisms to persist conversation context across turns
  • Agents: LLMs that dynamically decide which tools to call to complete a task

Building Your First Chain with LCEL

LCEL (LangChain Expression Language) is the modern way to compose chains using the | operator:

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
 
llm = ChatOpenAI(model="gpt-4o", temperature=0.7)
 
prompt = ChatPromptTemplate.from_template(
    "Explain {topic} in simple terms for a {audience}."
)
 
chain = prompt | llm | StrOutputParser()
 
result = chain.invoke({
    "topic": "transformer attention mechanisms",
    "audience": "junior developer"
})
print(result)

Every component in LCEL is a Runnable. Chains are composable, streamable, and support async out of the box.

Working with Memory

Production chatbots need to remember previous turns. LangChain supports several memory patterns:

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage
 
llm = ChatOpenAI(model="gpt-4o")
 
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful AI assistant."),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}"),
])
 
chain = prompt | llm | StrOutputParser()
 
# Manage history manually for full control
history = []
 
def chat(user_input: str) -> str:
    response = chain.invoke({
        "input": user_input,
        "history": history
    })
    history.append(HumanMessage(content=user_input))
    history.append(AIMessage(content=response))
    return response
 
print(chat("What is RAG?"))
print(chat("Can you give me a code example?"))

For long conversations, use ConversationSummaryMemory to compress history and stay within context limits.

Structured Output Parsing

Extract typed data from LLM responses using Pydantic:

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field
from typing import List
 
class BlogOutline(BaseModel):
    title: str = Field(description="SEO-optimized blog title")
    sections: List[str] = Field(description="List of section headings")
    target_keywords: List[str] = Field(description="Primary SEO keywords")
 
llm = ChatOpenAI(model="gpt-4o")
parser = JsonOutputParser(pydantic_object=BlogOutline)
 
prompt = ChatPromptTemplate.from_template(
    "Create a blog outline about {topic}.\n\n{format_instructions}"
).partial(format_instructions=parser.get_format_instructions())
 
chain = prompt | llm | parser
 
result = chain.invoke({"topic": "LLM prompt engineering techniques"})
print(result["title"])
print(result["sections"])

Alternatively, use .with_structured_output() for cleaner syntax with models that support function calling natively.

Building Agents with Tools

Agents let an LLM decide which tools to invoke to answer a query:

from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
 
@tool
def search_docs(query: str) -> str:
    """Search internal documentation for information."""
    # Replace with actual vector store retrieval
    return f"Documentation result for: {query}"
 
@tool
def get_stock_price(ticker: str) -> str:
    """Get the current stock price for a ticker symbol."""
    return f"${ticker}: $150.25 (mock)"
 
tools = [search_docs, get_stock_price]
llm = ChatOpenAI(model="gpt-4o")
 
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant with access to tools."),
    MessagesPlaceholder("chat_history", optional=True),
    ("human", "{input}"),
    MessagesPlaceholder("agent_scratchpad"),
])
 
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
 
result = executor.invoke({"input": "What does our doc say about rate limits and what is AAPL price?"})
print(result["output"])

RAG Pipeline with Document Loaders

from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
 
# Load and chunk documents
loader = PyPDFLoader("company_handbook.pdf")
docs = loader.load()
 
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = splitter.split_documents(docs)
 
# Embed and store
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(chunks, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
 
# RAG chain
rag_prompt = ChatPromptTemplate.from_template(
    "Answer based only on the context below.\n\nContext: {context}\n\nQuestion: {question}"
)
 
llm = ChatOpenAI(model="gpt-4o")
 
rag_chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | rag_prompt
    | llm
    | StrOutputParser()
)
 
answer = rag_chain.invoke("What is our PTO policy?")
print(answer)

Streaming Responses

Streaming improves perceived latency for end users:

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
 
llm = ChatOpenAI(model="gpt-4o", streaming=True)
prompt = ChatPromptTemplate.from_template("Write a paragraph about {topic}.")
chain = prompt | llm
 
for chunk in chain.stream({"topic": "the future of AI agents"}):
    print(chunk.content, end="", flush=True)

Common Mistakes / Pitfalls

  • Using deprecated LLMChain instead of LCEL — migrate to the | operator pattern
  • Building unbounded memory buffers that overflow the context window on long conversations
  • Not handling rate limit errors with retry logic — use langchain.globals.set_llm_cache and tenacity
  • Running synchronous chains in async FastAPI routes — use chain.ainvoke() instead
  • Hardcoding model names — parameterize them so you can A/B test providers

Best Practices

  • Use RunnableParallel to fan out multiple LLM calls in a single step when they are independent
  • Add callbacks=[LangSmithTracer()] to every chain in production for observability
  • Set max_tokens explicitly to prevent runaway generation costs
  • Use .batch() for processing large datasets efficiently with automatic parallelism
  • Cache expensive embeddings with langchain.cache backed by Redis or SQLite
  • Version your prompt templates — small wording changes cause large output differences

Key Takeaways

  • LangChain is the dominant Python framework for LLM application development with 90K+ GitHub stars as of 2025
  • LCEL (LangChain Expression Language) uses the pipe | operator to compose prompts, models, and parsers declaratively
  • Agents combine an LLM with tool definitions — the model decides which tools to call at runtime
  • RAG pipelines in LangChain involve loaders, splitters, embedding models, vector stores, and retrieval chains
  • Memory in LangChain must be managed explicitly — append messages to history and compress when context gets long
  • Streaming with .stream() reduces perceived latency and improves user experience significantly
  • LangChain supports 100+ integrations including OpenAI, Anthropic, Cohere, Hugging Face, Ollama, and all major vector stores
  • Production LangChain apps benefit from LangSmith tracing for debugging prompt failures and measuring chain latency

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading