LangChain AI Agent Tutorial: Build Reasoning Agents from Scratch

langchain
ai agents
tutorial
python
langgraph

LangChain has become the go‑to framework for turning large language models into practical AI agents that can reason, act, and interact with the world. This tutorial walks you through the essential concepts, shows you how to wire up tools, and gives you a complete, runnable example you can extend for your own projects. Whether you’re new to LangChain or looking to solidify your understanding, the steps below provide a clear path from a simple prompt‑response loop to a production‑ready agent.

Why LangChain for AI Agents?

At its core, LangChain separates the reasoning engine (the language model) from the action layer (tools, APIs, memory). This separation lets you:

  • Swap models without rewriting agent logic
  • Add or remove capabilities by plugging in new tools
  • Persist conversation state across turns with checkpointers
  • Inspect and debug every step using LangSmith

Because the framework is model‑agnostic, you can start with a quick prototype using OpenAI’s GPT‑4o and later switch to a local Ollama model or an Anthropic Claude variant with only a one‑line change.

Core Concepts You Need to Know

Before diving into code, familiarize yourself with the building blocks that LangChain provides:

ComponentRole
ModelThe LLM that generates text and decides which tool to call.
ToolsPython functions (decorated with @tool) that the model can invoke to fetch data, run calculations, or interact with external systems.
System PromptInstructions that shape the agent’s personality and behavior (e.g., “You are a helpful assistant that always cites sources”).
MemoryA mechanism for storing short‑term or long‑term context so the agent remembers earlier exchanges.
MiddlewarePluggable components that add features like human‑in‑the‑loop approval, PII filtering, or automatic summarization.
LangGraphThe low‑level orchestration layer that gives agents durability, checkpointing, and the ability to run sub‑agents or complex workflows.

When you combine these pieces, you get an agent that follows the ReAct (Reason‑Act) loop: the model reasons about what to do, selects a tool, executes it, observes the result, and repeats until the task is complete.

Setting Up Your Environment

Start by creating a fresh Python environment and installing the core packages:

python -m venv langchain-env
source langchain-env/bin/activate   # Windows: langchain-env\Scripts\activate
pip install langchain langchain-openai langgraph langchain-community

You’ll also need an API key for the model you plan to use. For OpenAI, add it to a .env file:

OPENAI_API_KEY=sk-...

Load the variables in your script with from dotenv import load_dotenv; load_dotenv().

Building a Minimal ReAct Agent

The fastest way to get a working agent is with LangChain’s create_agent helper. Below is a self‑contained example that equips the model with a weather tool and a simple calculator.

from langchain.agents import create_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv

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

@tool
def get_weather(city: str) -> str:
    """Return a fake weather report for a given city."""
    # In a real project you would call a weather API here.
    return f"The weather in {city} is sunny with a high of 22°C."

@tool
def multiply(a: float, b: float) -> float:
    """Multiply two numbers."""
    return a * b

tools = [get_weather, multiply]

agent = create_agent(
    model=llm,
    tools=tools,
    system_prompt="You are a helpful assistant. Use tools when needed.",
)

# Invoke the agent
result = agent.invoke({
    "messages": [{"role": "user", "content": "What's the weather in Paris and what is 6 times 7?"}]
})
print(result["messages"][-1].content)

When you run this snippet, the model first decides it needs the weather tool, calls get_weather, observes the sunny forecast, then determines it also needs the multiplication tool, calls multiply, and finally combines both pieces of information into a natural‑language answer.

What’s Happening Under the Hood?

create_agent builds a LangGraph‑based ReAct agent automatically. The flow looks like this:

  1. User message enters the agent’s state.
  2. The LLM receives the message plus the descriptions of all available tools.
  3. If the LLM decides a tool is required, it outputs a tool call (a structured JSON‑like object).
  4. The agent executes the corresponding Python function and stores the result as a ToolMessage.
  5. The LLM is called again with the full conversation history (including the tool result) to produce the final answer.
  6. Steps 3‑5 repeat until the LLM returns a plain text response with no further tool calls.

This loop gives the agent the ability to chain multiple tool uses, reason about intermediate results, and adapt its plan on the fly.

Adding Memory for Contextual Conversations

A stateless agent forgets everything after each turn, which feels unnatural for a chat‑style experience. LangChain lets you plug in a checkpointer that stores the message history. The simplest option for local development is InMemorySaver.

from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.utils.uuid import uuid7

memory = InMemorySaver()
config = {"configurable": {"thread_id": str(uuid7())}}

result = agent.invoke({
    "messages": [{"role": "user", "content": "What's the weather in Tokyo?"}]
}, config=config)

# Follow‑up question in the same conversation
result = agent.invoke({
    "messages": [{"role": "user", "content": "And what about tomorrow?"}]
}, config=config)

Because the same thread_id is reused, the agent remembers that we asked about Tokyo and can refer back to that information if needed. For production, you’d replace InMemorySaver with a persistent backend such as SqliteSaver or a remote store managed by LangSmith.

Equipping the Agent with Real‑World Tools

The true power of agents emerges when tools connect to live data sources. Below are three common patterns you’ll encounter:

1. API‑Wrapping Tools

Wrap any HTTP endpoint with a function that returns a string. Use the @tool decorator so the LLM knows when to call it.


@tool
def fetch_news(topic: str) -> str:
    """Get the latest headlines for a topic using NewsAPI."""
    url = f"https://newsapi.org/v2/everything?q={topic}&apiKey={os.getenv('NEWSAPI_KEY')}"
    data = requests.get(url).json()
    headlines = [article["title"] for article in data.get("articles", [])[:3]]
    return "\n".join(headlines) or "No news found."

2. Retrieval‑Augmented Generation (RAG) Tools

When you need to search over private documents, turn a vector store into a tool.

from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain.tools import tool

embeddings = OpenAIEmbeddings()
vectorstore = FAISS.load_local("faiss_index", embeddings)

@tool
def search_docs(query: str) -> str:
    """Return the top‑matching paragraph from internal documentation."""
    docs = vectorstore.similarity_search(query, k=1)
    return docs[0].page_content if docs else "No relevant information."

3. Computation and Code Execution Tools

Agents can safely run Python snippets or shell commands when you sandbox them.

from langchain_experimental.tools import PythonREPLTool

python_repl = PythonREPLTool()

Remember to expose only the tools you truly need and to validate inputs—especially when allowing arbitrary code execution.

Using Middleware for Safety and Flexibility

Middleware lets you inject cross‑cutting concerns without cluttering your core logic. Some useful built‑in options include:

  • HumanInTheLoopMiddleware – pauses execution before a tool runs, asking a human to approve or edit the proposed action. Ideal for actions that modify data or incur cost.
  • ModelRetryMiddleware – automatically retries failed model calls (e.g., due to rate limits).
  • SummarizationMiddleware – compresses the conversation history when it approaches the model’s token limit.
  • PIIMiddleware – scrubs personally identifiable information from tool outputs before they reach the LLM.

To add middleware, simply pass a list to create_agent:

from langchain.agents.middleware import HumanInTheLoopMiddleware, ModelRetryMiddleware

agent = create_agent(
    model=llm,
    tools=tools,
    system_prompt="You are a cautious assistant.",
    middleware=[
        HumanInTheLoopMiddleware(interrupt_on={"fetch_news": True}),
        ModelRetryMiddleware(max_retries=2),
    ],
)

Observability with LangSmith

As your agent grows more complex, you’ll want to trace each step, measure latency, and spot inefficiencies. LangChain’s companion platform, LangSmith, integrates seamlessly:

  1. Sign up for a free account at smith.langchain.com.
  2. Create a project and copy the API key.
  3. Set the environment variables:
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=your‑key
LANGCHAIN_PROJECT=my‑agent‑tutorial

Every call to agent.invoke will now send a detailed trace to LangSmith, showing the LLM prompts, tool calls, token usage, and timing. You can use this view to debug why a particular tool wasn’t selected or to optimize slow steps.

Deploying Your Agent as a Service

Once you’re satisfied with the agent locally, wrap it in a lightweight web framework (FastAPI works well) and deploy it to any cloud provider. Below is a minimal FastAPI endpoint that accepts a JSON payload and returns the agent’s reply.

from fastapi import FastAPI, Body
from pydantic import BaseModel

app = FastAPI()

class ChatRequest(BaseModel):
    message: str

@app.post("/chat")
def chat(req: ChatRequest):
    result = agent.invoke({"messages": [{"role": "user", "content": req.message}]}, config=config)
    return {"reply": result["messages"][-1].content}

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

Deploy the container to a platform like Sevalla, Render, Fly.io, or a Kubernetes cluster. Remember to keep your .env file (or the platform’s secret manager) out of the repository.

Best Practices for Reliable Agents

Drawing from the patterns seen across the sources, keep these guidelines in mind:

  • Start small – Begin with one or two tools, verify the loop works, then expand.
  • Document tool behavior – The docstring you provide to @tool is the primary way the LLM learns when to use the tool. Be explicit about inputs, outputs, and side effects.
  • Limit tool exposure – Only give the agent the capabilities it truly needs; this reduces the risk of unwanted actions or cost spikes.
  • Use checkpointers – Persistent memory makes multi‑turn interactions feel natural and enables features like conversation resumption.
  • Leverage middleware – Rather than baking safety checks into each tool, use declarative middleware for retries, human approval, and PII filtering.
  • Monitor with LangSmith – Traces reveal hidden bottlenecks and help you tune temperature, retry policies, or tool selection thresholds.
  • Test edge cases – What happens if a tool returns an error or empty result? Guard your agent with fallback logic or a clarifying question to the user.

Extending the Example: A Travel‑Planning Agent

To solidify the concepts, here’s a slightly more elaborate agent that helps a user plan a short trip. It combines a weather tool, a currency‑conversion tool, and a simple itinerary builder.

@tool
def convert_currency(amount: float, from_cur: str, to_cur: str) -> float:
    """Convert an amount from one currency to another using a fixed rate."""
    rates = {"USD": {"EUR": 0.92, "GBP": 0.79},
             "EUR": {"USD": 1.09, "GBP": 0.86},
             "GBP": {"USD": 1.27, "EUR": 1.16}}
    return amount * rates.get(from_cur, {}).get(to_cur, 1.0)

@tool
def suggest_itinerary(city: str, days: int) -> str:
    """Return a terse, day‑by‑day itinerary for a city."""
    # In a real app you could query a knowledge base or LLM with a prompt.
    return f"Day 1: Explore {city} center.\nDay 2: Museum day.\nDay 3: Excursion outside {city}."

travel_tools = [get_weather, convert_currency, suggest_itinerary]
travel_agent = create_agent(
    model=llm,
    tools=travel_tools,
    system_prompt="You are a travel‑planning assistant. Use tools to give practical advice.",
)

# Example query
reply = travel_agent.invoke({
    "messages": [{"role": "user",
                  "content": "I want to spend 2 days in Lisbon. What's the weather like and how much is 100 USD in EUR?"}]
}, config=config)
print(reply["messages"][-1].content)

The agent will first call get_weather for Lisbon, then convert_currency for the money question, and finally may invoke suggest_itinerary if the user asks for activities. The final answer weaves together all three pieces of information.

Wrapping Up

LangChain removes much of the boilerplate that used to accompany agent development. By declaratively defining tools, supplying a clear system prompt, and optionally adding memory or middleware, you can turn any LLM into a capable reasoning engine that interacts with APIs, databases, file systems, or even runs code. The framework’s seamless integration with LangGraph gives you durability and the ability to build sophisticated multi‑agent workflows without leaving the Python ecosystem.

Whether you’re building a customer‑support bot, a data‑analysis assistant, or a personal travel planner, the patterns shown here scale from a quick prototype to a production service. Install the packages, experiment with the examples, and let LangChain handle the heavy lifting while you focus on the unique logic of your application.

Happy building!

Share this post:
LangChain AI Agent Tutorial: Build Reasoning Agents from Scratch