Chapter 1
AI Agent Architecture: How to Make an Agent Controllable
The main architectural question is who controls the 'reasoning-action' loop. A free loop where the model decides when to stop is dangerous. A controlled graph with explicit nodes and transitions provides predictability. Let's see how this works in practice.
- AI Agent Architecture: Graphs, ReAct, and HITL
- AI Agent Tools: Contracts and Safety
- AI Agent Memory: State, Checkpointer, and RAG
- AI Agent Production: Reliability and Observability
On this page
Controlled Graph vs. Free LoopStateGraph: Nodes, Edges, and StateReAct as an Observable LoopThe Critic Does Not Establish TruthHITL: Approval of a Specific ActionSubagents: Delegation with ConstraintsPractical Example: State MachineControlled Graph vs. Free Loop
In its simplest form, an agent is a loop: the model receives messages, decides which tool to call, executes it, adds the result to history, and repeats. Such a loop is easy to write but hard to control. The model can get stuck in a loop, call a dangerous tool, or exhaust the entire token budget.
LangGraph offers a different approach: you describe a graph where each node is a function, and edges determine which node executes next. The graph defines allowed transitions, including cycles; the actual route depends on state. Dynamic delegation and external results can make that route more complex. This does not limit the agent but makes its behavior predictable.
The key difference: in a free loop, the model decides both what to do and when to stop. In a graph, the model decides which node to choose from the proposed ones, and stopping is also an explicit edge in the graph.
- Free loop: while True, the model decides when to stop.
- Graph: nodes and edges, all paths are known in advance.
- The graph can be cyclic but controllable.
- Stopping is an explicit edge in the graph, not a hope for the model.
- User request
- Node: decide if search is needed
- Conditional edge: yes/no
- Node: search for information
- Node: generate response
- End
StateGraph: Nodes, Edges, and State
State is a data structure passed between nodes. In LangGraph, state is typically described using TypedDict. A node is a Python function that receives the current state and returns an updated one.
Edges can be unconditional or conditional. An unconditional edge A -> B means that node B always executes after node A. A conditional edge uses a routing function that analyzes the state and returns the name of the next node.
Keep a conditional edge’s route selection separate from state updates: return the route and update data in nodes. LangGraph also provides Command when an update and a transition need to be combined. This separation makes the transition logic explicit and testable.
- State: TypedDict, dataclass, or Pydantic BaseModel.
- Node: a function that reads and updates the state.
- Edge: unconditional or conditional transition.
- Routing function: reads state but does not change it.
- Compilation: checking the graph's correctness before execution.
ReAct as an Observable Loop
ReAct is a 'reasoning-action-observation' pattern. The model receives a task, decides which action to perform, executes it through a tool, receives the result (observation), and repeats. In graph terms, this is a Plan -> Act -> Observe loop.
It is important to understand that 'reasoning' here is not a hidden chain-of-thought that we must show or hide. It is simply the choice of the next action based on the current state. The model may return text with justification, but for the system, only the chosen tool and its parameters matter.
Observability of the ReAct loop is achieved through logging: we see which tool was called, with what arguments, and what result was obtained. This allows debugging and analyzing agent behavior.
- ReAct: Plan -> Act -> Observe -> Plan.
- The model chooses the action but does not execute it itself.
- The tool executes trusted code, not the model.
- The tool result is returned to the context as an observation.
- Logging tool calls is the foundation of observability.
- Task: 'Where is my order?'
- Model: choose tool get_order_status
- Executor: call get_order_status('12345')
- Result: 'Order is in transit, expected date is tomorrow'
- Model: generate response for the user
The Critic Does Not Establish Truth
The 'critic' pattern assumes that a separate node checks the response generated by the main node and suggests improvements. This can improve quality, but it is important to understand the limitations.
The critic is the same LLM, and it can make mistakes. It does not verify facts but assesses plausibility. If the main node generated a fictional fact, the critic might not notice it. Therefore, the critic is useful for improving style, structure, and completeness of the response, but not for verifying factual accuracy.
To verify facts, you need tools: search, database access, API. The critic can suggest verifying a fact, but the verification itself must be performed by trusted code.
- Critic: LLM evaluates and improves the response.
- The critic does not verify facts; it assesses plausibility.
- Use tools, not the critic, to verify facts.
- The critic loop must have an iteration limit.
HITL: Approval of a Specific Action
Human-in-the-loop (HITL) is a mechanism where the agent pauses execution and requests confirmation from a human. It is important that the approval is tied to a specific action, not to a general plan.
For example, if the agent is about to send an email to a customer, it should show the email text and recipient and ask 'Send?'. If the agent is about to transfer money, it should show the amount and account. Approving a general plan ('I am going to send an email, transfer money, and update the database') does not provide control over each action.
In LangGraph, HITL is implemented via interrupt. A node calls interrupt(question), the graph pauses and returns the question to the client. The client sends the answer via Command(resume=answer), and the graph resumes execution from the same point.
- HITL: pausing the graph to request human input.
- Approval must be tied to a specific action.
- interrupt(question) pauses the graph.
- Command(resume=answer) resumes execution.
- HITL requires a durable checkpointer and thread_id.
- Identify which actions require approval.
- Formulate the interrupt question clearly and unambiguously.
- Ensure that after resume, the node does not repeat a side effect.
- Add a timeout for waiting for the user's response.
Subagents: Delegation with Constraints
A subagent is a separate run of an LLM agent with its own context, task, and completion criteria. Subagents are useful when a task naturally breaks down into independent parts that can be executed in parallel.
It is important to understand: a separate context does not mean isolation. If the subagent has access to a write tool, it can modify files, even if the instruction says 'do not modify files'. Technical constraints must be implemented at the level of tool configuration and environment permissions, not just in the prompt text.
Use subagents sparingly. Each subagent is additional LLM calls, time, and cost. If a single agent with tools can solve the task, do not create a multi-agent system.
- Subagent: a separate agent run with its own task.
- A separate context does not mean file system isolation.
- Constraints must be in configuration, not only in the prompt.
- Subagents are useful for parallel independent tasks.
- Evaluate the cost of parallelism: setup, coordination, verification.
Practical Example: State Machine
Consider an agent for processing an order request. The graph consists of nodes: parse_input, check_status, ask_clarification, search_info, format_answer. The state contains user_input, order_id, order_status, need_clarification.
The parse_input node extracts the order number from the text. The route routing function checks if the number is found. If not, it goes to ask_clarification. If yes, it goes to check_status. The check_status node calls the get_order_status tool. If the status is 'delayed', the route_delay routing function decides whether to search for additional information in search_info. The format_answer node generates a response for the user.
This graph has clear paths and limits. If the user does not provide the order number after clarification, the graph terminates with a message about the inability to process. The maximum number of clarification iterations is limited.
- parse_input: extract the order number.
- check_status: call the get_order_status tool.
- ask_clarification: request the number from the user.
- search_info: search for additional information about the delay.
- format_answer: generate the response.
- Clarification iteration limit: 1.
- START -> parse_input
- parse_input -> route: is order_id found?
- route(yes) -> check_status
- route(no) -> ask_clarification
- ask_clarification -> parse_input (again)
- check_status -> route_delay: is search needed?
- route_delay(yes) -> search_info -> format_answer
- route_delay(no) -> format_answer -> END
What to carry into your project
Agent architecture is a choice between freedom and control. A controlled graph with explicit nodes, conditional transitions, and limits is the foundation of a reliable agent. HITL should be tied to specific actions, and subagents should have technical constraints.
Sources and further reading
- LangGraph Library for Designing AI Agents (Sber educational materials)
- Practice of AI Agent Development (Sber educational materials)
- Architecture of Modern Agentic Harnesses (Sber educational materials)
- Development and Application of Multi-Agent Systems in a Corporate Environment (Sber)