Skip to content
AI Agents — Architecture, LangGraph, Multi-Agent Systems, Tool Use, Planning, Memory & Production Deployment

AI Agents — Architecture, LangGraph, Multi-Agent Systems, Tool Use, Planning, Memory & Production Deployment

DodaTech Updated Jun 20, 2026 12 min read

AI agents are autonomous programs that perceive their environment, reason about goals, take actions using tools, and learn from feedback — instead of responding to a single prompt, agents maintain state, plan multi-step tasks, and execute complex workflows without human intervention.

What You’ll Learn

  • The three-layer agent architecture: perception, reasoning, action
  • AutoGPT, CrewAI, and LangGraph — comparing agent frameworks
  • LangGraph state graphs with nodes, edges, and conditional routing
  • Multi-agent systems: specialized agents collaborating
  • Tool use: function calling with custom tools and APIs
  • Planning strategies: ReAct (reasoning + acting), Plan-and-Execute
  • Memory systems: short-term, long-term, and entity memory
  • Error recovery and retry strategies
  • Production deployment: monitoring, rate limiting, safety

Why AI Agents Matter

The most powerful LLM is useless if it cannot act on its knowledge. Agents bridge the gap between “knowing” and “doing” — they search the web, query databases, send emails, write code, and control APIs. Companies like Salesforce, Microsoft, and Google are betting their entire product strategy on agentic AI. By 2027, Gartner predicts 40% of AI applications will use agent-based architectures.

Doda Browser uses an agent architecture for its smart research mode — the agent searches, summarizes, cross-references sources, and builds comprehensive reports autonomously. Durga Antivirus Pro deploys specialized agents for file scanning, network monitoring, and threat intelligence — each agent handles one domain but shares findings.

Learning Path

    flowchart LR
  A["OpenAI API Guide"] --> B["LangChain Guide"]
  B --> C["Vector Databases"]
  B --> D["Fine-Tuning LLMs"]
  C --> E["AI Agents<br/>You are here"]
  D --> E
  style E fill:#f90,color:#fff
  

Agent Architecture

Every AI agent follows the same three-layer pattern:

    flowchart TB
  subgraph "Agent Architecture"
    ENV["Environment<br/>Web, APIs, Files, DB"] --> PERC["Perception Layer<br/>Interpret input,<br/>parse tool results"]
    PERC --> REASON["Reasoning Layer<br/>LLM decides next action<br/>based on state + goal"]
    REASON --> ACT["Action Layer<br/>Execute tool calls,<br/>update memory"]
    ACT --> ENV
    MEM["Memory<br/>Short/Long Term"] --> REASON
  end
  

Perception Layer

Receives and interprets input — user messages, tool outputs, environment changes.

Reasoning Layer

The LLM core that decides what to do next. It receives the current state, goal, and available tools, then outputs the next action or final answer.

Action Layer

Executes tool calls — search, file operations, API calls, code execution — and feeds results back to the reasoning layer.

Agent Frameworks

AutoGPT

The pioneer autonomous agent. It maintains a task list, executes tasks sequentially, and dynamically adds new tasks.

# Simplified AutoGPT pattern
class AutoGPTAgent:
    def __init__(self, tools, llm):
        self.tools = tools
        self.llm = llm
        self.task_list = []
        self.memory = []
    
    def run(self, goal):
        self.task_list.append({"id": 1, "description": goal, "status": "pending"})
        
        while self.task_list:
            current = next(t for t in self.task_list if t["status"] == "pending")
            print(f"Executing: {current['description']}")
            
            # LLM decides next action
            action = self.plan_next_action(current)
            result = self.execute_action(action)
            
            # Update memory with result
            self.memory.append({"task": current["id"], "result": result})
            
            # Generate new tasks if needed
            new_tasks = self.create_tasks(result, current)
            self.task_list.extend(new_tasks)
            
            current["status"] = "completed"
            
            if self.is_goal_achieved(goal):
                break
        
        return self.summarize()

CrewAI

Multi-agent orchestration — define agents with roles, goals, and tasks. Agents collaborate to achieve complex objectives.

from crewai import Agent, Task, Crew

# Define specialized agents
researcher = Agent(
    role="Research Analyst",
    goal="Find the latest information on AI agents",
    backstory="Expert in AI research and technology trends",
    verbose=True,
    allow_delegation=False,
)

writer = Agent(
    role="Technical Writer",
    goal="Write a clear, engaging blog post about AI agents",
    backstory="Experienced technical writer specializing in AI topics",
    verbose=True,
    allow_delegation=True,
)

# Define tasks
research_task = Task(
    description="Research the latest developments in AI agents for 2026",
    expected_output="A summary of key trends and technologies",
    agent=researcher,
)

write_task = Task(
    description="Write a 500-word blog post based on the research",
    expected_output="A complete blog post in markdown format",
    agent=writer,
)

# Assemble the crew
crew = Crew(
    agents=[researcher, writer],
    tasks=[research_task, write_task],
    verbose=True,
)

result = crew.kickoff()
print(result)

Expected output: The researcher gathers data, passes it to the writer, and the writer produces a complete blog post — all autonomously.

LangGraph — State Graphs

LangGraph (from LangChain) models agent workflows as directed graphs with state passing through nodes.

    flowchart TB
  START["START"] --> AGENT["Agent Node<br/>LLM decides action"]
  AGENT --> TOOL["Tool Node<br/>Execute tool call"]
  TOOL --> AGENT
  AGENT --> END["END<br/>Final answer"]
  AGENT -.-> COND{"Conditional<br/>Edge"}
  COND --> TOOL
  COND --> END
  
from typing import TypedDict, Annotated, Sequence
import operator
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage

# Define state
class AgentState(TypedDict):
    messages: Annotated[Sequence, operator.add]
    next_step: str

# Define tools
def search_web(query: str) -> str:
    """Search the web for recent information."""
    return f"Search results for: {query}"

def calculate(expression: str) -> str:
    """Evaluate a mathematical expression."""
    try:
        return str(eval(expression))
    except:
        return "Error in calculation"

# Create the graph
def create_agent_graph():
    llm = ChatOpenAI(model="gpt-4")
    tools = [search_web, calculate]
    llm_with_tools = llm.bind_tools(tools)
    
    def agent_node(state: AgentState):
        messages = state["messages"]
        response = llm_with_tools.invoke(messages)
        return {"messages": [response], "next_step": "continue"}
    
    def tool_node(state: AgentState):
        last_message = state["messages"][-1]
        tool_calls = last_message.tool_calls
        
        if not tool_calls:
            return {"messages": [], "next_step": "end"}
        
        results = []
        for tc in tool_calls:
            if tc["name"] == "search_web":
                result = search_web(tc["args"]["query"])
            elif tc["name"] == "calculate":
                result = calculate(tc["args"]["expression"])
            else:
                result = "Unknown tool"
            
            results.append(ToolMessage(content=result, tool_call_id=tc["id"]))
        
        return {"messages": results, "next_step": "continue"}
    
    def should_continue(state: AgentState):
        messages = state["messages"]
        last_message = messages[-1]
        
        if hasattr(last_message, "tool_calls") and last_message.tool_calls:
            return "tools"
        return "end"
    
    # Build graph
    graph = StateGraph(AgentState)
    graph.add_node("agent", agent_node)
    graph.add_node("tools", tool_node)
    graph.set_entry_point("agent")
    graph.add_conditional_edges("agent", should_continue, {
        "tools": "tools",
        "end": END,
    })
    graph.add_edge("tools", "agent")
    
    return graph.compile()

# Run the agent
agent = create_agent_graph()
result = agent.invoke({
    "messages": [HumanMessage(
        content="Search for the latest AI news and calculate 2+2"
    )]
})
for msg in result["messages"]:
    print(f"{msg.type}: {msg.content[:100] if msg.content else '[tool call]'}")

Expected output: The agent processes the user request, decides to search the web and do a calculation, calls the appropriate tools, and returns results.

Multi-Agent Systems

Complex tasks benefit from multiple specialized agents working together.

    flowchart TB
  ORCH["Orchestrator Agent"] --> SEARCH["Search Agent<br/>Web/DB queries"]
  ORCH --> ANALYZE["Analysis Agent<br/>Data processing"]
  ORCH --> WRITE["Writing Agent<br/>Content generation"]
  ORCH --> CODE["Code Agent<br/>Implementation"]
  SEARCH --> ORCH
  ANALYZE --> ORCH
  WRITE --> ORCH
  CODE --> ORCH
  ORCH --> QUALITY["Quality Agent<br/>Validation"]
  QUALITY --> USER["Final Output"]
  
# Multi-agent system with LangGraph
class MultiAgentSystem:
    def __init__(self):
        self.orchestrator = self.create_orchestrator()
        self.specialists = {
            "search": SearchAgent(),
            "analyze": AnalysisAgent(),
            "code": CodeAgent(),
            "write": WritingAgent(),
        }
    
    def run(self, task):
        # Orchestrator creates plan with specialist sub-tasks
        plan = self.orchestrator.create_plan(task)
        results = {}
        
        for subtask in plan["subtasks"]:
            specialist_type = subtask["specialist"]
            agent = self.specialists[specialist_type]
            results[subtask["id"]] = agent.execute(subtask)
        
        # Orchestrator synthesizes results
        final = self.orchestrator.synthesize(task, results)
        return final

Planning Strategies

ReAct (Reasoning + Acting)

The most popular agent pattern. The LLM alternates between reasoning (thinking about what to do) and acting (executing tool calls).

Thought: I need to find the current weather for London.
Action: search_web(query="London weather 2026")
Observation: London is currently 18°C and partly cloudy.
Thought: The user asked for the weather. I have the answer.
Final: The current weather in London is 18°C and partly cloudy.

Plan-and-Execute

The agent creates a complete plan upfront, then executes step by step — useful for tasks with clear sequential dependencies.

def plan_and_execute(task, tools, llm):
    # Step 1: Create plan
    plan_prompt = f"Create a step-by-step plan for: {task}"
    plan = llm.invoke(plan_prompt)
    
    # Step 2: Execute each step
    results = []
    for i, step in enumerate(plan.steps):
        result = execute_step(step, tools)
        results.append(result)
        
        # Step 3: Possibly revise plan based on results
        if should_replan(step, result):
            plan = llm.invoke(f"Revise remaining plan given: {result}")
    
    # Step 4: Synthesize final output
    return llm.invoke(f"Synthesize results: {results}")

Memory Systems

TypeScopePersistenceExample
Short-termCurrent conversationSession onlyConversation history (context window)
Long-termAcross sessionsPersistent (vector DB)Summarized past conversations
EntityKnowledge about entitiesPersistent (graph DB)Facts about people, places, concepts
# Agent with long-term memory using vector store
class MemoryAgent:
    def __init__(self, vector_store):
        self.short_term = []
        self.long_term = vector_store
    
    def retrieve_relevant_memories(self, query):
        results = self.long_term.similarity_search(query, k=3)
        return [r.page_content for r in results]
    
    def store_memory(self, content, metadata=None):
        self.long_term.add_texts([content], metadatas=[metadata])
    
    def process(self, user_input):
        # Retrieve relevant past memories
        memories = self.retrieve_relevant_memories(user_input)
        context = "\n".join(memories)
        
        # Build prompt with memories + conversation
        prompt = f"Relevant context:\n{context}\n\nUser: {user_input}"
        response = llm.invoke(prompt)
        
        # Store this interaction
        self.short_term.append({"user": user_input, "assistant": response})
        self.store_memory(f"User asked: {user_input}. Response: {response}")
        
        return response

Tool Use

Tools are the agent’s interface to the world. Each tool has a name, description, parameters, and implementation.

from langchain_core.tools import tool

@tool
def get_stock_price(symbol: str) -> str:
    """Get the current stock price for a given symbol.
    
    Args:
        symbol: Stock ticker symbol (e.g., AAPL, GOOGL)
    """
    import yfinance as yf
    stock = yf.Ticker(symbol)
    price = stock.info.get("currentPrice", "N/A")
    return f"{symbol}: ${price}"

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email to the specified recipient.
    
    Args:
        to: Recipient email address
        subject: Email subject line
        body: Email body content
    """
    # Email sending logic here
    return f"Email sent to {to} with subject: {subject}"

# Register tools with agent
tools = [get_stock_price, send_email]
agent = create_agent_with_tools(tools)
agent.run("What's Apple's stock price and email it to me")

Error Recovery

Agents must handle failures gracefully — tools fail, APIs timeout, LLMs produce invalid JSON.

class ResilientAgent:
    def execute_with_retry(self, tool_call, max_retries=3):
        for attempt in range(max_retries):
            try:
                return tool_call()
            except ToolExecutionError as e:
                print(f"Attempt {attempt + 1} failed: {e}")
                if attempt == max_retries - 1:
                    return f"Failed after {max_retries} attempts: {e}"
                time.sleep(2 ** attempt)  # Exponential backoff
    
    def handle_invalid_action(self, llm_output):
        """If LLM produces invalid JSON, ask it to fix it."""
        correction_prompt = f"Your previous output was invalid: {llm_output}. Please output a valid tool call."
        return llm.invoke(correction_prompt)

Production Deployment

ConsiderationStrategy
Rate limitingToken bucket per agent, queue overflow requests
MonitoringLog all steps — thought, action, observation, final output
TimeoutsMaximum 30s per step, 5min per agent run
Cost controlCap total tool calls per run, use cheaper models for simple steps
Human-in-the-loopPause for approval on destructive actions (send email, delete files)
CachingCache identical tool results to reduce cost
# Production agent monitoring
import logging
import time

class MonitoredAgent:
    def __init__(self):
        self.logger = logging.getLogger("agent")
        self.step_count = 0
        self.max_steps = 25
    
    def process(self, user_input):
        start_time = time.time()
        self.logger.info(f"Starting agent run. Input: {user_input[:50]}")
        
        state = {"input": user_input, "steps": [], "output": None}
        
        while self.step_count < self.max_steps:
            step_start = time.time()
            action = self.plan(state)
            
            if action["type"] == "final":
                state["output"] = action["content"]
                break
            
            result = self.execute(action)
            state["steps"].append({
                "action": action,
                "result": result,
                "duration_ms": (time.time() - step_start) * 1000,
            })
            self.step_count += 1
        
        duration = time.time() - start_time
        self.logger.info(f"Agent completed in {duration:.2f}s, "
                        f"{self.step_count} steps")
        
        # Cost tracking
        self.track_cost(self.step_count, duration)
        
        return state["output"]

Safety Considerations

RiskMitigation
Tool misuseRestrictive tool permissions, approve lists
Prompt injectionInput sanitization, separate system/user messages
Runaway loopsMax step limit, human-in-the-loop after N steps
Data leakageNever expose internal tools to untrusted users
Resource exhaustionRate limiting, concurrent agent caps
Hallucinated tool resultsValidate tool outputs, cross-reference

Common Errors

1. Agent enters infinite loop

The agent keeps calling tools without reaching a conclusion. Set max_iterations=10 or implement a loop detector that forces termination after repeated patterns.

2. Tool call JSON parsing fails

LLM generates invalid JSON for tool arguments. Use response_format={"type": "json_object"} or parse with json.loads() wrapped in try/except with retry.

3. Agent ignores system instructions

The model is distracted by user input. Repeat critical instructions in every prompt turn: “REMEMBER: never delete files.”

4. Multi-agent communication failure

Agents don’t share context properly. Use a shared state object (like LangGraph’s State) that all agents read/write. Standardize on a message format.

5. Memory overload

Short-term memory grows beyond the context window. Summarize old messages: “Previous conversation summary: …” and keep only the summary + last N messages.

6. Tool not found

The agent tried to call a tool that doesn’t exist. Include tool descriptions in the system prompt and validate tool names before execution.

7. Agent gives up too early

The model returns “I cannot do this” instead of trying alternative approaches. Add a system instruction: “Try at least 3 different approaches before giving up.”

Practice Questions

  1. What are the three layers of agent architecture? Perception (interpret input), Reasoning (LLM decides next action), Action (execute tools, update state).

  2. How does ReAct differ from Plan-and-Execute? ReAct alternates reasoning and acting dynamically — decide after each step. Plan-and-Execute creates a full plan first, then executes sequentially.

  3. What is the purpose of memory in agents? Short-term memory manages conversation context. Long-term memory persists knowledge across sessions. Entity memory tracks facts about specific people, places, or concepts.

  4. How do multi-agent systems improve complex tasks? Specialized agents handle specific domains (search, analysis, coding) better than a single generalist. The orchestrator coordinates them, and the quality agent validates the final output.

  5. What safety measures are essential for production agents? Rate limiting, max step limits, human-in-the-loop for destructive actions, input sanitization, tool permission scoping, and comprehensive logging.

Challenge: Design a multi-agent research system that: (1) receives a complex topic question, (2) creates a research plan with sub-questions, (3) assigns search tasks to parallel search agents, (4) each search agent summarizes findings, (5) a synthesis agent combines all summaries into a coherent report, (6) a quality agent validates facts and citations. Implement the state graph in LangGraph with error recovery and human-in-the-loop for ambiguous queries.

FAQ

What is the difference between an agent and a chain?
A chain executes a fixed sequence of LLM calls — the path is predetermined. An agent dynamically decides which tools to call and in what order based on the current state and goal.
Which agent framework is best for production?
LangGraph is the most mature for production with built-in state management, error handling, and human-in-the-loop. CrewAI is excellent for multi-agent orchestration. AutoGPT is better for prototyping autonomous research agents.
How many steps should an agent run?
5-15 steps is typical. Beyond 25 steps, agents tend to loop or degrade in quality. Set a hard limit of 25-50 steps with automatic termination.
Can agents use other agents as tools?
Yes — this is called agent delegation. A manager agent can invoke specialist sub-agents as “tools.” LangGraph supports this natively through sub-graphs.
How do I prevent an agent from calling expensive APIs too often?
Add cost tracking per tool, rate limiting, and a budget per agent run. Cache identical API results. Use cheaper models for simple reasoning steps.
What is human-in-the-loop?
The agent pauses execution and asks for human approval before taking destructive actions (sending emails, making purchases, deleting data). Implemented via conditional edges in LangGraph that route to a human approval node.

Try It Yourself

Build a minimal agent loop with Python:

import json

def simple_agent(user_input, tools, llm, max_steps=5):
    messages = [{"role": "user", "content": user_input}]
    
    for step in range(max_steps):
        # LLM decides next action
        response = llm.invoke(messages)
        messages.append(response)
        
        # Check if final answer
        if response.get("type") == "final":
            return response["content"]
        
        # Execute tool
        tool_call = json.loads(response["tool_call"])
        tool_name = tool_call["name"]
        tool_args = tool_call["arguments"]
        
        if tool_name in tools:
            result = tools[tool_name](**tool_args)
            messages.append({"role": "tool", "content": str(result), 
                            "tool_call_id": tool_call.get("id")})
        else:
            messages.append({"role": "tool", "content": f"Tool {tool_name} not found",
                            "tool_call_id": tool_call.get("id")})
    
    return "Max steps reached"

# Simulate a tool
def search(query):
    return f"Results for: {query}"

# Run
result = simple_agent(
    "Find the capital of France",
    {"search": search},
    lambda msgs: {"type": "final", "content": "Paris"},
)
print(result)

Expected output: The agent will process the query through the tool loop and return the answer.

What’s Next

TutorialWhat You’ll Learn
LangChain GuideFoundation for LangGraph agents
Vector Databases GuideLong-term memory with vector stores
OpenAI API GuideFunction calling for agent tools

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro