Skip to content
Module 5 · Advanced AI EngineeringAdvanced4 min read · 890 words

Lesson 17: LangGraph & Corrective RAG (Concepts)

"Lesson 08e showed you the code line by line. This lesson steps back and asks why a graph, specifically, was the right tool."

Why Not Just an if Statement?

The entire Corrective RAG pipeline could, in principle, be written as one function with an if:

python
def answer_question(question, agent_id, top_k):
    chunks = search_agent_documents(agent_id, question, top_k)
    if not chunks:
        return "I could not find this information in the uploaded documents."
    evaluation = evaluate_retrieved_context(question, chunks)
    if not evaluation["has_answer"]:
        return "I could not find this information in the uploaded documents."
    return generate_rag_answer(question, chunks, ...)

This would work, and would be shorter than rag_graph_service.py. So why does this codebase use LangGraph's StateGraph instead? Three concrete reasons visible in the actual code:

  1. The state is explicit and typed. RAGGraphState names every piece of data that flows through the pipeline (retrieved_chunks, retrieval_evaluation, route, sources, answer) as a TypedDict. A plain function accumulates the same information in local variables with no single place documenting what exists at each stage.
  2. The branch is a first-class object, not buried logic. route_after_evaluation and add_conditional_edges(...) make the "answer or refuse" decision a named, inspectable part of the graph structure — you can list the graph's nodes and edges without reading every line of a function body.
  3. It's extendable without rewriting control flow. Adding a fifth node (see the exercise in Lesson 08e) means adding a node and an edge, not restructuring nested if statements.

The trade-off: for a 4-node graph this is arguably more machinery than the problem needs. LangGraph earns its complexity as graphs grow branchier — 3+ possible routes, retries, loops back to earlier nodes. This codebase's graph is close to the size where a plain function would still be reasonable; it's a good teaching example specifically because it's small enough to hold in your head while still using every core LangGraph concept.

The Four LangGraph Concepts This Codebase Actually Uses

ConceptWhere
StateGraph(SomeTypedDict)build_corrective_rag_graph() — the container for nodes and edges
add_node(name, fn)Registers retrieve_node, evaluate_node, answer_node, refusal_node
add_edge(a, b)Unconditional transitions: START → retrieve_node, retrieve_node → evaluate_node, both terminal nodes → END
add_conditional_edges(source, router_fn, mapping)The one branch point: evaluate_node → (answer_node | refusal_node)

LangGraph has many more capabilities this codebase doesn't touch — cycles/loops, persistent checkpointing across calls, human-in-the-loop interrupts, parallel node execution. None of that is here. .invoke() runs the graph once, synchronously, start to finish, in a single request — that's the entire runtime model in use.

What "Corrective" Actually Buys You

Compare two failure scenarios for a question with no good answer in the uploaded documents:

  • Plain RAG: retrieve (gets 3 irrelevant chunks) → generate (LLM does its best with bad context) → a fluent, wrong-sounding answer, presented with the same confidence as a correct one.
  • Corrective RAG (this codebase): retrieve (gets 3 irrelevant chunks) → evaluate (LLM says has_answer: false) → refuse, with an explicit, honest "I could not find this information."

The evaluator step costs one extra LLM call on every question, but it converts a category of confident-wrong answers into honest refusals. Whether that trade is worth it depends entirely on how costly a hallucinated answer is in your product — for this codebase (an internal agent answering questions about uploaded business documents), refusing is clearly the safer default.

Where This Sits Relative to the Rest of the Codebase

This is the only graph-based orchestration in the backend. Contrast:

  • Multi-agent email workflow (Lesson 12) — sequential function calls, no branching, no graph.
  • Corrective RAG (this lesson, Lesson 08e) — a real StateGraph with one conditional branch.

If you're asked "does this backend use LangGraph?" in an interview setting, the accurate answer is: yes, specifically for the document-question-answering path, and nowhere else.

Common Mistakes

  • Assuming every "agent" or "workflow" in this codebase runs through a graph. Only one does.
  • Assuming more nodes/edges automatically means better engineering. For a 4-node linear-with-one-branch graph, the value is documentation and extendability, not raw necessity.
  • Confusing "conditional edge" with "the LLM decides." The routing function (route_after_evaluation) is plain Python reading a state key — the LLM's judgment (has_answer) was already captured into that state key by evaluate_node one step earlier.

Exercises

  1. Without looking at Lesson 08e, draw the graph from memory using only this lesson's description of its nodes and one conditional edge. Then compare against the actual build_corrective_rag_graph() code.
  2. Rewrite run_corrective_rag_graph as a plain function (no LangGraph) that produces identical output for identical input. Time how long each version takes you to write, and note which one would be easier to extend with a fifth node.
  3. Identify one other place in this backend where a conditional, multi-step process happens (hint: the multi-agent email workflow's try/except with three sequential agent calls) and argue for or against converting it to a StateGraph.

Key Takeaways

🎯 LangGraph's value here is explicit, typed state and a first-class conditional branch — not raw necessity for a graph this small. 🎯 The Corrective RAG graph trades one extra LLM call for converting hallucinations into honest refusals. 🎯 This is the only LangGraph-orchestrated flow in the entire backend.

Next Steps

👉 Lesson 18: Background Tasks →


Module 5 · Advanced AI Engineering