Skip to content

Planning and Reasoning Patterns

Up to this point, we've given our agent a brain (the LLM), hands (tools), and a memory (context management). But there's a crucial piece missing: strategy. Without planning and reasoning, an agent is reactive - it responds to inputs but doesn't think ahead. It's the difference between a chess player who only looks one move ahead and one who thinks five moves deep.

Planning is what separates a simple chatbot from an agent that can tackle complex, multi-step problems. In this chapter, we'll explore the major reasoning patterns that have emerged from research and practice, implement them from scratch, and help you decide which ones to use for your specific use cases.

Reactive vs. Deliberative Agents

Let's start with the fundamental distinction. A reactive agent processes each input independently. It gets a query, generates a response, done. There's no internal deliberation, no looking ahead.

A deliberative agent thinks before it acts. It might:

  • Break a complex problem into sub-tasks
  • Consider multiple approaches before choosing one
  • Evaluate its progress and adjust its plan
  • Recover from failures by trying alternative strategies
# Reactive: just answer the question
response = llm.generate("What's the capital of France?")
# Output: "Paris"

# Deliberative: plan, execute, verify
plan = llm.generate("I need to find quarterly revenue by region. Let me plan my approach...")
# Step 1: Query the database for revenue data
# Step 2: Group by region and quarter
# Step 3: Calculate growth rates
# Step 4: Verify the numbers add up to total revenue
# Step 5: Format as a report

Most real-world agent tasks require deliberation. When someone asks your agent to "analyze our customer churn and suggest retention strategies," there isn't a single tool call that solves that. The agent needs to figure out what data to pull, what analysis to run, and how to synthesize findings into actionable recommendations.

Chain of Thought (CoT)

Chain of Thought is the simplest and most foundational reasoning pattern. The idea is straightforward: instead of jumping straight to an answer, the model is encouraged to show its work - reasoning step by step.

Why It Works

LLMs are autoregressive - each token is generated based on all previous tokens. When you force the model to output intermediate reasoning steps, those steps become part of the context for subsequent tokens. The model literally uses its own reasoning to arrive at better conclusions.

Basic CoT Prompting

# Without CoT
prompt = "If a store has 15 apples and sells 40% of them, how many are left?"
# Model might say: "6" (wrong) or "9" (right, but no confidence)

# With CoT
prompt = """If a store has 15 apples and sells 40% of them, how many are left?

Let me think through this step by step:"""
# Model: "Step 1: Calculate 40% of 15 = 0.40 * 15 = 6
#         Step 2: Subtract from total: 15 - 6 = 9
#         The store has 9 apples left."

Zero-Shot CoT

You don't even need examples. Just adding "Let's think step by step" to a prompt dramatically improves performance on reasoning tasks:

system_prompt = """You are a helpful assistant. When solving problems,
always think through your reasoning step by step before providing an answer."""

Structured CoT for Agents

For agents, we can structure CoT into a specific format:

REASONING_PROMPT = """Before taking action, reason through the problem:

1. **Understanding**: What is the user actually asking for?
2. **Information needed**: What information do I need to answer this?
3. **Available tools**: Which tools can help me get this information?
4. **Plan**: What's the sequence of steps?
5. **Potential issues**: What could go wrong?

Then execute your plan."""
Tip

CoT doesn't add latency in a meaningful way - the "thinking" tokens are fast to generate. But they significantly improve accuracy on complex tasks. There's almost no reason not to use CoT in agent systems.

ReAct: Reasoning + Acting

ReAct (Reason + Act) is the workhorse pattern of modern agent systems. It interleaves thinking and doing in a loop: the agent reasons about what to do, takes an action, observes the result, reasons again, and continues until the task is complete.

The pattern follows a simple cycle:

Thought → Action → Observation → Thought → Action → Observation → ... → Final Answer

How ReAct Differs from Pure CoT

CoT reasons but doesn't act. ReAct does both:

Aspect Chain of Thought ReAct
Reasoning Yes Yes
Tool use No Yes
Iterative No (single pass) Yes (loop)
Self-correction No Yes (via observations)
Best for Single-step reasoning Multi-step tasks with tools

Implementing ReAct from Scratch

Let's build a complete ReAct agent. This is production-ready code, not a toy example.

import json
import re
from typing import Any, Optional
from dataclasses import dataclass


@dataclass
class ReActStep:
    """A single step in the ReAct loop."""
    thought: str
    action: Optional[str] = None
    action_input: Optional[dict] = None
    observation: Optional[str] = None


class ReActAgent:
    """A ReAct agent that interleaves reasoning and acting."""

    def __init__(self, llm_client, tool_registry, max_steps: int = 10):
        self.llm = llm_client
        self.tools = tool_registry
        self.max_steps = max_steps
        self.steps: list[ReActStep] = []

    def _build_system_prompt(self) -> str:
        tool_descriptions = "\n".join(
            f"- **{t.name}**: {t.description}\n  Parameters: {json.dumps(t.parameters)}"
            for t in self.tools.list_tools()
        )
        return f"""You are an AI agent that solves problems by thinking and acting iteratively.

Available tools:
{tool_descriptions}

For each step, respond in EXACTLY this format:

Thought: <your reasoning about what to do next>
Action: <tool_name>
Action Input: <JSON arguments for the tool>

OR, if you have the final answer:

Thought: <your reasoning about why you're done>
Final Answer: <your complete answer to the user's question>

Important rules:
- Always start with a Thought
- Use tools to gather information - don't make up facts
- If a tool returns an error, reason about what went wrong and try a different approach
- When you have enough information, provide a Final Answer"""

    def _parse_response(self, response: str) -> ReActStep:
        """Parse the LLM's response into a structured step."""
        step = ReActStep(thought="")

        # Extract Thought
        thought_match = re.search(r'Thought:\s*(.+?)(?=\nAction:|\nFinal Answer:)', response, re.DOTALL)
        if thought_match:
            step.thought = thought_match.group(1).strip()

        # Check for Final Answer
        final_match = re.search(r'Final Answer:\s*(.+)', response, re.DOTALL)
        if final_match:
            step.thought += f"\nFinal Answer: {final_match.group(1).strip()}"
            return step

        # Extract Action and Action Input
        action_match = re.search(r'Action:\s*(\S+)', response)
        input_match = re.search(r'Action Input:\s*({.+?})', response, re.DOTALL)

        if action_match:
            step.action = action_match.group(1).strip()
        if input_match:
            try:
                step.action_input = json.loads(input_match.group(1))
            except json.JSONDecodeError:
                step.action_input = {}

        return step

    def _build_messages(self, query: str) -> list[dict]:
        """Build the full message list including history of steps."""
        messages = [
            {"role": "system", "content": self._build_system_prompt()},
            {"role": "user", "content": query}
        ]

        for step in self.steps:
            # Add the agent's thought + action
            step_text = f"Thought: {step.thought}"
            if step.action:
                step_text += f"\nAction: {step.action}"
                step_text += f"\nAction Input: {json.dumps(step.action_input)}"
            messages.append({"role": "assistant", "content": step_text})

            # Add the observation
            if step.observation is not None:
                messages.append({
                    "role": "user",
                    "content": f"Observation: {step.observation}"
                })

        return messages

    async def run(self, query: str) -> str:
        """Execute the ReAct loop until completion or max steps."""
        self.steps = []

        for i in range(self.max_steps):
            # Generate next thought/action
            messages = self._build_messages(query)
            response = await self.llm.chat.completions.create(
                model="gpt-4o",
                messages=messages,
                temperature=0.1  # Low temp for consistent reasoning
            )
            response_text = response.choices[0].message.content
            step = self._parse_response(response_text)

            # Check if we have a final answer
            if "Final Answer:" in step.thought:
                final = step.thought.split("Final Answer:")[-1].strip()
                self.steps.append(step)
                return final

            # Execute the action
            if step.action:
                result = await self.tools.execute(step.action, step.action_input or {})
                step.observation = result.to_message()

                # Truncate very long observations
                if len(step.observation) > 3000:
                    step.observation = step.observation[:3000] + "\n... [truncated]"
            else:
                step.observation = "No action was taken. Please specify an Action and Action Input, or provide a Final Answer."

            self.steps.append(step)

        return "I was unable to complete the task within the maximum number of steps. Here's what I found so far: " + \
               self.steps[-1].thought if self.steps else "No progress was made."

Usage:

agent = ReActAgent(llm_client, tool_registry, max_steps=10)

answer = await agent.run(
    "What were our top 3 products by revenue last quarter, and how did they compare to the quarter before?"
)

The agent would then go through steps like:

Thought: I need to find the top 3 products by revenue for last quarter. First, let me query the database for Q4 2025 revenue by product. Action: run_sql_query Action Input: {"sql": "SELECT product_name, SUM(amount) as revenue FROM orders WHERE created_at BETWEEN '2025-10-01' AND '2025-12-31' GROUP BY product_name ORDER BY revenue DESC LIMIT 3"} Observation: {"columns": ["product_name", "revenue"], "rows": [["Widget Pro", 245000], ...]}

Thought: Now I have Q4 data. I need Q3 data for comparison...

Note

The ReAct pattern works well because it forces the agent to explain its reasoning before acting. This makes the agent's behavior interpretable and debuggable - you can read the thought traces to understand why it made each decision.

Plan-and-Execute

While ReAct interleaves thinking and acting, Plan-and-Execute separates them into two distinct phases. First, the agent creates a complete plan. Then it executes the plan step by step.

This is better for complex tasks where you want to see the full plan before execution begins.

class PlanAndExecuteAgent:
    """Agent that creates a plan first, then executes it."""

    def __init__(self, llm_client, tool_registry):
        self.llm = llm_client
        self.tools = tool_registry
        self.react_executor = ReActAgent(llm_client, tool_registry, max_steps=5)

    async def plan(self, task: str) -> list[str]:
        """Generate a step-by-step plan."""
        response = await self.llm.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": """You are a planning agent. Given a task,
                create a detailed step-by-step plan. Return a JSON array of strings,
                where each string is one step. Be specific and actionable.
                Each step should be independently executable."""},
                {"role": "user", "content": f"Create a plan for: {task}"}
            ],
            response_format={"type": "json_object"}
        )
        result = json.loads(response.choices[0].message.content)
        return result["steps"]

    async def execute(self, task: str) -> dict:
        """Plan and then execute."""
        # Phase 1: Plan
        steps = await self.plan(task)
        print(f"Plan created with {len(steps)} steps:")
        for i, step in enumerate(steps, 1):
            print(f"  {i}. {step}")

        # Phase 2: Execute each step
        results = []
        context = ""  # Accumulate context from completed steps

        for i, step in enumerate(steps):
            print(f"\nExecuting step {i + 1}: {step}")
            step_query = f"""Context from previous steps:
{context}

Current task: {step}

Execute this step and report what you found or did."""

            result = await self.react_executor.run(step_query)
            results.append({"step": step, "result": result})
            context += f"\nStep {i + 1} ({step}): {result}\n"

        # Phase 3: Synthesize
        synthesis = await self.llm.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": "Synthesize the results of a multi-step task into a coherent final response."},
                {"role": "user", "content": f"Original task: {task}\n\nStep results:\n{json.dumps(results, indent=2)}"}
            ]
        )
        return {
            "plan": steps,
            "step_results": results,
            "final_answer": synthesis.choices[0].message.content
        }

Tree of Thoughts (ToT)

Sometimes a problem has multiple possible approaches, and you're not sure which one will work. Tree of Thoughts explores multiple reasoning paths simultaneously and evaluates them.

Think of it as brainstorming three different strategies, rating each one, and pursuing the most promising.

class TreeOfThoughts:
    """Explore multiple reasoning paths and select the best."""

    def __init__(self, llm_client, num_branches: int = 3):
        self.llm = llm_client
        self.num_branches = num_branches

    async def generate_thoughts(self, problem: str, context: str = "") -> list[dict]:
        """Generate multiple candidate thoughts/approaches."""
        response = await self.llm.chat.completions.create(
            model="gpt-4o",
            messages=[{
                "role": "user",
                "content": f"""Given this problem, generate {self.num_branches} different approaches.
                For each approach, explain the strategy and assess its likelihood of success (0-1).

                Problem: {problem}
                {f'Context: {context}' if context else ''}

                Return JSON: {{"thoughts": [{{"strategy": "...", "reasoning": "...", "confidence": 0.0-1.0}}]}}"""
            }],
            response_format={"type": "json_object"}
        )
        return json.loads(response.choices[0].message.content)["thoughts"]

    async def evaluate_thought(self, problem: str, thought: dict, result: str) -> float:
        """Evaluate how well a thought/approach worked."""
        response = await self.llm.chat.completions.create(
            model="gpt-4o",
            messages=[{
                "role": "user",
                "content": f"""Rate how well this approach worked for the problem.

                Problem: {problem}
                Strategy: {thought['strategy']}
                Result: {result}

                Return JSON: {{"score": 0.0-1.0, "reasoning": "..."}}"""
            }],
            response_format={"type": "json_object"}
        )
        return json.loads(response.choices[0].message.content)["score"]

    async def solve(self, problem: str) -> str:
        """Solve by exploring multiple thought branches."""
        thoughts = await self.generate_thoughts(problem)

        # Sort by confidence and try the most promising first
        thoughts.sort(key=lambda t: t["confidence"], reverse=True)

        best_result = None
        best_score = -1

        for thought in thoughts:
            # Use a ReAct agent to execute each strategy
            result = await self.react_agent.run(
                f"Solve this problem using this specific strategy:\n"
                f"Problem: {problem}\n"
                f"Strategy: {thought['strategy']}"
            )
            score = await self.evaluate_thought(problem, thought, result)

            if score > best_score:
                best_score = score
                best_result = result

            # Early termination if we find a great solution
            if score > 0.9:
                break

        return best_result

Reflexion: Learning from Mistakes

Reflexion adds a critical capability: the agent evaluates its own performance and learns from failures. After completing a task (or failing), it generates a reflection that can improve future attempts.

class ReflexionAgent:
    """Agent that reflects on its performance and improves."""

    def __init__(self, llm_client, tool_registry, max_retries: int = 3):
        self.llm = llm_client
        self.tools = tool_registry
        self.max_retries = max_retries
        self.reflections: list[str] = []

    async def reflect(self, task: str, attempt_trace: str, outcome: str) -> str:
        """Generate a reflection on what went wrong and how to improve."""
        response = await self.llm.chat.completions.create(
            model="gpt-4o",
            messages=[{
                "role": "user",
                "content": f"""You attempted a task and the outcome was unsatisfactory.
                Reflect on what went wrong and what you should do differently.

                Task: {task}
                Your actions: {attempt_trace}
                Outcome: {outcome}

                Previous reflections: {json.dumps(self.reflections[-3:])}

                Write a concise reflection focusing on:
                1. What specific mistake was made
                2. Why it happened
                3. What to do differently next time"""
            }]
        )
        return response.choices[0].message.content

    async def evaluate(self, task: str, result: str) -> tuple[bool, str]:
        """Evaluate whether the result is satisfactory."""
        response = await self.llm.chat.completions.create(
            model="gpt-4o",
            messages=[{
                "role": "user",
                "content": f"""Evaluate this result. Is it correct and complete?
                Task: {task}
                Result: {result}
                Return JSON: {{"success": true/false, "reason": "..."}}"""
            }],
            response_format={"type": "json_object"}
        )
        eval_result = json.loads(response.choices[0].message.content)
        return eval_result["success"], eval_result["reason"]

    async def run(self, task: str) -> str:
        """Run with reflection and retry."""
        react_agent = ReActAgent(self.llm, self.tools)

        for attempt in range(self.max_retries):
            # Include past reflections in the query
            enhanced_task = task
            if self.reflections:
                enhanced_task += "\n\nLessons from previous attempts:\n"
                enhanced_task += "\n".join(f"- {r}" for r in self.reflections[-3:])

            result = await react_agent.run(enhanced_task)

            # Evaluate
            success, reason = await self.evaluate(task, result)
            if success:
                return result

            # Reflect on failure
            trace = "\n".join(
                f"Step {i}: {s.thought} -> {s.action}({s.action_input}) -> {s.observation}"
                for i, s in enumerate(react_agent.steps)
            )
            reflection = await self.reflect(task, trace, f"Failed: {reason}")
            self.reflections.append(reflection)
            print(f"Attempt {attempt + 1} failed. Reflection: {reflection}")

        return f"Failed after {self.max_retries} attempts. Last result: {result}"

LATS: Language Agent Tree Search

LATS combines tree search (like Monte Carlo Tree Search in game AI) with language agent reasoning. It explores multiple action paths, backtracks when needed, and uses evaluations to guide the search.

This is the most sophisticated pattern - use it for high-stakes tasks where getting the right answer matters more than speed.

class LATSNode:
    """A node in the LATS search tree."""
    def __init__(self, state: str, parent=None, action: str = ""):
        self.state = state
        self.parent = parent
        self.action = action
        self.children: list["LATSNode"] = []
        self.value: float = 0.0
        self.visits: int = 0
        self.is_terminal: bool = False

    def ucb_score(self, exploration_weight: float = 1.41) -> float:
        """Upper Confidence Bound for tree search."""
        if self.visits == 0:
            return float("inf")
        exploitation = self.value / self.visits
        exploration = exploration_weight * (
            (2 * np.log(self.parent.visits) / self.visits) ** 0.5
        )
        return exploitation + exploration


class LATS:
    """Language Agent Tree Search."""

    def __init__(self, llm_client, tool_registry, max_iterations: int = 20):
        self.llm = llm_client
        self.tools = tool_registry
        self.max_iterations = max_iterations

    async def expand(self, node: LATSNode, task: str) -> list[LATSNode]:
        """Generate child nodes (possible next actions)."""
        response = await self.llm.chat.completions.create(
            model="gpt-4o",
            messages=[{
                "role": "user",
                "content": f"""Task: {task}
                Current state: {node.state}

                Generate 3 different possible next actions. For each, describe
                the action and the expected resulting state.

                Return JSON: {{"actions": [{{"action": "...", "expected_state": "..."}}]}}"""
            }],
            response_format={"type": "json_object"}
        )
        actions = json.loads(response.choices[0].message.content)["actions"]
        children = []
        for a in actions:
            child = LATSNode(state=a["expected_state"], parent=node, action=a["action"])
            node.children.append(child)
            children.append(child)
        return children

    async def evaluate_node(self, node: LATSNode, task: str) -> float:
        """Evaluate how promising a node's state is."""
        response = await self.llm.chat.completions.create(
            model="gpt-4o",
            messages=[{
                "role": "user",
                "content": f"""Rate how close this state is to solving the task (0.0 to 1.0).
                Task: {task}
                Current state: {node.state}
                Return JSON: {{"score": 0.0-1.0, "is_terminal": true/false}}"""
            }],
            response_format={"type": "json_object"}
        )
        result = json.loads(response.choices[0].message.content)
        node.is_terminal = result.get("is_terminal", False)
        return result["score"]

    async def search(self, task: str) -> str:
        """Run the LATS search."""
        root = LATSNode(state="Starting state: no actions taken yet")
        root.visits = 1

        for iteration in range(self.max_iterations):
            # Select: traverse tree using UCB
            node = root
            while node.children and not node.is_terminal:
                node = max(node.children, key=lambda n: n.ucb_score())

            # Expand: generate possible actions
            if not node.is_terminal:
                children = await self.expand(node, task)

                # Simulate: evaluate each child
                for child in children:
                    score = await self.evaluate_node(child, task)
                    child.value = score
                    child.visits = 1

                    # Backpropagate
                    current = child
                    while current.parent:
                        current.parent.value += score
                        current.parent.visits += 1
                        current = current.parent

                    # Check for terminal (solved) state
                    if child.is_terminal and score > 0.9:
                        return self._reconstruct_path(child)

        # Return best path found
        best_leaf = self._find_best_leaf(root)
        return self._reconstruct_path(best_leaf)

    def _reconstruct_path(self, node: LATSNode) -> str:
        """Reconstruct the action sequence from root to this node."""
        path = []
        current = node
        while current.parent:
            path.append(f"Action: {current.action}\nState: {current.state}")
            current = current.parent
        path.reverse()
        return "\n\n".join(path)

    def _find_best_leaf(self, root: LATSNode) -> LATSNode:
        """Find the leaf node with the highest value."""
        best = root
        stack = [root]
        while stack:
            node = stack.pop()
            if not node.children and node.value > best.value:
                best = node
            stack.extend(node.children)
        return best

Pattern Comparison

Here's the comprehensive comparison to help you choose:

Pattern Complexity Latency Cost Best For Strengths Weaknesses
CoT Low Low Low Single-step reasoning Simple, always helps No tool use, no iteration
ReAct Medium Medium Medium General agent tasks Flexible, interpretable Can loop, no upfront plan
Plan-and-Execute Medium High Medium-High Complex multi-step tasks Structured, predictable Rigid plans, expensive planning
Tree of Thoughts High High High Problems with multiple paths Explores alternatives Slow, expensive
Reflexion Medium Very High High Tasks requiring precision Self-improving, learns Multiple attempts = high cost
LATS Very High Very High Very High Critical tasks, optimization Thorough, systematic Very expensive, slow

When Planning Goes Wrong

Plans fail. It's not a question of if, but when. Here's how to handle it:

Common Failure Modes

  1. Stale plans: The world changed since the plan was made
  2. Wrong assumptions: A step assumed data existed that doesn't
  3. Tool failures: A required API is down
  4. Scope creep: The task was bigger than initially assessed
  5. Dead ends: A reasoning path leads nowhere

Dynamic Replanning

class AdaptivePlanExecutor:
    """Executes plans with dynamic replanning on failure."""

    def __init__(self, llm_client, tool_registry):
        self.llm = llm_client
        self.tools = tool_registry
        self.planner = PlanAndExecuteAgent(llm_client, tool_registry)

    async def execute_with_replanning(self, task: str) -> str:
        plan = await self.planner.plan(task)
        completed = []
        context = ""

        i = 0
        replan_count = 0
        max_replans = 3

        while i < len(plan) and replan_count < max_replans:
            step = plan[i]
            try:
                react = ReActAgent(self.llm, self.tools, max_steps=5)
                result = await react.run(
                    f"Context: {context}\n\nExecute: {step}"
                )

                # Evaluate step success
                success, reason = await self._evaluate_step(step, result)

                if success:
                    completed.append({"step": step, "result": result})
                    context += f"\nCompleted: {step} -> {result}"
                    i += 1
                else:
                    # Replan from current state
                    print(f"Step failed: {reason}. Replanning...")
                    remaining_goal = f"""Original task: {task}
                    Completed so far: {json.dumps(completed)}
                    Failed step: {step} (reason: {reason})

                    Create a new plan for the remaining work."""

                    plan = await self.planner.plan(remaining_goal)
                    i = 0  # Reset to start of new plan
                    replan_count += 1

            except Exception as e:
                print(f"Error executing step: {e}. Replanning...")
                plan = await self.planner.plan(
                    f"{task}\n\nContext: {context}\n\nError encountered: {str(e)}"
                )
                i = 0
                replan_count += 1

        return await self._synthesize(task, completed)
Warning

Set a replan limit. Without one, an agent in a failure loop will burn through your API budget generating plan after plan. Three replans is usually a good limit - if the third plan fails, escalate to a human.

Practical Example: A Coding Agent

Let's bring everything together with a practical example - a coding agent that plans, implements, tests, and debugs.

class CodingAgent:
    """An agent that writes, tests, and debugs code."""

    def __init__(self, llm_client, tool_registry):
        self.llm = llm_client
        self.tools = tool_registry
        self.reflexion = ReflexionAgent(llm_client, tool_registry, max_retries=3)

    async def solve_coding_task(self, task: str) -> dict:
        # Phase 1: Understand and plan
        plan = await self.llm.chat.completions.create(
            model="gpt-4o",
            messages=[{
                "role": "system",
                "content": "You are a senior software engineer. Plan the implementation."
            }, {
                "role": "user",
                "content": f"""Task: {task}

                Create an implementation plan with:
                1. Requirements analysis
                2. Design decisions
                3. Implementation steps
                4. Test cases to write
                5. Edge cases to handle

                Return JSON."""
            }],
            response_format={"type": "json_object"}
        )
        impl_plan = json.loads(plan.choices[0].message.content)

        # Phase 2: Implement with Reflexion (auto-retry on test failure)
        implementation = await self.reflexion.run(
            f"""Implement the following:
            {json.dumps(impl_plan)}

            Steps:
            1. Write the code using the write_file tool
            2. Write tests using the write_file tool
            3. Run the tests using the run_tests tool
            4. If tests fail, debug and fix the code
            5. Ensure all tests pass before finishing

            The task is complete ONLY when all tests pass."""
        )

        return {
            "plan": impl_plan,
            "implementation": implementation,
            "attempts": len(self.reflexion.reflections) + 1,
            "reflections": self.reflexion.reflections
        }

How to Choose the Right Pattern

Use this decision guide:

Is the task a single question/step?
├── Yes → Use Chain of Thought
└── No → Does it require tools?
    ├── No → Use CoT with structured reasoning
    └── Yes → Is the task well-defined with clear steps?
        ├── Yes → Use Plan-and-Execute
        └── No → Is correctness critical (worth extra cost)?
            ├── No → Use ReAct
            └── Yes → Do you need to explore multiple approaches?
                ├── No → Use Reflexion
                └── Yes → Is it an optimization problem?
                    ├── Yes → Use LATS
                    └── No → Use Tree of Thoughts
Situation Recommended Pattern Reason
Customer support agent ReAct Flexible, handles varied queries
Data analysis pipeline Plan-and-Execute Clear steps, predictable workflow
Code generation Reflexion Needs to test and iterate
Research agent Tree of Thoughts Multiple viable approaches
Simple Q&A with tools ReAct Good default for tool-using agents
Safety-critical decisions LATS Thorough exploration of options
Quick internal automation CoT + single tool call Minimal overhead

Key Takeaways

Planning and reasoning patterns are the strategic layer of your agent architecture. Here's what to remember:

  1. Always use CoT - it's free and always helps
  2. ReAct is your default - start here for any tool-using agent
  3. Plan-and-Execute for complex tasks - when you need structure and predictability
  4. Reflexion for correctness - when getting it right matters more than speed
  5. Tree of Thoughts / LATS for exploration - when the solution path is unclear
  6. Always implement replanning - plans fail, and your agent needs to adapt
  7. Set resource limits - every pattern can loop; guard against runaway costs

The best agents often combine patterns: use Plan-and-Execute at the top level, ReAct for executing individual steps, and Reflexion for critical sub-tasks. The patterns are composable - mix and match based on your specific needs.

In the next chapter, we'll look at multi-agent systems - what happens when multiple agents work together, each potentially using different reasoning patterns.