Build Your First LangGraph Agent: End-to-End Restaurant Order System
A hands-on architectural and practical guide to building stateful, resilient AI agents using LangGraph. This lesson covers system architecture design, failure recovery, state management via the "whiteboard" mental model, prompt-driven code generation, and test-driven verification for autonomous AI workflows.
1. The AI Engineering Philosophy: Claude as a Junior Engineer
A common anti-pattern in AI engineering is sending simple, vague prompts to an LLM (e.g., "Write Python code for a restaurant order bot") and expecting production-ready code. Vague prompts lead to unmaintainable, buggy codebases.
The Junior Engineer Mental Model
Treat advanced LLMs (such as Claude 3.5 Sonnet) as brilliant Junior Engineers:
- They possess deep syntax knowledge and fast execution capabilities.
- They lack domain intuition and business-logic context unless explicitly provided.
- They require detailed specifications covering edge cases, state schema, and failure boundaries before writing code.
Key Rule of AI Code Generation: Reading 1,000 lines of LLM-generated code to verify logic takes days. Instead of reading generated code line-by-line, write explicit architecture specs and automated test cases upfront. If the generated code passes the test scenarios, it is structurally sound.
2. High-Level System Architecture & User Flow Logic
Before writing code or prompts, map out the entire system flow on paper or in a specification file.
System Rules & Logic Constraints
- Extraction Scope: The LLM extracts only two fields from raw text:
dish_nameandrequested_quantity. - Intent Guardrails: If a user submits off-topic requests (e.g., asking for programming algorithms like Two Sum or generic chat), the LLM must decline processing and clarify that it is an exclusive food-ordering agent.
- Single-Dish Simplification: For initial system simplicity, each transaction processes one dish type per request, with support for arbitrary quantities.
3. Handling System Failures & Retry Counters
Real-world production environments experience hardware faults, resource exhaustion, and execution failures. An AI agent must handle physical system failures cleanly.
If an agent retries an operation endlessly without limits, it introduces an unbounded loop ( token consumption), exposing the system to denial-of-service vulnerabilities and excessive API costs.
System Retry Matrix
4. LangGraph State Modeling: The Whiteboard Mental Model
In LangGraph, State acts as a central Whiteboard accessible to every node (LLM, Order Confirm, Cook, Serve) in the execution graph.
LangGraph State Schema Fields
messages(Annotated List): Appends user prompts and agent responses continuously.Note: The
Annotatedreducer pattern ensures new messages are appended to the ledger rather than overwriting previous message turns.order_details(Dictionary):dish_name(str): The requested food item name.required_quantity(int): Quantity requested by the customer.available_quantity(int): Quantity currently available in the menu database.
status(str): Global status indicator updated by active nodes (CONFIRMED,PARTIAL,NOT_AVAILABLE,READY,COOK_FAILED,SERVE_FAILED,COMPLETED).retry_counters(int): Tracks decrements fororder_retries,cook_retries, andserve_retries.final_result(str): Text summary of the final transaction outcome and termination reason.
5. Probabilistic Simulation of Real-World System Failures
To test agent resilience in software simulations without physical kitchen hardware, use probabilistic failure functions inside execution nodes.
The Dice Roll Method
When the Cook or Serve node runs, it executes a random distribution function:
import random
def simulate_cook_execution() -> str:
# Simulates a dice roll: 1-2 = Failure (40%), 3-5 = Success (60%)
roll = random.uniform(0, 1)
if roll < 0.40:
return "FAIL"
return "SUCCESS"
This ensures that routing logic, retry decrements, and refund loops are thoroughly exercised during testing.
6. Prompt Specification File (code_prompt.md)
Below is the complete architectural specification file written in Markdown. This document serves as the prompt input for Claude to generate the production implementation.
# LangGraph Restaurant Agent Specification
## System Overview
I am building a stateful restaurant order management agent using LangGraph.
You must generate the complete implementation and run test cases to verify system stability.
## Architecture & Logic
1. User provides an order string via CLI.
2. LLM Node parses `dish_name` and `requested_quantity`. Rejects off-topic queries.
3. OrderConfirm Node checks inventory against the menu:
- Fully Available -> Set status = "CONFIRMED" -> Route to Cook Node.
- Partially Available -> Set status = "PARTIAL" -> Prompt User via LLM.
- Not Available -> Set status = "NOT_AVAILABLE" -> Prompt User via LLM.
4. Retry Rules:
- Max `order_retries` = 3.
- Max `cook_retries` = 2.
- Max `serve_retries` = 2.
5. Cook Node uses probabilistic simulation (60% success, 40% failure).
- Success -> Set status = "READY" -> Route to Serve Node.
- Failure -> Decrement `cook_retries`. If 0, issue apology and terminate.
6. Serve Node uses probabilistic simulation (60% success, 40% failure).
- Success -> Set status = "COMPLETED" -> Terminate.
- Failure -> Call Cook Node to re-make dish if `cook_retries` > 0. Otherwise issue apology.
## State Schema Specification
- `messages`: Annotated[List[BaseMessage], add_messages]
- `dish_name`: str
- `requested_quantity`: int
- `available_quantity`: int
- `status`: str
- `order_retries`: int (default 3)
- `cook_retries`: int (default 2)
- `serve_retries`: int (default 2)
- `final_result`: str
7. Interactive Disambiguation & Open Questions
When provided with a detailed architecture document, an LLM acting as a reviewer will flag ambiguous edge cases before generating code.
Resolved Ambiguities
- Menu Definition: The original prompt referenced a menu without listing items.
- Resolution: Explicitly provide a structured dictionary:
PYTHON
MENU = { "burger": {"price": 60, "quantity": 10}, "pizza": {"price": 200, "quantity": 20}, "momos": {"price": 80, "quantity": 4} }
- Resolution: Explicitly provide a structured dictionary:
- Retry Attribution: Does a service failure (e.g., dropped delivery) consume a
cook_retrywhen remaking food?- Resolution: Yes. Remaking food consumes kitchen inventory and time; thus, re-cook requests decrement
cook_retriesregardless of which node triggered them.
- Resolution: Yes. Remaking food consumes kitchen inventory and time; thus, re-cook requests decrement
- String Normalization: Should requested dishes require exact string matching?
- Resolution: Implement fuzzy matching (lowercase conversion and substring checks) so requests for "margherit pizza" match "pizza".
8. Test-Driven AI Code Generation (Simulation & Verification)
To verify the generated codebase without reading hundreds of lines of code, evaluate the agent against a suite of scenario test cases.
Test Case Suite Definitions
# TC1: Off-Topic / Invalid Retries Exhaustion
# Sequence: User asks coding query -> Attempts unavailable dish 3x -> System aborts cleanly.
# TC2: Happy Path with Single Kitchen Retry
# Sequence: User orders 2 burgers -> Cook fails roll 1 -> Cook succeeds roll 2 -> Serve succeeds -> COMPLETED.
# TC3: Kitchen Recovery Exhaustion on Service Drop
# Sequence: Cook succeeds -> Serve fails -> Recook requested -> Cook retries exhausted -> Abort with Apology.
9. Implementation Code Structure
The following code illustrates the generated state schema, nodes, conditional edge routers, and execution loops for the LangGraph agent:
import random
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, END, START
from langgraph.graph.message import add_messages
from langchain_core.messages import HumanMessage, AIMessage
# --- 1. MENU CONFIGURATION ---
MENU = {
"burger": 10,
"pizza": 20,
"momos": 4
}
# --- 2. STATE DEFINITION ---
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
dish_name: str
requested_quantity: int
available_quantity: int
status: str
order_retries: int
cook_retries: int
serve_retries: int
final_result: str
# --- 3. NODE DEFINITIONS ---
def order_confirm_node(state: AgentState) -> dict:
dish = state.get("dish_name", "").lower()
req_qty = state.get("requested_quantity", 0)
if dish not in MENU:
return {"status": "NOT_AVAILABLE", "available_quantity": 0}
avail_qty = MENU[dish]
if avail_qty >= req_qty:
return {"status": "CONFIRMED", "available_quantity": avail_qty}
elif avail_qty > 0:
return {"status": "PARTIAL", "available_quantity": avail_qty}
else:
return {"status": "NOT_AVAILABLE", "available_quantity": 0}
def cook_node(state: AgentState) -> dict:
retries = state.get("cook_retries", 2)
if retries <= 0:
return {"status": "COOK_FAILED", "final_result": "FAILED - Kitchen retries exhausted."}
# Probabilistic Simulation: 60% Success, 40% Fail
success = random.random() >= 0.40
if success:
return {"status": "READY"}
else:
return {
"status": "COOK_FAILED",
"cook_retries": retries - 1
}
def serve_node(state: AgentState) -> dict:
retries = state.get("serve_retries", 2)
if retries <= 0:
return {"status": "SERVE_FAILED", "final_result": "FAILED - Delivery retries exhausted."}
# Probabilistic Simulation: 60% Success, 40% Fail
success = random.random() >= 0.40
if success:
return {"status": "COMPLETED", "final_result": "SUCCESS - Order delivered."}
else:
return {
"status": "SERVE_FAILED",
"serve_retries": retries - 1
}
# --- 4. CONDITIONAL EDGE ROUTERS ---
def route_after_order(state: AgentState) -> str:
status = state.get("status")
if status == "CONFIRMED":
return "cook_node"
elif state.get("order_retries", 0) <= 0:
return "abort_node"
return "user_prompt_node"
def route_after_cook(state: AgentState) -> str:
status = state.get("status")
if status == "READY":
return "serve_node"
elif state.get("cook_retries", 0) > 0:
return "cook_node" # Retry cook
return "abort_node"
def route_after_serve(state: AgentState) -> str:
status = state.get("status")
if status == "COMPLETED":
return END
elif state.get("serve_retries", 0) > 0 and state.get("cook_retries", 0) > 0:
return "cook_node" # Remake food via Cook node
return "abort_node"
Summary and Key Takeaways
- The AI Architecture Principle: Never ask an LLM to generate code without providing explicit architectural specs. Treat the LLM as a Junior Engineer that requires clear documentation, schema constraints, and edge-case boundaries.
- Spec-First Prompt Engineering: Draft system architectures in structured Markdown files (
code_prompt.md). Document nodes, state structures, conditional paths, and limits before triggering code generation. - The "Whiteboard" State Mental Model: LangGraph state functions as a central whiteboard where nodes read prior context and post updates without clobbering history.
- Infinite Loop Prevention: Real-world AI workflows require strict retry counters (
order_retries,cook_retries,serve_retries) to bound execution iterations and prevent infinite token drain. - Probabilistic Failure Testing: Use probabilistic roll functions () to simulate hardware/physical system failures, verifying agent fault tolerance programmatically.
- Interactive Disambiguation: Prompt LLMs to ask clarifying questions about missing specs (such as menu items or fuzzy matching rules) before code output begins.
- Test-Driven AI Verification: Validate LLM-generated agents using automated test scenario suites (
TC1,TC2,TC3) to verify path coverage and error recovery without manually reading thousands of lines of code.