LangGraph + LEGO Robots

An interactive tutorial: state machines, edge-cloud control, & a real Color-Seeker bot

🎧 Audio overview 🗺️ Live mind map ⚡ Latency visualizer 🤖 Platform picker ❓ Quiz

You can already build an AI agent that chats on a laptop. Pointing one at a LEGO robot — so it can see, decide, and move — adds the one dimension that breaks naive agent loops: the physical world is non-deterministic. Motors stall, grippers miss, cameras lose the line. A while True: loop crumbles on the first surprise.

This tutorial walks through the architecture that doesn't crumble: LangGraph as the planner running on a PC, Pybricks as a fast reflexive controller running on the LEGO hub, talking over Bluetooth Low Energy. Every section is interactive — click things, drag the slider, flip the cards. The content is grounded in a NotebookLM deep-research pass over 26 sources.

🎧 Audio overview

A 10–15 minute deep dive, narrated. Same notebook, generated via NotebookLM's audio overview feature.

Generating…

The audio file is being generated. Once it's uploaded to s3://krueng.ai/audio/langgraph_lego/deep_dive.mp3, it'll auto-appear here on refresh.

🗺️ Concept mind map

Click any node with a ▸ to expand or collapse. Six top-level branches; the diagram is generated from the same source set.

Loading media/langgraph_lego/mind_map.json…

🎬 The state machine, animated

A simulated mission. Click Run to watch a token travel through the graph — perception polls a sensor, plan asks the LLM, act executes a tool, then the loop repeats until the LLM decides to END. The status panel logs each transition.

START perception poll BLE plan LLM call act (tool) END tool_calls re-observe no tool_calls
Press Run mission to see the cycle. Each act loops back to perception — that's what makes the agent stateful instead of fire-and-forget.

⚡ Latency budgets — why brain and body have to split

An LLM takes seconds to decide. A motor PID loop reacts in milliseconds. Drag the slider to set how fast your LLM call is, and watch what happens to a robot moving at 30 cm/s between decisions.

4.0 s
Brain (LLM on PC)
Body (Pybricks PID)
2 ms
Without edge-cloud partitioning: at 30 cm/s, the robot travels 120 cm between LLM decisions. That's the entire diameter of most tables. Reflexive control (PID, collision stop) must live on the hub.

🤖 Pick your LEGO platform

Four real options today; toggle filters to highlight what fits your constraints.

Pybricks ⭐

Hubs: SPIKE Prime, Robot Inventor, Technic, BOOST, City, EV3.

Connection: custom MicroPython firmware on the hub, PC speaks BLE (Nordic UART) via pybricksdev.

Strength: ≪ 1 ms motor loops, PID built-in, HuskyLens AI camera supported. The default pick.

modernblevisionfast

LEGO SPIKE App Python

Hubs: SPIKE Prime, SPIKE Essential.

Connection: official LEGO app uploads to the hub over BLE/USB.

Strength: zero setup, official, safe sandbox. Loses low-level access; v3.0 removed UART.

modernblereversiblesafe

ev3dev (EV3 only)

Hubs: MINDSTORMS EV3.

Connection: full Debian Linux from microSD card; SSH over Wi-Fi.

Strength: any pip package runs on the brick. EV3 is officially retired; boot is slow.

piplegacyssh

Build HAT on Raspberry Pi

Hubs: any Pi + Build HAT shield + SPIKE / Robot Inventor motors.

Connection: standard Python on the Pi drives motors via serial.

Strength: full Pi compute (OpenCV, local LLMs, ROS). Bulky — needs a power bank.

pipvisionmoderntethered

💻 Code walkthrough — click any annotated line

The PC side, ~55 lines. Tap a line marked with the yellow border to read what it does and why.

# 1. The state object — what flows through the graph
class RobotState(TypedDict):
    messages: Annotated[List[BaseMessage], add_messages]
    sensor_data: dict

# 2. Tools — Python functions the LLM is allowed to "call"
@tool
async def drive_target(distance_cm: float) -> str:
    """Drive forward by a specific distance, in centimetres."""
    # Real: await pybricksdev.write(b"fwd " + str(distance_cm).encode())
    return f"drove {distance_cm} cm; stall=False"

# 3. The three nodes
async def perception_node(state: RobotState):
    # Real: await ble.write(b"color?\n"); reading = await ble.read()
    return {"sensor_data": {"color": "red", "is_stalled": False}}

async def plan_node(state: RobotState):
    ctx = f"Sensors right now: {state['sensor_data']}"
    response = await llm_with_tools.ainvoke([ctx] + state["messages"])
    return {"messages": [response]}

async def act_node(state: RobotState):
    last = state["messages"][-1]
    results = []
    for call in last.tool_calls:
        out = await drive_target.ainvoke(call["args"])
        results.append(ToolMessage(content=out, tool_call_id=call["id"]))
    return {"messages": results}

# 4. Routing — should we act, or are we done?
def should_continue(state):
    return "act" if state["messages"][-1].tool_calls else END

# 5. Wire the graph
g = StateGraph(RobotState)
g.add_node("perception", perception_node)
g.add_node("plan", plan_node)
g.add_node("act", act_node)
g.add_edge(START, "perception")
g.add_edge("perception", "plan")
g.add_conditional_edges("plan", should_continue, {"act": "act", END: END})
g.add_edge("act", "perception")   # key line: re-observe after acting
robot_app = g.compile()

What does each line do?

Click any code line to see its annotation here. Highlighted lines are grouped — clicking one highlights all related lines.

🎯 Mission picker — same graph, four shapes

The basic perception → plan → act cycle morphs to fit the mission. Click each to see the graph variant and why hand-coded loops fail at it.

⚠️ Pitfalls — flip a card to see the fix

Six classes of failure that bite real LangGraph-on-LEGO projects. Click any card to flip to the mitigation experienced teams use.

🚀 Build the Color-Seeker — your progress sticks

A 2-hour starter project. Check boxes as you finish each step; progress is saved in your browser's localStorage.

Color-Seeker build checklist

🖼️ Visual summary

Generating…

The infographic is rendering. Once it's uploaded to s3://krueng.ai/imgs/langgraph_lego/infographic.png, it'll auto-appear here on refresh.

❓ Quick quiz

Generated from the same source set. Pick an answer; the explanation appears below.

Generating…

Quiz loading from media/langgraph_lego/quiz.json once ready…