Agent Components and State Model Design
When developing LLM applications, you eventually hit the limits of simple prompt engineering.
"First, search, and if the results are insufficient, search again with different keywords, then summarize the information and respond."
To reliably execute multi-step logical structures like the one above, a new paradigm is needed.
It's not feasible to type out prompts every time; if you miss a step, the common guidelines fall apart.
What's the Difference Between a Simple LLM Chain and a Stateful Agent System? #
The biggest difference between traditional LangChain or pipeline-structured Chains and modern Agent Systems lies in state preservation (Statfulness) and cyclic execution.
| Category | Simple LLM Chain (Stateless Pipeline) | Stateful Agent System (LangGraph-based) |
|---|---|---|
| Execution Flow | Unidirectional (DAG). Ends with A → B → C. | Cyclic. Can repeat A → B → C → A. |
| Decision Making | Executed only in a sequence predefined by the developer. | LLM directly decides the next action (Routing) based on the current 'state'. |
| Error Recovery | Entire pipeline halts if an error occurs in an intermediate step (e.g., API call failure). | Records state (Error Log) on failure, and LLM selects another tool to retry. |
| Data Sharing | Output of the previous step is only passed as input to the next step. | All nodes can access past thoughts, actions, and results via a global state object. |
| Interruption & Resumption | Not possible. Once executed, it runs to completion. | Human-in-the-loop. Can pause at a specific step (Interrupt) and resume after user approval. |
If a simple Chain is a one-off function, an Agent System operates like an operating system process with memory and control flow.
Agent Components: The Nodes #
An Agent solving complex problems doesn't operate as one giant prompt.
It separates concerns into multiple specialized Nodes.
- Planner: Analyzes complex user requests and breaks them down into smaller sub-tasks.
- Router: Evaluates the current state and acts as a Conditional Edge, deciding which node to move to next (e.g., tool call -> Tool Executor, search needed -> Retriever, complete -> END).
- Tool Executor: Calls actual external APIs (web search, DB query, code execution) based on JSON parameters generated by the LLM.
- Retriever: Integrates with the RAG pipeline to fetch context from a VectorDB or external Knowledge base.
- Memory: Persistently stores conversation history (short-term) and summarized key information (long-term) within the State object.
- Validator (Reflector): Self-validates whether the results from the Tool Executor or Retriever are sufficient to answer the user's question, or if there's hallucination. If it fails, it adds feedback and returns to the Planner or Router.
Designing the Agent State Schema (Pydantic & TypedDict) #
The heart of an Agent System is its state.
LangGraph uses Python's TypedDict to strictly define the data structure that nodes will read and write as they pass through the graph.
At this point, Pydantic is used in conjunction to validate the structured output of the LLM.
from typing import Annotated, List, Sequence, Optional
from typing_extensions import TypedDict
from pydantic import BaseModel, Field
import operator
from langchain_core.messages import BaseMessage
# 1. Schema for LLM output validation using Pydantic (for Router and Validator)
class RouteDecision(BaseModel):
next_action: str = Field(description="Next action to execute: 'search', 'execute_tool', 'respond', 'retry'")
reason: str = Field(description="Reason for choosing this action")
tool_name: Optional[str] = Field(default=None, description="Name of the tool to execute")
tool_args: Optional[dict] = Field(default=None, description="Parameters required for tool execution")
# 2. Global State Schema for LangGraph using TypedDict
class AgentState(TypedDict):
# Using Annotated and operator.add means that when a node returns messages, they are 'appended' to the existing list.
messages: Annotated[Sequence[BaseMessage], operator.add]
# Original user request
user_query: str
# List of sub-tasks broken down by the Planner
sub_tasks: List[str]
# Currently executing tool and its results
current_tool: Optional[str]
tool_results: List[dict]
# Errors identified by the Validator and feedback loop count
errors: List[str]
iteration_count: int
Engineering Considerations for State Design #
- Reducer (
operator.add): Themessagesfield should accumulate (append) values rather than overwrite them. This allows the agent to remember which tools it used and what failures it encountered in the past. - Explicit Control Variables: Variables like
iteration_countorerrorsare included in the State to prevent infinite loops. If the loop count exceeds 5, the Router should implement logic to forcibly terminate the process.
Building the Execution Graph #
Based on the defined State, nodes are connected to create a directed graph.
from langgraph.graph import StateGraph, END
# 1. Initialize the graph (inject the defined AgentState)
workflow = StateGraph(AgentState)
# 2. Add nodes (each function takes AgentState as input and returns a modified subset)
workflow.add_node("planner", plan_tasks_node)
workflow.add_node("router", routing_node)
workflow.add_node("tool_executor", execute_tool_node)
workflow.add_node("validator", validate_result_node)
# 3. Connect edges (define execution flow)
workflow.set_entry_point("planner")
workflow.add_edge("planner", "router")
# 4. Conditional edges (dynamically branch based on Router's decision)
def route_condition(state: AgentState) -> str:
# Read the next_action value decided by the Router node to branch
last_message = state["messages"][-1].content
if "execute_tool" in last_message:
return "tool_executor"
elif "respond" in last_message:
return "validator"
return END
workflow.add_conditional_edges(
"router",
route_condition,
{
"tool_executor": "tool_executor",
"validator": "validator",
END: END
}
)
# After executing a tool, return to the Router (or LLM) to decide the next action (cyclic structure)
workflow.add_edge("tool_executor", "router")
# If Validator passes, END; if it fails, send feedback back to the Router
def validation_condition(state: AgentState) -> str:
if state["errors"]: # If there are errors
return "router"
return END
workflow.add_conditional_edges(
"validator",
validation_condition,
{
"router": "router",
END: END
}
)
# 5. Compile the graph (complete the Durable Runtime)
app = workflow.compile()
Agent State Schema and Execution Graph Operation Example #
Visually representing the above code, the agent operates not as a unidirectional pipeline, but as an ecosystem that dynamically uses tools and reflects on itself based on conditions.
Agent Execution Flow and State Change Example #
- [Planner]: Updates
sub_tasksstate -> [1. Search Apple stock price,2. Summarize results in 3 lines in Korean] - [Router]: LLM decides to call
Search APItool and accumulates state inmessages. - [Tool Executor]: Calls external API, updates
tool_resultsState ->[{"tool": "search", "result": "Apple closed at $195.00..."}] - [Router]: Recognizes that information is sufficient based on search results. Drafts a summary prompt and routes to
validator. - [Validator]: Checks if the summarized response meets the 3-line Korean condition.
- If failed, updates errors state -> "Summarized in 2 lines, please rewrite in 3 lines" executes loopback.
- If successful, returns final message and reaches END.
The most important aspect of agent architecture is systematically structuring how LLMs at each stage share and collaborate through a state object (JSON).