TechByteByByte

The LLM as the Agent's Brain

Closing Level 2: precisely what role the LLM plays within the agent loop, and why — despite being central — the LLM alone is never the entire agent.

#AI Agents#AI#LLM#Level 2

Begin with the problem

The LLM is a decision-making component, not the entire agent. Application code owns state, tools, permissions, retries, budgets, and termination.

application builds context → LLM proposes next step → application validates and executes

What you will learn

  • Explain what the LLM decides and what the surrounding application controls.
  • Follow how context becomes a tool request, answer, clarification, or stop decision.
  • Understand why fluent language does not guarantee correct plans or facts.
  • Choose model responsibilities without giving the model unnecessary authority.

Current real-system grounding: Google’s current Agents overview documents managed agent harnesses with tools, loops, context management, sandboxed execution, and multi-step research.

These official links document available product features. They do not reveal every provider’s private implementation, hidden reasoning, default setting, or internal limit.

1. The problem this module solves

Module 4 covered the loop’s mechanics without dwelling on what actually powers the “reason” step. This module closes Level 2 by giving that a precise answer — and, critically, by drawing the exact boundary between what the LLM does and what the surrounding system does, since conflating the two is a common source of confusion about agents.


2. Why LLMs Are Useful for Agents

Before LLMs, building a system that could "reason about what to do
next" required hand-coded rules for every possible
situation -- exactly Module 1's traditional-software limitation.

LLMs changed this: they can interpret an open-ended
situation described in natural language, reason about GOALS they
were never explicitly programmed for, and produce a DECISION about
what to do next -- directly connecting to your Generative AI course's
foundation model discussion.

3. The LLM’s Specific Job — Reasoning and Decision-Making

Given, at EACH iteration:

- The GOAL
- The CURRENT STATE
- The most recent OBSERVATION
- DESCRIPTIONS of available TOOLS (Module 6)

The LLM produces:

- A DECISION: which action to take next (or a determination that
  the goal is achieved)

This is the entire, precise job: reasoning over context to produce a decision. Everything else — actually running the loop, executing the chosen tool, updating state, enforcing safety limits — is NOT the LLM’s job. This module’s remaining sections make that boundary explicit.


4. Tool Descriptions — How the LLM Knows What It CAN Do

The LLM doesn't "know" what tools exist unless it's TOLD --
typically through a structured description GIVEN as part of its
context:

"check_shipping_carrier: looks up a package's current delivery
status given a tracking number. Parameters: tracking_number (string)."

The QUALITY of tool descriptions directly affects tool SELECTION accuracy — a vague or ambiguous description leads to the LLM choosing the wrong tool, or the right tool with wrong parameters (Module 6, 19’s failure modes cover this directly).


5. Structured Outputs — How a Decision Becomes Executable

The LLM's "decision" needs to be in a FORMAT the surrounding system
can act on -- not free-flowing prose, but something
STRUCTURED (directly connecting to your Prompt Engineering course's
structured output coverage):

{
  "action": "check_shipping_carrier",
  "parameters": {"tracking_number": "1Z999AA10123456784"}
}

Module 6 covers exactly how this structured decision gets translated into an actual tool execution — but the LLM’s role stops precisely at producing this structured output.


6. Model Limitations Matter for Agent Reliability

This directly connects to your Generative AI course’s coverage, applied specifically to agent decision-making:

HALLUCINATION:      the LLM can decide to call a tool that
                   doesn't exist, or fabricate plausible-sounding
                   parameters not actually grounded in the real
                   situation

NON-DETERMINISM:        the SAME state and observation can produce DIFFERENT decisions across separate
                       runs (Module 10 of your Generative AI course's
                       sampling discussion) -- a real, practical
                       consideration for agent reliability and
                       testing

These limitations don’t disappear because the LLM is now part of an agent — they directly translate into agent-level risks (Module 18 covers this in full: wrong tool selection, wrong parameters, hallucinated actions).


7. Why the LLM Alone Is Not the Entire Agent

flowchart TD
    Agent[Agent] --> LLM[LLM<br/>reasoning/decision engine]
    Agent --> State[State<br/>Module 3, 11]
    Agent --> Tools[Tools<br/>Module 6]
    Agent --> Memory[Memory<br/>Module 11]
    Agent --> Control[Control Logic<br/>runs the loop, Module 4]
    Agent --> Env[Environment<br/>Module 3]

This is precisely Module 2, Section 4’s point, now made fully explicit: the LLM is the reasoning COMPONENT. The control logic — real Python code (or a framework, Module 22) — is what actually runs the loop, calls the LLM at each iteration, executes whatever tool the LLM decided on, updates state, and enforces the safety limits from Module 4. Remove the control logic, and you have a single LLM call with no loop at all.


8. A Real Developer Example

TechCorp's late-order agent, with the LLM's EXACT role isolated:

1. CONTROL LOGIC observes: "Order is marked LATE"
2. CONTROL LOGIC updates STATE: order_status = "late"
3. CONTROL LOGIC assembles CONTEXT: goal + state + tool descriptions
4. LLM is called WITH this context -> LLM DECIDES:
   {"action": "check_shipping_carrier", "parameters": {...}}
5. CONTROL LOGIC receives this decision and EXECUTES the actual
   tool call (the LLM did NOT execute anything itself)
6. CONTROL LOGIC observes the tool's RESULT
7. CONTROL LOGIC updates STATE again
8. Loop continues -- back to step 3, calling the LLM again with the
   NEW context

Notice: the LLM was called TWICE in this trace (steps 4 and, implied,
again after step 7) -- but EVERY OTHER step was handled by
control logic, not the LLM.

9. A Simple Agentic AI Connection

This module’s boundary is the foundation for understanding every framework covered later (Module 22): LangChain and LangGraph are fundamentally control logic — they handle exactly the non-LLM-specific parts of Section 8’s trace (state management, loop execution, tool invocation), while still calling out to an LLM for the actual reasoning step, exactly as this module describes.


10. How Is This Used in AI?

🤖 How Is This Used in AI?

Production agent systems are architected around this precise boundary — the LLM is treated as a swappable reasoning component (you can switch models, Module 26 of your Generative AI course’s model selection discussion applies directly), while the control logic, state management, and tool execution remain stable, testable, non-LLM infrastructure.


11. Real-World Applications

  • Designing agent systems where the reasoning model can be upgraded or swapped without rewriting the entire system
  • Debugging: isolating whether a failure came from the LLM’s decision (Module 18) or from the surrounding control logic
  • Cost/latency optimization: routing simpler decisions to smaller, cheaper models while reserving larger models for complex reasoning steps

12. Common Mistakes

Incorrect idea: Believing the LLM directly executes tools or takes actions itself.

Why it is incorrect: As shown directly in Section 5, 7, and 8, the LLM only DECIDES — the surrounding control logic executes.

Incorrect idea: Writing vague or incomplete tool descriptions.

Why it is incorrect: As shown directly in Section 4, this directly degrades the LLM’s ability to select the right tool and generate correct parameters.

Incorrect idea: Ignoring non-determinism when testing or evaluating agent behavior.

Why it is incorrect: As shown directly in Section 6, the same input can produce different decisions across runs — Module 19’s evaluation discussion addresses this directly.


13. Limitations

  • No amount of prompt engineering completely eliminates hallucination or non-determinism risk (Section 6) — these are real, structural properties of how LLMs generate output, directly connecting to your Generative AI course’s coverage
  • The quality of an agent’s decisions is fundamentally bounded by the quality of the context and tool descriptions it’s given — a capable model with poor context still makes poor decisions

14. Quick Reference

flowchart LR
    CL[Control Logic] -->|assembles context:<br/>goal + state + tools| LLM[LLM]
    LLM -->|structured decision| CL
    CL -->|executes| Tool[Tool]
    Tool -->|result| CL
    CL -->|updates| State[State]

15. Code — Isolating the LLM’s Exact Role From Control Logic

🎯 Target of this example: implement Section 8’s real developer example directly — separating control logic (which orchestrates the loop) from the LLM’s role (which only decides), making the exact boundary from Section 7 directly observable in code.

Example 1 — Simple

from dataclasses import dataclass, field

@dataclass
class Agent:
    """Directly implements Section 7's composition diagram -- the
    LLM is ONE component, not the entire agent."""
    llm_reasoning_fn: callable
    state: dict = field(default_factory=dict)
    tools: dict = field(default_factory=dict)
    memory: list = field(default_factory=list)

    def run_step(self, observation: str) -> str:
        """The CONTROL LOGIC (not the LLM) orchestrates the step:
        update state, call the LLM for a decision, log to memory --
        the LLM only handles the 'decide' piece (Section 3)."""
        self.state["last_observation"] = observation
        decision = self.llm_reasoning_fn(self.state, list(self.tools.keys()))
        self.memory.append({"observation": observation, "decision": decision})
        return decision

def mock_llm_reasoning(state: dict, available_tools: list) -> str:
    """Simulates an LLM call -- given state and tools, returns a
    decision. In a real system this would be an actual model call."""
    if "late" in state.get("last_observation", "").lower():
        return "check_shipping_carrier" if "shipping_carrier" in available_tools else "no_suitable_tool"
    return "wait"

agent = Agent(llm_reasoning_fn=mock_llm_reasoning, tools={"shipping_carrier": lambda: "delivered"})
decision = agent.run_step("Order is marked LATE")

print(f"Decision: {decision}")
print(f"Memory log: {agent.memory}")
print(f"State: {agent.state}")

Expected Output:

Decision: check_shipping_carrier
Memory log: [{'observation': 'Order is marked LATE', 'decision':
'check_shipping_carrier'}]
State: {'last_observation': 'Order is marked LATE'}

What we conclude from this example: run_step (control logic) handles updating state and logging memory — separate responsibilities from mock_llm_reasoning (the LLM’s role), which does nothing but examine state and tools to produce a decision. This directly demonstrates Section 7’s boundary as real, working code structure.

Example 2 — Intermediate

from dataclasses import dataclass, field

@dataclass
class Agent:
    llm_reasoning_fn: callable
    state: dict = field(default_factory=dict)
    tools: dict = field(default_factory=dict)
    memory: list = field(default_factory=list)

    def execute_decision(self, decision: str) -> str:
        """CONTROL LOGIC executes whatever the LLM decided -- the
        LLM NEVER runs this itself (Section 5, 8's critical point)."""
        if decision in self.tools:
            return self.tools[decision]()
        return "no_action_taken"

    def run_step(self, observation: str) -> dict:
        self.state["last_observation"] = observation
        decision = self.llm_reasoning_fn(self.state, list(self.tools.keys()))
        result = self.execute_decision(decision)
        self.state["last_result"] = result
        self.memory.append({"observation": observation, "decision": decision, "result": result})
        return {"decision": decision, "result": result}

def mock_llm_reasoning(state: dict, available_tools: list) -> str:
    if "late" in state.get("last_observation", "").lower() and "shipping_carrier" in available_tools:
        return "shipping_carrier"
    return "wait"

def check_shipping_carrier_tool() -> str:
    return "Package delivered 2 days ago"

agent = Agent(llm_reasoning_fn=mock_llm_reasoning, tools={"shipping_carrier": check_shipping_carrier_tool})

step_result = agent.run_step("Order is marked LATE")
print(f"LLM decided: {step_result['decision']}")
print(f"Control logic EXECUTED and got: {step_result['result']}")
print(f"\nFull trace (memory): {agent.memory}")

Expected Output:

LLM decided: shipping_carrier
Control logic EXECUTED and got: Package delivered 2 days ago

Full trace (memory): [{'observation': 'Order is marked LATE',
'decision': 'shipping_carrier', 'result': 'Package delivered 2 days
ago'}]

What we conclude from this example: execute_decision is separate from mock_llm_reasoning — the “LLM decided” output and “control logic executed and got” output come from two distinct functions with two distinct responsibilities, exactly Section 8’s real developer trace made directly observable: the LLM decides, the surrounding system executes.

Example 3 — Production Grade

from dataclasses import dataclass, field
from enum import Enum

class DecisionSource(Enum):
    LLM = "llm_reasoning"
    CONTROL_LOGIC = "control_logic_execution"

@dataclass
class TraceStep:
    source: DecisionSource
    description: str

@dataclass
class ProductionAgent:
    """A production-style agent that EXPLICITLY tags every trace
    step with WHO was responsible -- the LLM or the control logic --
    making Section 7's boundary auditable, not just
    conceptually true."""
    llm_reasoning_fn: callable
    tools: dict = field(default_factory=dict)
    state: dict = field(default_factory=dict)
    trace: list = field(default_factory=list)

    def run_step(self, observation: str) -> dict:
        # CONTROL LOGIC responsibility
        self.state["last_observation"] = observation
        self.trace.append(TraceStep(DecisionSource.CONTROL_LOGIC, f"Updated state with observation: {observation}"))

        # LLM responsibility -- ONLY this line involves the LLM
        decision = self.llm_reasoning_fn(self.state, list(self.tools.keys()))
        self.trace.append(TraceStep(DecisionSource.LLM, f"Decided: {decision}"))

        # CONTROL LOGIC responsibility again
        if decision in self.tools:
            result = self.tools[decision]()
            self.trace.append(TraceStep(DecisionSource.CONTROL_LOGIC, f"Executed '{decision}', got: {result}"))
            self.state["last_result"] = result
        else:
            result = None
            self.trace.append(TraceStep(DecisionSource.CONTROL_LOGIC, f"No matching tool for '{decision}'"))

        return {"decision": decision, "result": result}

def mock_llm_reasoning(state: dict, available_tools: list) -> str:
    if "late" in state.get("last_observation", "").lower() and "shipping_carrier" in available_tools:
        return "shipping_carrier"
    return "wait"

agent = ProductionAgent(
    llm_reasoning_fn=mock_llm_reasoning,
    tools={"shipping_carrier": lambda: "Package delivered 2 days ago"},
)
agent.run_step("Order is marked LATE")

llm_steps = sum(1 for t in agent.trace if t.source == DecisionSource.LLM)
control_steps = sum(1 for t in agent.trace if t.source == DecisionSource.CONTROL_LOGIC)

print(f"LLM was responsible for {llm_steps} step(s), Control logic for {control_steps} step(s)\n")
for step in agent.trace:
    print(f"  [{step.source.value}] {step.description}")

Expected Output:

LLM was responsible for 1 step(s), Control logic for 2 step(s)

  [control_logic_execution] Updated state with observation: Order is
marked LATE
  [llm_reasoning] Decided: shipping_carrier
  [control_logic_execution] Executed 'shipping_carrier', got: Package
delivered 2 days ago

What we conclude from this example: the LLM was responsible for exactly ONE of three trace steps — a single, precise decision — while control logic handled the other two: the state update and the tool execution. This quantifies Section 7’s boundary directly, and this kind of explicit source-tagging is exactly what production observability (Module 21) needs to distinguish an LLM reasoning failure from a control-logic bug.


16. Interview Questions

Q: Precisely describe the LLM’s role within an agent, and what it is explicitly NOT responsible for.

Ans: The LLM’s role is reasoning over the current context — the goal, state, most recent observation, and available tool descriptions — to produce a structured decision about what to do next, or to determine the goal has been achieved. The LLM is explicitly not responsible for actually executing tools, running the loop, managing state persistence, or enforcing safety limits like max iterations — all of that is handled by surrounding control logic, which calls the LLM at each iteration but is a separate system.

Q: Why does the quality of tool descriptions directly affect an agent’s reliability?

Ans: The LLM doesn’t have any inherent knowledge of what tools are available or how to use them — it learns this entirely from the descriptions given to it as part of its context at each reasoning step. A vague, ambiguous, or incomplete tool description can lead the LLM to select the wrong tool for a given situation, or to generate incorrect parameters for the right tool, directly translating into downstream agent failures.

Q: How does LLM non-determinism affect agent behavior, and why does this matter for testing?

Ans: Because LLM generation involves sampling (covered in your Generative AI course), the same state and observation can produce different decisions across separate runs of the same agent. This means testing an agent isn’t as simple as checking one run’s output against an expected result — evaluation needs to account for this real variability, often by running multiple trials or by using deterministic settings like low temperature for consistency- critical reasoning steps.

Q: If an agent consistently fails to complete a task, how would you determine whether the problem lies with the LLM’s reasoning or with the surrounding control logic?

Ans: I’d examine a detailed trace of the agent’s execution, explicitly distinguishing which steps were LLM decisions versus control logic actions — checking whether the LLM is making poor or incorrect decisions given reasonable context (a reasoning problem, potentially fixable with better tool descriptions or a different model), versus whether the control logic is failing to correctly execute what the LLM actually decided, mismanaging state, or incorrectly assembling the context the LLM receives (a control logic bug, unrelated to the model’s reasoning quality).


17. What You Should Remember

  • The LLM’s real, precise job within an agent is reasoning over context to produce a structured decision — nothing more.
  • The LLM never executes tools or runs the loop itself — control logic does — verified directly through code that explicitly tags every trace step by responsible source (LLM vs. control logic).
  • Model limitations (hallucination, non-determinism) translate directly into agent-level risks — this is why tool description quality and context assembly matter as much as model capability itself.

18. Quick Practice

Take the agent trace from Section 8 and, for a task of your own choosing (planning a meeting, researching a topic), write out each step and explicitly label whether it’s the LLM’s responsibility or the control logic’s responsibility.

19. Next Step

Next: Module 6 — Tools and Actions — Level 3 begins here: the complete mechanics of what a tool actually is, how the agent selects and executes one, and everything that can go wrong along the way.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed