Welcome to Day 8 of #30DaysOfLangChain and the start of Week 2! We’ve spent the first week mastering LCEL, building robust chains, and setting up our RAG pipeline. Now, prepare to elevate your LLM applications to a new level of intelligence: Agents.

While a LangChain chain executes a predefined sequence of steps, an Agent is an autonomous decision-maker. It can reason about a problem, decide which tools to use, execute those tools, observe the results, and repeat this cycle until it reaches a solution or determines it cannot proceed. This ability to reason and act dynamically is what makes agents so powerful.

Understanding Agents: The LLM as a Reasoning Engine

At its core, a LangChain Agent consists of:

  1. An LLM: This acts as the agent’s “brain,” responsible for the reasoning step. It takes the current goal, past observations, and available tools, then decides the next Action to take or the final Answer.
  2. Tools: These are functions or capabilities that the agent can use to interact with the outside world. Tools can do anything an LLM can’t do natively or reliably (e.g., perform calculations, search the web, query a database, call an API).
  3. An Agent Executor: This is the runtime that drives the agent’s loop (thought, action, observation). It receives input from the user, passes it to the agent, executes the chosen tool, feeds the observation back, and continues until a final answer is produced.

Tools: The Agent’s Hands and Eyes

Tools are how agents extend their capabilities beyond pure text generation. They provide access to specific functions or external knowledge.

The @tool Decorator: LangChain 0.3 significantly simplified tool creation with the @tool decorator. You can now turn any Python function into a LangChain tool by simply decorating it. The function’s docstring becomes the tool’s description, making it super intuitive for the LLM to understand.

BaseTool: This is the fundamental class in LangChain for creating custom tools. You define the tool’s name, description (crucial for the LLM to understand its purpose), and the _run method that contains the tool’s logic.

create_react_agent: The ReAct Framework

Many LangChain agents, especially the ones created with create_react_agent, follow the ReAct (Reasoning and Acting) framework. This loop looks like:

  1. Thought: The LLM internally reasons about the problem, the tools available, and what step to take next.
  2. Action: Based on its thought, the LLM decides to use a specific tool with specific input.
  3. Observation: The tool is executed, and its output (the “observation”) is returned to the LLM.
  4. (Loop): The LLM receives the observation and incorporates it into its next “Thought,” continuing the cycle until it can formulate a final answer.

This iterative process allows agents to tackle complex, multi-step problems that a single LLM prompt might not be able to solve.

For more details, check out the official LangChain documentation:

Project: A Simple Agent with a Custom Tool

We’ll build a simple agent that uses a custom tool. For this demonstration, we’ll create a word_reverser tool that reverses a given string. The agent’s task will be to reverse a word using this tool.

Before you run the code:

  • Ensure Ollama is installed and running (ollama serve).
  • Pull any necessary Ollama models (e.g., llama2).
  • Ensure your OPENAI_API_KEY is set if using OpenAI models.
import os
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
from langchain_ollama import ChatOllama
from langchain.agents import AgentExecutor, create_react_agent
from langchain import tool # Correct import for @tool decorator
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

# --- Configuration ---
# LLM Provider (from Day 3)
LLM_PROVIDER = os.getenv("LLM_PROVIDER", "openai").lower()
OLLAMA_MODEL_CHAT = os.getenv("OLLAMA_MODEL_CHAT", "llama2").lower()

# --- Step 1: Define Custom Tools ---
# Use the @tool decorator to easily create a tool from a function
@tool
def word_reverser(word: str) -> str:
    """Reverses a given word or string."""
    print(f"\n--- Tool Action: Reversing '{word}' ---") # For demonstration visibility
    return word[::-1]

@tool
def character_counter(text: str) -> int:
    """Counts the number of characters in a given string."""
    print(f"\n--- Tool Action: Counting characters in '{text}' ---") # For demonstration visibility
    return len(text)

# List of tools available to the agent
tools = [word_reverser, character_counter]
print(f"Available tools: {[tool.name for tool in tools]}\n")


# --- Step 2: Initialize LLM ---
def initialize_llm(provider, model_name=None, temp=0.7):
    if provider == "openai":
        if not os.getenv("OPENAI_API_KEY"):
            raise ValueError("OPENAI_API_KEY not set for OpenAI provider.")
        return ChatOpenAI(model=model_name or "gpt-3.5-turbo", temperature=temp)
    elif provider == "ollama":
        try:
            llm = ChatOllama(model=model_name or OLLAMA_MODEL_CHAT, temperature=temp)
            llm.invoke("Hello!") # Test connection
            return llm
        except Exception as e:
            print(f"Error connecting to Ollama LLM or model '{model_name or OLLAMA_MODEL_CHAT}' not found: {e}")
            print("Please ensure Ollama is running and the specified model is pulled.")
            exit()
    else:
        raise ValueError(f"Invalid LLM provider: {provider}. Must be 'openai' or 'ollama'.")

llm = initialize_llm(LLM_PROVIDER)
print(f"Using LLM: {LLM_PROVIDER} ({llm.model_name if hasattr(llm, 'model_name') else OLLAMA_MODEL_CHAT})\n")

# --- Step 3: Create the Agent Prompt ---
# The prompt is crucial for guiding the LLM to act as an agent (ReAct style)
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful AI assistant. You have access to the following tools: {tools}. "
               "You should use these tools to answer the user's questions. "
               "If a tool is suitable, first think about what tool to use and its input. "
               "Respond in the following format:\n\n"
               "Thought: (Your reasoning about what to do)\n"
               "Action: (The tool to call, exactly as specified, with input)\n"
               "Action Input: (The input to the tool)\n"
               "Observation: (The result of the tool)\n"
               "... (this Thought/Action/Observation can repeat multiple times)\n"
               "Thought: (Final thought before providing the answer)\n"
               "Final Answer: (The ultimate answer to the user's question)\n\n"
               "If you don't need a tool, just provide a direct answer."),
    MessagesPlaceholder(variable_name="chat_history"), # For future history (optional for this simple demo)
    ("user", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad") # Important for ReAct, stores thought/action/observation
])

# --- Step 4: Create the ReAct Agent ---
agent = create_react_agent(llm, tools, prompt)
print("Agent created using create_react_agent.\n")

# --- Step 5: Create the Agent Executor ---
# The AgentExecutor runs the agent's loop
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True)
# verbose=True shows the thought/action/observation steps

# --- Step 6: Invoke the Agent with Questions ---
questions = [
    "Reverse the word 'hello'.",
    "How many characters are in the string 'LangChain is amazing'?",
    "What is the capital of Germany?", # Should not use a tool
    "Reverse the word 'python' and then count characters in the reversed word."
]

print("--- Invoking Agent ---")
for q in questions:
    print(f"\nUser Question: {q}")
    try:
        # chat_history is empty for this simple, single-turn demo
        response = agent_executor.invoke({"input": q, "chat_history": []})
        print(f"Agent Final Answer: {response['output']}")
    except Exception as e:
        print(f"Agent encountered an error: {e}")
    print("\n" + "="*70 + "\n")

Code Explanation:

  1. Custom Tools with @tool:
    • We define word_reverser and character_counter functions.
    • Applying @tool above them automatically converts them into LangChain Tool objects. The docstring of the function becomes the tool’s description, which the LLM reads to understand the tool’s purpose.
    • The tools list makes these tools available to the agent.
  2. LLM Initialization: We reuse our flexible LLM setup from Day 3. The LLM is the brain of our agent.
  3. Agent Prompt (prompt): This is crucial. It’s a ChatPromptTemplate that includes:
    • A system message instructing the LLM on its role as an agent and the exact ReAct format it should follow (Thought, Action, Action Input, Observation, Final Answer).
    • MessagesPlaceholder(variable_name="agent_scratchpad"): This is where the agent’s internal thought process (past thoughts, actions, and observations) are stored and fed back to the LLM in each loop iteration. This is vital for the ReAct framework.
  4. create_react_agent(): This function takes the LLM, the list of tools, and the specially crafted prompt to create an Agent instance. It configures the LLM to reason in the ReAct style.
  5. AgentExecutor: This is the runtime environment for the agent.
    • agent=agent: The agent we just created.
    • tools=tools: The list of tools the agent can use.
    • verbose=True: Extremely useful for debugging agents, as it prints out each “Thought,” “Action,” and “Observation” step the agent takes.
    • handle_parsing_errors=True: Helps the executor recover if the LLM generates an invalid action format.
  6. Invocation: We invoke the agent_executor with various questions. Observe the verbose output to see the agent’s reasoning process and how it utilizes the tools. Notice how it directly answers questions that don’t require tools.

This project introduces you to the exciting world of agents, allowing your LLM applications to become more dynamic and capable of solving complex problems by intelligently using external resources.

Leave a comment

I’m Arpan

I’m a Software Engineer driven by curiosity and a deep interest in Generative AI Technologies. I believe we’re standing at the frontier of a new era—where machines not only learn but create, and I’m excited to explore what’s possible at this intersection of intelligence and imagination.

When I’m not writing code or experimenting with new AI models, you’ll probably find me travelling, soaking in new cultures, or reading a book that challenges how I think. I thrive on new ideas—especially ones that can be turned into meaningful, impactful projects. If it’s bold, innovative, and GenAI-related, I’m all in.

“The future belongs to those who believe in the beauty of their dreams.”Eleanor Roosevelt

“Imagination is more important than knowledge. For knowledge is limited, whereas imagination embraces the entire world.”Albert Einstein

This blog, MLVector, is my space to share technical insights, project breakdowns, and explorations in GenAI—from the models shaping tomorrow to the code powering today.

Let’s build the future, one vector at a time.

Let’s connect