cd /news/artificial-intelligence/from-rag-to-agentic-ai-how-i-added-l… Β· home β€Ί topics β€Ί artificial-intelligence β€Ί article
[ARTICLE Β· art-79377] src=dev.to β†— pub= topic=artificial-intelligence verified=true sentiment=Β· neutral

From RAG to Agentic AI. How I Added LangGraph to My Local

A developer evolved a local RAG assistant into an agentic AI architecture using LangGraph, adding a classifier that routes questions to specialized agents for procedural queries, error code lookup, or escalation. The system uses a shared state with a 'niveau_support' field to communicate urgency between agents.

read7 min views1 publishedJul 29, 2026

In my previous article, I built a fully local RAG assistant Ollama, ChromaDB, LangChain, all running in Docker. It answered technical support questions by searching through documentation and citing sources. It worked.

But after using it for a while, I noticed something uncomfortable: it treated every question the same way.

Ask it "how to close monthly payroll?" it searches the docs. Fine.

Ask it "the server crashes at startup" it also searches the docs. Less fine.

Ask it something completely outside the documentation it searches the docs. Useless.

A real support technician doesn't do that. They first assess the situation, then decide what to do: look it up, run a diagnosis, or escalate to a human. My RAG had no such judgment.

That's what this article is about how I evolved the system into an Agentic AI architecture using LangGraph, where the assistant first decides which strategy to use, then acts accordingly.

Classic RAG is a linear pipeline. Every query follows the exact same path:

Question β†’ Embed β†’ Retrieve β†’ Prompt β†’ LLM β†’ Answer

No branching. No decision-making. No memory between steps.

This works perfectly for procedural questions where the answer lives in the docs. But technical support involves at least three distinct scenarios:

Scenario Example Best strategy
Procedural question "How do I create an account?" Search documentation
Known error code "ERR-COMP-001 appears" Lookup error database
Unknown incident "Server crashes, no idea why" Diagnose + escalate if needed

A single RAG pipeline handles the first case well and the other two poorly. The solution is to add a layer of reasoning before retrieval.

The shift from RAG to Agentic AI comes down to one thing: the system plans before it acts.

Instead of one fixed pipeline, you have:

Question
    ↓
Classifier (what kind of question is this?)
    ↓
    β”œβ”€β”€ Procedural β†’ RAG Agent (search docs)
    β”œβ”€β”€ Error code β†’ Diagnostic Agent (lookup + LLM analysis)
    └── Complex    β†’ Diagnostic Agent β†’ Escalation Agent (generate ticket)

Each branch is an agent; an autonomous function that receives a shared state, does its job, updates the state, and returns it. LangGraph connects these agents into a directed graph with conditional routing between them.

Before building agents, you define the state they all share. Think of it as a case file that every agent reads and updates:


from typing import TypedDict, List, Annotated, Sequence
from langchain_core.messages import BaseMessage
import operator

class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]
    question: str          # Original question never changes
    plan: List[str]
    current_step: int
    past_steps: List[str]  # Audit trail of what each agent did
    response: str
    next_agent: str
    niveau_support: int    # 1=resolved, 2=partial, 3=needs human

The niveau_support

field is key it's how agents communicate urgency to each other. If an agent sets it to 3, the orchestrator escalates automatically.

Annotated[Sequence[BaseMessage], operator.add]

means messages accumulate β€” each agent appends rather than overwrites. You get a full conversation trace at the end.

The pipeline from my previous article, repackaged as an autonomous agent:


def rag_agent(state: AgentState) -> AgentState:
    """Searches the documentation and generates a grounded answer."""

    docs = vectorstore.similarity_search(state["question"], k=3)

    if not docs:
        return {
            **state,
            "messages": list(state["messages"]) + [
                AIMessage(content="[RAG] No relevant document found.")
            ],
            "past_steps": state["past_steps"] + ["RAG: no results"],
            "niveau_support": 3   # Nothing found β€” escalate
        }

    context = "\n\n".join([
        f"[Source: {d.metadata.get('module', '?')}]\n{d.page_content}"
        for d in docs
    ])

    answer = llm.invoke(
        f"Answer from context only.\nContext: {context}\nQuestion: {state['question']}"
    )

    return {
        **state,
        "messages": list(state["messages"]) + [AIMessage(content=f"[RAG] {answer}")],
        "past_steps": state["past_steps"] + ["RAG: documentation search completed"],
        "niveau_support": 1
    }

The critical difference from the classic RAG: the agent returns an updated state, not just an answer. Every agent follows this same pattern β€” receive state, do work, return updated state.

Handles error codes and technical incidents with two modes:


KNOWN_ERRORS = {
    "ERR-COMP-001": {
        "description": "Debit/credit imbalance",
        "solution": "Check Total Debit = Total Credit. Go to Accounting > Journal Entry."
    },
    "ERR-PAIE-042": {
        "description": "Payroll line missing rate",
        "solution": "Go to Settings > Payroll Lines, fill in the missing rate."
    },
}

def diagnostic_agent(state: AgentState) -> AgentState:
    question = state["question"]
    found_codes = [code for code in KNOWN_ERRORS if code.lower() in question.lower()]

    if found_codes:
        responses = [
            f"Code {code}: {KNOWN_ERRORS[code]['description']}\n"
            f"Solution: {KNOWN_ERRORS[code]['solution']}"
            for code in found_codes
        ]
        return {
            **state,
            "messages": list(state["messages"]) + [
                AIMessage(content=f"[Diagnostic] {chr(10).join(responses)}")
            ],
            "past_steps": state["past_steps"] + ["Diagnostic: known error, resolved"],
            "niveau_support": 1
        }

    answer = llm.invoke(
        f"Analyze this technical issue. If unsure, say so.\nIssue: {question}"
    )
    return {
        **state,
        "messages": list(state["messages"]) + [AIMessage(content=f"[Diagnostic] {answer}")],
        "past_steps": state["past_steps"] + ["Diagnostic: LLM analysis, uncertain"],
        "niveau_support": 2
    }

The known error lookup runs in milliseconds without touching the LLM; a meaningful performance gain for common incidents.

Called only when niveau_support >= 3

. It doesn't try to answer; it generates a structured ticket with the full diagnostic history already attached:


def escalade_agent(state: AgentState) -> AgentState:
    ticket = f"""
=== SUPPORT TICKET β€” LEVEL 3 ===
Date     : {datetime.now().strftime("%Y-%m-%d %H:%M")}
Priority : HIGH

ISSUE:
{state["question"]}

DIAGNOSTIC STEPS ALREADY TAKEN:
{chr(10).join(state["past_steps"])}

CONCLUSION: Human intervention required.
================================"""

    return {
        **state,
        "messages": list(state["messages"]) + [AIMessage(content=ticket)],
        "past_steps": state["past_steps"] + ["Escalation: level 3 ticket generated"],
        "niveau_support": 3
    }

This is what separates a real support system from a chatbot; it knows when to stop and hand off, with context already attached so the user doesn't have to repeat themselves.


def classifier(state: AgentState) -> str:
    question = state["question"].lower()

    if re.search(r'err-\w+-\d+', question):
        return "diagnostic"

    problem_words = ["error", "crash", "bug", "not working", "fails", "problem"]
    if any(word in question for word in problem_words):
        return "diagnostic"

    return "rag"  # Default for procedural questions

def should_escalate(state: AgentState) -> str:
    return "escalade" if state.get("niveau_support", 1) >= 3 else END

def create_orchestrator():
    workflow = StateGraph(AgentState)

    workflow.add_node("rag_agent", rag_agent)
    workflow.add_node("diagnostic_agent", diagnostic_agent)
    workflow.add_node("escalade_agent", escalade_agent)

    workflow.set_conditional_entry_point(
        classifier,
        {"rag": "rag_agent", "diagnostic": "diagnostic_agent"}
    )

    workflow.add_edge("rag_agent", END)
    workflow.add_conditional_edges(
        "diagnostic_agent",
        should_escalate,
        {"escalade": "escalade_agent", END: END}
    )
    workflow.add_edge("escalade_agent", END)

    return workflow.compile()

Three questions, three different paths:

Procedural question

Input  : "How to close monthly payroll?"
Path   : classifier β†’ rag_agent β†’ END
Output : agents_used: ["RAG: documentation search completed"]
         niveau: 1

Known error code

Input  : "ERR-COMP-001 appears on journal validation"
Path   : classifier β†’ diagnostic_agent β†’ END
Output : agents_used: ["Diagnostic: known error, resolved"]
         niveau: 1  (no LLM call, instant response)

Unknown incident

Input  : "Server crashes at startup, no idea why"
Path   : classifier β†’ diagnostic_agent β†’ escalade_agent β†’ END
Output : agents_used: ["Diagnostic: LLM analysis", "Escalation: ticket generated"]
         niveau: 3

The classifier is fragile. Keyword matching breaks on edge cases "I don't have a problem, just need help configuring payroll" contains "problem" and routes to diagnostic instead of RAG. A small LLM classification call, or a fine-tuned classifier, would be more robust.

Agents reload the embedding model on every call. Instantiating HuggingFaceEmbeddings

on each agent invocation adds unnecessary overhead. Initialize once at startup and pass the instance through the state.

The escalation threshold is hardcoded. A feedback loop where user ratings influence the confidence threshold over time would make this genuinely adaptive that's what reinforcement learning adds in the next phase of this project.

Classic RAG Agentic AI
Best for
Uniform question types Mixed question types
Complexity
Low Medium-High
Latency
One LLM call One or more LLM calls
Flexibility
Fixed pipeline Extensible β€” add agents without rewriting
When to choose
Start here When RAG fails on edge cases

My honest take: start with RAG. Get it working, understand its failure modes, then add agents where you actually need branching logic. Adding LangGraph too early is over-engineering.

Design your AgentState first. It's the contract between all components. Changing it mid-project means updating every agent get it right upfront.

Agents should do one thing. The moment an agent tries to retrieve and diagnose, it becomes untestable and unroutable. Keep them single-responsibility.

** past_steps is your audit trail.** When a ticket escalates to a human, they see exactly what the system tried. Small design choice, professional result.

The classifier is your most important and most brittle piece of code. Test it with as many edge cases as you can before trusting it.

These two articles cover Phases 1 and 2 of my internship project. The logical next step is a reinforcement learning feedback loop; user ratings adjusting the classifier's routing decisions over time; which I may write about once it's implemented and tested.

I'm a Master's student in AI & Big Data based, building these systems with open-source tools and no GPU. If you're working on multi-agent systems or ran into similar design decisions; especially around the classifier or state design; I'd love to hear your approach in the comments.

── more in #artificial-intelligence 4 stories Β· sorted by recency
── more on @langgraph 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/from-rag-to-agentic-…] indexed:0 read:7min 2026-07-29 Β· β€”