Building AI Agents with LangChain and LangGraph: Part 2 › Advanced Workflow Patterns

Michael BrenndoerferAugust 2, 20253 min read
Building AI Agents with LangChain and LangGraph: Part 2 › Advanced Workflow PatternsOne section · Sections in this article5 / 9 ↓
Reading tools Definitions & inline links

Toggle tooltip visibility. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

While our first example was linear, real agentic systems need conditional logic, loops, and decision points. Let's build a more sophisticated workflow that can handle different types of responses from the language model.

Building a Conversational Agent

This workflow introduces several sophisticated concepts:

  1. MessagesState: LangGraph's built-in state for handling conversations
  2. Conditional edges: Logic that determines which node to execute next
  3. Tool message handling: Proper formatting of tool responses for the model
  4. Conversation loops: The ability to continue dialogues naturally

The should_continue function demonstrates conditional routing - a key pattern in agentic systems where the workflow's next step depends on the current state.

In[8]:
Code
def call_llm(state: MessagesState) -> MessagesState:
    """Run LLM"""
    # Call the language model with the current messages in the state
    output = model_with_tools.invoke(state["messages"])
    # Return the output as a new messages list in the state
    return {"messages": [output]}


def run_tool(state: MessagesState):
    """Performs the tool call"""
    # Initialize a list to store tool responses
    result = []
    # Iterate over all tool calls in the last message
    for tool_call in state["messages"][-1].tool_calls:
        # Invoke the tool with the provided arguments
        observation = draft_customer_reply.invoke(tool_call["args"])
        # Append the tool's response in the required format
        result.append(
            {"role": "tool", "content": observation, "tool_call_id": tool_call["id"]}
        )
    # Return the tool responses as the new messages in the state
    return {"messages": result}


def should_continue(state: MessagesState) -> Literal["run_tool", "__end__"]:
    """Route to tool handler, or end if Done tool called"""
    # Get the list of messages from the state
    messages = state["messages"]
    # Get the last message in the conversation
    last_message = messages[-1]

    # If the last message contains tool calls, continue to the tool handler
    if last_message.tool_calls:
        return "run_tool"
    # Otherwise, end the workflow (reply to the user)
    return END


# Initialize the workflow graph with the MessagesState schema
workflow = StateGraph(MessagesState)

# Add the node that calls the LLM
workflow.add_node("call_llm", call_llm)
# Add the node that runs the tool
workflow.add_node("run_tool", run_tool)

# Add an edge from the start node to the call_llm node
workflow.add_edge(START, "call_llm")
# Add conditional edges from call_llm node based on should_continue function
workflow.add_conditional_edges(
    # The node to branch from
    "call_llm",
    # The function that determines which edge to take next
    should_continue,
    # Mapping of possible return values to the next node
    {
        "run_tool": "run_tool",  # If should_continue returns "run_tool", go to run_tool node
        END: END,  # If should_continue returns END, end the workflow
    },
)
# Add an edge from run_tool node to the end node
workflow.add_edge("run_tool", END)

# Compile the workflow into an executable application
app = workflow.compile()

Visualizing Complex Workflows

This graph shows the conditional logic in action. Notice how call_llm can either end the conversation or route to run_tool, depending on whether tool calls are present in the response.

In[9]:
Code
Image(app.get_graph().draw_mermaid_png())
Out[9]:
Visualization
Notebook output

Testing the Conversational Agent

Watch how our agent processes the request:

  1. Receives the human message
  2. Generates a tool call with appropriate parameters
  3. Executes the tool
  4. Returns the formatted result

This demonstrates the complete agent lifecycle in a production-ready pattern.

In[10]:
Code
result = app.invoke(
    {
        "messages": [
            {
                "role": "user",
                # content is the user's request
                "content": "Let Frank know that his refund has been processed.",
            },
        ]
    }
)
for m in result["messages"]:
    m.pretty_print()
Out[10]:
Console
================================ Human Message =================================

Let Frank know that his refund has been processed.
================================== Ai Message ==================================
Tool Calls:
  draft_customer_reply (8ad00c73-7cae-4011-9a08-7a082d7f9798)
 Call ID: 8ad00c73-7cae-4011-9a08-7a082d7f9798
  Args:
    request_category: Refund
    customer_name: Frank
    reply: Your refund has been processed.
================================= Tool Message =================================

I am drafting a reply to Frank in the category Refund.
 Content: Your refund has been processed.