mech.app

The mech.app newsletter

Agentic AI, minus the noise.

Get practical field notes on AI agents, automation, developer tools and security delivered to your inbox.

No spam. Unsubscribe anytime.

AI Agents

Building AI Agents in Python: What 27 Minutes of Tutorial Code Reveals About Orchestration Patterns

A technical dissection of beginner agent patterns: tool boundaries, state flow, error handling, and the gap between tutorial code and production systems.

Source: dev.to
Building AI Agents in Python: What 27 Minutes of Tutorial Code Reveals About Orchestration Patterns

A 27-minute Python agent tutorial published in June 2026 offers a window into what patterns are being taught to thousands of developers entering the agentic AI space. The tutorial uses the OpenAI SDK with minimal abstractions, which means the orchestration logic, tool boundaries, and error handling are all visible in application code.

This is useful. Not because the tutorial represents production-grade architecture, but because it exposes the default patterns that will shape how most teams build their first agents. Understanding these patterns and their limitations helps you decide when to follow them and when to reach for something more robust.

The Core Loop: ReAct Without Saying ReAct

The tutorial implements a classic ReAct loop (Reason, Act, Observe) without naming it:

  1. Send user request + conversation history to LLM
  2. LLM responds with either text or a tool call request
  3. If tool call: execute function, append result to history
  4. Loop back to step 1 until LLM returns final text

This is the simplest orchestration pattern that works. State lives entirely in a list of messages. Each iteration appends to that list. The loop continues until the model stops requesting tools.

messages = [
    {"role": "system", "content": system_prompt},
    {"role": "user", "content": user_query}
]

while True:
    response = client.chat.completions.create(
        model="gpt-4",
        messages=messages,
        tools=tool_definitions
    )
    
    if response.choices[0].finish_reason == "tool_calls":
        tool_calls = response.choices[0].message.tool_calls
        messages.append(response.choices[0].message)
        
        for tool_call in tool_calls:
            result = execute_tool(tool_call.function.name, 
                                 tool_call.function.arguments)
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": str(result)
            })
    else:
        return response.choices[0].message.content

The loop has no explicit step counter, no timeout, no cost tracking. It trusts the model to eventually stop calling tools. In practice, you hit token limits or API rate limits before infinite loops become a problem, but neither failure mode is handled.

Tool Boundaries: JSON Schema and Hope

Tools are defined using OpenAI’s function calling schema. You provide a name, description, and parameter schema. The model returns structured JSON that matches your schema (usually).

tools = [
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": "Search the web for current information",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"}
                },
                "required": ["query"]
            }
        }
    }
]

The boundary is soft. The model can hallucinate tool names, pass malformed JSON, or ignore required parameters. The tutorial code assumes happy path: parse the JSON, call the function, return the result. No validation layer. No fallback when the tool returns an error or unexpected format.

This works for demos. In production, you need:

  • Schema validation before execution (Pydantic, JSON Schema validators)
  • Error wrapping so tool failures become observable events, not crashes
  • Retry logic with exponential backoff for transient failures
  • Result truncation so a 10MB API response does not blow your context window

State Management: The Message List Is Your Database

State lives in a Python list. Every LLM response, every tool call, every tool result gets appended. This list is your entire agent state. When you serialize it to JSON and send it to the API, you are sending the full history every time.

This has consequences:

  • Context window pressure: Each loop iteration consumes more tokens. A 10-step agent run might use 50k tokens just on history.
  • No persistence: If the process crashes, state is gone. No checkpointing, no recovery.
  • No branching: You cannot explore multiple paths or backtrack to an earlier decision point.
  • No summarization: Old tool results stay in context forever, even if they are no longer relevant.

Production systems add:

  • Sliding window memory: Keep last N messages, summarize older context
  • Semantic memory: Store tool results in a vector DB, retrieve only relevant chunks
  • State snapshots: Serialize state to disk or Redis after each step
  • Structured state objects: Replace the message list with a state machine that tracks goals, completed actions, and pending decisions

Error Handling: Silent Failures and Blind Retries

The tutorial code has no explicit error handling. If a tool call fails, the exception propagates up and crashes the loop. If the model returns malformed JSON, the JSON parser throws an error. If the API rate limits you, the request fails.

The OpenAI SDK has built-in retry logic for transient failures (network errors, 5xx responses), but it does not handle:

  • Tool execution failures (API down, invalid parameters, timeout)
  • Model refusals (safety filters, content policy violations)
  • Context overflow (request exceeds token limit)
  • Cost overruns (agent burns through your API budget)

A production agent needs observability and control:

class AgentExecutor:
    def __init__(self, max_steps=10, max_cost_usd=1.0):
        self.max_steps = max_steps
        self.max_cost_usd = max_cost_usd
        self.total_cost = 0.0
        self.step_count = 0
        
    def run(self, messages, tools):
        while self.step_count < self.max_steps:
            self.step_count += 1
            
            try:
                response = self.call_llm(messages, tools)
                self.total_cost += self.estimate_cost(response)
                
                if self.total_cost > self.max_cost_usd:
                    raise CostLimitExceeded()
                    
                # Process response...
                
            except ToolExecutionError as e:
                # Log error, append to messages, let model handle it
                messages.append({
                    "role": "tool",
                    "content": f"Error: {e}"
                })
            except RateLimitError:
                # Exponential backoff
                time.sleep(2 ** self.step_count)
                continue
                
        raise MaxStepsExceeded()

Memory: Short-Term Only

The tutorial implements short-term memory (the message list) but no long-term memory. The agent cannot remember facts across sessions. If you ask “What did I tell you yesterday?” it has no answer unless you manually inject that context.

Long-term memory requires:

  • Session storage: Persist conversation history to a database keyed by user ID
  • Fact extraction: Pull key facts from conversations and store them separately
  • Retrieval logic: When a new query arrives, search past sessions for relevant context
  • Memory pruning: Delete or archive old sessions to control storage costs

The tutorial does not address this because it adds complexity and external dependencies (database, vector store). But without it, your agent is stateless across sessions.

Tool Calling: The Model Decides Everything

The model decides which tools to call, when to call them, and what parameters to pass. You have no control over the execution plan. If the model decides to call delete_all_files() instead of search_files(), your only defense is the system prompt.

This is the default pattern in 2026: trust the model, constrain it with prompts, hope for the best. It works when:

  • Tools have low blast radius (read-only APIs, sandboxed execution)
  • The task is well-defined and low-stakes
  • You can afford occasional mistakes

It breaks when:

  • Tools have side effects (write to database, send emails, charge credit cards)
  • The task requires multi-step planning with dependencies
  • Mistakes are expensive or dangerous

Production systems add guardrails:

  • Approval workflows: Require human confirmation before executing high-risk tools
  • Capability-based security: Tools declare required permissions, agent checks before execution
  • Plan validation: Model generates a plan, separate validator checks it before execution
  • Rollback mechanisms: Tools support undo operations or transactional semantics

Observability: Print Statements and Crossed Fingers

The tutorial has no observability. You cannot see what the agent is thinking, which tools it tried, or why it made a decision. Debugging requires reading the message list and guessing.

Production agents need:

  • Structured logging: Every LLM call, tool execution, and state transition logged with timestamps and trace IDs
  • Span tracing: Integrate with OpenTelemetry or similar to track request flow across services
  • Cost tracking: Log token usage and API costs per step
  • Decision capture: Store the model’s reasoning (chain-of-thought) before each action

Tools like Langfuse, Helicone, and LangSmith provide this out of the box. The tutorial does not mention them because it is teaching the core loop, not production operations.

The MCP Mention: A Future Pattern

The tutorial mentions the Model Context Protocol (MCP) as a way to standardize tool interfaces. MCP defines a JSON-RPC protocol for exposing tools to agents. Instead of writing custom Python functions, you implement an MCP server. The agent talks to the server over stdio or HTTP.

This is useful when:

  • You want to share tools across multiple agents or frameworks
  • Tools need to run in separate processes or containers (security, isolation)
  • You are building a marketplace or ecosystem of agent tools

It is overkill when:

  • You have three tools and they are all Python functions
  • Your agent runs in a single process
  • You control both the agent and the tools

MCP is a bet on a future where tools are commoditized and agents are interoperable. In 2026, most teams are still writing custom tool wrappers.

What the Tutorial Gets Right

Despite the limitations, the tutorial teaches the right mental model:

  • Agents are loops, not single calls
  • State is conversation history plus tool results
  • Tools are functions with schemas
  • The model is the orchestrator

This is the 80% case. If you are building a Slack bot that searches Notion and summarizes documents, this pattern works. You can ship it in a weekend.

What Production Systems Add

ComponentTutorial ApproachProduction Approach
State managementIn-memory listPersistent store with checkpointing
Error handlingCrash on failureRetry logic, fallbacks, observability
MemoryShort-term onlyLong-term storage with retrieval
Tool securityTrust the modelApproval workflows, capability checks
ObservabilityNoneStructured logging, tracing, cost tracking
Context managementSend everythingSliding window, summarization, semantic search
OrchestrationSimple loopState machines, planners, multi-agent coordination

Technical Verdict

Use this tutorial pattern if:

  • You are prototyping in under 4 hours and need to demonstrate basic agent behavior
  • All tools are read-only (search APIs, file reads, GET requests with no side effects)
  • The agent will run attended (human watching, can restart on failure)
  • You have fewer than 5 tools and expect fewer than 10 steps per run
  • Failure is non-critical and you can afford to lose in-flight state
  • Your budget allows $0.50 to $2.00 per run without tracking (GPT-4 pricing)

Avoid this pattern if:

  • Tools write to production systems (databases, external APIs, file systems)
  • The agent must run unattended for more than 5 minutes
  • You need audit trails for compliance or debugging
  • Context window pressure is likely (long documents, many tool results, multi-turn conversations)
  • You are building a product feature that ships to end users
  • Cost control matters (you need per-run budgets or usage alerts)
  • Tools have variable latency or failure rates (external APIs, web scraping)

The tutorial teaches the core loop correctly. It is a valid starting point for learning. But production agents need persistent state, structured error handling, observability hooks, and security boundaries. Add those before you ship. If you cannot add them in your current environment, use a framework (LangGraph, Semantic Kernel, or AutoGen) that provides them out of the box.

Tags

agentic-ai orchestration infrastructure

Primary Source

dev.to