DEV Community

Cover image for Master LLM Workflows with LangGraph: A Beginner's Guide
Ankit Sharma
Ankit Sharma

Posted on

Master LLM Workflows with LangGraph: A Beginner's Guide

Have you ever tried to build a complex application with a Large Language Model (LLM) only to find yourself tangled in a mess of if-else statements and function calls? You start with a simple prompt, but then you need to check a database, call an external API, maybe ask the user for clarification, and then decide the next step based on all that information. Suddenly, your elegant LLM interaction becomes a spaghetti code nightmare.

That's a common struggle for developers pushing the boundaries of what LLMs can do. While a single LLM prompt is great for quick questions, real-world applications demand more. They need to remember past interactions, make decisions, use tools, and even correct themselves. This is where LangGraph steps in, transforming that tangled mess into a clear, manageable flow.

The Evolution of LLM Workflows: Why We Need LangGraph

An illustration showing a simple LLM query versus a complex, multi-step agent workflow.

Large Language Models (LLMs) burst onto the scene, making it incredibly easy to generate text, answer questions, and even write code with a single prompt. You ask a question, the LLM gives an answer. Simple, right? This "single-turn" interaction is powerful for many basic tasks.

However, the real world isn't always a single turn. Imagine building an AI assistant that needs to book a flight. It can't just respond once. It needs to ask for your destination, dates, preferences, check availability with an external tool, handle errors, and confirm with you. This requires a series of steps, decisions, and memory of previous information.

This is the limitation of simple LLM interactions. They lack state (memory of past events), multi-step reasoning, and the ability to act like an "agent" – an AI that can observe, think, and act. To build sophisticated applications, we need a way to orchestrate these complex interactions. We need to manage state, define decision points, and allow for iterative processes. LangGraph provides exactly this solution, letting you build sophisticated LLM workflows that go far beyond a single prompt.

What is LangGraph? Core Concepts Explained

[IMAGE: A simple diagram illustrating a graph with labeled nodes (e.g., 'LLM Call', 'Tool Use') and directed edges showing data flow.]

LangGraph is an open-source framework built on top of LangChain, a popular library for developing LLM applications. Its main purpose is to help you build stateful, cyclic LLM applications. What does that mean? It means applications that can remember information over time and can loop back to previous steps, allowing for complex reasoning and self-correction.

At its heart, LangGraph uses a graph-based architecture. Think of it like a flowchart for your LLM application. This graph consists of two main components:

  • Nodes: These are the individual steps or operations in your workflow. A node could be an LLM call (asking the model a question), a tool invocation (using an external function like a search engine or a calculator), or even a custom Python function you write. Each node performs a specific task.
  • Edges: These are the connections between nodes. Edges dictate the flow of data and control. They tell the graph which node to execute next after the current one finishes. Edges can be simple (always go from A to B) or conditional (go to B if X happens, or to C if Y happens).

The crucial concept that ties everything together is State. The state is a shared object that holds all the information relevant to your workflow at any given moment. As the graph executes, each node can read from and write to this state. This allows information to be passed seamlessly between different parts of your application, giving your LLM application memory and context.

This graph-based approach makes it easy to model complex interactions, including human-in-the-loop scenarios (where a human might review or approve a step) and multi-agent systems (where different AI agents collaborate to solve a problem).

graph TD
    A[Start] --> B(User Input);
    B --> C{Decide Action};
    C -- "Call Tool" --> D[Tool Node];
    C -- "Respond Directly" --> E[LLM Response Node];
    D --> F(Process Tool Output);
    F --> C;
    E --> G[End];
Enter fullscreen mode Exit fullscreen mode

Building Blocks: Nodes, Edges, and State in Action

[IMAGE: A flowchart showing a basic LangGraph workflow with an input, a processing node, and an output node, highlighting state transitions.]

Let's dive into how you actually define these components in LangGraph. Everything starts with defining your application's state. The state is typically a dictionary or a Pydantic model that holds all the information your graph needs. Each node will receive the current state, perform its operation, and then return updates to that state.

Defining Your State

You define your state using a TypedDict or a Pydantic model. This helps keep your state organized and type-safe. For example, if your application needs to track messages and a decision, your state might look like this:

from typing import List, Annotated, TypedDict
from langchain_core.messages import BaseMessage, HumanMessage

# Define the state for our graph
class AgentState(TypedDict):
    # The list of messages exchanged so far
    messages: Annotated[List[BaseMessage], lambda x, y: x + y]
    # A decision string, e.g., "call_tool" or "respond"
    next_action: str
Enter fullscreen mode Exit fullscreen mode

Here, messages is a list of BaseMessage objects (like user inputs or AI responses), and next_action is a string that an LLM might use to decide what to do next. The Annotated type hint with a reducer function (lambda x, y: x + y) tells LangGraph how to combine updates to this list – in this case, by appending new messages.

Defining Nodes

Nodes are simply Python functions that take the current state as input and return a dictionary of updates to the state.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

# Initialize your LLM (replace with your actual API key)
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# A simple LLM node
def call_llm(state: AgentState):
    messages = state["messages"]
    prompt = ChatPromptTemplate.from_messages([
        ("system", "You are a helpful AI assistant."),
        ("human", "{input}")
    ])
    # Assuming the last message is the user's input for simplicity
    # In a real agent, you'd pass the full conversation history
    response = llm.invoke(prompt.format(input=messages[-1].content))
    return {"messages": [response]}

# A simple custom function node
def greet_user(state: AgentState):
    messages = state["messages"]
    user_name = messages[-1].content.split(" ")[1] if len(messages) > 0 and " " in messages[-1].content else "there"
    greeting = f"Hello, {user_name}! How can I help you today?"
    return {"messages": [HumanMessage(content=greeting)]}
Enter fullscreen mode Exit fullscreen mode

Each node function receives the AgentState and returns a dictionary. LangGraph then merges this dictionary into the existing state.

Connecting Nodes with Edges

Once you have your nodes, you connect them using StateGraph. You add nodes and then define the entry point (where the graph starts), the exit point (where it ends), and the edges between nodes.

from langgraph.graph import StateGraph, END

# Initialize the graph
workflow = StateGraph(AgentState)

# Add nodes to the graph
workflow.add_node("llm_node", call_llm)
workflow.add_node("greet_node", greet_user)

# Set the entry point
workflow.set_entry_point("greet_node")

# Define a simple edge: after greeting, always go to the LLM
workflow.add_edge("greet_node", "llm_node")

# Define the exit point
workflow.add_edge("llm_node", END)

# Compile the graph
app = workflow.compile()
Enter fullscreen mode Exit fullscreen mode

This code snippet shows how to define nodes, set an entry point, and add a simple edge. The END special node signifies the termination of the graph's execution. Compiling the workflow turns your graph definition into a runnable application.

graph TD
    A[Start] --> B(greet_node);
    B --> C(llm_node);
    C --> D[END];
Enter fullscreen mode Exit fullscreen mode

Crafting Your First LangGraph Workflow: A Simple Example

[IMAGE: A screenshot of Python code demonstrating the creation and execution of a simple LangGraph.]

Let's put it all together and build a very basic LangGraph workflow. This example will take a user's name, greet them, and then use an LLM to respond to a follow-up question.

First, make sure you have LangChain and LangGraph installed:
pip install langchain langchain-openai langgraph

You'll also need an OpenAI API key. Set it as an environment variable: export OPENAI_API_KEY="your_api_key_here".

import os
from typing import List, Annotated, TypedDict
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langgraph.graph import StateGraph, END

# 1. Define the AgentState
class AgentState(TypedDict):
    messages: Annotated[List[BaseMessage], lambda x, y: x + y]

# 2. Initialize the LLM
# Ensure your OPENAI_API_KEY is set as an environment variable
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# 3. Define the Nodes

# Node 1: Greet the user
def greet_user_node(state: AgentState):
    print("---EXECUTING GREET NODE---")
    messages = state["messages"]
    # Assuming the first message is the user's initial input like "Hi, John"
    user_input = messages[0].content

    user_name = "there"
    if " " in user_input:
        parts = user_input.split(" ", 1) # Split only on the first space
        if len(parts) > 1:
            user_name = parts[1].strip() # Take the rest as the name

    greeting = f"Hello, {user_name}! How can I help you today?"
    print(f"Greeting: {greeting}")
    return {"messages": [AIMessage(content=greeting)]}

# Node 2: Call the LLM to respond to the user's query
def llm_response_node(state: AgentState):
    print("---EXECUTING LLM RESPONSE NODE---")
    messages = state["messages"]

    # The prompt should include the full conversation history
    prompt = ChatPromptTemplate.from_messages([
        ("system", "You are a helpful AI assistant. Keep your responses concise."),
        *messages # Unpack all messages into the prompt
    ])

    response = llm.invoke(prompt)
    print(f"LLM Response: {response.content}")
    return {"messages": [response]}

# 4. Build the LangGraph Workflow
workflow = StateGraph(AgentState)

# Add the nodes
workflow.add_node("greet", greet_user_node)
workflow.add_node("llm_respond", llm_response_node)

# Set the entry point
workflow.set_entry_point("greet")

# Define the edges
# After greeting, always go to the LLM to respond to the user's actual query
workflow.add_edge("greet", "llm_respond")

# After the LLM responds, the workflow ends for this simple example
workflow.add_edge("llm_respond", END)

# 5. Compile the graph
app = workflow.compile()

# 6. Execute the graph
print("\n--- Running Workflow 1 ---")
initial_input = HumanMessage(content="Hi, Alice")
final_state = app.invoke({"messages": [initial_input]})
print(f"Final State Messages: {[m.content for m in final_state['messages']]}")

print("\n--- Running Workflow 2 ---")
initial_input_2 = HumanMessage(content="Hello, Bob. Tell me a fun fact about cats.")
final_state_2 = app.invoke({"messages": [initial_input_2]})
print(f"Final State Messages: {[m.content for m in final_state_2['messages']]}")
Enter fullscreen mode Exit fullscreen mode

In this example:

  1. We define AgentState to hold our conversation messages.
  2. We create two nodes: greet_user_node to generate a personalized greeting and llm_response_node to use the LLM for a general response.
  3. We set greet as the starting point.
  4. We define an edge from greet to llm_respond, meaning after the greeting, the LLM will always generate a response.
  5. Finally, we define an edge from llm_respond to END, marking the end of this particular workflow.

When you run this, you'll see the greet_user_node execute first, adding a greeting to the state. Then, llm_response_node takes the updated state (including the greeting and the original user input) and generates a final LLM response.

Adding Intelligence: Conditional Logic and Loops

[IMAGE: A more complex flowchart illustrating a LangGraph workflow with a conditional branch and a potential loop back to an earlier node.]

The real power of LangGraph shines when you introduce conditional logic and loops. This allows your LLM applications to make decisions and adapt their behavior dynamically. Instead of a fixed path, your graph can branch based on the output of a node, often an LLM's decision.

Conditional Edges

Conditional edges allow you to define a function that determines the next node based on the current state. This function typically returns the name of the next node to execute.

Let's extend our previous example. Now, after the user's initial input, we want an LLM to decide if it needs to call a specific tool (like a "joke tool") or just respond directly.

import os
from typing import List, Annotated, TypedDict
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langgraph.graph import StateGraph, END

# 1. Define the AgentState
class AgentState(TypedDict):
    messages: Annotated[List[BaseMessage], lambda x, y: x + y]
    # New field to store the LLM's decision
    next_step: str | None

# 2. Initialize the LLM
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# 3. Define the Nodes

# Node 1: Decide what to do next based on user input
def decide_action_node(state: AgentState):
    print("---EXECUTING DECISION NODE---")
    messages = state["messages"]

    # Use a small LLM call to decide the next action
    decision_prompt = ChatPromptTemplate.from_messages([
        ("system", "You are a helpful assistant. Based on the user's last message, decide if a 'joke_tool' should be called or if you should 'respond_directly'. Respond with 'joke_tool' or 'respond_directly'."),
        messages[-1] # Only consider the last message for decision
    ])

    decision = llm.invoke(decision_prompt).content.strip().lower()
    print(f"Decision: {decision}")

    # Ensure the decision is one of the expected values
    if "joke_tool" in decision:
        return {"next_step": "joke_tool"}
    else:
        return {"next_step": "respond_directly"}

# Node 2: Call a "joke tool"
def joke_tool_node(state: AgentState):
    print("---EXECUTING JOKE TOOL NODE---")
    # In a real app, this would call an external API for a joke
    joke = "Why don't scientists trust atoms? Because they make up everything!"
    print(f"Joke: {joke}")
    return {"messages": [AIMessage(content=joke)]}

# Node 3: Respond directly using the LLM
def llm_respond_node(state: AgentState):
    print("---EXECUTING LLM RESPOND NODE---")
    messages = state["messages"]

    prompt = ChatPromptTemplate.from_messages([
        ("system", "You are a helpful AI assistant. Keep your responses concise."),
        *messages
    ])

    response = llm.invoke(prompt)
    print(f"LLM Response: {response.content}")
    return {"messages": [response]}

# 4. Build the LangGraph Workflow
workflow = StateGraph(AgentState)

# Add the nodes
workflow.add_node("decide_action", decide_action_node)
workflow.add_node("joke_tool", joke_tool_node)
workflow.add_node("llm_respond", llm_respond_node)

# Set the entry point
workflow.set_entry_point("decide_action")

# Define the conditional edge
# This function will be called after 'decide_action' to determine the next step
def route_action(state: AgentState):
    if state["next_step"] == "joke_tool":
        return "joke_tool"
    elif state["next_step"] == "respond_directly":
        return "llm_respond"
    else:
        # Fallback or error handling
        return "llm_respond" # Default to LLM response

workflow.add_conditional_edges(
    "decide_action", # Source node
    route_action,    # Function to determine next node
    {
        "joke_tool": "joke_tool",
        "llm_respond": "llm_respond"
    }
)

# After joke tool or LLM response, the workflow ends
workflow.add_edge("joke_tool", END)
workflow.add_edge("llm_respond", END)

# 5. Compile the graph
app = workflow.compile()

# 6. Execute the graph with different inputs
print("\n--- Running Workflow (Joke Request) ---")
joke_request = HumanMessage(content="Tell me a joke!")
final_state_joke = app.invoke({"messages": [joke_request]})
print(f"Final State Messages: {[m.content for m in final_state_joke['messages']]}")

print("\n--- Running Workflow (General Question) ---")
general_question = HumanMessage(content="What is the capital of France?")
final_state_general = app.invoke({"messages": [general_question]})
print(f"Final State Messages: {[m.content for m in final_state_general['messages']]}")
Enter fullscreen mode Exit fullscreen mode

In this enhanced example:

  • We added a next_step field to our AgentState.
  • The decide_action_node uses an LLM to determine if the user wants a joke or a general response, updating next_step in the state.
  • route_action is a Python function that reads state["next_step"] and returns the name of the next node ("joke_tool" or "llm_respond").
  • add_conditional_edges uses route_action to dynamically route the workflow.

Loops and Iterative Processes

LangGraph also supports cyclic graphs, meaning your workflow can loop back to an earlier node. This is incredibly powerful for:

  • Self-correction: An agent might try an action, evaluate its outcome, and if it fails, loop back to a planning stage to try a different approach.
  • Iterative refinement: A summarization agent might generate a summary, then review it against criteria, and if it's not good enough, loop back to refine it.
  • Human-in-the-loop: An agent might perform a task, then ask a human for approval, and if denied, loop back to modify its output.

To create a loop, you simply define an edge that points back to a previous node in the graph. The conditional routing function can decide whether to loop or proceed to END.

graph TD
    A[Start] --> B(User Input);
    B --> C{Decide Action};
    C -- "Call Tool" --> D[Tool Node];
    C -- "Respond Directly" --> E[LLM Response Node];
    D --> F{Evaluate Tool Output};
    F -- "Needs More Info" --> C;
    F -- "Done" --> G[End];
    E --> G;
Enter fullscreen mode Exit fullscreen mode

This diagram shows a loop where, after using a tool, the system evaluates the output. If more information is needed, it loops back to the "Decide Action" node, potentially asking the LLM to re-plan or ask the user for clarification.

Beyond Basics: Benefits and Use Cases of LangGraph

[IMAGE: An abstract graphic representing the various complex LLM applications that can be built with LangGraph, such as agents and multi-step reasoning.]

LangGraph offers significant advantages for building sophisticated LLM applications:

  • Explicit Control: You have clear, step-by-step control over your application's logic. No more guessing what the LLM will do next; you define the exact flow.
  • State Management: The shared AgentState makes it easy to manage context and memory across multiple steps, which is crucial for conversational agents.
  • Modularity: Each node is a self-contained unit, making your code easier to understand, test, and maintain. You can swap out LLMs, tools, or logic within a node without affecting the entire workflow.
  • Robustness: By explicitly defining paths and decision points, you can build more reliable applications that handle different scenarios gracefully, including error states.
  • Observability: LangGraph integrates well with tools like LangSmith, which allows you to visualize the execution path of your graph. This is invaluable for debugging complex, multi-step LLM applications.

These benefits make LangGraph ideal for a wide range of advanced LLM use cases:

  • Advanced LLM Agents: Build agents that can plan, execute actions using tools, reflect on their results, and self-correct.
  • Multi-Agent Systems: Orchestrate multiple specialized AI agents that collaborate to solve complex problems, each handling a different part of the workflow.
  • Retrieval-Augmented Generation (RAG) with Self-Correction: Create RAG pipelines that can search for information, generate a response, and then critically evaluate if the response is good enough, looping back to refine the search or generation if needed.
  • Human-in-the-Loop Workflows: Design applications where human input or approval is required at specific stages, ensuring quality and control.

If you're moving beyond simple prompt-response interactions and want to build intelligent, stateful, and dynamic LLM applications, LangGraph is a powerful framework to explore. It provides the structure and flexibility you need to bring your most ambitious AI ideas to life.

Key Takeaways

  • LangGraph is a framework for building stateful, cyclic LLM applications using a graph-based architecture.
  • It uses Nodes (operations like LLM calls or tool use), Edges (data flow and control), and a shared State (context) to manage complex workflows.
  • You define your application's state, create Python functions for each node, and then connect them using StateGraph.
  • Conditional edges allow your workflow to branch dynamically based on decisions made by LLMs or custom logic.
  • Cyclic graphs enable powerful features like self-correction, iterative refinement, and human-in-the-loop interactions.
  • LangGraph provides explicit control, modularity, and robust state management, making it ideal for advanced LLM agents and multi-step reasoning.
  • Tools like LangSmith offer excellent observability for debugging complex LangGraph applications.

What complex, multi-step LLM workflow are you excited to build with LangGraph?

Top comments (0)