AI (Artificial Intelligence) · Lesson 15 of 20
LangGraph, LlamaIndex and LangChain
Graphs as agent control flow (with a mini engine you can run), LlamaIndex's data-first model, and when to use frameworks.
- Advanced
- 28 min read
- 3 objectives
Before this lessonLesson 14: The Agent Harness and Agent SDKs
What you will learn
- Explain nodes, edges and state
- Describe LlamaIndex's core objects
- Decide framework vs plain code
Your Progress
0 of 20 lessons 0%
- Lessons0 / 20
- Completed0
- Est. time left~ 9 hours
Create a free account to keep your progress on every device.
You have now written an agent loop by hand. Frameworks exist to save you from rewriting it, and to give you structure as it grows. Three names dominate: LangGraph (agents as graphs), LlamaIndex (data and retrieval first), and LangChain (the broad toolkit). Understanding what each is for is more valuable than memorising any one API, because the APIs change monthly while the ideas stay put.
LangGraph: your agent as a state machine
A simple loop ("call model, run tools, repeat") gets awkward once you need branches, retries, parallel steps, human approval pauses and resumable runs. LangGraph models the agent as a graph:
- State: a shared object (usually a dict) that flows through the graph: messages, retrieved documents, counters, flags.
- Nodes: functions that read the state and return updates. A node might call an LLM, run a tool, or apply a rule.
- Edges: which node runs next. Conditional edges choose the next node by looking at the state ("if the model asked for a tool go to
tools, else finish"). - Checkpointing: the state is saved after every step, so you can pause for human approval, resume after a crash, replay, or "time travel" to debug.
The best way to understand it is to build the engine. This 30-line mini-LangGraph has state, nodes, edges and a conditional router:
class Graph:
def __init__(self):
self.nodes, self.edges, self.routers = {}, {}, {}
def add_node(self, name, fn):
self.nodes[name] = fn
def add_edge(self, a, b):
self.edges[a] = b
def add_conditional_edges(self, a, router):
self.routers[a] = router # router(state) -> name of next node
def run(self, state, start, max_steps=20):
node = start
for _ in range(max_steps):
if node == "END":
return state
update = self.nodes[node](state) # node reads state, returns changes
state = {**state, **(update or {})}
print(f" ran {node:<8} state now: {state}")
node = self.routers[node](state) if node in self.routers else self.edges[node]
raise RuntimeError("too many steps")
# --- a tiny agent: draft an answer, critique it, revise until good enough ---
def draft(state):
return {"text": state["text"] + "!", "attempts": state["attempts"] + 1}
def critique(state):
return {"good": len(state["text"]) >= 5} # pretend judge: longer = better
def finish(state):
return {"done": True}
g = Graph()
g.add_node("draft", draft)
g.add_node("critique", critique)
g.add_node("finish", finish)
g.add_edge("draft", "critique")
g.add_conditional_edges("critique", lambda s: "finish" if s["good"] else "draft") # the loop!
g.add_edge("finish", "END")
final = g.run({"text": "hi", "attempts": 0, "good": False}, start="draft")
print("\nFINAL:", final) ran draft state now: {'text': 'hi!', 'attempts': 1, 'good': False}
ran critique state now: {'text': 'hi!', 'attempts': 1, 'good': False}
ran draft state now: {'text': 'hi!!', 'attempts': 2, 'good': False}
ran critique state now: {'text': 'hi!!', 'attempts': 2, 'good': False}
ran draft state now: {'text': 'hi!!!', 'attempts': 3, 'good': False}
ran critique state now: {'text': 'hi!!!', 'attempts': 3, 'good': True}
ran finish state now: {'text': 'hi!!!', 'attempts': 3, 'good': True, 'done': True}
FINAL: {'text': 'hi!!!', 'attempts': 3, 'good': True, 'done': True}That is a real evaluator-optimiser pattern: the conditional edge sends control back to draft until the critic is happy. Swap the toy functions for LLM calls and you have a working self-improving writer. The real LangGraph adds persistence, streaming, human-in-the-loop interrupts and parallel branches. Here is what the equivalent looks like (illustrative):
from langgraph.graph import StateGraph, START, END
from typing import TypedDict
class State(TypedDict):
messages: list
attempts: int
def call_model(state: State):
reply = llm_with_tools.invoke(state["messages"])
return {"messages": [reply]}
def run_tools(state: State):
... # execute tool calls in state["messages"][-1], append results
def should_continue(state: State):
return "tools" if state["messages"][-1].tool_calls else END
graph = StateGraph(State)
graph.add_node("agent", call_model)
graph.add_node("tools", run_tools)
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", should_continue)
graph.add_edge("tools", "agent")
app = graph.compile(checkpointer=memory) # saves state after every step
result = app.invoke({"messages": [("user", "Where is order 4471?")]}, config={"configurable": {"thread_id": "abc"}})Why graphs? The real benefits
- Explicit control flow. You can draw the agent on a whiteboard and it matches the code.
- Human-in-the-loop. Pause at a node ("approve this refund?"), wait days, then resume from the checkpoint.
- Durability. A crash mid-run is not fatal; resume from the last saved state.
- Parallelism and sub-graphs. Fan out to several nodes and merge; nest one graph inside another (multi-agent).
- Debuggability. Every step's state is inspectable, replayable and testable in isolation.
LlamaIndex: data first
Where LangGraph focuses on control flow, LlamaIndex focuses on getting your data in front of the model. It began as a RAG toolkit and its vocabulary maps exactly onto the pipeline you learned:
- Documents: raw loaded data (PDFs, Notion, databases) via hundreds of readers (LlamaHub).
- Nodes: chunks, with metadata and relationships to their neighbours and parents.
- Index: a searchable structure over nodes: a
VectorStoreIndex, keyword/BM25 index, knowledge graph or summary index. - Retriever: fetches relevant nodes (vector, BM25, hybrid, with rerank and metadata filters).
- Query engine: retriever + synthesiser: retrieve, then have the LLM write the answer with citations.
- Agents and workflows: tool-using agents and event-driven Workflows for multi-step pipelines.
- Parsing: strong document parsing (LlamaParse) for messy PDFs, tables and scans, which is where many RAG projects actually fail.
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
documents = SimpleDirectoryReader("./handbook").load_data() # read files
index = VectorStoreIndex.from_documents(documents) # chunk + embed + store
query_engine = index.as_query_engine(similarity_top_k=5) # retriever + answer synthesiser
response = query_engine.query("How long do I have to return a laptop?")
print(response) # the answer
for node in response.source_nodes: # the citations
print(node.score, node.metadata["file_name"])Five lines from folder of files to a cited answer. That speed is the appeal. The cost is that defaults hide decisions (chunk size, embedding model, prompt), which you must eventually understand and tune, and now you can.
LangChain: the broad toolkit
LangChain is the widest of the three: model wrappers for every provider, prompt templates, output parsers, retrievers, document loaders, tool abstractions, and LCEL (a pipe syntax for composing steps: prompt | model | parser). Its ecosystem includes LangGraph for agents and LangSmith for tracing and evals. The common modern advice: use LangChain's provider integrations and components where convenient, and LangGraph when you need real control flow.
Choosing: frameworks or plain code?
- Plain code + provider SDK: best for simple flows (single call, small chain, a basic RAG). Fewest surprises, easiest to debug, smallest dependency surface. Many production teams stay here longer than they expect.
- LlamaIndex: when your main challenge is ingesting and retrieving from lots of messy, varied data sources.
- LangGraph: when the agent has branching, loops, approvals, long-running or resumable work, or several cooperating agents.
- LangChain: when you want many ready-made integrations and swappable components.
- Vendor Agent SDKs: when you are committed to one provider and want their best-practice harness.
# Write your solution here
