AI Agentic Design Patterns

Overview of LangChain framework

Design patterns in agentic AI serve the same purpose they do in software engineering, they are reusable solutions to recurring problems. An agent system that works well for a simple Q&A task will break under a complex multi-step research task. An orchestrator pattern that works for sequential workflows is wrong for parallel ones. Choosing the right pattern before writing code is what separates systems that scale from systems that get rewritten.

This post covers every major pattern, what problem each solves, when to use it, when not to, and how it is implemented. A decision guide at the end maps task characteristics to the right pattern.

Pattern 1 – Single-Shot Inference

The simplest pattern. One prompt in, one response out. No loops, no tools, no iteration. The LLM answers from its training knowledge alone.

User prompt
    ↓
   LLM
    ↓
Response

Real-world example:

  • Classifying support tickets by category
  • Well-defined tasks where the answer space is bounded
  • Task with no external lookup is needed.

When to use it: This type of pattern comes in handy when tasks are fully answerable from training knowledge. Classification, summarization of provided text, simple Q&A, format conversion, translation. The task must be completable in one pass with no external data needed.


When not to use it: Any task requiring current information, multi-step reasoning, external data, or verification. Single-shot inference hallucinates when pushed beyond training knowledge and has no mechanism to detect or correct errors.

Python

from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import ChatOllama

llm = ChatOllama(model="llama3", temperature=0)

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a precise technical classifier."),
    ("user",   "{input}")
])

chain    = prompt | llm
response = chain.invoke({"input": "Classify this error: ConnectionTimeoutError on s3:PutObject"})
# → "Network/connectivity error — S3 endpoint unreachable or timeout too short"

print(response.content)

Pattern 2 — Chain of Thought (CoT)

Chain of Thought prompting instructs the LLM to show its reasoning step by step before arriving at an answer. It does not add tools or loops, it changes how the LLM uses its existing knowledge by forcing it to decompose the problem before answering.

User prompt + "think step by step"
    ↓
LLM generates reasoning chain
    Step 1: [intermediate thought]
    Step 2: [intermediate thought]
    Step 3: [intermediate thought]
    ↓
Final answer derived from reasoning chain

Real-world example: Cost analysis, IAM policy reasoning, incident root cause analysis, any task where the correct answer requires working through multiple conditional steps.

When to use it: Mathematical reasoning, logical deduction, multi-condition problems, tasks where the answer depends on a sequence of inferences. CoT significantly improves accuracy on tasks that require intermediate steps to reach a correct conclusion.

When not to use it: Simple factual retrieval where reasoning steps add no value. Tasks requiring external data — CoT only improves the use of existing knowledge, it cannot supply knowledge the model does not have. Also avoid when output must be concise — CoT responses are verbose by design.

Python

from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import ChatOllama

llm = ChatOllama(model="llama3", temperature=0.1)

# The key is in the system prompt, explicitly request step-by-step reasoning
cot_prompt = ChatPromptTemplate.from_messages([
    ("system", """You are a precise technical analyst.
    When given a problem, think through it step by step.
    Show each reasoning step clearly before giving your final answer.
    Format:
    Step 1: [your first reasoning step]
    Step 2: [your next reasoning step]
    ...
    Final Answer: [your conclusion]"""),
    ("user", "{problem}")
])

chain    = cot_prompt | llm

response = chain.invoke({
    "problem": "An S3 bucket has versioning enabled. A lifecycle rule deletes objects after 30 days. A delete marker is created on day 10. What happens on day 40?"
})
# Step 1: Versioning is enabled, so delete markers are objects themselves...
# Step 2: The lifecycle rule applies to object versions, not delete markers by default...
# Final Answer: The original version is deleted on day 40. The delete marker persists...

Zero-shot CoT uses the phrase “think step by step” in the prompt. Few-shot CoT provides worked examples of the reasoning format before the actual question. Few-shot produces more reliable output for complex domains but requires curating good examples.

# Few-shot CoT, provide worked examples before the actual question
few_shot_examples = """
Example problem: If a Lambda has 512MB RAM and runs for 2 seconds, what is the cost?
Step 1: Lambda pricing is $0.0000166667 per GB-second
Step 2: 512MB = 0.5GB
Step 3: 0.5GB × 2 seconds = 1 GB-second
Step 4: 1 × $0.0000166667 = $0.0000166667
Final Answer: $0.0000166667 per invocation

Now solve: {problem}
"""

Pattern 3 — Self-Consistency

Self-consistency addresses a fundamental weakness of single-path CoT, a reasoning chain can reach a wrong conclusion confidently. Self-consistency generates multiple independent reasoning chains for the same problem, then selects the answer that appears most frequently across chains. The intuition is that correct reasoning paths will tend to converge on the right answer even when they take different routes.


User prompt
    ↓
Generate N independent reasoning chains (temperature > 0)
    Chain 1: [reasoning] → Answer A
    Chain 2: [reasoning] → Answer A
    Chain 3: [reasoning] → Answer B
    Chain 4: [reasoning] → Answer A
    Chain 5: [reasoning] → Answer A
    ↓
Majority vote across answers
    ↓
Final answer: A (4/5 chains agree)

Real-world example: Self-Consistency pattern is best suitable in use cases such as cloud cost estimation, compliance policy evaluation, security rule checking and any calculation or logical deduction where we need to know how confident the model is, not just what it thinks.

When to use it: High-stakes reasoning tasks where a single wrong inference could propagate into a bad decision. Mathematical problems, logic puzzles, compliance checks, cost calculations. Any task where we want calibrated confidence, not just an answer.

When not to use it: Self-consistency is not well-suited for tasks with open-ended outputs, such as creative writing or summarization, where a “majority vote” does not meaningfully define correctness. It is also impractical in latency-sensitive applications, as generating multiple reasoning chains increases both inference cost and response time linearly with NN. Additionally, it offers limited benefit for tasks that depend on external or up-to-date information, since Self consistency increases the reliability of internal reasoning not the quality of underlying knowledge.

Python

from langchain_ollama import ChatOllama
from collections import Counter

llm = ChatOllama(model="llama3", temperature=0.7)

def self_consistent_answer(problem: str, n_samples: int = 5) -> str:
    prompt = f"""Solve this step by step, then give a single word or number as Final Answer.

Problem: {problem}

Think step by step, then end with:
Final Answer: [your answer]"""

    answers = []
    for i in range(n_samples):
        response = llm.invoke(prompt)
        for line in response.content.split("\n"):
            if line.startswith("Final Answer:"):
                answer = line.replace("Final Answer:", "").strip()
                answers.append(answer)
                break

    if not answers:
        return "Could not extract answers"

    vote_counts = Counter(answers)
    winner, votes = vote_counts.most_common(1)[0]
    confidence = votes / len(answers)

    return f"Answer: {winner} (confidence: {confidence:.0%})"

print(self_consistent_answer("..."))


Pattern 4 — ReAct (Reasoning + Acting)

This is one of the widely adapted agent pattern.

ReAct interleaves reasoning with action. Before every tool call, the agent generates a Thought explaining why it is taking that action. After every tool call, it receives an Observation and reasons again before deciding what to do next. This is the foundation of most production agent systems.


Task input
    ↓
┌─────────────────────────────────┐
│  Thought: what do I need to do? │
│  Action: tool_name              │ ← LLM decides
│  Action Input: {...}            │
└────────────┬────────────────────┘
             ↓
        Tool executes (real Python)
             ↓
┌─────────────────────────────────┐
│  Observation: tool result       │ ← framework feeds back
│  Thought: what does this mean?  │
│  Action: next_tool              │ ← LLM decides again
└────────────┬────────────────────┘
             ↓
        [loop continues]
             ↓
┌─────────────────────────────────┐
│  Thought: I have enough info    │
│  Final Answer: [response]       │ ← loop exits
└─────────────────────────────────┘

Real-world example: ReAct agent shines in cases like any agent that calls APIs, reads databases, writes files, or coordinates external systems. The is the workhorse pattern of production agentic AI.

When to use it: Any task requiring external data, multi-step execution, or real-world actions. This is thee default pattern for most production agents. Especially useful and valuable pattern when we need the agent’s reasoning to be transparent and debuggable.

When not to use it: It is tasks answerable without tools or tasks requiring parallel execution as ReAct is inherently sequential. When we need ultra-low latency systems choosing ReAct agent won’t helps as the thought generation adds tokens and time to every step.

Python

from langchain_ollama import ChatOllama
from langchain.agents import create_react_agent, AgentExecutor
from langchain_core.prompts import PromptTemplate
from langchain_core.tools import tool

llm = ChatOllama(model="llama3", temperature=0.1)

@tool
def get_aws_service_status(service: str) -> str:
    """Check the current operational status of an AWS service. Pass the service name."""
    # In production: call AWS Health API or status page
    return f"{service}: Operational (simulated response)"

@tool
def calculate_monthly_cost(service: str, usage_amount: float, unit: str) -> str:
    """Calculate estimated monthly AWS cost. Pass service name, usage amount, and unit (GB, hours, requests)."""
    # In production: call AWS Pricing API
    rates = {"s3": 0.023, "ec2": 0.10, "lambda": 0.0000002}
    rate  = rates.get(service.lower(), 0.05)
    cost  = usage_amount * rate
    return f"Estimated monthly cost for {usage_amount} {unit} of {service}: ${cost:.4f}"

react_prompt = PromptTemplate.from_template("""
You are an AWS cost and operations analyst.
Available tools: {tools}
Tool names: {tool_names}

Use this format:
Thought: [your reasoning]
Action: [tool name]
Action Input: [tool input]
Observation: [tool result]
... (repeat as needed)
Thought: I have all the information needed
Final Answer: [your complete analysis]

Task: {input}
{agent_scratchpad}
""")

agent   = create_react_agent(llm, [get_aws_service_status, calculate_monthly_cost], react_prompt)
executor = AgentExecutor(agent=agent, tools=[get_aws_service_status, calculate_monthly_cost],
                         verbose=True, max_iterations=6)

result = executor.invoke({"input": "Check S3 status and estimate cost for storing 500GB monthly"})

RAG gives the agent access to a knowledge base that is too large to fit in a context window. When a query arrives, the system first retrieves the most relevant documents from the knowledge base using semantic search, then provides both the query and the retrieved documents to the LLM as context. The LLM answers using the retrieved information rather than relying solely on training knowledge.

In Agentic RAG, the key shift is that RAG becomes a tool rather than a predefined pipeline. The agent dynamically decides when retrieval is needed and what information to query, instead of retrieving context for every response.


User query
    ↓
Query embedding (convert query to vector)
    ↓
Vector similarity search against knowledge base
    ↓
Top-K most relevant documents retrieved
    ↓
[Query + Retrieved documents] → LLM
    ↓
Answer grounded in retrieved documents

Real-world example: This pattern is particularly effective for enterprise use cases such as Internal documentation Q&A, compliance policy checking, runbook lookup during incident response, answering questions about proprietary systems not in the LLM’s training data.

When to use it: Agentic RAG systems are particularly valuable when responses must be grounded in specific source documents. They are also well suited for domains where knowledge evolves over time, such as documentation, policies, and operational runbooks, or when the knowledge base is too large to fit within a single context window.

When not to use it: This pattern is less effective for general knowledge questions that the model already knows from training. It is also not ideal for tasks that require deep reasoning over retrieved information rather than simple retrieval and citation, since RAG enhances access to knowledge but does not inherently improve reasoning. Additionally, its effectiveness depends heavily on document quality, poor data leads to poor results. Finally, RAG struggles with real-time information that changes faster than the indexing pipeline can update.

Python

rom langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough

# ── Build the knowledge base ───────────────────────────────────────
# In production: load from S3, Confluence, PDFs, databases, etc.
documents = [
    "Our S3 buckets must have versioning enabled in production environments.",
    "IAM roles must follow least-privilege principle. No wildcard actions in production.",
    "All EC2 instances must use IMDSv2. IMDSv1 is disabled org-wide.",
    "RDS instances must have automated backups with 7-day retention minimum.",
    "CloudTrail must be enabled in all regions. Logs retained for 365 days.",
]

# Split, embed, and store in vector database
embeddings   = OllamaEmbeddings(model="llama3")
text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
vectorstore  = FAISS.from_texts(documents, embeddings)
retriever    = vectorstore.as_retriever(search_kwargs={"k": 3})

# ── Build the RAG chain ────────────────────────────────────────────
llm = ChatOllama(model="llama3", temperature=0)

rag_prompt = ChatPromptTemplate.from_messages([
    ("system", """You are a cloud compliance assistant.
    Answer questions using ONLY the provided context documents.
    If the context does not contain the answer, say so explicitly.
    Never invent compliance rules not present in the context.

    Context documents:
    {context}"""),
    ("user", "{question}")
])

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | rag_prompt
    | llm
)

answer = rag_chain.invoke("What are the backup requirements for RDS?")
print(answer.content)
# → "According to the provided context, RDS instances must have automated
#    backups with a minimum 7-day retention period."

In an agent system, RAG becomes a tool rather than a pipeline. The agent decides when to retrieve and what to query, rather than always retrieving before every response.

Python

@tool
def search_compliance_docs(query: str) -> str:
    """
    Search internal compliance documentation for policies and requirements.
    Use this when asked about org policies, security requirements, or approved configurations.
    Returns the top 3 most relevant policy excerpts.
    """
    docs = retriever.invoke(query)
    return "\n\n".join([f"Policy {i+1}: {doc.page_content}" for i, doc in enumerate(docs)])


The orchestrator-worker pattern decomposes a complex task into subtasks and delegates each to a specialized agent. The orchestrator handles planning and coordination. Workers handle execution. No worker knows about any other worker, they only communicate through shared state (a database, S3, or message queue) coordinated by the orchestrator.


Complex task
    ↓
Orchestrator agent
    ├── Plans decomposition
    ├── Assigns subtasks to workers
    └── Synthesizes results
         ↓              ↓              ↓
    Worker A        Worker B        Worker C
    (extraction)  (transform)    (validation)
         ↓              ↓              ↓
    S3 result      S3 result      S3 result
         ↓              ↓              ↓
    Orchestrator collects all results
         ↓
    Final synthesized output

Real-world example

  • Data processing pipelines,
  • document analysis (extract → summarize → classify → route),
  • software development agents (plan → code → test → review),
  • content workflows (research → draft → edit → format).

When to use it: The Orchestrator pattern is a strong fit for workflows with clearly separable subtasks or tasks requiring diverse expertise, such as SQL generation, narrative writing, and validation. It is also valuable for problems that exceed a single agent’s context window and for workflows that benefit from parallelism, specialization, and coordinated execution.

When not to use it: The Orchestrator pattern is a poor fit for simple workflows that lack natural task decomposition. It also struggles in scenarios where subtask boundaries are unclear, since poorly coordinated decomposition can degrade performance compared to a single capable agent. In many cases, the coordination and communication overhead may outweigh the advantages of specialization and parallelism.

Python

from langchain_ollama import ChatOllama
from langchain_core.tools import tool
import json

llm = ChatOllama(model="llama3", temperature=0.2)

# Worker implementations — each focused on one thing
class DataExtractionWorker:
    def run(self, raw_data: str, run_id: str) -> dict:
        prompt = f"Extract all structured fields from this data as JSON:\n{raw_data}"
        result = llm.invoke(prompt).content
        return {"worker": "extraction", "run_id": run_id, "result": result}

class TransformationWorker:
    def run(self, extracted_data: dict, schema: str, run_id: str) -> dict:
        prompt = f"Transform this data to match the target schema.\nData: {extracted_data}\nSchema: {schema}"
        result = llm.invoke(prompt).content
        return {"worker": "transformation", "run_id": run_id, "result": result}

class ValidationWorker:
    def run(self, transformed_data: dict, rules: list, run_id: str) -> dict:
        prompt = f"Validate this data against these rules: {rules}\nData: {transformed_data}"
        result = llm.invoke(prompt).content
        return {"worker": "validation", "run_id": run_id, "result": result}

# Orchestrator coordinates all workers
class Orchestrator:
    def __init__(self):
        self.extractor   = DataExtractionWorker()
        self.transformer = TransformationWorker()
        self.validator   = ValidationWorker()

    def run(self, task: dict) -> dict:
        import uuid
        run_id = str(uuid.uuid4())[:8]

        print(f"\n[Orchestrator] Starting workflow. run_id={run_id}")

        # Step 1: Extract
        print("[Orchestrator] Delegating to extraction worker...")
        extracted = self.extractor.run(task["raw_data"], run_id)

        # Step 2: Transform (uses extraction output)
        print("[Orchestrator] Delegating to transformation worker...")
        transformed = self.transformer.run(extracted["result"], task["target_schema"], run_id)

        # Step 3: Validate (uses transformation output)
        print("[Orchestrator] Delegating to validation worker...")
        validated = self.validator.run(transformed["result"], task["validation_rules"], run_id)

        # Step 4: Synthesize
        print("[Orchestrator] Synthesizing results...")
        return {
            "run_id":      run_id,
            "extraction":  extracted["result"],
            "transformed": transformed["result"],
            "validation":  validated["result"],
            "status":      "complete"
        }

orchestrator = Orchestrator()
result = orchestrator.run({
    "raw_data":         "John Smith, 35, [email protected], joined 2019-03-15",
    "target_schema":    "{ name: string, age: int, email: string, tenure_years: int }",
    "validation_rules": ["email must contain @", "age must be between 18-100", "tenure_years must be positive"]
})

Evaluator-Optimizer pattern uses one LLM to generate solution and a separate LLM or a same LLM with different set of prompt instructions to evaluate, against a explicit criteria and provide structural feedback. The generator LLM then uses the feedback to improve. Unlike reflection which is self-critique, evaluator optimizer uses an independent evaluator with explicit scoring criteria.


Task + Criteria
    ↓
Generator LLM → Candidate solution
    ↓
Evaluator LLM
    ├── Score: 6/10
    ├── Criterion 1: Pass
    ├── Criterion 2: Fail — reason
    └── Criterion 3: Partial — reason
    ↓
Feedback passed to Generator
    ↓
Generator LLM → Improved solution
    ↓
Evaluator LLM → Score: 9/10 → PASS
    ↓
Final solution

Real-world example

  • IAM policy generation with compliance evaluation
  • infrastructure-as-code with security scoring
  • API contract generation with schema validation
  • any task with explicit, automatable quality criteria.

When to use it :The Evaluator-Optimizer pattern is particularly well suited for tasks where the quality bar can be defined precisely enough for automated evaluation. It works especially well for tasks with explicit, scoreable quality criteria, such as coding tasks that must pass tests, writing tasks that must adhere to style guidelines, and configuration tasks that must satisfy compliance rules.

When NOT to use it: This pattern is less effective for tasks where quality criteria cannot be defined explicitly, or where the generator and evaluator share the same blind spots. In such cases, the evaluator may fail to identify the mistakes missed by the generator. Additionally, this pattern is not well suited for tasks where no clear ground truth exists for evaluation.

Python

from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from dataclasses import dataclass

@dataclass
class EvaluationResult:
    score: float          # 0-10
    passed: bool          # True if score >= threshold
    feedback: str         # specific improvement suggestions
    criteria_scores: dict # per-criterion breakdown

generator_llm = ChatOllama(model="llama3", temperature=0.5)
evaluator_llm = ChatOllama(model="llama3", temperature=0.0)  # deterministic evaluation

def evaluate(solution: str, task: str, criteria: dict) -> EvaluationResult:
    """Score a solution against explicit criteria and return structured feedback."""
    criteria_text = "\n".join([f"- {k}: {v}" for k, v in criteria.items()])

    eval_prompt = f"""Evaluate this solution against the criteria. Be strict and specific.

Task: {task}
Solution: {solution}

Criteria (score each 0-10):
{criteria_text}

Respond in this exact format:
CRITERION_SCORES: {{"criterion1": score, "criterion2": score, ...}}
OVERALL_SCORE: [0-10]
PASS: [YES/NO]
FEEDBACK: [specific, actionable improvement suggestions]"""

    response = evaluator_llm.invoke(eval_prompt).content

    # Parse response
    lines         = {l.split(":")[0].strip(): ":".join(l.split(":")[1:]).strip()
                     for l in response.split("\n") if ":" in l}
    overall_score = float(lines.get("OVERALL_SCORE", "5").strip())
    passed        = "YES" in lines.get("PASS", "NO").upper()
    feedback      = lines.get("FEEDBACK", "No specific feedback provided")

    return EvaluationResult(
        score=overall_score, passed=passed,
        feedback=feedback,   criteria_scores={}
    )

def evaluator_optimizer(task: str, criteria: dict,
                         threshold: float = 8.0, max_rounds: int = 4) -> str:
    """Generate and refine a solution until it meets the quality threshold."""

    solution = generator_llm.invoke(f"Complete this task:\n{task}").content

    for round_num in range(max_rounds):
        result = evaluate(solution, task, criteria)
        print(f"Round {round_num + 1}: Score {result.score:.1f}/10 — {'PASS' if result.passed else 'FAIL'}")

        if result.passed and result.score >= threshold:
            print(f"Threshold met. Final score: {result.score:.1f}")
            break

        # Improve based on feedback
        improve_prompt = f"""Improve your solution based on this feedback.
Original task: {task}
Your solution: {solution}
Evaluator feedback: {result.feedback}
Provide the improved solution:"""
        solution = generator_llm.invoke(improve_prompt).content

    return solution

final_policy = evaluator_optimizer(
    task="Write an IAM policy for a Lambda function that reads from DynamoDB and writes to S3",
    criteria={
        "least_privilege":   "Only exact permissions needed, no wildcards in production actions",
        "resource_scoping":  "All resources scoped to specific ARNs, not *",
        "correct_syntax":    "Valid IAM policy JSON with proper structure",
        "completeness":      "All required actions for the described functionality included"
    },
    threshold=8.5
)

Pattern 8 — Reflection and Self-Critique

Reflection gives an agent the ability to evaluate and improve its own output. After producing an initial response, the agent critiques it, checking for errors, gaps, or quality issues and then revises based on the critique. This can be a single critique-and-revise cycle or a loop that continues until the agent judges its output to be satisfactory.

Task input
    ↓
Initial response generation
    ↓
Critique phase (same or different LLM evaluates the response)
    "Issues found:
     - Missing error handling for edge case X
     - Claim Y is not supported by the data
     - Step 3 assumes Z which may not hold"
    ↓
Revision phase (LLM revises based on critique)
    ↓
[Optional: loop back to critique if not satisfactory]
    ↓
Final revised response

Real-world example

  • code generation with automated review,
  • architecture documentation with quality gates,
  • security policy drafting
  • any task where iterative improvement produces meaningfully better output than a single pass.

When to use it:Reflection and Self-Critique patterns are especially well-suited for tasks such as code generation, where correctness is critical, long-form writing that benefits from quality checkpoints, and any task where the initial output is likely to be imperfect and the cost of an incorrect response is high. They are particularly effective in scenarios where clear quality criteria can be explicitly defined within the critique prompt.

When not to use it: This pattern is less effective for tasks where the LLM cannot reliably evaluate its own output, particularly in cases where it cannot recognize gaps or mistakes in its own reasoning. It is also unnecessary for simple tasks where the additional critique step adds more overhead than value. Additionally, for high-stakes scenarios that require domain expertise or accountability, human review often serves as a more appropriate quality gate than LLM self-evaluation.

Python

from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOllama(model="llama3", temperature=0.3)

def reflect_and_revise(task: str, max_iterations: int = 3) -> str:
    """
    Generate, critique, and revise until the critique finds no significant issues
    or max_iterations is reached.
    """

    # ── Step 1: Initial generation ─────────────────────────────────
    gen_prompt = ChatPromptTemplate.from_messages([
        ("system", "You are an expert cloud engineer. Produce your best response."),
        ("user",   "{task}")
    ])
    response = (gen_prompt | llm).invoke({"task": task}).content
    print(f"Initial response generated ({len(response.split())} words)")

    for iteration in range(max_iterations):
        # ── Step 2: Critique ────────────────────────────────────────
        critique_prompt = ChatPromptTemplate.from_messages([
            ("system", """You are a strict technical reviewer.
            Critique the following response for:
            1. Technical errors or inaccuracies
            2. Missing edge cases or failure modes
            3. Security or reliability concerns
            4. Clarity and completeness gaps

            Be specific. If the response is satisfactory on all criteria, respond with exactly: APPROVED
            Otherwise list the specific issues found."""),
            ("user", f"Original task: {task}\n\nResponse to critique:\n{response}")
        ])

        critique = (critique_prompt | llm).invoke({}).content
        print(f"Iteration {iteration + 1} critique: {critique[:100]}...")

        # ── Step 3: Check if approved ───────────────────────────────
        if "APPROVED" in critique.upper():
            print(f"Response approved after {iteration + 1} iteration(s)")
            break

        # ── Step 4: Revise based on critique ────────────────────────
        revise_prompt = ChatPromptTemplate.from_messages([
            ("system", "You are an expert cloud engineer. Revise your response based on the critique."),
            ("user", f"Original task: {task}\n\nYour previous response:\n{response}\n\nCritique:\n{critique}\n\nProvide the revised response addressing all critique points:")
        ])
        response = (revise_prompt | llm).invoke({}).content
        print(f"Revised response generated ({len(response.split())} words)")

    return response

final = reflect_and_revise(
    "Write a Terraform module for a secure S3 bucket with versioning, encryption, and access logging"
)

Summary

This blog explored some of the most important agentic AI patterns and how they can be applied to build more capable, reliable, and scalable AI systems. In real-world production environments, systems rarely rely on a single pattern. Instead, multiple patterns are often combined based on the nature of the task and system requirements.

  • Routing + Orchestrator-Worker : router classifies the task, orchestrator decomposes and delegates to the right workers
  • RAG + ReAct : agent uses retrieval as one of several tools in a ReAct loop, fetching documents when needed
  • Plan and Solve + Reflection : generate a plan, execute it, reflect on the output, revise if needed
  • Self-Consistency + Chain of Thought : run CoT multiple times, take the majority answer for high-confidence responses
  • Orchestrator-Worker + Parallelization : orchestrator assigns independent workers to run in parallel, collects results when all complete

The choice and composition of these patterns are typically driven by four key task characteristics: the complexity of the task, whether external data is required to accomplish it, whether the task can be decomposed into smaller subtasks, and the quality constraints expected from the final output.

While this blog post covered foundational and widely used patterns, there are several additional patterns : Plan and Solve, Tree of Thoughts (ToT), Parallelization, Routing, Language Agent Tree Search (LATS) and Multi-Agent Debate that can be explored in follow-up blog posts.