Ver en Español
Less Hype, More Architecture: Design Patterns for AI Agent Systems
Sep 01, 2026
Updated: Sep 01, 2026

Less Hype, More Architecture: Design Patterns for AI Agent Systems

Throughout our careers as software developers and architects, we have seen the same phenomenon repeat itself: a technology with genuine transformative potential emerges, and overnight, the ecosystem fills with a thick layer of hype. Buzzwords take over, libraries reinvent the wheel, and conferences start claiming that programming now requires discarding three decades of established engineering practices.

In the AI space, this has become the daily norm. Everyone is talking about loop engineering, graph engineering, multi-agent orchestration, or "diamond diagrams," presenting them as if they were brand new computing paradigms.

The reality is much more grounded: the vast majority of agentic systems that work reliably in production are not black magic; they are classical software design and asynchronous messaging patterns adapted to Large Language Models (LLMs).

If you understand separation of concerns, modular composition, and data flow, you already have 80% of what you need. In this article, we cut through the noise to explore the four foundational agentic design patterns with clear code examples, and discuss the operational traps around cost, latency, and observability you must avoid when shipping to production.

From Enterprise Integration Patterns to AI Agents

In 2003, Gregor Hohpe and Bobby Woolf published Enterprise Integration Patterns, a classic reference cataloging 65 messaging and integration patterns for distributed systems. If you open that book today and compare its diagrams to the AI workflows described by labs like Anthropic in Building Effective Agents, the parallels are obvious:

  • The classic Pipes and Filters pattern is what we now call Prompt Chaining or a Pipeline.
  • The Content-Based Router is an Agentic Router.
  • The Scatter-Gather (Fan-out / Fan-in) pattern is branded as the "Agentic Diamond" or Orchestrator-Workers.
  • Test-driven feedback loops are Evaluator-Optimizer Loops.

Recognizing that these are established architectural patterns gives us a huge advantage: it frees us from relying blindly on heavyweight frameworks and lets us build clean, maintainable solutions using standard code and official model SDKs.

Let us examine each of these four patterns in detail.

1. The Pipeline Pattern (Prompt Chaining): The Assembly Line

The most straightforward pattern is the pipeline or sequential chain. It decomposes a complex problem into a sequence of deterministic stages where the output of one stage becomes the injected input for the next.

A direct parallel in traditional development is Spec-Driven Development: first you define the design, that design generates the technical specification, the specification is broken into discrete tasks, and finally those tasks move to implementation and review.

[User Input] ──> [ Stage 1: Outline ] ──> [ Stage 2: Drafting ] ──> [ Stage 3: Title ] ──> [Result]

Use case: Structured technical article generator

If you ask an LLM in a single massive prompt to "Write a blog post about WebSockets covering architecture, code, and title," the output is usually superficial. Dividing it into a pipeline lets you specialize each step:

  • Step 1 (Planner): Generates the section structure and key technical themes.
  • Step 2 (Writer): Takes the outline and develops each section in depth.
  • Step 3 (Editor): Reviews the complete draft, generates an engaging title, and adds an executive summary.

Code example

import OpenAI from "openai";

const openai = new OpenAI();

async function runPrompt(systemPrompt, userPrompt, model = "gpt-4o-mini") {
  const response = await openai.chat.completions.create({
    model,
    messages: [
      { role: "system", content: systemPrompt },
      { role: "user", content: userPrompt }
    ],
    temperature: 0.7,
  });
  return response.choices[0].message.content;
}

async function generateTechnicalPost(topic) {
  console.log(`[1/3] Generating outline for: "${topic}"...`);
  const outline = await runPrompt(
    "You are a software architect and technical instructor. Generate a detailed section outline for a technical blog post.",
    `Topic: ${topic}`
  );

  console.log(`[2/3] Writing content from outline...`);
  const draft = await runPrompt(
    "You are a specialized technical writer. Develop the full content for each section of this outline pragmatically and clearly.",
    `Outline:\n${outline}`
  );

  console.log(`[3/3] Polishing title and executive summary...`);
  const finalPost = await runPrompt(
    "You are an engineering publication editor. Generate 3 engaging title options and add an executive summary at the start.",
    `Post draft:\n${draft}`
  );

  return finalPost;
}

2. The Router Pattern: Intelligent Branching

In production systems, not every request follows the same path. The Router pattern intercepts user input, analyzes semantic intent, and dispatches it to the appropriate specialized agent or workflow.

                      ┌──> [ Billing Agent ] ──> Output 1
                      │
[ User Query ] ──> [ Router ] ──> [ Technical Support Agent ] ──> Output 2
                      │
                      └──> [ Fallback: Human Agent ]

How to implement a Router

You do not always need a frontier LLM call to route:

  • Classifier LLM (Structured Output): Ask a fast model to return a JSON payload with the target department and a confidence score ($0.0$ to $1.0$).
  • Semantic Search (Embeddings): Convert the query into an embedding vector and compute cosine similarity against reference vectors for each department (much faster and cheaper).
  • Context / UI Logic: If the user is already on the /billing page, the router can simply be a deterministic condition in your application state.

Code example with Human Fallback

async function routeCustomerQuery(userMessage) {
  // 1. Classification using a fast model
  const classificationPrompt = `
You are a customer support classifier. Classify user intent into one of these departments:
- BILLING (payment issues, invoices, credit cards)
- TECH_SUPPORT (bugs, downtime, API errors)
- REFUNDS (refund requests)

Respond ONLY in valid JSON format: {"department": "BILLING" | "TECH_SUPPORT" | "REFUNDS" | "UNKNOWN", "confidence": 0.0 - 1.0}
`;

  const rawDecision = await runPrompt(
    classificationPrompt,
    `Customer message: "${userMessage}"`,
    "gpt-4o-mini"
  );
  
  const decision = JSON.parse(rawDecision);
  console.log(`[Router] Classified as ${decision.department} with confidence ${decision.confidence}`);

  // 2. Guardrail and Human Fallback (Human-in-the-Loop)
  if (decision.confidence < 0.75 || decision.department === "UNKNOWN") {
    return {
      status: "ESCALATED_TO_HUMAN",
      message: "Your query requires specialized assistance. We have escalated this ticket to our team."
    };
  }

  // 3. Specialized agent execution
  const departmentPrompts = {
    BILLING: "You are a financial support specialist. Resolve questions regarding charges and billing disputes.",
    TECH_SUPPORT: "You are an L2 support engineer. Diagnose technical issues and integration errors.",
    REFUNDS: "You are a retention and refund agent. Apply company refund policies accurately."
  };

  const response = await runPrompt(departmentPrompts[decision.department], userMessage);
  return { status: "RESOLVED", department: decision.department, response };
}

3. The Planner-Executor Pattern (Diamond or Fan-Out / Fan-In)

When a task is broad or open-ended, handing total control to a single agent to generate everything at once often leads to hallucinations and lost context.

The Planner-Executor pattern divides the work into two distinct roles:

  • The Planner (Orchestrator): A strong reasoning model that receives the objective, evaluates available tools, and breaks down the problem into structured sub-tasks.
  • The Workers (Parallel Executors): Specialized agents that execute sub-tasks concurrently (Fan-Out).
  • The Synthesizer (Gather): Collects partial results and produces a consolidated verdict or deliverable (Fan-In).
                 ┌──> [ Worker 1: Technical Feasibility ] ──┐
                 │                                          │
[ Goal ] ──> [ Planner ] ──> [ Worker 2: Market & Unit Economics ] ──┼──> [ Synthesizer ] ──> [Verdict]
                 │                                          │
                 └──> [ Worker 3: Risk & Compliance ] ──────┘

Code example: Parallel product idea evaluation

async function evaluateProductIdea(ideaDescription) {
  console.log("[Planner] Deploying evaluation committee in parallel...");

  // Fan-Out: Concurrent execution via Promise.all
  const [techEval, marketEval, riskEval] = await Promise.all([
    runPrompt(
      "You are a Principal Engineer. Evaluate the technical feasibility and architectural complexity of this idea in 2 paragraphs.",
      ideaDescription
    ),
    runPrompt(
      "You are a Product Manager. Evaluate market demand, monetization, and target audience in 2 paragraphs.",
      ideaDescription
    ),
    runPrompt(
      "You are a Security and Compliance Auditor. Evaluate operational and regulatory risks in 2 paragraphs.",
      ideaDescription
    )
  ]);

  console.log("[Gather] Synthesizing committee findings...");

  // Fan-In: Final consolidation
  const synthesis = await runPrompt(
    "You are the Head of Strategy. Integrate the 3 committee evaluations and produce a final executive recommendation with a GO / NO-GO verdict.",
    `IDEA: ${ideaDescription}\n\nTECHNICAL EVALUATION:\n${techEval}\n\nMARKET EVALUATION:\n${marketEval}\n\nRISKS:\n${riskEval}`
  );

  return synthesis;
}

4. The Evaluator-Optimizer Pattern: Adversarial Feedback Loops

The Evaluator-Optimizer pattern (or generator-critic loop) relies on a powerful dynamic: using adversarial agents with separated responsibilities.

One agent generates a first draft of the content or code, and a second agent (the evaluator) scores the result against a strict quality rubric. If the critic finds defects, the generator receives specific feedback and rewrites only the flagged items. The cycle repeats until the evaluator approves the output or a maximum round limit is reached.

[ Input ] ──> [ Generator ] <──── Feedback ────┐
                    │                           │
                    ▼                           │
              [ Evaluator ] ── (Passes Rubric?) ┘
                    │ (Yes)
                    ▼
                [ Output ]

Code example: E-commerce product copy optimizer

async function optimizeProductDescription(rawDraft, rubric, maxRounds = 3) {
  let currentText = rawDraft;

  for (let round = 1; round <= maxRounds; round++) {
    console.log(`\n--- Optimization Round ${round}/${maxRounds} ---`);

    // 1. Critic evaluates against the rubric
    const evaluation = await runPrompt(
      `You are a strict catalog editor. Evaluate the text against this RUBRIC:
${rubric}

Respond in JSON format:
{
  "approved": boolean,
  "defects": ["list of specific issues found"],
  "notes": "concise explanation"
}`,
      `Text to evaluate:\n"${currentText}"`,
      "gpt-4o-mini"
    );

    const result = JSON.parse(evaluation);

    if (result.approved) {
      console.log(`[Critic] Approved in round ${round}!`);
      return { text: currentText, rounds: round, status: "APPROVED" };
    }

    console.log(`[Critic] Rejected. Flagged defects:`, result.defects);

    // 2. Optimizer rewrites addressing only flagged defects
    currentText = await runPrompt(
      `Rewrite the text addressing ONLY the defects flagged by the editor. Keep valid data and brand tone intact.`,
      `Current text: "${currentText}"\nDefects to resolve: ${result.defects.join(", ")}`
    );
  }

  return { text: currentText, rounds: maxRounds, status: "MAX_ROUNDS_REACHED" };
}

Operational Failure Modes in Production

When moving from a local demo to a production service with real users, these patterns encounter distinct operational challenges:

1. Silent Failures and Cascading Errors

In traditional software, when a service fails, you get an HTTP 500 status code or an explicit exception. In agentic systems, failures are frequently silent: the model returns grammatically coherent text based on a subtle hallucination or faulty premise. In a pipeline or planner-executor workflow, that error propagates downstream and corrupts every subsequent step.

  • Mitigation: Always validate intermediate outputs with strict schemas (JSON Schema / Zod) and deterministic assertions before feeding them into the next stage.

2. Runaway Loops and Budget Explosions

In an Evaluator-Optimizer loop, if your rubric contains contradictory criteria (such as "be extremely concise" alongside "provide exhaustive historical background"), the critic and optimizer can get stuck in an endless cycle of revisions.

  • Mitigation: Implement a strict Circuit Breaker with an unyielding maximum iteration count (2 to 3 rounds maximum) and token budget alerts.

3. Unnecessary Router Latency

Using heavy reasoning models (like GPT-4o or Claude Sonnet) solely to classify whether a user message belongs in Support or Sales adds 2 to 3 seconds of unnecessary latency to every user interaction.

  • Mitigation: Use fast, lightweight models (Flash / Mini / Haiku) for classification, or local vector embeddings for high-frequency routing.

What has been your experience? Which of these patterns are you currently running in production, and where have you encountered the most friction?

Share your thoughts in the comments, and let us keep building with architectural rigor.

Sebastian Gomez

Sebastian Gomez

Creador de contenido principalmente acerca de tecnología.

Leave a Reply

0 Comments

Advertisements

Related Posts

Categorias