Skip to content

AI Agents

An AI agent is an LLM that can observe, reason, act, and reflect in a loop — not just answer a single question. Agents use tools, maintain memory, plan multi-step tasks, and self-correct. This sheet covers what agents are, how they work, and how to build them reliably.

Table of Contents

Installation / Setup

pip install langgraph langchain-openai    # or: uv add langgraph langchain-openai

Core Syntax

What is an agent?

Component Role
LLM The "brain" — reasons about what to do
Tools Actions the agent can take (API calls, code exec, search)
Memory Context from past interactions (conversation, learned facts)
Planning Strategy for breaking down tasks (ReAct, plan-and-execute)
Environment External world the agent operates in

The agent loop

graph TD
    A[User Input] --> B{Observe}
    B --> C[Reason / Plan]
    C --> D{Need tool?}
    D -->|Yes| E[Select Tool]
    E --> F[Execute Tool]
    F --> G[Observe Result]
    G --> C
    D -->|No| H[Generate Answer]
    H --> I[Return to User]
    style A fill:#4CAF50,color:#fff
    style I fill:#2196F3,color:#fff
    style D fill:#FF9800,color:#fff

Agent types

Type Description When to use
ReAct Reason → Act → Observe loop Most common; general-purpose
Plan-and-execute Plan all steps, then execute Complex multi-step tasks
Reflection Generate → critique → improve High-quality output needed
Tool-use Single tool call, no loop Simple tool invocations
Multi-agent Multiple agents collaborate Specialized sub-tasks

ReAct pattern (most common)

graph LR
    A[Thought] --> B[Action]
    B --> C[Observation]
    C --> A
    A --> D[Final Answer]
Thought: I need to find the current stock price of AAPL.
Action: lookup_stock(ticker="AAPL")
Observation: AAPL is trading at $195.50
Thought: I have the price. I can answer now.
Final Answer: AAPL is currently trading at $195.50.

Common Operations

Minimal agent (LangGraph)

from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """Search the web for information."""
    return f"Results for '{query}': ..."

@tool
def calculator(expression: str) -> str:
    """Evaluate a math expression."""
    return str(eval(expression))

llm = ChatOpenAI(model="gpt-4o")
agent = create_react_agent(llm, [search, calculator])

result = agent.invoke({"messages": [("human", "What's 15% of AAPL's price?")]})
print(result["messages"][-1].content)

Agent with custom loop

from langchain_core.messages import HumanMessage, AIMessage, ToolMessage

def agent_loop(query, llm, tools, max_steps=10):
    messages = [HumanMessage(content=query)]
    tool_map = {t.name: t for t in tools}

    for step in range(max_steps):
        response = llm.bind_tools(tools).invoke(messages)
        messages.append(response)

        if not response.tool_calls:
            return response.content    # final answer

        for tc in response.tool_calls:
            result = tool_map[tc["name"]].invoke(tc["args"])
            messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))

    return "Max steps reached"

Observation management

# truncate long tool outputs to prevent context overflow
def truncate_observation(result, max_chars=2000):
    if len(result) > max_chars:
        return result[:max_chars] + "\n... [truncated]"
    return result

Practical Examples

Example 1 — Research agent

from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def web_search(query: str) -> str:
    """Search the web for current information."""
    # in production, use a real search API
    return f"Search results for: {query}"

@tool
def read_file(path: str) -> str:
    """Read a file from the local filesystem."""
    with open(path) as f:
        return f.read()

agent = create_react_agent(
    ChatOpenAI(model="gpt-4o"),
    [web_search, read_file],
    state_modifier="You are a research assistant. Use tools to find and analyze information."
)

result = agent.invoke({"messages": [("human", "Summarize the latest AI trends from the news")]})

Example 2 — Code-writing agent

@tool
def run_python(code: str) -> str:
    """Execute Python code and return the output."""
    import io, contextlib
    f = io.StringIO()
    with contextlib.redirect_stdout(f):
        exec(code)
    return f.getvalue()

agent = create_react_agent(
    ChatOpenAI(model="gpt-4o"),
    [run_python],
    state_modifier="You write and test Python code. Always run your code to verify."
)

Example 3 — Reflection agent

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o")

def reflection_generate(task, iterations=3):
    draft = llm.invoke(f"Write a response to: {task}").content

    for i in range(iterations):
        critique = llm.invoke(
            f"Critically review this response. What's wrong?\n\n{draft}"
        ).content
        draft = llm.invoke(
            f"Improve this response based on feedback:\n\nFeedback: {critique}\n\nDraft: {draft}"
        ).content

    return draft

Common Patterns

Pattern: guardrails

def safe_agent(query, llm, tools, max_steps=5):
    """Agent with safety limits."""
    messages = [HumanMessage(content=query)]
    tool_map = {t.name: t for t in tools}

    for step in range(max_steps):
        response = llm.bind_tools(tools).invoke(messages)
        messages.append(response)

        if not response.tool_calls:
            return response.content

        for tc in response.tool_calls:
            if tc["name"] not in tool_map:
                messages.append(ToolMessage(content="Tool not allowed", tool_call_id=tc["id"]))
                continue
            result = tool_map[tc["name"]].invoke(tc["args"])
            messages.append(ToolMessage(content=str(result)[:2000], tool_call_id=tc["id"]))

    return "Task incomplete — too many steps"

Pattern: state persistence

# save agent state across runs
import json

def run_agent_with_state(agent, query, state_file="agent_state.json"):
    # load previous state
    state = json.load(open(state_file)) if os.path.exists(state_file) else {"messages": []}
    state["messages"].append({"role": "user", "content": query})

    result = agent.invoke({"messages": state["messages"]})

    # save state
    state["messages"] = [{"type": m.type, "content": m.content} for m in result["messages"]]
    json.dump(state, open(state_file, "w"))
    return result["messages"][-1].content

Best Practices

  • Start simple — single agent with a few tools before building multi-agent systems.
  • Set max_steps — prevent infinite loops (5–10 for most tasks).
  • Truncate tool outputs — long outputs waste context and confuse the LLM.
  • Use structured tool descriptions — the LLM chooses tools based on descriptions.
  • Log everything — agent reasoning is opaque; logs help debug.
  • Add human-in-the-loop for high-stakes actions (deployments, payments).
  • Test with fixed seeds — agents are non-deterministic; test with temperature=0.
  • Plan before coding — sketch the agent loop on paper first.

Common Mistakes

Mistake Fix
No max_steps limit Add a step limit to prevent infinite loops
Tools with vague descriptions Write clear, specific tool descriptions
Not truncating tool outputs Cap at 2000 chars to preserve context
Over-engineering early Start with single agent; add complexity only when needed
Ignoring failure modes Add retry logic and error handling
No human-in-the-loop for risky actions Add approval gates for destructive operations
Trusting agent output blindly Validate critical outputs before acting

Troubleshooting

Symptom Cause / Fix
Agent loops forever Missing max_steps or stuck in tool call cycle
Agent ignores tools Tool descriptions are unclear — rewrite them
Agent gives wrong answers Add reflection step; reduce temperature
Context length exceeded Truncate tool outputs; reduce history
Agent can't find information Tools are limited — add more tools or RAG
Slow agent Reduce steps; use faster model; cache tool calls

Official Documentation

Last Updated

2026-09-06 — Initial version.