Getting Started with LangChain -- Chains, Agents, and Memory

What Is LangChain?

LangChain is a framework for building applications powered by LLMs (Large Language Models). Like LEGO blocks, you can combine components such as prompts, models, tools, and memory to create complex AI workflows.

The difference between simply calling an LLM API and using LangChain is like the difference between having ingredients and having a recipe. LangChain provides proven patterns and abstractions that reduce repetitive code.

Core Concepts Overview

ConceptDescriptionAnalogy
ChainPipeline that executes multiple steps sequentiallyFactory assembly line
AgentAutonomous system where the LLM selects and executes toolsAll-purpose assistant
MemoryModule that stores and references conversation historyConversation notes
ToolExternal capabilities that agents can useAssistant’s toolbox
Prompt TemplateDynamically generated prompt frameworkFill-in-the-blank form

Installation and Basic Setup

# Install LangChain and OpenAI packages
pip install langchain langchain-openai langchain-community

# Set environment variable (.env file or export)
export OPENAI_API_KEY="sk-your-api-key"

Chain: Step-by-Step Pipeline Composition

A Chain is the core concept of linking multiple processing steps together. Using LangChain Expression Language (LCEL), you can intuitively compose chains with the | operator.

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

# Initialize model
llm = ChatOpenAI(model="gpt-4o", temperature=0.7)

# Define prompt template
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a {role} expert. Answer in detail."),
    ("human", "{question}")
])

# Compose chain with LCEL (prompt -> model -> output parser)
chain = prompt | llm | StrOutputParser()

# Execute chain
result = chain.invoke({
    "role": "Python",
    "question": "Explain how decorators work"
})
print(result)
# Output: A decorator is a higher-order function that takes a function as an argument and returns a new function...

Connecting Multiple Chains

You can connect the output of one chain to the input of another.

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

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

# Step 1: Generate a summary about a topic
summary_prompt = ChatPromptTemplate.from_template(
    "Summarize {topic} in 3 sentences."
)
summary_chain = summary_prompt | llm | StrOutputParser()

# Step 2: Generate quiz based on the summary
quiz_prompt = ChatPromptTemplate.from_template(
    "Create 2 multiple-choice quiz questions based on the following content:\n{summary}"
)
quiz_chain = quiz_prompt | llm | StrOutputParser()

# Connect and execute both chains
full_chain = (
    summary_chain
    | (lambda summary: {"summary": summary})
    | quiz_chain
)

result = full_chain.invoke({"topic": "Python async programming"})
print(result)
# Output: 1. Which keyword is used to define an async function in Python?
#          a) async  b) await  c) yield  d) thread
#          ...

Agent: LLM Selects and Executes Tools

An Agent is a system where the LLM autonomously decides which tools to use and executes them. You can connect various tools such as calculators, search, and API calls.

from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool

# Define custom tools
@tool
def calculate(expression: str) -> str:
    """Evaluates a mathematical expression. Example: '2 + 3 * 4'"""
    try:
        # Safe expression evaluation
        result = eval(expression, {"__builtins__": {}})
        return f"Result: {result}"
    except Exception as e:
        return f"Calculation error: {e}"

@tool
def get_current_weather(city: str) -> str:
    """Looks up the current weather for a city."""
    # In practice, this would call an external API
    weather_data = {
        "Seoul": "Clear, 15C",
        "Busan": "Cloudy, 18C",
        "Jeju": "Rain, 16C"
    }
    return weather_data.get(city, f"Weather info for {city} not found")

# Configure agent
llm = ChatOpenAI(model="gpt-4o", temperature=0)
tools = [calculate, get_current_weather]

prompt = ChatPromptTemplate.from_messages([
    ("system", "Use tools to answer the user's questions."),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}")
])

agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# Execute agent
response = executor.invoke({
    "input": "Tell me the weather in Seoul and calculate the square root of 15"
})
print(response["output"])
# Output:
# > Entering new AgentExecutor chain...
# > Tool: get_current_weather("Seoul") -> Clear, 15C
# > Tool: calculate("15 ** 0.5") -> Result: 3.872983...
# > The current weather in Seoul is Clear, 15C, and the square root of 15 is approximately 3.87.

Memory: Maintaining Conversation Context

LLMs don’t inherently remember previous conversations. Using the Memory module, you can store conversation history and reference previous context for natural dialogue.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

# Per-session conversation history store
store = {}

def get_session_history(session_id: str):
    """Returns conversation history for a given session ID."""
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]

# Include conversation history in prompt
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a friendly AI assistant."),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}")
])

llm = ChatOpenAI(model="gpt-4o")
chain = prompt | llm

# Create chain with memory
chain_with_history = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="history"
)

# Execute conversation -- maintain context with the same session ID
config = {"configurable": {"session_id": "user-001"}}

# First question
response1 = chain_with_history.invoke(
    {"input": "My name is John"},
    config=config
)
print(response1.content)
# Output: Hello, John! How can I help you?

# Second question -- remembers previous context
response2 = chain_with_history.invoke(
    {"input": "What did I say my name was?"},
    config=config
)
print(response2.content)
# Output: You said your name is John!

RAG with LangChain

LangChain also makes it easy to build RAG (Retrieval-Augmented Generation) pipelines.

from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 1. Prepare and split documents
documents = [
    "LangChain is an LLM application development framework.",
    "LCEL stands for LangChain Expression Language.",
    "An agent is a system where the LLM selects and executes tools.",
]

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,       # Max chunk length
    chunk_overlap=50      # Overlap between chunks
)

# 2. Create vector store
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_texts(documents, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})

# 3. Build RAG chain
rag_prompt = ChatPromptTemplate.from_template("""
Answer the question based on the following context.

Context: {context}
Question: {question}
""")

def format_docs(docs):
    """Combines retrieved documents into a single string."""
    return "\n".join(doc.page_content for doc in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | rag_prompt
    | ChatOpenAI(model="gpt-4o")
    | StrOutputParser()
)

# 4. Execute RAG chain
answer = rag_chain.invoke("What is LCEL?")
print(answer)
# Output: LCEL stands for LangChain Expression Language,
#          a language for composing chains in LangChain.

For real projects, the following structure is recommended.

my-langchain-app/
├── chains/           # Chain definitions
│   ├── summary.py
│   └── rag.py
├── agents/           # Agent definitions
│   └── assistant.py
├── tools/            # Custom tools
│   ├── calculator.py
│   └── search.py
├── prompts/          # Prompt templates
│   └── templates.py
├── config.py         # Configuration (model, parameters)
├── main.py           # Entry point
└── .env              # API keys (must be in gitignore)

Practical Tips

  • Use LCEL: The | operator-based LCEL is the currently recommended approach over legacy APIs like LLMChain.
  • Leverage streaming: chain.stream() enables token-by-token streaming, greatly improving UX.
  • Debug with LangSmith: Integrating LangSmith lets you trace inputs and outputs at each step of the chain for easier debugging.
  • Adjust temperature for the task: Use 0 for fact-based answers and 0.7—1.0 for creative generation.
  • Write clear tool descriptions: The docstring in the @tool decorator is what the LLM uses to decide which tool to select.
  • Manage costs: Use callbacks to track token usage, and caching (SQLiteCache) to reduce duplicate calls for identical queries.

Was this article helpful?