Learn / AI / AI: One Course / LangGraph, LlamaIndex and LangChain

AI: One Course · 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.
        +----------+      tool requested?       +-----------+
 START->|  agent   |------------ yes ----------->|   tools   |
        | (LLM call)|<---------------------------- +-----------+
        +----+-----+       results appended to state
             | no
             v
           END

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)
Output
  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
Up next · Lesson 16Multi-Agent Systems, MCP and AI SafetyOrchestrator-worker and handoff patterns, the Model Context Protocol, prompt injection and defence in depth.