Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
LangChain, LangGraph, LlamaIndex, AutoGen, CrewAI — every month a new framework promises to make agents easier. Most of them are heavy and opinionated; few survive their first real production deployment. The honest assessment: for simple agents (1-3 tools, max 5 iterations, single workflow), raw SDK code wins on simplicity and debuggability. For complex agents (graph-based control flow, many tool combinations, multi-agent), LangGraph or similar pays for itself. Start raw; adopt only when the boilerplate starts hurting.
When raw wins: small tool surface, predictable control flow, debugging-by-print-statement, no team-coordination overhead. When LangGraph (or similar) wins: graph-based control flow that you'd otherwise code by hand, multi-agent orchestration with checkpointing, state persistence across long-running sessions. LangChain's main lib has grown so large it often hurts more than helps; LangGraph (smaller, focused on agent state) is the more defensible choice in 2026.
Use these three in order. Each builds on the one before.
When does an agent framework help, and when does it hurt?
Walk me through LangGraph's StateGraph model — what's a node, what's a conditional edge, what's the checkpointing?
I have a multi-agent system: orchestrator + 3 workers, all sharing partial state. Should I build it raw, use LangGraph, or pick something else? Why?
# Raw — for simple agents
def run_agent_raw(user_msg, tools, max_steps=6):
messages = [{"role": "user", "content": user_msg}]
for _ in range(max_steps):
resp = client.messages.create(model="claude-sonnet-4-6", tools=tools, messages=messages)
# ... handle stop reasons and tool dispatch
# 50 lines total, no dependencies, fully debuggable.
# LangGraph — for graph-based control flow
from langgraph.graph import StateGraph, END
class AgentState(TypedDict):
messages: list
next_node: str
def call_llm(state: AgentState) -> AgentState:
resp = client.messages.create(...)
state["messages"].append({"role": "assistant", "content": resp.content})
state["next_node"] = "tools" if resp.stop_reason == "tool_use" else "end"
return state
def call_tools(state: AgentState) -> AgentState:
# dispatch tools, append results to messages
state["next_node"] = "llm"
return state
graph = StateGraph(AgentState)
graph.add_node("llm", call_llm)
graph.add_node("tools", call_tools)
graph.add_conditional_edges("llm", lambda s: s["next_node"], {"tools": "tools", "end": END})
graph.add_edge("tools", "llm")
graph.set_entry_point("llm")
app = graph.compile()
# Pays off when you have:
# - branching control flow ('if user wants summary, do A; if action, do B')
# - persisted state (checkpoint across user turns)
# - human-in-the-loop interruptions
# - multi-agent (orchestrator + workers)python3 main.py