Building Intelligent Feedback Systems: A Deep Dive into Conditional Agentic Workflows with LangGraph
Author(s): Sandip Palit
Originally published on Towards AI.
Building Intelligent Feedback Systems: A Deep Dive into Conditional Agentic Workflows with LangGraph
The landscape of Artificial Intelligence has shifted dramatically over the past couple of years. We are no longer simply chatting with isolated Large Language Models (LLMs) to generate text or summarize documents. Instead, the industry has aggressively moved toward Agentic Workflows, systems where LLMs act as the reasoning engine within a structured, multi-step process, capable of making decisions, routing information, and executing tasks autonomously.
To build these robust systems, developers need tools that can manage complex control flows, maintain state across multiple interactions, and ensure that the outputs from the LLM are predictable and strictly formatted. This brings us to the modern AI stack demonstrated in this guide: LangGraph, LangChain, Groq, and Pydantic.
In this comprehensive blog post, we will explore every theoretical concept required to understand how to build a fully automated, intelligent customer review triage system.

The Shift from Simple Prompts to Agentic Workflows
When LLMs first became widely accessible, the standard interaction model was a direct query-response loop. A user inputs a prompt, and the model outputs a response. While powerful for simple tasks like drafting an email or explaining a concept, this paradigm falls short for complex business processes.
A standard LLM call is stateless and linear. It does not possess a memory of past interactions unless explicitly provided in the prompt, and it cannot easily route its own output to different tools based on conditional logic without external scaffolding.
Enter the Agentic Workflow. In an agentic workflow, the LLM is not just a text generator; it is a decision-maker. It is integrated into a larger architectural framework that allows it to:
- Analyze an input and determine the next best step.
- Route data through different pathways based on its own reasoning.
- Interact with external tools, APIs, or databases.
- Maintain a “state” (a running memory of variables) that is updated as the workflow progresses.
In our specific use case: processing customer reviews, a simple prompt might just ask the LLM to write a reply. But an agentic workflow allows the system to first read the review, mathematically determine its sentiment, route positive reviews to a simple “thank you” generator, and route negative reviews through a complex diagnostic protocol to determine the urgency, tone, and specific issue type before finally drafting a highly tailored empathetic response.
The Engine: Large Language Models and LLaMA 3
At the core of this system is the Large Language Model. The demo utilizes the LLaMA 3 family of models, specifically llama-3.3-70b-versatile.
To understand why this model is chosen, we must understand its parameters and architecture:
- Parameters (70b): The “70b” refers to 70 billion parameters. Parameters are the internal variables (weights and biases) that the neural network uses to make predictions. A 70 billion parameter model is considered a “heavyweight” open-weights model. It is large enough to possess exceptional reasoning capabilities, nuance comprehension, and instruction-following skills, making it perfectly suited for complex tasks like multi-dimensional sentiment analysis.
- Temperature Parameter: In AI, “temperature” controls the randomness or creativity of the model’s output. A high temperature (e.g., 0.8 or 1.0) makes the model’s responses highly varied and creative, great for writing poetry, but terrible for writing code or categorizing data. In our architecture, the temperature is set to
0. This forces the model to be deterministic. When we ask it to categorize an issue as "Bug" or "UX", we want the most mathematically probable answer every single time, without creative deviation.
The Framework: LangChain Ecosystem
LangChain is an open-source framework designed to simplify the creation of applications using large language models. Before LangChain, developers had to write custom API wrappers, manage complex prompt templates mathematically, and write extensive regex (regular expressions) to parse the output from LLMs.
LangChain provides standardized abstractions for:
- Models: A unified interface to interact with models from OpenAI, Anthropic, Groq, Google, etc. If we want to swap out Groq for another provider, LangChain allows us to do it by changing just one line of code.
- Prompts: Dynamic templates that allow developers to inject variables into their prompts programmatically.
- Chains: Sequences of operations where the output of one step becomes the input of the next.
However, standard LangChain (often utilizing LCEL — LangChain Expression Language) is inherently designed for linear chains (A goes to B goes to C). It struggles with complex, cyclical workflows, loops, and branching conditional logic. This limitation birthed LangGraph.
The Orchestrator: State Machines and LangGraph
To understand the demo, we must understand the concept of a Finite State Machine (FSM) and Directed Graphs.
In computer science, a graph is a structure amounting to a set of objects in which some pairs of the objects are in some sense “related.” The objects are called nodes (or vertices), and the relationships are called edges.
- Directed Graph: The edges have a direction (Node A points to Node B, but B does not necessarily point to A).
- Directed Acyclic Graph (DAG): A directed graph with no cycles (we cannot loop back to a previous node).
- Cyclic Graph: A graph where paths can loop back on themselves, allowing for retry mechanisms or iterative refinement.
LangGraph is an extension of LangChain specifically built for creating stateful, multi-actor applications with LLMs. It models workflows as graphs.
Data Validation and Schemas: Pydantic
One of the most notoriously difficult aspects of working with LLMs is that their natural output is raw, unstructured text. If we ask an LLM to “Diagnose this review and give me the tone and urgency,” it might reply:
- “The tone is angry and the urgency is high.”
- “Tone: Angry, Urgency: High.”
- “I have analyzed the review. The user is angry. This is highly urgent.”
This variability is a nightmare for software engineering. If we are trying to write a Python script that automatically flags “high” urgency reviews for immediate human intervention, we cannot rely on regex to parse unpredictable conversational text. We need guaranteed, structured data — like a JSON object.
This is where Pydantic comes in. Pydantic is a data validation library for Python. It allows developers to define strict data schemas using standard Python type hints.
In Pydantic, we create a BaseModel and define exactly what fields we expect, what data types they must be, and even restrict the allowed values using Literal.
The Architecture of the Triage Demo
Let us synthesize these theories into the architecture of the provided demo. The goal is automated Customer Service Triage.
- Input: The system receives a raw string of text (a customer review).
- State Initialization: The string is loaded into the
ReviewState. - Sentiment Extraction (Node 1): An LLM is forced by a Pydantic schema to output exactly one word: “positive” or “negative”. This updates the state.
- Routing (Conditional Edge): A Python router function checks the state’s sentiment.
If positive -> Route to Node 2.
If negative -> Route to Node 3. - Positive Pathway (Node 2): A standard LLM call writes a warm thank-you note. The graph ends.
- Negative Pathway Phase 1 — Diagnosis (Node 3): An LLM is forced by a complex Pydantic schema to categorize the issue type, gauge the user’s emotional tone, and assess the technical urgency. This data updates the state.
- Negative Pathway Phase 2 — Resolution (Node 4): A final LLM call receives the extracted diagnosis (tone, urgency, issue type) and uses those specific variables as context to draft a highly tailored, empathetic apology and resolution strategy. The graph ends.
This architecture ensures that cheap, simple tasks (positive reviews) are handled quickly in one step, while complex, sensitive tasks (negative reviews) are broken down into logical, analytical steps before a final response is generated.
Demystifying the LangGraph Routing Demo
We will use LangGraph to orchestrate a dynamic workflow. We start with a customer review, use an LLM to extract its sentiment via structured output, and then use a conditional edge to route the graph to entirely different specialized responder nodes.
Here is the cell-by-cell breakdown of the code in Microsoft Fabric Notebook.

!pip install -q langgraph langchain-groq python-dotenv typing-extensions
# Import necessary libraries
from langgraph.graph import StateGraph, START, END
from langchain_groq import ChatGroq
from typing import TypedDict, Literal
from pydantic import BaseModel, Field
import operator
from IPython.display import Image
# Initialize the Groq model
model = ChatGroq(model='llama-3.3-70b-versatile', temperature=0)
First, we install our core dependencies. The langgraph library acts as the primary framework we use to build cyclical, stateful agent architectures, while langchain-groq serves as the integration package connecting us to Groq's cloud infrastructure. Groq uses specialized LPU (Language Processing Unit) hardware to serve LLMs at blazing-fast speeds, which is essential for multi-agent workflows where multiple LLM calls happen simultaneously. We import BaseModel and Field from Pydantic, which are critical for forcing the LLM to reply in a strict data format rather than rambling text. We also import TypedDict and Literal from the typing module to tightly structure our variables and memory limits. Finally, we instantiate our "Brain": the llama-3.3-70b-versatile model via Groq. Crucially, we set the temperature to 0 because we want highly deterministic, analytical categorization of our data, not creative fiction.

class SentimentSchema(BaseModel):
sentiment: Literal["positive", "negative"] = Field(description='Sentiment of the review')
class DiagnosisSchema(BaseModel):
issue_type: Literal["UX", "Performance", "Bug", "Support", "Other"] = Field(description='The category of issue mentioned in the review')
tone: Literal["angry", "frustrated", "disappointed", "calm"] = Field(description='The emotional tone expressed by the user')
urgency: Literal["low", "medium", "high"] = Field(description='How urgent or critical the issue appears to be')
# Groq supports with_structured_output using tool calling under the hood
structured_model = model.with_structured_output(SentimentSchema)
structured_model2 = model.with_structured_output(DiagnosisSchema)
class ReviewState(TypedDict):
review: str
sentiment: Literal["positive", "negative"]
diagnosis: dict
response: str
def find_sentiment(state: ReviewState):
prompt = f'For the following review find out the sentiment \n {state["review"]}'
sentiment = structured_model.invoke(prompt).sentiment
return {'sentiment': sentiment}
def check_sentiment(state: ReviewState) -> Literal["positive_response", "run_diagnosis"]:
if state['sentiment'] == 'positive':
return 'positive_response'
else:
return 'run_diagnosis'
def positive_response(state: ReviewState):
prompt = f"""Write a warm thank-you message in response to this review:
\n\n\"{state['review']}\"\nAlso, kindly ask the user to leave feedback on our website."""
response = model.invoke(prompt).content
return {'response': response}
def run_diagnosis(state: ReviewState):
prompt = f"""Diagnose this negative review:\n\n{state['review']}\nReturn issue_type, tone, and urgency."""
response = structured_model2.invoke(prompt)
return {'diagnosis': response.model_dump()}
def negative_response(state: ReviewState):
diagnosis = state['diagnosis']
prompt = f"""You are a support assistant.
The user had a '{diagnosis['issue_type']}' issue, sounded '{diagnosis['tone']}', and marked urgency as '{diagnosis['urgency']}'.
Write an empathetic, helpful resolution message."""
response = model.invoke(prompt).content
return {'response': response}
This cell is the architectural marvel of our application, establishing how our agents think, remember, and act. We need our LLM to act as a strict data parser, so we define a SentimentSchema that expects exactly one field for sentiment, using Literal["positive", "negative"] to tell the AI that it is only allowed to choose between those two exact strings. For negative reviews, we force the AI to output three specific fields (issue_type, tone, and urgency) via the DiagnosisSchema , and we wrap our Groq LLM in these strict rules using the .with_structured_output() method. We then define our agent's Short-Term Memory scratchpad to dictate exactly what our graph will remember by creating a ReviewState class that inherits from TypedDict. Next, we define functions to represent our specialized, narrow-focus agents : find_sentiment reads the raw review and updates the sentiment key; positive_response crafts a warm thank-you note using rich, unstructured conversational text; run_diagnosis uses structured_model2 to break down exactly why the user is mad and saves this rich metadata; and negative_response reads the rich diagnosis context to craft a highly tailored, empathetic resolution message. Finally, check_sentiment acts as our Python routing logic function. It looks at the sentiment saved in the state , returning 'positive_response' if positive or 'run_diagnosis' if negative, effectively acting as the switch tracks on our railway.


# Build Graph
graph = StateGraph(ReviewState)
graph.add_node('find_sentiment', find_sentiment)
graph.add_node('positive_response', positive_response)
graph.add_node('run_diagnosis', run_diagnosis)
graph.add_node('negative_response', negative_response)
graph.add_edge(START, 'find_sentiment')
graph.add_conditional_edges('find_sentiment', check_sentiment)
graph.add_edge('positive_response', END)
graph.add_edge('run_diagnosis', 'negative_response')
graph.add_edge('negative_response', END)
workflow = graph.compile()
# Display the graph
Image(workflow.get_graph().draw_mermaid_png())
We have our workers (nodes) and our memory (state), but right now, they are just isolated functions until this cell acts as the choreographer. We initialize the StateGraph and physically bind our ReviewState memory structure to it, meaning every node in this graph will now share this exact scratchpad. We use add_node to tell LangGraph about our Python functions, and then we draw the edges, ensuring that the absolute first step is always analyzing the sentiment via graph.add_edge(START, 'find_sentiment'). The line graph.add_conditional_edges('find_sentiment', check_sentiment) is revolutionary, telling LangGraph that once find_sentiment is done, it should not blindly go to the next node; instead, it runs the check_sentiment function and navigates to whichever node name that function returns. If routed to positive_response, the graph ends; if routed to run_diagnosis, the graph passes data to negative_response, and then ends. Finally, workflow.compile() fuses these rules into an executable application.

{'review': 'I’ve been trying to log in for over an hour now, and the app keeps freezing on the authentication screen. I even tried reinstalling it, but no luck. This kind of bug is unacceptable, especially when it affects basic functionality.', 'sentiment': 'negative', 'diagnosis': {'issue_type': 'Bug', 'tone': 'angry', 'urgency': 'high'}, 'response': "I'm so sorry to hear that you're experiencing a bug issue and that it's causing frustration for you. I can imagine how annoying it must be, and I'm here to help resolve the problem as quickly as possible.\n\nI've marked your issue as high priority, and I'm working on it immediately. I want to assure you that I'm committed to finding a solution and getting you back up and running smoothly.\n\nTo better understand the issue, could you please provide me with more details about the bug you're experiencing? This will help me to investigate and troubleshoot the problem more efficiently. Please include any error messages you've seen, the steps you took leading up to the issue, and any other relevant information.\n\nI appreciate your patience and cooperation, and I'm confident that we can resolve this issue together. If there's anything I can do to prevent similar issues in the future, I'll make sure to pass on your feedback to our development team.\n\nYou can expect a follow-up from me within the next [insert timeframe, e.g., 30 minutes] with an update on the status of your issue. If you have any further questions or concerns, please don't hesitate to reach out.\n\nThank you for bringing this to my attention, and I look forward to resolving the issue for you soon."}
# Run the workflow
initial_state = {
'review': """I’ve been trying to log in for over an hour now, and the app keeps freezing on the authentication screen.
I even tried reinstalling it, but no luck. This kind of bug is unacceptable, especially when it affects basic functionality."""
}
final_state = workflow.invoke(initial_state)
print(final_state)
This is the ignition switch where we load a highly critical user review into the initial_state dictionary. By calling workflow.invoke(initial_state), the system springs into action. First, find_sentiment reads the text and structurally outputs "negative". The conditional edge evaluates "negative" and routes the flow away from the happy path, sending it to run_diagnosis. Next, run_diagnosis deeply analyzes the text, determining the issue is a "Bug", the tone is "frustrated", and the urgency is "high", saving this to memory. Then, negative_response takes this exact context and crafts a deeply empathetic apology tailored to a high-urgency authentication bug. The resulting output printed to our terminal will be a highly structured, deeply analyzed evaluation.
Conclusion
We are no longer programming software by writing explicit lines of hardcoded logic; we are orchestrating intelligence by defining goals, providing toolsets, and designing cognitive architectures. The transition from standard prompting to agentic graphs is an evolution from software as a tool to software as an entity.
By mastering state management, structured JSON outputs, and conditional routing in LangGraph, we are learning the intricate dance of memory and planning required to build the autonomous synthetic workforces of the future. This specific demo proves that AI no longer has to treat all inputs the same. By granting an AI the ability to structure data and route its own execution path, we transition from generating text to fundamentally solving business logic autonomously.
Hey, I am Sandip Palit, from Kolkata, India. I love to explore what’s new in the Data Science space and share it with the community. I am a Fabric Super User, and in this Agentic AI using Microsoft Fabric Playlist, I will share my learnings and hands-on projects on Agentic AI.
Thank You for reading this article. Please feel free to share your thoughts in the comments section, and give this article a 🌟.
Join thousands of data leaders on the AI newsletter. Join over 80,000 subscribers and keep up to date with the latest developments in AI. From research to projects and ideas. If you are building an AI startup, an AI-related product, or a service, we invite you to consider becoming a sponsor.
Published via Towards AI
Towards AI Academy
We Build Enterprise-Grade AI. We'll Teach You to Master It Too.
15 engineers. 100,000+ students. Towards AI Academy teaches what actually survives production.
Start free — no commitment:
→ 6-Day Agentic AI Engineering Email Guide — one practical lesson per day
→ Agents Architecture Cheatsheet — 3 years of architecture decisions in 6 pages
Our courses:
→ AI Engineering Certification — 90+ lessons from project selection to deployed product. The most comprehensive practical LLM course out there.
→ Agent Engineering Course — Hands on with production agent architectures, memory, routing, and eval frameworks — built from real enterprise engagements.
→ AI for Work — Understand, evaluate, and apply AI for complex work tasks.
Note: Article content contains the views of the contributing authors and not Towards AI.