← Back to DevBytes

LangChain Agents Explained: Tools, Memory, and Chains

Introduction to LangChain Agents

LangChain agents represent one of the most powerful abstractions in the LangChain ecosystem. At their core, agents are systems that use an LLM to decide which actions to take and in what order, based on the user's input. Unlike a simple chain that follows a predetermined sequence of operations, an agent dynamically reasons about what tools to call, when to call them, and how to interpret their results before formulating a final response.

Think of an agent as an autonomous decision-maker. You provide it with a goal and a set of capabilities (tools), and it figures out the path to achieve that goal. This shifts the paradigm from "hard-coded logic" to "reasoning-driven execution."

Why LangChain Agents Matter

Modern applications frequently need to interact with external APIs, databases, file systems, and other services. Hard-coding every possible interaction path becomes impossible when the sequence of steps depends on context. Agents solve this by:

In short, agents turn your LLM from a text generator into an action-taking orchestrator.

Core Concepts: The Agent Architecture

Every LangChain agent is built from three fundamental building blocks that work in concert:

Let's explore each in depth, then see how they combine into a complete agent.

Understanding Chains: The Execution Backbone

A chain in LangChain is a sequence of processing steps that transforms input into output. For agents, the chain typically follows this pattern:

User Input → Prompt Template → LLM Call → Output Parser → [Tool Call or Final Answer]

The agent's chain is more sophisticated than a simple linear chain because it includes a reasoning loop. When the LLM decides a tool is needed, the chain intercepts that decision, executes the tool, feeds the result back into the prompt, and repeats until the LLM produces a final answer.

Building a Simple Chain

Here's a basic chain that demonstrates the pattern before we add agent logic:

from langchain.chains import LLMChain
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate

llm = OpenAI(temperature=0)

prompt = PromptTemplate(
    input_variables=["question"],
    template="""You are a helpful assistant. Answer the following question
accurately and concisely.

Question: {question}
Answer:"""
)

chain = LLMChain(llm=llm, prompt=prompt)

# Execute the chain
response = chain.run(question="What is the capital of France?")
print(response)  # Paris

This is a fixed chain—it always follows the same path. An agent extends this by inserting decision points.

The Agent's Reasoning Loop (Pseudo-Chain)

The actual agent chain incorporates a loop that looks conceptually like this:

def agent_loop(user_input, tools, llm, memory):
    # 1. Build prompt with current state, tools, and history
    prompt = construct_prompt(user_input, tools, memory)
    
    # 2. Call the LLM
    response = llm(prompt)
    
    # 3. Parse the response
    if response contains a tool call:
        tool_name, tool_input = parse_tool_call(response)
        tool_result = execute_tool(tool_name, tool_input)
        # Feed result back into memory and loop again
        memory.add(tool_result)
        return agent_loop(user_input, tools, llm, memory)  # Recursive loop
    else:
        return response  # Final answer

This loop is what makes agents powerful—the LLM keeps "thinking" and acting until it's satisfied with the result.

Tools: Giving Agents Capabilities

Tools are the actionable components an agent can invoke. Each tool has:

Defining Custom Tools

LangChain provides multiple ways to define tools. Here's the most common approach using the @tool decorator:

from langchain.agents import tool
from datetime import datetime
import requests

@tool
def get_current_time(timezone: str = "UTC") -> str:
    """Returns the current date and time for a given timezone.
    Use this tool when the user asks about the current time or date.
    The timezone parameter should be a valid IANA timezone like 'America/New_York'
    or 'Europe/London'. Defaults to 'UTC' if not specified."""
    # This is a simplified example; in production you'd use a timezone library
    if timezone == "UTC":
        return f"Current UTC time: {datetime.utcnow().isoformat()}"
    return f"Requested time for timezone: {timezone} (simulated)"

@tool
def search_database(query: str) -> str:
    """Searches an internal knowledge base for information.
    Use this tool when you need factual data from the company database.
    The query should be a natural language search string."""
    # Simulated database lookup
    mock_data = {
        "revenue 2023": "Total revenue for 2023 was $45.2 million",
        "employee count": "The company currently has 1,247 employees",
        "founding date": "The company was founded on March 15, 2005"
    }
    for key in mock_data:
        if key.lower() in query.lower():
            return mock_data[key]
    return f"No results found for query: '{query}'"

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Sends an email to a specified recipient.
    Use this tool when the user asks to send an email or communicate with someone.
    Requires: to (email address), subject (email subject line),
    and body (email content)."""
    # In production, integrate with your email service (SendGrid, SMTP, etc.)
    return f"Email successfully sent to {to} with subject '{subject}'"

Notice how each tool's docstring is critically important—the agent uses it to decide whether and when to invoke the tool. Write clear, specific descriptions that include usage guidance.

Using Built-in Tools

LangChain ships with many pre-built tools for common use cases:

from langchain.agents import load_tools
from langchain.llms import OpenAI

llm = OpenAI(temperature=0)

# Load popular built-in tools
tools = load_tools(
    ["serpapi", "llm-math", "wikipedia"],
    llm=llm,
    serpapi_api_key="your-serpapi-key"  # For Google Search
)

# Tools list now contains:
# - serpapi: Google Search via SerpAPI
# - llm-math: Calculator powered by an LLM for complex math
# - wikipedia: Wikipedia search and retrieval

Built-in tools cover search engines, calculators, Wikipedia, file system operations, Python REPL, and many more. Always check the existing catalog before building custom tools.

Tool Input Validation with Pydantic

For production tools, define explicit input schemas using Pydantic models:

from pydantic import BaseModel, Field
from langchain.tools import StructuredTool

class WeatherInput(BaseModel):
    city: str = Field(description="The city name to get weather for")
    country: str = Field(
        default="US",
        description="Two-letter country code (ISO 3166-1 alpha-2)"
    )

def get_weather(city: str, country: str = "US") -> str:
    """Fetches current weather data for a given city and country."""
    # Simulated weather API call
    weather_data = {
        ("New York", "US"): "72°F, Partly Cloudy, Humidity: 65%",
        ("London", "GB"): "58°F, Overcast, Humidity: 78%",
        ("Tokyo", "JP"): "80°F, Clear, Humidity: 45%"
    }
    key = (city, country)
    return weather_data.get(key, f"Weather data unavailable for {city}, {country}")

weather_tool = StructuredTool.from_function(
    func=get_weather,
    name="get_weather",
    description="Gets current weather conditions for a city. Specify city name and optional country code.",
    args_schema=WeatherInput
)

Using StructuredTool with Pydantic schemas reduces parsing errors and helps the agent supply correct arguments.

Memory: Persisting Context Across Interactions

Without memory, each agent interaction starts from scratch—the agent has no recollection of previous conversations or tool results. Memory solves this by storing conversation history, intermediate reasoning steps, and retrieved facts.

Types of Memory in LangChain

LangChain offers several memory classes, each suited to different scenarios:

Implementing Memory with Agents

Here's how to equip an agent with conversation memory:

from langchain.agents import initialize_agent, AgentType
from langchain.memory import ConversationBufferMemory
from langchain.chat_models import ChatOpenAI

# Create the LLM
llm = ChatOpenAI(temperature=0, model="gpt-4")

# Define tools (using the ones we created earlier)
tools = [get_current_time, search_database, send_email]

# Create memory
memory = ConversationBufferMemory(
    memory_key="chat_history",
    return_messages=True  # Returns messages as objects, not strings
)

# Initialize the agent with memory
agent = initialize_agent(
    tools=tools,
    llm=llm,
    agent=AgentType.OPENAI_FUNCTIONS,  # Most reliable agent type
    memory=memory,
    verbose=True  # Shows the agent's reasoning steps
)

# First interaction
response1 = agent.run("What's the company's revenue for 2023?")
print(response1)

# Second interaction - agent remembers context from first
response2 = agent.run("Now send that revenue figure in an email to ceo@company.com")
print(response2)
# Agent will recall the revenue figure from the previous tool call

The memory_key="chat_history" parameter tells the agent where to store and retrieve the conversation history within the prompt. The agent automatically injects this history into each new prompt.

Advanced Memory: Using Summarization

For long-running agents that accumulate extensive history, use summary memory:

from langchain.memory import ConversationSummaryMemory
from langchain.chat_models import ChatOpenAI

# A separate LLM for summarization (can be cheaper/faster)
summary_llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo")

memory = ConversationSummaryMemory(
    llm=summary_llm,
    memory_key="chat_history",
    return_messages=True,
    max_token_limit=200  # Trigger summarization when buffer exceeds this
)

agent = initialize_agent(
    tools=tools,
    llm=llm,
    agent=AgentType.OPENAI_FUNCTIONS,
    memory=memory,
    verbose=True
)

The summary memory automatically compresses older messages into a concise summary, preserving semantic context while staying within token limits. This is essential for production agents that handle extended sessions.

Storing Tool Results in Memory

Agents can also explicitly store retrieved facts for later use:

from langchain.memory import ReadOnlySharedMemory
from langchain.agents import tool

# Create a shared memory store
shared_memory = ConversationBufferMemory(
    memory_key="shared_facts",
    return_messages=False
)

@tool
def remember_fact(fact: str) -> str:
    """Stores an important fact for later reference.
    Use this when the user shares information worth remembering."""
    shared_memory.save_context(
        {"input": "remembering fact"},
        {"output": fact}
    )
    return f"Stored fact: {fact}"

# The agent can now recall facts across sessions

This pattern is powerful for building agents that accumulate knowledge over time, similar to how a human assistant builds understanding of a project.

Bringing It All Together: Building a Complete Agent

Now let's construct a full-featured agent that combines custom tools, memory, and a well-configured chain. This example creates a research assistant agent capable of searching the web, doing calculations, and remembering context.

from langchain.agents import initialize_agent, AgentType
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationSummaryBufferMemory
from langchain.agents import tool
from langchain.tools import Tool
from datetime import datetime
import math

# =============================================
# Step 1: Define Custom Tools
# =============================================

@tool
def calculate_expression(expression: str) -> str:
    """Evaluates a mathematical expression.
    Use this for any calculation. The expression should be valid Python
    math syntax, e.g., '2 + 2', 'sqrt(16)', '100 * 0.15'.
    Supports: +, -, *, /, **, sqrt, sin, cos, tan, log, abs."""
    try:
        # Restricted eval with math functions available
        result = eval(expression, {"__builtins__": {}}, {
            "sqrt": math.sqrt,
            "sin": math.sin,
            "cos": math.cos,
            "tan": math.tan,
            "log": math.log,
            "abs": abs,
            "pi": math.pi,
            "e": math.e
        })
        return f"Result: {result}"
    except Exception as e:
        return f"Error evaluating expression: {str(e)}"

@tool
def get_date_info(query: str) -> str:
    """Provides date and time information.
    Use this for questions about today's date, day of week, or current time.
    The query can be 'today', 'now', 'day_of_week', or 'full_datetime'."""
    now = datetime.now()
    query_lower = query.lower()
    if "day_of_week" in query_lower or "day" in query_lower:
        return f"Today is {now.strftime('%A')}"
    elif "full" in query_lower:
        return f"Current datetime: {now.strftime('%Y-%m-%d %H:%M:%S %Z')}"
    elif "time" in query_lower or "now" in query_lower:
        return f"Current time: {now.strftime('%H:%M:%S')}"
    else:
        return f"Today's date: {now.strftime('%Y-%m-%d')}"

# =============================================
# Step 2: Configure Memory
# =============================================

memory = ConversationSummaryBufferMemory(
    llm=ChatOpenAI(temperature=0, model="gpt-3.5-turbo"),
    memory_key="chat_history",
    return_messages=True,
    max_token_limit=300,  # Summarize after 300 tokens
    max_history=5  # Keep last 5 messages in full
)

# =============================================
# Step 3: Initialize the LLM
# =============================================

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

# =============================================
# Step 4: Assemble Tools
# =============================================

tools = [calculate_expression, get_date_info]

# =============================================
# Step 5: Create the Agent
# =============================================

agent = initialize_agent(
    tools=tools,
    llm=llm,
    agent=AgentType.OPENAI_FUNCTIONS,
    memory=memory,
    verbose=True,
    handle_parsing_errors=True,
    max_iterations=8,  # Prevent infinite loops
    early_stopping_method="generate"  # Graceful fallback
)

# =============================================
# Step 6: Execute the Agent
# =============================================

# Example 1: Multi-step reasoning
response = agent.run(
    "If the diameter of Earth is 12,742 km, what is its volume in cubic km? "
    "Assume Earth is a perfect sphere."
)
print(response)
# Agent will: recognize math needed → call calculate_expression
# → compute (4/3)*pi*(12742/2)^3 → return result

# Example 2: Context-dependent follow-up
response2 = agent.run(
    "What day of the week is it today, and how many days until the weekend?"
)
print(response2)
# Agent will: call get_date_info → calculate days until Saturday
# → combine results into coherent answer

Agent Types: Choosing the Right One

LangChain offers several agent types, each with different reasoning strategies:

For new projects, start with OPENAI_FUNCTIONS—it's the most mature and reliable agent type in LangChain.

Best Practices for Production Agents

1. Write Exceptional Tool Descriptions

The tool description is the single most important factor in whether your agent uses tools correctly. Follow these guidelines:

# BAD - Vague description
@tool
def lookup(query: str) -> str:
    """Does a lookup."""
    ...

# GOOD - Specific, with usage guidance and parameter hints
@tool
def lookup_customer_by_id(customer_id: str) -> str:
    """Retrieves customer details from the CRM by customer ID.
    Use this tool when you need to find a specific customer's information
    like name, email, or account status.
    The customer_id parameter must be a valid UUID string (e.g.,
    'a1b2c3d4-e5f6-7890-abcd-ef1234567890').
    Returns a JSON string with customer fields."""
    ...

2. Set Reasonable Iteration Limits

Agents can get stuck in loops if they repeatedly call the same tool with the same input. Always set max_iterations:

agent = initialize_agent(
    ...,
    max_iterations=10,
    early_stopping_method="generate"
)

The early_stopping_method="generate" tells the agent to produce the best answer it can when it hits the iteration limit, rather than throwing an error.

3. Implement Error Handling

Tools can fail. Your agent should handle failures gracefully:

@tool
def robust_api_call(endpoint: str) -> str:
    """Makes an API call with proper error handling."""
    try:
        response = requests.get(endpoint, timeout=10)
        response.raise_for_status()
        return response.json()
    except requests.Timeout:
        return "Error: API request timed out. Try a simpler query."
    except requests.HTTPError as e:
        return f"Error: API returned status {e.response.status_code}. "
               f"Please check the endpoint URL."
    except Exception as e:
        return f"Error: Unexpected failure - {str(e)}. "
               f"Report this to the user and suggest alternatives."

Return descriptive error strings—the agent can read them and adapt its strategy.

4. Use Structured Tools for Complex Inputs

When tools require multiple parameters or specific data types, always use StructuredTool with Pydantic schemas. This dramatically reduces argument parsing errors.

5. Monitor and Log Agent Decisions

Enable verbose mode during development and implement production logging:

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Wrap agent execution with logging
def run_agent_with_logging(agent, user_input):
    logger.info(f"User input: {user_input}")
    try:
        result = agent.run(user_input)
        logger.info(f"Agent response: {result[:200]}...")
        return result
    except Exception as e:
        logger.error(f"Agent failed: {str(e)}")
        raise

6. Test Tools Independently

Before integrating tools into an agent, test them in isolation:

# Unit test your tools
def test_calculate_expression():
    result = calculate_expression("2 + 2")
    assert "4" in result
    
    result = calculate_expression("sqrt(144)")
    assert "12" in result
    
    result = calculate_expression("invalid!!")
    assert "Error" in result

# Run tests before agent integration
test_calculate_expression()

7. Choose the Right Memory Strategy

Match your memory type to your use case:

8. Validate Agent Output

Never trust agent output blindly—especially in production:

from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel

class AgentResponse(BaseModel):
    answer: str
    sources: list[str]
    confidence: float

parser = PydanticOutputParser(pydantic_object=AgentResponse)

# Use with agent to ensure structured, validated output
# This catches malformed responses before they reach users

Common Pitfalls and How to Avoid Them

Complete Working Example: Customer Support Agent

Here's a production-ready customer support agent that demonstrates all the concepts we've covered—custom tools, structured inputs, memory, error handling, and iteration limits:

from langchain.agents import initialize_agent, AgentType
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationSummaryBufferMemory
from langchain.tools import StructuredTool
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
import json

# =============================================
# Pydantic Schemas for Structured Tools
# =============================================

class OrderLookupInput(BaseModel):
    order_id: str = Field(
        description="The order ID to look up, e.g., 'ORD-12345'"
    )

class CreateTicketInput(BaseModel):
    customer_email: str = Field(
        description="Customer's email address"
    )
    issue_summary: str = Field(
        description="One-line summary of the customer's issue"
    )
    priority: str = Field(
        default="medium",
        description="Priority level: 'low', 'medium', 'high', or 'urgent'"
    )

# =============================================
# Simulated Backend Services
# =============================================

# Mock order database
ORDERS_DB = {
    "ORD-12345": {
        "status": "shipped",
        "tracking": "1Z999AA10123456784",
        "estimated_delivery": "2024-12-20",
        "items": ["Widget Pro X", "Gadget Y"],
        "total": 149.99
    },
    "ORD-67890": {
        "status": "processing",
        "tracking": None,
        "estimated_delivery": "2024-12-22",
        "items": ["SuperTool 3000"],
        "total": 89.50
    }
}

# Mock ticket system
TICKETS = []

# =============================================
# Tool Implementations
# =============================================

def lookup_order(order_id: str) -> str:
    """Looks up an order by its ID and returns full order details.
    Use this when a customer asks about their order status, tracking,
    delivery date, or items in an order."""
    order = ORDERS_DB.get(order_id.upper())
    if order:
        return json.dumps(order, indent=2)
    return f"Order {order_id} not found. Please verify the order ID."

def create_support_ticket(customer_email: str, issue_summary: str, priority: str = "medium") -> str:
    """Creates a support ticket for a customer issue.
    Use this when a customer reports a problem that cannot be resolved
    immediately and needs escalation."""
    ticket = {
        "ticket_id": f"TICK-{len(TICKETS) + 1:04d}",
        "customer_email": customer_email,
        "issue_summary": issue_summary,
        "priority": priority,
        "created_at": datetime.now().isoformat(),
        "status": "open"
    }
    TICKETS.append(ticket)
    return f"Ticket created successfully: {ticket['ticket_id']} "
           f"(Priority: {priority}). Support will respond within "
           f"{'1 hour' if priority == 'urgent' else '24 hours'}."

def check_shipping_status(tracking_number: str) -> str:
    """Checks the shipping status for a given tracking number.
    Use this when a customer wants to know where their package is."""
    # Simulated carrier lookup
    if tracking_number == "1Z999AA10123456784":
        return ("Status: In Transit - Package is at the local distribution center. "
                "Expected delivery: December 20, 2024 by 8:00 PM")
    return f"Tracking number {tracking_number} not found in carrier system."

# =============================================
# Create Structured Tools
# =============================================

tools = [
    StructuredTool.from_function(
        func=lookup_order,
        name="lookup_order",
        description="Looks up order details by order ID. Returns order status, tracking number, estimated delivery, items, and total.",
        args_schema=OrderLookupInput
    ),
    StructuredTool.from_function(
        func=create_support_ticket,
        name="create_support_ticket",
        description="Creates a support ticket for issues that need escalation. Requires customer email and issue summary.",
        args_schema=CreateTicketInput
    ),
    StructuredTool.from_function(
        func=check_shipping_status,
        name="check_shipping_status",
        description="Checks real-time shipping status using a tracking number from the carrier.",
        args_schema=None  # Simple string input
    )
]

# =============================================
# Configure Memory
# =============================================

memory = ConversationSummaryBufferMemory(
    llm=ChatOpenAI(temperature=0, model="gpt-3.5-turbo"),
    memory_key="chat_history",
    return_messages=True,
    max_token_limit=500,
    max_history=8
)

# =============================================
# Initialize Agent
# =============================================

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

agent = initialize_agent(
    tools=tools,
    llm=llm,
    agent=AgentType.OPENAI_FUNCTIONS,
    memory=memory,
    verbose=True,
    max_iterations=6,
    early_stopping_method="generate",
    handle_parsing_errors=True,
    # System prompt to guide agent behavior
    agent_kwargs={
        "system_message": """You are a helpful customer support agent for
TechCorp Inc. Be empathetic and professional. Always:
1. Greet the customer appropriately
2. Use tools to find accurate information
3. Explain what you're doing at each step
4. Summarize findings clearly
5. Ask if the customer needs anything else before ending"""
    }
)

# =============================================
# Example Conversations
# =============================================

# Scenario 1: Order inquiry
print("=== Scenario 1: Order Inquiry ===")
response = agent.run(
    "Hi, I'm trying to find out when my order ORD-12345 will arrive. "
    "Can you help me track it?"
)
print(response)

# Scenario 2: Follow-up with memory
print("\n=== Scenario 2: Follow-up ===")
response = agent.run(
    "Actually, that delivery date won't work. Can you escalate this "
    "and create a support ticket? My email is jane@example.com"
)
print(response)
# Agent remembers the order details from previous interaction

# Scenario 3: Complex multi-step
print("\n=== Scenario 3: Multi-Step ===")
response = agent.run(
    "I have two orders: ORD-12345 and ORD-67890. Which one will arrive "
    "first, and what's the total I spent across both orders?"
)
print(response)
# Agent will: lookup both orders → compare dates → calculate sum

Debugging Agents: Tracing and Observability

When agents behave unexpectedly, systematic debugging is essential. LangChain provides built-in tracing through LangSmith (formerly LangChain Hub) and verbose mode:

# Enable verbose mode for step-by-step visibility
agent = initialize_agent(
    ...,
    verbose=True  # Prints each thought, action, and observation
)

# For production observability, use LangSmith integration
import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "your-langsmith-api-key"
os.environ["LANGCHAIN_PROJECT"] = "my-agent-project"

# Now every agent run is logged with:
# - Full prompt sent to LLM
# - LLM response (including tool calls)
# - Tool execution and results
# - Timing and token usage
# - Error traces with stack traces

With tracing, you can pinpoint exactly where an agent made a wrong decision—was it a misleading tool description, an ambiguous prompt, or a tool returning unexpected data?

Conclusion

LangChain agents represent a fundamental shift in how we build LLM-powered applications. By combining chains (structured execution pipelines), tools (actionable capabilities), and memory (persistent context), agents transform language models from passive text generators into active, reasoning orchestrators capable of solving complex, multi-step problems.

The key takeaways for building effective agents are:

— Ad —

Google AdSense will appear here after approval

← Back to all articles