Build an AI Agent with LangChain: Complete Python Tutorial

Step-by-step guide to building a functional AI agent with LangChain in Python. Covers tools, ReAct agents, memory, chains, and deployment best practices.

Child and robot interacting with projected bubbles
Photo by Enchanted Tools on Unsplash
Doctor, girl, and robot in a medical room.
Photo by Enchanted Tools on Unsplash

Build an AI Agent with LangChain: Complete Python Tutorial

LangChain is the most popular framework for building LLM-powered AI agents in Python. It provides pre-built components for tools, memory, chains, and agent orchestration — so you can focus on your agent's logic instead of infrastructure. In this tutorial, you'll build a fully functional agent from scratch.

What You'll Learn#

  • Setting up a LangChain project from scratch
  • Creating custom tools for your agent
  • Building a ReAct agent with reasoning and action
  • Adding conversation memory for multi-turn interactions
  • Error handling and deployment considerations

Prerequisites#

Step 1: Project Setup#

Create your project and install dependencies:

mkdir my-langchain-agent && cd my-langchain-agent
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

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

Create a .env file for your API keys:

OPENAI_API_KEY=your-api-key-here

Create agent.py:

import os
from dotenv import load_dotenv

load_dotenv()

from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_react_agent
from langchain.tools import tool
from langchain import hub

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

Step 2: Create Custom Tools#

Tools give your agent the ability to interact with the real world. Each tool is a Python function decorated with @tool:

from langchain.tools import tool
from datetime import datetime
import json

@tool
def get_current_time() -> str:
    """Get the current date and time. Use when the user asks
    about today's date or current time."""
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

@tool
def calculate(expression: str) -> str:
    """Evaluate a mathematical expression. Use for any math
    calculations. Input should be a valid Python math expression.

    Args:
        expression: A mathematical expression like '2 + 2' or
            'math.sqrt(144)'
    """
    import math
    try:
        result = eval(expression, {"__builtins__": {}}, {"math": math})
        return str(result)
    except Exception as e:
        return f"Error: {e}"

@tool
def search_knowledge_base(query: str) -> str:
    """Search the internal knowledge base for information.
    Use when the user asks about company policies, products,
    or procedures.

    Args:
        query: The search query to look up
    """
    # In production, this would query a vector database
    knowledge = {
        "refund policy": "Refunds are available within 30 days "
            "of purchase. Contact support@example.com.",
        "business hours": "Monday-Friday, 9 AM to 6 PM EST. "
            "Closed on federal holidays.",
        "pricing": "Starter: $19/mo, Pro: $49/mo, "
            "Enterprise: Custom pricing."
    }

    query_lower = query.lower()
    for key, value in knowledge.items():
        if key in query_lower:
            return value

    return "No relevant information found in knowledge base."

tools = [get_current_time, calculate, search_knowledge_base]

Tool Design Best Practices#

| Practice | Why It Matters | |----------|---------------| | Clear docstrings | The LLM reads them to decide when to use each tool | | Specific input types | Reduces tool call errors | | Error handling | Prevents agent crashes on tool failures | | Return strings | LLM processes text — always return string results |

Step 3: Build the ReAct Agent#

The ReAct (Reasoning + Acting) pattern alternates between thinking and tool-calling:

from langchain.agents import AgentExecutor, create_react_agent
from langchain_core.prompts import PromptTemplate

# Define the agent prompt
prompt = PromptTemplate.from_template("""You are a helpful assistant with access to tools.

You have access to the following tools:

{tools}

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought:{agent_scratchpad}""")

# Create the agent
agent = create_react_agent(llm, tools, prompt)

# Create the executor (handles the agent loop)
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,       # Print reasoning steps
    max_iterations=5,   # Prevent infinite loops
    handle_parsing_errors=True
)

Run the Agent#

# Simple query
result = agent_executor.invoke({
    "input": "What's our refund policy and how many days is that in hours?"
})
print(result["output"])

Expected agent reasoning:

Thought: The user wants to know the refund policy and convert
the days to hours. I'll search the knowledge base first.

Action: search_knowledge_base
Action Input: refund policy
Observation: Refunds are available within 30 days of purchase.

Thought: Now I need to calculate 30 days in hours.
Action: calculate
Action Input: 30 * 24
Observation: 720

Thought: I now know the final answer.
Final Answer: Our refund policy allows refunds within 30 days
(720 hours) of purchase. Contact support@example.com.

Step 4: Add Conversation Memory#

Without memory, each query starts from scratch. Add memory for multi-turn conversations:

from langchain.memory import ConversationBufferWindowMemory
from langchain.agents import AgentExecutor, create_react_agent
from langchain_core.prompts import PromptTemplate

# Memory stores the last 10 exchanges
memory = ConversationBufferWindowMemory(
    memory_key="chat_history",
    return_messages=True,
    k=10
)

# Updated prompt with memory
prompt_with_memory = PromptTemplate.from_template("""You are a helpful assistant.

Previous conversation:
{chat_history}

You have access to the following tools:

{tools}

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought:{agent_scratchpad}""")

# Rebuild agent with memory
agent_with_memory = create_react_agent(llm, tools, prompt_with_memory)

agent_executor_with_memory = AgentExecutor(
    agent=agent_with_memory,
    tools=tools,
    memory=memory,
    verbose=True,
    max_iterations=5,
    handle_parsing_errors=True
)

# Multi-turn conversation
agent_executor_with_memory.invoke({"input": "What's the pricing?"})
agent_executor_with_memory.invoke({"input": "Which plan do you recommend for a startup?"})
# The agent remembers the pricing info from the first query

Memory Types#

| Memory Type | Use Case | Trade-off | |-------------|----------|-----------| | ConversationBufferMemory | Short conversations | Uses more tokens | | ConversationBufferWindowMemory | Most use cases | Forgets old messages | | ConversationSummaryMemory | Long conversations | Loses detail | | ConversationTokenBufferMemory | Token-limited contexts | Truncates by tokens |

Step 5: Structured Output#

For production agents, you often need structured JSON output:

from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field

class AgentResponse(BaseModel):
    answer: str = Field(description="The answer to the user's question")
    confidence: float = Field(description="Confidence score 0-1")
    sources: list[str] = Field(description="Data sources used")

parser = JsonOutputParser(pydantic_object=AgentResponse)

# Add format instructions to your prompt
format_instructions = parser.get_format_instructions()

Step 6: Error Handling & Production Readiness#

import logging

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

def run_agent_safely(query: str) -> dict:
    """Run the agent with error handling and timeouts."""
    try:
        result = agent_executor.invoke(
            {"input": query},
            config={"max_execution_time": 30}  # 30 second timeout
        )
        return {
            "status": "success",
            "output": result["output"]
        }
    except Exception as e:
        logger.error(f"Agent error for query '{query}': {e}")
        return {
            "status": "error",
            "output": "I encountered an error processing your "
                     "request. Please try rephrasing."
        }

Production Checklist#

  • [ ] Set max_iterations to prevent infinite loops
  • [ ] Add timeout limits for tool calls
  • [ ] Handle parsing errors gracefully
  • [ ] Log all agent reasoning steps
  • [ ] Monitor token usage and costs
  • [ ] Add rate limiting for external tool calls
  • [ ] Test with adversarial inputs

Complete Working Example#

"""Complete LangChain agent example."""
import os
from dotenv import load_dotenv
from datetime import datetime

load_dotenv()

from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_react_agent
from langchain.tools import tool
from langchain.memory import ConversationBufferWindowMemory
from langchain_core.prompts import PromptTemplate

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

# Tools
@tool
def get_current_time() -> str:
    """Get the current date and time."""
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

@tool
def calculate(expression: str) -> str:
    """Evaluate a math expression. Input: valid Python math."""
    import math
    try:
        return str(eval(expression, {"__builtins__": {}}, {"math": math}))
    except Exception as e:
        return f"Error: {e}"

tools = [get_current_time, calculate]

# Prompt
prompt = PromptTemplate.from_template("""You are a helpful assistant.

Chat history: {chat_history}

Tools: {tools}

Format:
Question: {input}
Thought: think about what to do
Action: one of [{tool_names}]
Action Input: the input
Observation: the result
Thought: I now know the final answer
Final Answer: the answer

Question: {input}
Thought:{agent_scratchpad}""")

# Memory
memory = ConversationBufferWindowMemory(
    memory_key="chat_history", return_messages=True, k=10
)

# Agent
agent = create_react_agent(llm, tools, prompt)
executor = AgentExecutor(
    agent=agent, tools=tools, memory=memory,
    verbose=True, max_iterations=5, handle_parsing_errors=True
)

# Run
if __name__ == "__main__":
    while True:
        query = input("\nYou: ")
        if query.lower() in ("quit", "exit"):
            break
        result = executor.invoke({"input": query})
        print(f"\nAgent: {result['output']}")

Common Mistakes to Avoid#

  1. No max_iterations: Without a limit, agents can loop forever, burning tokens
  2. Vague tool descriptions: The LLM relies on docstrings to pick tools — be specific
  3. Missing error handling: Tools fail in production — always catch exceptions
  4. Too many tools: Start with 3-5 tools, add more only when needed
  5. Ignoring verbose output: During development, always set verbose=True to debug reasoning

Next Steps#


Frequently Asked Questions#

LangChain vs. LangGraph — which should I use?#

LangChain is best for linear agent workflows with tools and memory. LangGraph (part of the LangChain ecosystem) is for complex stateful graphs with cycles, conditional routing, and human-in-the-loop patterns. Start with LangChain, migrate to LangGraph when your agent needs branching logic.

How much does it cost to run a LangChain agent?#

Costs depend on LLM calls per query. A typical ReAct agent makes 2-5 LLM calls per task. With GPT-4o at ~$5/1M input tokens, a simple agent costs $0.01-0.05 per interaction. Complex agents with many tool calls can reach $0.10-0.50 per task.

Can I use local LLMs instead of OpenAI?#

Yes. LangChain supports Ollama, llama.cpp, vLLM, and other local inference providers. Replace ChatOpenAI with ChatOllama and point it to your local model. Note that smaller models may struggle with ReAct reasoning — test thoroughly.

How do I deploy a LangChain agent?#

Common patterns: wrap the agent in a FastAPI server, deploy with Docker, and expose via REST API. LangServe (official LangChain deployment tool) provides built-in REST endpoints. For serverless, use AWS Lambda or Google Cloud Functions with lightweight dependencies.