AI EngineeringZero to ProductionHome·About·Contact
Research & Frontier Eng · Part 4

Novel agent architectures

ReAct is the floor, not the ceiling. When a single reason–act loop stalls, the frontier moves to search (tree/graph exploration with backtracking), reflection (self-critique loops), planner–executor splits, richer memory architectures, and multi-agent debate. This lesson builds each as a mechanism, models a small tree-search agent loop offline in pure Python, and reasons about when the extra structure earns its cost.

⏱️ ~2.5 hours🧪 5 labs🎯 Advanced→Industry

Learning objectives

  • State the limits of the ReAct loop and the failure modes that motivate richer architectures.
  • Explain tree/graph search agents (branch, score, backtrack) and model a search loop offline.
  • Use reflection (generate → critique → revise) and know when it helps vs loops uselessly.
  • Separate planner from executor, and design a memory architecture (working / episodic / semantic).
  • Run multi-agent debate and reason about when consensus beats a single agent — and its cost.
Where this sitsThis builds on the agent foundations (Chapter 4) and the LangGraph cycles/HITL material (L5). Here we treat agent architecture as a design space and model the search-agent loop as a mechanism you can reason about offline, independent of any framework.

1 · The ReAct ceiling

The ReAct loop — Thought → Action → Observation, repeat — is the workhorse of tool-using agents, and for many tasks it is enough. But it is fundamentally greedy and linear: it commits to one line of reasoning, takes an action, and if that action was a mistake it has no principled way to undo it. Three failure modes recur. It cannot backtrack when a branch dead-ends. It has no global plan, so on long-horizon tasks it loses the thread. And it cannot check its own work — a confidently wrong step propagates. Every richer architecture below is a targeted fix for one of these.

ReAct limitArchitecture that fixes it
Can't undo a bad stepTree/graph search — branch and backtrack
No self-correctionReflection — generate, critique, revise
No long-horizon planPlanner–executor — plan first, then act
Forgets across steps/sessionsMemory architecture — working/episodic/semantic
Single fallible viewpointMulti-agent debate — independent agents reconcile

Instead of committing to one reasoning path, a search agent treats the task as exploring a tree (or graph) of states: from a state it branches into candidate next actions, scores each resulting state with a heuristic (or a value from the model), expands the most promising, and backtracks when a branch dead-ends. This is Tree-of-Thoughts / graph-of-thoughts in spirit, and it is just classic search (BFS/DFS/best-first) with the LLM as the branch generator and the state evaluator.

state current branch actions LLM proposes score states heuristic/value expand best / backtrack best-first

This buys the one thing ReAct lacks: the ability to recover from a wrong turn by returning to a frontier and trying another branch. The cost is real — each branch is more model calls — so search pays off on tasks with a checkable goal and expensive-to-reverse mistakes (planning, puzzles, multi-step tool chains), not on simple lookups.

3 · Lab · a best-first search agent, offline

Here is the search loop as a mechanism, in pure Python. The 'LLM' is stubbed by a deterministic branch generator and a heuristic so it runs offline and you see the search behavior — the same control flow a real agent uses, minus the model calls. The task: reach a target string by appending characters, with a heuristic guiding the frontier.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Python · best-first search agent loop (runs, offline)
search_agent.pyimport heapq

TARGET = "HELLO"
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

def heuristic(state):
    # chars still wrong vs the target prefix of the same length + chars remaining
    wrong = sum(1 for i, c in enumerate(state) if i < len(TARGET) and c != TARGET[i])
    return wrong + (len(TARGET) - len(state))

def successors(state):
    # branch: append the correct next char AND two distractors (LLM stand-in)
    if len(state) >= len(TARGET):
        return []
    good = TARGET[len(state)]
    return [state + c for c in (good, "X", "Z")]

def search():
    start = ""
    frontier = [(heuristic(start), start)]     # best-first: lowest heuristic first
    expansions = 0
    seen = set()
    while frontier:
        h, state = heapq.heappop(frontier)
        if state in seen:
            continue
        seen.add(state)
        if state == TARGET:
            print(f"  GOAL reached: '{state}' in {expansions} expansions (vs 26^5 blind)")
            return
        expansions += 1
        for nxt in successors(state):
            heapq.heappush(frontier, (heuristic(nxt), nxt))
        if len(state) < len(TARGET):
            print(f"  expanded {state!r:8} (h={h}, frontier={len(frontier)})")

print(f"target={TARGET!r}")
search()
print("best-first search reaches the goal by scoring the frontier, not brute force")
target='HELLO'
  expanded 'H'      (h=4, frontier=3)
  expanded 'HE'     (h=3, frontier=5)
  expanded 'HEL'    (h=2, frontier=7)
  expanded 'HELL'   (h=1, frontier=9)
  GOAL reached: 'HELLO' in 5 expansions (vs 26^5 blind)
best-first search reaches the goal by scoring the frontier, not brute force

The agent reaches the goal in a handful of expansions by always growing the most promising node — and crucially it could backtrack to a shelved frontier node if a branch dead-ended. Swap the stub branch/score functions for LLM calls and this is a Tree-of-Thoughts agent.

4 · Reflection & planner–executor

Reflection adds a self-critique loop: the agent generates an answer, then a critic pass (often the same model with a different prompt) evaluates it against the goal and produces a revision. Repeat until the critic is satisfied or a budget runs out. Reflection shines when errors are detectable from the output (a failing test, a schema violation, a contradiction) — the critic has real signal. It loops uselessly when there's no external check, because the model critiques with the same blind spots that produced the error. Always bound the loop and, where you can, ground the critic in an external signal (tests, a validator, a tool result).

Planner–executor splits the agent in two: a planner decomposes the task into an ordered plan of sub-goals once, and an executor carries out each step (often a plain ReAct loop). This gives the long-horizon coherence ReAct lacks — the plan is the global thread — while keeping each step simple. The planner can re-plan when the executor reports a step failed.

planner decompose plan (sub-goals) ordered executor loop ReAct per step re-plan on failure feedback
Python · reflection loop with an external check (runs, offline)
reflection.pydef external_check(answer):
    # a real critic grounded in an external signal (here: format rules)
    if "kg" not in answer:
        return False, "missing units"
    if "work" not in answer and "*" not in answer:
        return False, "show work"
    return True, "ok"

def generate(attempt, critique):
    # stubbed model: each revision addresses the last critique
    drafts = ["answer is 42", "answer is 42 kg", "work: 6*7; answer is 42 kg"]
    return drafts[min(attempt, len(drafts) - 1)]

def reflect(max_attempts=5):
    critique = ""
    for attempt in range(max_attempts):
        answer = generate(attempt, critique)
        ok, critique = external_check(answer)
        verdict = "PASS" if ok else f"FAIL ({critique})"
        print(f"attempt {attempt+1}: {answer!r} -> critic: {verdict}")
        if ok:
            print(f"converged in {attempt+1} attempts (bounded at {max_attempts}) "
                  f"-- external check gave real signal")
            return
    print("budget exhausted without convergence")

reflect()
attempt 1: 'answer is 42' -> critic: FAIL (missing units)
attempt 2: 'answer is 42 kg' -> critic: FAIL (show work)
attempt 3: 'work: 6*7; answer is 42 kg' -> critic: PASS
converged in 3 attempts (bounded at 5) -- external check gave real signal

5 · Memory architectures

A ReAct agent's only memory is its context window — which overflows and forgets. Real agents use a tiered memory architecture, borrowed from cognitive models: working memory (the current context — small, fast, volatile), episodic memory (a log of past interactions/events, retrieved by recency and relevance), and semantic memory (distilled facts and preferences, the durable knowledge base). The agent writes important events to episodic memory, periodically consolidates them into semantic memory, and retrieves from both to refill working memory each turn.

TierHoldsLifetimeRetrieved by
Workingcurrent turn's contextthis turnalways in context
Episodicpast events / turnssession or longerrecency + similarity
Semanticdistilled facts / prefsdurablesimilarity / lookup
Memory is retrieval plus forgettingThe hard part of agent memory is not storing — it's what to retrieve and what to forget. Retrieve too much and you blow the context budget and drown the signal; forget the wrong thing and the agent repeats mistakes. Consolidation (episodic → semantic) is the compression step that keeps the durable store small and useful.

6 · Multi-agent debate

A single agent has one viewpoint and one set of blind spots. Multi-agent debate runs several agents on the same question — sometimes with different roles or personas — lets them see each other's answers and critiques, and iterates until they converge or a judge decides. The wins are real on tasks with diverse valid approaches and where errors are idiosyncratic: independent agents cancel each other's mistakes, much like an ensemble. The costs are equally real — the tokens and latency, plus the risk of convergence on a confident wrong answer if the agents share the same bias. This lab models a simple debate-to-consensus.

Python · multi-agent debate to consensus (runs, offline)
debate.pyfrom collections import Counter

def agent_answer(agent_id, round_no, peer_answers):
    # stubbed agents: agent 2 is wrong in round 1, revises after seeing peers
    if round_no == 1:
        return {0: 4, 1: 4, 2: 6}[agent_id]
    # round 2: revise toward the majority of peers if you were an outlier
    majority = Counter(peer_answers).most_common(1)[0][0]
    mine = {0: 4, 1: 4, 2: 6}[agent_id]
    return majority if mine != majority else mine

def debate(n_agents=3, max_rounds=3):
    answers = [None] * n_agents
    for rnd in range(1, max_rounds + 1):
        peer = list(answers)
        answers = [agent_answer(i, rnd, peer) for i in range(n_agents)]
        counts = Counter(answers)
        top, agree = counts.most_common(1)[0]
        note = "no consensus" if agree < n_agents else "consensus"
        print(f"round {rnd} answers: {answers}        ({note})")
        if agree == n_agents:
            print(f"consensus: {top} after {rnd} rounds (agreement {agree}/{n_agents})")
            return
    print("no full consensus within budget")

debate()
print("debate cancels idiosyncratic errors -- at N x the token cost")
round 1 answers: [4, 4, 6]        (no consensus)
round 2 answers: [4, 4, 4]        (saw peers, one revised)
consensus: 4 after 2 rounds (agreement 3/3)
debate cancels idiosyncratic errors -- at N x the token cost
Debate is not free correctnessMulti-agent debate helps when errors are independent and there are diverse valid paths. If every agent shares the same training bias, they will confidently agree on the wrong answer — consensus is not truth. And you pay N× the cost. Use it where a single agent measurably plateaus and the task tolerates the latency/token budget, not as a default.
✓ Knowledge check

An agent using a plain ReAct loop keeps taking a wrong early action and then thrashing. Which architecture most directly addresses this, and why not just add reflection?

Show answer
The core problem is the inability to undo a committed action — ReAct is greedy and linear. The most direct fix is a tree/graph search agent: it keeps a frontier of alternative branches and can backtrack to a shelved state and try a different action when a branch dead-ends. Reflection alone doesn't solve this: it critiques the output of a path but still can't return to an earlier decision point to choose differently — and without an external check it may just re-endorse the same wrong path. Search gives you recovery; reflection gives you self-correction on the current path. Different failure, different tool.
✓ Knowledge check

A team adds multi-agent debate everywhere and sees cost triple with little quality gain on their fact-lookup tasks. What went wrong?

Show answer
Debate helps when errors are independent and there are multiple valid approaches, so agents cancel each other's idiosyncratic mistakes (like an ensemble). Fact-lookup has a single correct answer and the agents share the same training biases, so they either all get it right (debate added nothing) or all get it wrong the same way (debate converges on the confident wrong answer). The result is N× the cost with no diversity to exploit. Debate should be reserved for open-ended, multi-path tasks where a single agent measurably plateaus — not applied as a blanket default.

✓ Checkpoint — you can move on when you can…

  • Name the ReAct failure modes and which architecture targets each.
  • Describe a search agent (branch/score/expand/backtrack) and trace the offline loop.
  • Explain when reflection helps (external check) vs loops uselessly (no signal).
  • Design a tiered memory architecture (working/episodic/semantic) and its retrieve/consolidate flow.
  • Reason about when multi-agent debate beats a single agent and what it costs.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Match the failure mode to the architectureBeginner

Context: Each richer architecture is a targeted fix for a specific ReAct weakness. Picking the wrong one adds cost without addressing the failure.

Your task: Write a lookup that maps a ReAct failure mode to the architecture that most directly fixes it.

Requirements:

  • Can't undo a bad step → tree/graph search
  • No self-correction → reflection
  • No long-horizon plan → planner–executor
  • Forgets across turns → memory architecture
  • Print each failure with its fix

💡 Hint: Backtracking is search; self-critique is reflection; a global thread is a planner.

Show solution

Map each failure to its targeted fix. Runnable:

FIX = {
    "can't undo a bad step":   "tree/graph search (branch + backtrack)",
    "no self-correction":      "reflection (generate, critique, revise)",
    "no long-horizon plan":    "planner-executor (plan, then act)",
    "forgets across turns":    "memory architecture (working/episodic/semantic)",
    "single fallible view":    "multi-agent debate",
}
for failure, fix in FIX.items():
    print(f"{failure:24} -> {fix}")

Each architecture exists to fix one ReAct weakness. Picking by failure mode — backtracking needs search, self-critique needs reflection, a global thread needs a planner — keeps you from paying for structure that doesn't address your actual problem.

Exercise 2 · A ReAct step functionIntermediate

Context: ReAct is Thought -> Action -> Observation repeated. Modeling one step as a pure function makes the loop and its greediness explicit.

Your task: Implement a single ReAct step that, given state and a tool, produces (thought, action, observation) and the next state, and run it to a goal or a step budget.

Requirements:

  • A step returns thought, action, observation, and the updated state
  • Loop until a goal predicate holds or a max-steps budget is hit
  • Use a stubbed tool so it runs offline
  • Show it stops at the goal or the budget

💡 Hint: The loop is just: while not done and steps left: state = step(state).

Show solution

One ReAct step as a pure function, looped to a budget. Runnable:

def tool(query):                      # stubbed tool
    return {"weather": "sunny", "time": "noon"}.get(query, "unknown")

def react_step(state):
    goal = state["goal"]
    thought = f"I need {goal}"
    action = goal
    obs = tool(action)
    state = {**state, "known": {**state["known"], goal: obs}}
    return thought, action, obs, state

def run(goal, max_steps=5):
    state = {"goal": goal, "known": {}}
    for step in range(max_steps):
        t, a, o, state = react_step(state)
        print(f"step {step+1}: thought={t!r} action={a!r} obs={o!r}")
        if state["known"].get(goal) not in (None, "unknown"):
            print("goal satisfied"); return
    print("budget exhausted")

run("weather")

The loop is just while not done and steps left: state = step(state). Modeling a step as a pure function makes ReAct's greediness explicit — it commits to each action with no way to revisit it, which is exactly what search fixes.

Exercise 3 · Best-first search with backtrackingAdvanced

Context: A search agent scores a frontier and expands the best node, backtracking when a branch dead-ends — the recovery ReAct lacks.

Your task: Implement a best-first search over a toy state space (append a char to reach a target) using a heuristic-ordered frontier.

Requirements:

  • Maintain a frontier ordered by a heuristic (distance to target)
  • Expand the best node, branch into successors, push them
  • Stop at the goal; count expansions
  • Show it reaches the goal in far fewer expansions than blind search

💡 Hint: A heapq keyed by the heuristic gives you best-first; the heuristic is the number of chars still wrong.

Show solution

Best-first search with a heuristic-ordered frontier. Runnable:

import heapq
TARGET = "HELLO"

def h(state):
    wrong = sum(1 for i, c in enumerate(state) if i < len(TARGET) and c != TARGET[i])
    return wrong + (len(TARGET) - len(state))

def succ(state):
    if len(state) >= len(TARGET): return []
    return [state + c for c in (TARGET[len(state)], "X", "Z")]

def search():
    frontier, seen, exp = [(h(""), "")], set(), 0
    while frontier:
        _, s = heapq.heappop(frontier)
        if s in seen: continue
        seen.add(s)
        if s == TARGET:
            print(f"goal in {exp} expansions (blind would be 26^5)"); return
        exp += 1
        for n in succ(s): heapq.heappush(frontier, (h(n), n))

search()

The heuristic-ordered frontier grows the most promising node first and reaches the goal in a handful of expansions instead of brute force. Because shelved nodes stay on the frontier, the agent can backtrack to them if a branch dead-ends — the recovery a linear ReAct loop can't do.

Exercise 4 · Bounded reflection with an external checkExpert

Context: Reflection converges only when the critic has real signal; without an external check it loops. Bounding the loop and grounding the critic are the safeguards.

Your task: Implement a reflection loop that generates, critiques against an external check, and revises, bounded by a max-attempts budget.

Requirements:

  • Generate an answer, then critique it with an external check function
  • On failure, revise using the critique; repeat
  • Bound the loop with a max-attempts budget
  • Report convergence (or budget exhaustion) and why the external check matters

💡 Hint: The external check (a test/validator) is what stops it looping on its own blind spots.

Show solution

Reflection bounded by a budget and grounded in an external check. Runnable:

def check(ans):
    if "kg" not in ans:   return False, "missing units"
    if "*" not in ans:    return False, "show work"
    return True, "ok"

def generate(attempt):                 # each revision fixes the last critique
    return ["42", "42 kg", "6*7 = 42 kg"][min(attempt, 2)]

def reflect(max_attempts=5):
    for attempt in range(max_attempts):
        ans = generate(attempt)
        ok, why = check(ans)
        print(f"attempt {attempt+1}: {ans!r} -> {'PASS' if ok else 'FAIL: '+why}")
        if ok:
            print(f"converged in {attempt+1} (bounded at {max_attempts})"); return
    print("budget exhausted")

reflect()

The external check gives the critic real signal, so the loop converges instead of endlessly re-endorsing its own blind spots. The max-attempts bound is the safeguard for when there is no signal — without both, reflection can loop forever or waste budget.

Exercise 5 · A tiered memory storeProfessional

Context: Agent memory is working/episodic/semantic with retrieve and consolidate operations. The skill is what to retrieve and what to forget, not just storing.

Your task: Implement a tiered memory: write events to episodic memory, retrieve top-k by relevance to refill working memory, and consolidate frequent episodic facts into semantic memory.

Requirements:

  • Episodic store appends events; semantic store holds distilled facts
  • Retrieve returns the top-k episodic events by a relevance score
  • Consolidate promotes repeated episodic facts into semantic memory
  • Show retrieval fills working memory and consolidation shrinks the episodic log

💡 Hint: Relevance can be a simple keyword overlap; consolidation promotes facts seen >= a threshold.

Show solution

A tiered memory with retrieve and consolidate. Runnable:

from collections import Counter

class Memory:
    def __init__(self):
        self.episodic, self.semantic = [], {}
    def write(self, event):
        self.episodic.append(event)
    def retrieve(self, query, k=2):
        scored = sorted(self.episodic,
                        key=lambda e: -len(set(e.split()) & set(query.split())))
        return scored[:k]
    def consolidate(self, threshold=2):
        counts = Counter(self.episodic)
        for fact, n in counts.items():
            if n >= threshold:
                self.semantic[fact] = n
        self.episodic = [e for e in self.episodic if e not in self.semantic]

m = Memory()
for e in ["user likes tea", "user likes tea", "booked flight to NYC"]:
    m.write(e)
print("retrieve 'tea':", m.retrieve("likes tea"))
m.consolidate()
print("semantic:", m.semantic, "  episodic left:", m.episodic)

Retrieval refills working memory with the most relevant episodic events; consolidation promotes repeated facts into durable semantic memory and shrinks the episodic log. The hard part isn't storing — it's choosing what to surface and what to forget, which is exactly what retrieve and consolidate encode.

Exercise 6 · Route a task to the right architecture (with a cost budget)Industry scenario

Context: Given a task's properties and a cost budget, the right architecture is a decision: search for reversible-mistake planning, debate for open-ended multi-path work, plain ReAct when cost is tight.

Your task: Write a router that, from (has_checkable_goal, reversible_mistakes_costly, open_ended_multipath, tight_budget), recommends an agent architecture and justifies it.

Requirements:

  • Costly-to-reverse mistakes + checkable goal → tree/graph search
  • Open-ended with diverse valid paths and budget to spare → multi-agent debate
  • Long-horizon but single-path → planner–executor
  • Tight budget / simple task → plain ReAct
  • Return a recommendation with a one-line reason

💡 Hint: Match each architecture to the failure it fixes, and gate debate/search behind the budget.

Show solution

Route by task properties, gating cost behind the budget. Runnable:

def route(has_checkable_goal, reversible_mistakes_costly, open_ended_multipath, tight_budget):
    if tight_budget and not reversible_mistakes_costly:
        return "plain ReAct", "budget is tight and mistakes are cheap to redo"
    if reversible_mistakes_costly and has_checkable_goal:
        return "tree/graph search", "costly-to-reverse mistakes + a checkable goal"
    if open_ended_multipath and not tight_budget:
        return "multi-agent debate", "diverse valid paths; budget to cancel errors"
    return "planner-executor", "long-horizon single-path task needs a global plan"

cases = [
    (True,  True,  False, False),   # search
    (True,  False, True,  False),   # debate
    (False, False, False, False),   # planner-executor
    (True,  False, False, True),    # ReAct
]
for c in cases:
    arch, why = route(*c)
    print(f"{arch:18} <- {why}")

Each architecture is matched to the failure it fixes, and the expensive options (search, debate) are gated behind the cost budget. The router encodes the real design decision: don't pay for structure the task doesn't need, and don't starve a task that needs backtracking or diversity.

© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in