← Back to list

I Built an AI Agent in Python — Here’s What No One Tells You

A brutally honest, step-by-step walkthrough for 2026 — with real code, real mistakes, and the lessons that actually matter.

KoshurAI · 2026-03-10 12:52 · 0 claps · 8.0 min read paywalled
#langchain #large-language-models #machine-learning #artificial-intelligence #ai-agent-tutorial
Open on Medium ↗
Wiki topics: AGT · AI Agents ML · Machine Learning AI · AI · General EDU · Education & Learning

I Built an AI Agent in Python — Here’s What No One Tells You

A brutally honest, step-by-step walkthrough for 2026 — with real code, real mistakes, and the lessons that actually matter.

TL;DR: I spent 3 weeks building and deploying a fully functional AI agent using Python, LangChain, and LangGraph. This article is everything I wish I’d known before I started — the setup, the code, the gotchas, and the production pitfalls no tutorial covers.

Why Everyone Is Talking About AI Agents (And Why Most Tutorials Miss the Point)

You’ve probably seen the hype. “AI agents will replace software engineers.” “Build an autonomous AI in 10 minutes.” Every LinkedIn post promises a revolutionary breakthrough, and every YouTube thumbnail shows a robot taking over the world.

Here’s the truth: AI agents are genuinely powerful — but not for the reasons most people think.

An AI agent isn’t magic. It’s a system where a large language model (LLM) can reason through a problem, decide which tool to use, call that tool, observe the result, and decide what to do next — all without you hardcoding every step. That’s a real paradigm shift. But it requires understanding the fundamentals, not just copy-pasting boilerplate.

I’m a data scientist who went from “what even is an agent?” to shipping a production-grade research assistant agent in three weeks. This is the article I needed when I started.

What You’ll Build (And Why This Specific Project)

By the end of this tutorial, you’ll have a Research Assistant Agent that can:

  • 🔍 Search the web for real-time information
  • 📄 Scrape and parse web content
  • 🧠 Synthesize findings into a coherent summary
  • 💾 Save results to a file

Why this project? Because it touches every core concept you need: tools, memory, state management, and output formatting. It’s not a toy. It’s the kind of agent you’d actually use.

Tech stack:

  • Python 3.11+
  • LangChain (agent orchestration)
  • LangGraph (stateful workflows)
  • OpenAI GPT-4o (or any supported LLM)
  • DuckDuckGo Search (free, no API key needed)
  • BeautifulSoup4 (web scraping)

The Part Every Tutorial Skips: What Is an Agent, Really?

Before a single line of code, let’s nail the mental model. Most tutorials jump straight to pip install langchain and leave you confused when things break.

An AI agent has four core components:

1. The Brain (LLM): Decides what to do next based on the current state of the conversation and available tools.

2. The Tools: Python functions the LLM can call. Think of them as the agent’s hands — the ability to actually do something in the world.

3. The Memory / State: Everything the agent knows — conversation history, tool outputs, and intermediate results.

4. The Executor: The loop that runs the agent: give the LLM the current state → get a decision → execute it → update state → repeat until done.

Here’s the key insight that took me too long to understand: The LLM doesn’t run your tools. It just tells the executor which tool to run and with what inputs. The executor runs the tool and feeds the result back to the LLM. This distinction matters enormously when debugging.

Step 1: Environment Setup

Start clean. Create a virtual environment and install dependencies:

# Create virtual environment
python -m venv agent-env
source agent-env/bin/activate  # On Windows: agent-env\Scripts\activate

# Install dependencies
pip install langchain langchain-community langgraph langchain-openai
pip install duckduckgo-search beautifulsoup4 requests python-dotenv pydantic

Create a .env file in your project root:

OPENAI_API_KEY=your_openai_api_key_here

Note: This tutorial uses OpenAI, but LangChain supports Anthropic, Google Gemini, Groq, and more. Swap langchain-openai for langchain-anthropic and change the model import — everything else stays identical.

Your folder structure:

research-agent/
├── .env
├── main.py
├── tools.py
└── agent.py

Step 2: Build Your Tools

Tools are just Python functions with clear docstrings. The docstring is critical — it’s how the LLM decides when to use each tool.

Create tools.py:

import requests
from datetime import datetime
from bs4 import BeautifulSoup
from langchain_community.tools import DuckDuckGoSearchRun
from langchain.tools import Tool
from langchain_core.tools import tool

# --- Tool 1: Web Search ---
search_engine = DuckDuckGoSearchRun()

search_tool = Tool(
    name="web_search",
    func=search_engine.run,
    description=(
        "Searches the internet for current information on any topic. "
        "Use this when you need up-to-date facts, news, or data. "
        "Input should be a clear, concise search query."
    )
)

# --- Tool 2: Website Scraper ---
@tool
def scrape_website(url: str) -> str:
    """
    Fetches and extracts the main text content from a given URL.
    Use this when you have a specific webpage URL and need its full content.
    Returns cleaned text from the page body.
    """
    try:
        headers = {"User-Agent": "Mozilla/5.0 (compatible; ResearchBot/1.0)"}
        response = requests.get(url, headers=headers, timeout=10)
        response.raise_for_status()

        soup = BeautifulSoup(response.text, "html.parser")

        # Remove noise elements
        for tag in soup(["script", "style", "nav", "footer", "header", "aside"]):
            tag.decompose()

        text = soup.get_text(separator="\n", strip=True)

        # Trim to avoid token overflow
        return text[:4000] if len(text) > 4000 else text

    except requests.RequestException as e:
        return f"Error fetching URL: {str(e)}"

# --- Tool 3: Save Results ---
@tool
def save_research(content: str, filename: str = "research_output.txt") -> str:
    """
    Saves research findings to a text file.
    Use this when the user asks to save or export the results.
    Input: the content to save and an optional filename.
    """
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    output = f"Research Report\nGenerated: {timestamp}\n{'='*50}\n\n{content}"

    with open(filename, "w", encoding="utf-8") as f:
        f.write(output)

    return f"✅ Research saved to '{filename}' successfully."

# Export tools list
tools = [search_tool, scrape_website, save_research]

The #1 mistake beginners make with tools: Writing vague docstrings like "Searches the web". The LLM reads these descriptions to decide which tool to use. Be specific. Explain when to use the tool, not just what it does.

Step 3: Build the Agent with LangChain

Now the core. Create agent.py:

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.agents import create_react_agent, AgentExecutor
from langchain_core.prompts import PromptTemplate
from tools import tools

load_dotenv()

# Initialize the LLM
llm = ChatOpenAI(
    model="gpt-4o",
    temperature=0,      # 0 = deterministic; better for tool-calling agents
    streaming=True      # Stream tokens for better UX
)

# The ReAct prompt template
# ReAct = Reasoning + Acting. The agent thinks out loud before each action.
REACT_TEMPLATE = """You are a research assistant agent. You help users find, 
analyze, and summarize information from the web.

You have access to the following tools:
{tools}

Use the following format EXACTLY:

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}"""

prompt = PromptTemplate.from_template(REACT_TEMPLATE)

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

# Create the executor — this is the loop that runs the agent
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,           # Show reasoning steps
    max_iterations=10,      # Prevent infinite loops
    max_execution_time=60,  # 60-second timeout
    handle_parsing_errors=True  # Gracefully handle LLM output issues
)

Step 4: Upgrade to LangGraph for Production

Here’s what no tutorial tells you: LangChain’s simple AgentExecutor is great for prototypes. But the moment you need:

  • Conditional branching
  • Human-in-the-loop checkpoints
  • Persistent memory across sessions
  • Multi-agent orchestration

…you need LangGraph. It models your agent as a stateful graph where nodes are actions and edges are decisions.

Create a LangGraph version in agent.py:

from typing import TypedDict, Annotated, Sequence
import operator
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from tools import tools

# 1. Define State — this flows through every node in the graph
class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]

# 2. Initialize LLM and bind tools
llm = ChatOpenAI(model="gpt-4o", temperature=0)
llm_with_tools = llm.bind_tools(tools)

# 3. Define the reasoning node
def should_continue(state: AgentState) -> str:
    """Router: decide whether to call a tool or end the conversation."""
    last_message = state["messages"][-1]

    # If the LLM called a tool, route to the tool node
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        return "tools"

    # Otherwise, we're done
    return END

def call_model(state: AgentState) -> AgentState:
    """The LLM node — ask the model what to do next."""
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

# 4. Build the graph
workflow = StateGraph(AgentState)

# Add nodes
workflow.add_node("agent", call_model)
workflow.add_node("tools", ToolNode(tools))

# Define the flow
workflow.set_entry_point("agent")
workflow.add_conditional_edges("agent", should_continue)
workflow.add_edge("tools", "agent")  # After a tool runs, go back to agent

# Compile the graph
graph = workflow.compile()

This is the architecture that powers production AI systems. The agent reasons → calls a tool → observes the result → reasons again → until it has a final answer.

Step 5: Build the Main Interface

Create main.py:

from dotenv import load_dotenv
from langchain_core.messages import HumanMessage
from agent import graph  # Using the LangGraph version

load_dotenv()

def run_research_agent(query: str) -> str:
    """Run the research agent on a given query."""

    print(f"\n🔍 Research Query: {query}")
    print("=" * 60)

    # Initialize state with the user's message
    initial_state = {
        "messages": [HumanMessage(content=query)]
    }

    # Stream the agent's execution step by step
    final_response = ""

    for step in graph.stream(initial_state, stream_mode="updates"):
        for node_name, node_output in step.items():
            if node_name == "agent":
                messages = node_output.get("messages", [])
                for msg in messages:
                    if hasattr(msg, "tool_calls") and msg.tool_calls:
                        for call in msg.tool_calls:
                            print(f"\n🔧 Calling tool: {call['name']}")
                            print(f"   Input: {call['args']}")

            elif node_name == "tools":
                messages = node_output.get("messages", [])
                for msg in messages:
                    print(f"\n✅ Tool result received ({len(str(msg.content))} chars)")

    # Get final answer
    final_state = graph.invoke(initial_state)
    final_message = final_state["messages"][-1]
    return final_message.content

if __name__ == "__main__":
    # Example queries
    queries = [
        "What are the most important AI developments in the last 30 days?",
        "Research the pros and cons of RAG vs fine-tuning for LLMs and save the results.",
    ]

    for query in queries:
        result = run_research_agent(query)
        print(f"\n📋 Final Answer:\n{result}")
        print("\n" + "=" * 60 + "\n")

What No One Tells You: The 5 Real-World Gotchas

After 3 weeks of building, here are the things that actually hurt me:

1. Your tool docstrings ARE your prompt engineering

The LLM uses docstrings to decide which tool to use and when. Vague descriptions = wrong tool calls = broken agents. Invest 80% of your “prompting” effort here.

2. temperature=0 is non-negotiable for tool-calling agents

Higher temperatures introduce randomness into tool selection and argument generation. For agents, you want deterministic reasoning. Always set temperature=0.

3. Max iterations will save your AWS bill

Without max_iterations, a confused agent can loop indefinitely, burning tokens and money. Set it to 10 for most use cases. Add a timeout too.

4. Handle parsing errors or your agent will silently fail

LLMs occasionally produce malformed output. handle_parsing_errors=True in AgentExecutor tells the agent to try again instead of crashing. In production, log these failures.

5. LangGraph > AgentExecutor for anything beyond prototypes

The simple executor is great for learning. But LangGraph gives you observability, human-in-the-loop, persistence, and the ability to build multi-agent systems. Start with the executor; migrate to LangGraph before shipping.

Running Your Agent

bash

python main.py

Expected output:

🔍 Research Query: What are the most important AI developments in the last 30 days?
============================================================
🔧 Calling tool: web_search
   Input: {'query': 'most important AI developments March 2026'}
✅ Tool result received (1842 chars)
🔧 Calling tool: web_search
   Input: {'query': 'latest LLM releases breakthroughs 2026'}
✅ Tool result received (2103 chars)
📋 Final Answer:
Here are the key AI developments from the past 30 days...

Next Steps: Where to Go From Here

You’ve built a working AI agent. Here’s how to level it up:

Add Memory: Use LangGraph’s built-in checkpointing to give your agent persistent memory across sessions. One line of code: graph = workflow.compile(checkpointer=MemorySaver()).

Go Multi-Agent: The architecture you learned here scales directly to multi-agent systems where specialized agents collaborate on complex tasks. That’s the topic of my next article.

Add Guardrails: Use LangChain middleware to add PII detection, content filtering, or human-in-the-loop verification before critical actions.

Deploy It: Wrap your agent in a FastAPI endpoint and deploy it as a microservice. Your research assistant becomes an API any application can call.

The Bottom Line

AI agents are not magic. They’re an elegant architecture: an LLM that reasons, tools it can call, and a loop that connects them. Once you internalize that mental model, everything else follows.

The code in this tutorial is production-ready and the patterns scale. Build this, break it, fix it, and you’ll understand more about modern AI systems than 90% of the people posting LinkedIn hot takes.

Found this useful? Hit the clap button 50 times (yes, you can do that on Medium). It genuinely helps this reach more developers who need it.


메타데이터
post_id
ed2d01e1b4ce
slug
i-built-an-ai-agent-in-python-heres-what-no-one-tells-you-ed2d01e1b4ce
url
https://medium.com/@koshurai/i-built-an-ai-agent-in-python-heres-what-no-one-tells-you-ed2d01e1b4ce
canonical_url
https://medium.com/@koshurai/i-built-an-ai-agent-in-python-heres-what-no-one-tells-you-ed2d01e1b4ce
author_url
https://medium.com/@koshurai
status
ok
fetched_at
2026-06-22 07:15:07