Artificial Intelligence

Building Agentic Workflows in Python with LangGraph

In the rapidly evolving landscape of artificial intelligence, the ability to construct sophisticated and responsive AI agents is paramount. This article delves into the practical implementation of building a complete agentic workflow in Python utilizing the powerful LangGraph library. We will traverse the journey from a simple, single model call to a fully functional tool-using agent equipped with persistent conversation memory, offering a comprehensive guide for developers seeking to harness the capabilities of advanced AI agents.

The Challenge of Complex Agent Interactions

Traditional AI agent setups often excel at handling single-turn interactions: a user poses a question, the model processes it, and a response is generated. However, real-world applications frequently demand more intricate capabilities. An agent might need to query external databases, maintain context across multiple conversational turns, or provide transparent insights into its decision-making processes. Implementing these complexities without a robust framework can lead to the development of custom, brittle plumbing for each unique use case, hindering scalability and maintainability.

LangGraph emerges as a solution to these challenges by providing a structured and intuitive approach to building agentic workflows. It represents an agent as a directed graph, where distinct units of work are encapsulated in nodes, the flow of execution is dictated by edges, and a shared state object meticulously tracks the entire conversation history throughout each step. Crucially, model inferences, tool calls, and responses are integrated directly into the graph’s state. This transparency makes the entire execution flow visible, inspectable, and readily available for subsequent nodes, fostering a more understandable and debuggable AI system.

This article will guide you through understanding the fundamental primitives of LangGraph: state, nodes, and edges. We will explore how to automatically manage conversation history using MessagesState, integrate language model calls within nodes, and connect them seamlessly to the graph. Furthermore, we will cover the process of registering tools, routing tool calls back to the model for processing, tracing the complete message sequence to dissect the model’s reasoning, and implementing persistent conversations across separate invocations using a checkpointer. We will construct this agentic graph step-by-step, beginning with the essential setup procedures.

Setting Up Your Development Environment

To embark on building your LangGraph agent, the first step is to install the necessary Python packages. This includes langgraph itself, langchain-openai for leveraging OpenAI’s models, and python-dotenv for managing sensitive API keys.

pip install langgraph langchain-openai python-dotenv

Following the installation, it is crucial to secure your OpenAI API key. Create a file named .env in the root directory of your project and add your key as follows:

OPENAI_API_KEY="your_key_here"

This .env file will be used to load your API key as an environment variable. Ensure this is done at the beginning of your script, before any LangChain or LangGraph imports, to make the key accessible to the libraries:

from dotenv import load_dotenv
load_dotenv()

The python-dotenv library simplifies this process by reading the .env file and setting the specified environment variables, which are then utilized by the OpenAI API client.

Understanding the Core Components: State, Nodes, and Edges

Every LangGraph graph is fundamentally constructed from three core components: State, Nodes, and Edges. Grasping these concepts upfront is essential for navigating the intricacies of more complex graph structures.

State: The state serves as the central, shared memory for the entire graph. It is defined using Python’s TypedDict and holds all the information that needs to be passed between different parts of the agent’s workflow. Each node in the graph reads from this state and writes its updates back to it. Any fields in the state that are not explicitly modified by a node remain unchanged, ensuring that only the intended data is altered.

Building Agentic Workflows in Python with LangGraph

Nodes: Nodes are essentially plain Python functions. A node accepts the current state as its input and returns a dictionary representing the fields it intends to update. By registering these functions with the add_node method of the StateGraph builder, they become integral parts of the graph without requiring special decorators or base classes. If a function is passed to add_node without an explicit name, LangGraph automatically uses the function’s name for identification.

Edges: Edges define the sequence and logic of execution within the graph. An add_edge(A, B) statement signifies that after node A completes its execution, node B should be run next. For more dynamic control flow, add_conditional_edges allows for routing decisions based on the output of a routing function. Every LangGraph graph must have a designated entry point, typically labeled START, and at least one path leading to an END node, signifying the completion of a workflow.

A key feature for managing cumulative data, such as logs or message histories, is the use of reducers. When a state field is annotated with a reducer function (e.g., operator.add for lists), the updates from different nodes are combined rather than simply overwriting the previous value. This is demonstrated in the following example, where the log field accumulates messages:

from typing import Annotated
import operator
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END

class TicketState(TypedDict):
    customer_message: str
    log: Annotated[list, operator.add] # Uses operator.add to append messages

def log_received(state: TicketState) -> dict:
    return "log": [f"Received: state['customer_message']"]

def log_assigned(state: TicketState) -> dict:
    return "log": ["Assigned to support queue"]

builder = StateGraph(TicketState)
builder.add_node("log_received", log_received)
builder.add_node("log_assigned", log_assigned)
builder.add_edge(START, "log_received")
builder.add_edge("log_received", "log_assigned")
builder.add_edge("log_assigned", END)

graph = builder.compile()

result = graph.invoke(
    "customer_message": "My invoice looks wrong",
    "log": []
)

print(result)

This code snippet produces the following output, illustrating how both nodes contribute to the log list:

'customer_message': 'My invoice looks wrong', 'log': ['Received: My invoice looks wrong', 'Assigned to support queue']

The customer_message remains unchanged as neither node returned an update for it. This behavior is mirrored by MessagesState, which employs a specialized reducer, add_messages, designed for handling chat message objects, including deduplication and ordering.

Streamlining Conversation History with MessagesState

For conversational agents, maintaining a comprehensive message history is critical for providing context-aware responses. LangGraph offers a built-in state type, MessagesState, specifically designed for this purpose. This TypedDict contains a single messages field that automatically utilizes the add_messages reducer. Consequently, any new messages generated by a node are appended to the existing list, effectively building the conversation history without manual concatenation.

from langgraph.graph import MessagesState

MessagesState is suitable for most single-agent graphs. It can be extended with additional fields, such as customer_id or a priority flag, to accommodate specific application needs. However, the core functionality of accumulating messages is already built-in and efficiently handled.

Integrating Language Model Calls within Nodes

The primary function of many agent nodes is to interact with a language model. Within LangGraph, this involves creating a Python function that takes the current message list from the state, passes it to a language model, and then returns the model’s response as an AIMessage within the state’s messages field.

from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage

llm = ChatOpenAI(model="gpt-4o-mini")

def run_model(state: MessagesState) -> dict:
    system = SystemMessage("You are a support agent for a SaaS product. Be concise and helpful.")
    response = llm.invoke([system] + state["messages"])
    return "messages": [response]

The ChatOpenAI class provides a standard interface for interacting with OpenAI’s API. This node structure remains largely consistent even when switching to different LLM providers, such as Anthropic, Google, or local models via Ollama, requiring only minor adjustments to the import statements and model string. The SystemMessage is included in each invocation without being stored in the state, ensuring the persistent history remains clean and focused on the user-agent dialogue.

To integrate this node into a graph and execute a conversation, the following setup can be used:

from langgraph.graph import StateGraph, START, END
from langchain_core.messages import HumanMessage

builder = StateGraph(MessagesState)
builder.add_node("run_model", run_model)
builder.add_edge(START, "run_model")
builder.add_edge("run_model", END)

graph = builder.compile()

result = graph.invoke(
    "messages": [HumanMessage("My dashboard isn't loading. What should I try?")]
)

print(result["messages"][-1].content)

The result["messages"] variable contains the complete message history, including the initial HumanMessage and the subsequent AIMessage generated by the model. Accessing [-1] retrieves the most recent message.

Building Agentic Workflows in Python with LangGraph

Implementing Tool Usage and Routing

While language models can access their training data, specialized information such as account details, subscription tiers, or historical ticket data requires the use of tools. The model determines when a tool is necessary, and your code defines the tool’s functionality.

Tools can be defined using the @tool decorator, which automatically generates the necessary schema for the model to understand and utilize. The docstring of the tool is crucial, as it serves as the descriptive text the model reads to decide whether to invoke the tool and with what arguments. Precision in these docstrings is key to avoiding misinterpretations or incorrect argument assignments.

from langchain_core.tools import tool

@tool
def get_customer_tier(customer_id: str) -> str:
    """Look up the subscription tier for a customer by their ID.
    Returns 'free', 'pro', or 'enterprise'.
    """
    tiers = 
        "cust_1001": "enterprise",
        "cust_2002": "pro",
        "cust_3003": "free",
    
    return tiers.get(customer_id, "not found")

To enable the model to recognize and use this tool, it must be bound to the LLM instance. This is achieved using the bind_tools method. The run_model node is then updated to utilize this tool-aware LLM.

tools = [get_customer_tier]
llm_with_tools = llm.bind_tools(tools)

def run_model(state: MessagesState) -> dict:
    system = SystemMessage("You are a support agent for a SaaS product. Use available tools when you need account-specific information.")
    response = llm_with_tools.invoke([system] + state["messages"])
    return "messages": [response]

When the model decides to use a tool, its response will be an AIMessage with the tool_calls field populated, rather than plain text content. This structured output signals to LangGraph that a tool execution is required.

To handle these tool calls, LangGraph provides a pre-built ToolNode. This node inspects the tool_calls in the latest AIMessage, executes the corresponding tool with the provided arguments, and then appends the tool’s result as a ToolMessage back into the state. The tools_condition function is used to route the execution flow. It checks the last AIMessage for the presence of tool_calls. If tool calls are detected, the graph is routed to the tools node; otherwise, it proceeds to the END node. The edge connecting the tools node back to the run_model node creates a loop, allowing the model to process the tool’s output and generate a final response.

from langgraph.prebuilt import ToolNode, tools_condition

tool_node = ToolNode(tools)

builder = StateGraph(MessagesState)
builder.add_node("run_model", run_model)
builder.add_node("tools", tool_node)

builder.add_edge(START, "run_model")
builder.add_conditional_edges("run_model", tools_condition)
builder.add_edge("tools", "run_model")

graph = builder.compile()

This setup effectively implements the ReAct (Reasoning and Acting) pattern, a common paradigm in agent development. Each tool invocation involves two model calls: one to determine which tool to use and with what parameters, and a subsequent call to interpret the tool’s result and formulate a final answer. Understanding this two-step process is crucial for managing latency and cost, especially as the number of available tools increases.

Tracing the Reasoning Loop for Enhanced Visibility

To gain a deeper understanding of the agent’s decision-making process, especially when tools are involved, it’s beneficial to trace the complete message sequence. This involves invoking the graph and then iterating through the resulting messages to observe the flow of information and actions.

result = graph.invoke(
    "messages": [HumanMessage("Can you check what plan customer cust_1001 is on?")]
)

for msg in result["messages"]:
    print(type(msg).__name__, ":", msg.content or msg.tool_calls)

A sample output of this tracing might look like:

HumanMessage : Can you check what plan customer cust_1001 is on?
AIMessage : ['name': 'get_customer_tier', 'args': 'customer_id': 'cust_1001', 'id': 'call_Rx7kLmNpQ2wJtA3s', 'type': 'tool_call']
ToolMessage : enterprise
AIMessage : Customer cust_1001 is on the enterprise plan.

This output reveals a four-message exchange and two model calls. Initially, the model generates an AIMessage indicating a desire to call get_customer_tier with the customer_id ‘cust_1001’. The tools_condition detects this and routes the execution to the ToolNode. The ToolNode then executes get_customer_tier("cust_1001"), and its output, "enterprise", is returned as a ToolMessage. Subsequently, the graph loops back to the run_model node. With the ToolMessage now part of the context, the model can interpret the successful tool execution and generate a final AIMessage containing the answer. The tools_condition is evaluated again, finds no further tool calls, and directs the graph to terminate.

Preserving Conversations with Persistence

By default, each invocation of graph.invoke() starts with a fresh, uninitialized graph state. Without persistence mechanisms, the agent has no memory of previous interactions. To enable persistent conversations, LangGraph integrates a checkpointer mechanism.

When compiling the graph, a checkpointer can be provided. For development and testing, InMemorySaver is a convenient option, storing checkpoints in the process’s memory.

Building Agentic Workflows in Python with LangGraph
from langgraph.checkpoint.memory import InMemorySaver

checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)

To maintain conversation continuity, the same thread_id must be supplied with each invocation. This thread_id acts as a unique identifier for a specific conversation thread.

config = "configurable": "thread_id": "ticket-7741"

# First invocation
graph.invoke(
    
        "messages": [HumanMessage("Hi, I can't access my account.")]
    ,
    config,
)

# Second invocation for the same thread
result = graph.invoke(
    
        "messages": [HumanMessage("My ID is cust_2002, can you check my plan?")]
    ,
    config,
)

print(result["messages"][-1].content)

The expected output demonstrates how the agent recalls context from the previous interaction:

You're on the pro plan, cust_2002. Since you're having trouble accessing your account, I'd recommend resetting your password first. Pro accounts also have priority support available if the issue continues.

In this scenario, the second invocation benefits from the conversation history stored by the checkpointer. The checkpointer restores the thread’s state before execution and saves the updated state afterward. Using a different thread_id initiates a new, independent conversation.

For production environments, InMemorySaver is typically replaced with a more robust, persistent checkpointer backed by a database or other durable storage solutions. This allows for reliable state management across application restarts and distributed systems.

It’s important to distinguish between checkpointers and stores. Checkpointers are responsible for persisting the graph state for a specific thread. Stores, on the other hand, provide durable application-level storage for data independent of any particular conversation, such as user profiles, preferences, or long-term memories that might be shared across multiple threads. Stores complement checkpointers by offering a broader storage solution that graphs can access during their execution.

Conclusion: Building Scalable and Adaptable Agentic Workflows

Throughout this article, we have constructed a comprehensive LangGraph agent from the ground up, demystifying the flow of state within a graph, the execution of nodes, the integration of tools, and the critical role of persistence in maintaining conversational context. The principles and building blocks discussed here are scalable, extending from simple chatbots to significantly more complex agentic workflows.

One of LangGraph’s core strengths lies in the modularity of its components. Developers can seamlessly swap language models, introduce new tools, or modify persistence strategies without necessitating a complete redesign of the existing graph. The reliance on shared state as the primary communication channel ensures predictability and facilitates straightforward extensibility.

These architectural concepts also translate effectively to the development of multi-agent systems. A coordinating agent that orchestrates specialist agents can itself be represented as a graph, leveraging the same primitives of state, nodes, and conditional edges. While the scale of the architecture may increase, the fundamental building blocks remain consistent, providing a unified framework for diverse AI applications.

For those looking to further deepen their understanding and explore advanced applications, consulting the official LangGraph documentation and related resources on multi-agent systems and ReAct patterns is highly recommended. The journey into building intelligent, adaptable AI agents is ongoing, and LangGraph provides a powerful and elegant toolkit for this endeavor.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button
Device Kick
Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.