Learn how to evaluate LangGraph agents with Ragas. Step-by-step guide covering Faithfulness, Answer Relevancy, Context Precision, the bridge function, common errors, and agent-specific metrics.
Table of Contents
Why Your LangGraph Agent Needs an Inspector
What is Ragas?
The RAG Triad: The Three Things Every Agent Must Get Right
The Problem: LangGraph State vs Ragas Format
Step-by-Step: Building a Simple Agent with Evaluation
The Bridge: Converting LangGraph State to Ragas Format
Running Your First Evaluation
Common Beginner Mistakes and Fixes
Agent-Specific Metrics (Beyond Basic RAG)
Production Tips
Limitations of Ragas
FAQs
The Bottom Line
1. Why Your LangGraph Agent Needs an Inspector ?
You’ve built your LangGraph agent. It’s working. But how do you know if it’s actually working well? How do you know if it’s hallucinating, retrieving the wrong documents, or giving answers that don’t even answer the question?
This is where Ragas comes in. Think of Ragas as an inspector who grades your agent’s work.
The Detective Analogy :
Remember the detective from the previous article? The one with the messy desk and the assistant who hands them the right files?
Now imagine you have a supervisor (the Inspector) who grades the detective’s work:
Did you open the right books? → Context Precision
Did you make stuff up? → Faithfulness
Did you actually answer what was asked? → Answer Relevancy
Why Manual Grading Doesn’t Scale ?
You could manually check 10 conversations. But when your agent handles 1,000 conversations a day, you need automated evaluation.
Ragas gives you a quantitative “scorecard” from 0 to 1 for every metric. You can track improvements over time, compare different agent configurations, and catch failures before they reach production.
2. What is Ragas?
Ragas (Retrieval-Augmented Generation Assessment) is an open-source Python framework that scores RAG behavior using LLM-as-a-judge metrics.
The Simple Explanation :
Ragas doesn’t know the right answer. Instead, it uses an LLM (like GPT-4 or a local model) to judge your agent’s output against following key metrics:
What Ragas Measures:
| Metric | What It Checks | The Question It Answers |
|---|---|---|
| Faithfulness | Are claims supported by context? | “Did you make stuff up?” |
| Answer Relevancy | Does the answer address the question? | “Did you actually answer what was asked?” |
| Context Precision | Are retrieved chunks relevant? | “Did you open the right books?” |
| Context Recall | Did you retrieve all needed info? | “Did you miss anything important?” |
The Big Three :
For most LangGraph agents, start with these three metrics:
1. Faithfulness catches hallucinations. It checks if every claim in your agent’s answer is supported by the retrieved documents. Score below 0.7 means your agent is making things up.
2. Answer Relevancy catches dodging. It checks if the answer actually addresses the question—even if it’s factually correct. Score below 0.7 means your agent is off-topic.
3. Context Precision catches noise. It checks if your retrieval node is pulling relevant documents or just filling the context with junk.
3. The RAG Triad: The Three Things Every Agent Must Get Right
Here’s the visual anchor that ties everything together:
[ Question ]
/ \
Answer Relevancy Context Precision
/ \
[ Answer ] -------- [ Context ]
FaithfulnessUnderstanding Each Metric :
Faithfulness (Groundedness) -> “Did you make stuff up?”
How it works: Your agent’s answer is broken down into claims. Each claim is checked against the retrieved context. If a claim is not in the context, it’s a hallucination.
Score Interpretation:
>0.85: Your agent is trustworthy
0.70-0.85: Some hallucinations, needs work
<0.70: Your agent is hallucinating badly. Fix retrieval or prompt engineering
Answer Relevancy ->“Did you actually answer the question?”
How it works: The answer is compared to the original question. If the answer addresses the question, score is high. If it’s off-topic or generic, score is low.
Score Interpretation:
>0.85: Answers are on-point
0.70-0.85: Sometimes misses the mark
<0.70: Agent is dodging questions or going off on tangents
Context Precision -> “Did you retrieve the right documents?”
How it works: Retrieval results are checked. Are the retrieved chunks relevant to the query? Precision measures if the top-ranked chunks are the right ones.
Score Interpretation:
>0.85: Retrieval is solid
0.70-0.85: Some irrelevant chunks are getting through
<0.70: Retrieval is noisy. Fix your embeddings or chunking strategy
4. The Problem: LangGraph State vs Ragas Format
This is where every beginner gets stuck.
What LangGraph Gives You
LangGraph stores everything in a State object with messages (a list of BaseMessage objects):
class AgentState(TypedDict): messages: List[BaseMessage] # All conversation history # ... your custom fields
What Ragas Expects :
Ragas expects a flat dictionary with specific fields:
{ "user_input": ["What is LangGraph?"], # List of strings "retrieved_contexts": [["doc1", "doc2"]], # List of lists of strings "response": ["LangGraph is a framework..."], # List of strings "reference": ["LangGraph is used for stateful LLM apps."] # Optional }
The Mismatch
LangGraph gives you messages. Ragas needs user_input, response, and contexts. You need to bridge the two.
This is where beginners pull their hair out. Without the bridge, you get errors about missing columns or wrong data types.
5. Step-by-Step: Building a Simple Agent with Evaluation
Let’s build a simple LangGraph agent and evaluate it with Ragas.
Prerequisites
pip install langgraph langchain-openai ragas datasetsStep 1: Define Your State
from typing import List, TypedDict from langchain_core.messages import BaseMessage class GraphState(TypedDict): question: str # User's original question contexts: List[str] # Retrieved documents messages: List[BaseMessage] # All conversation history
Step 2: Define Your Nodes
from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) def retrieval_node(state: GraphState): """Simulates retrieving documents from a vector database.""" question = state["question"] # Mock retrieval - in production, this would query a vector DB if "langgraph" in question.lower(): docs = ["LangGraph is a library for building stateful, multi-actor LLM applications."] else: docs = ["Random document that doesn't answer the question."] return {"contexts": docs} def generation_node(state: GraphState): """Generates the final answer using the retrieved context.""" question = state["question"] contexts = "\n".join(state["contexts"]) prompt = f"Answer the question: '{question}' using ONLY this context:\n{contexts}" response = llm.invoke([HumanMessage(content=prompt)]) return {"messages": [response]}
Step 3: Build the Graph
from langgraph.graph import StateGraph, START, END workflow = StateGraph(GraphState) workflow.add_node("retrieve", retrieval_node) workflow.add_node("generate", generation_node) workflow.add_edge(START, "retrieve") workflow.add_edge("retrieve", "generate") workflow.add_edge("generate", END) graph = workflow.compile()
6. The Bridge: Converting LangGraph State to Ragas Format
This is the most important function you’ll write. It extracts data from your LangGraph state and formats it for Ragas.
What Exactly Is the “Bridge”?
Think of LangGraph and Ragas as two different countries that speak different languages.
- LangGraph speaks in State and Messages. Everything lives inside a state object: a list of HumanMessage, AIMessage, and ToolMessage objects, plus any custom fields you added (like contexts or question).
- Ragas speaks in a flat evaluation dictionary. It expects simple lists of strings with very specific keys: user_input, response, retrieved_contexts, and optionally reference.
The bridge is the small piece of code that translates LangGraph’s language into Ragas’ language.
Without this translation, Ragas simply cannot understand your agent’s output. You’ll get errors about missing columns, wrong data types, or mysterious MESSAGE_COERCION_FAILURE messages.
Why Do You Need It?
LangGraph is designed for running agents. It carefully tracks conversation history, tool calls, and intermediate state so the agent can continue reasoning.
Ragas is designed for grading agents. It doesn’t care about the full conversation history or the internal message objects. It only needs four clean pieces of information:
- What did the user ask? (user_input)
- What documents did the agent retrieve? (retrieved_contexts)
- What final answer did the agent give? (response)
- (Optional) What is the correct answer? (reference)
Because these two systems have completely different goals, you must convert the rich LangGraph state into the simple format Ragas expects.
When Do You Need the Bridge?
You need the bridge every time you want to run a Ragas evaluation on a LangGraph agent. Typical situations:
- After the agent finishes a single turn and you want to score it
- After a full multi-turn conversation (you evaluate the final answer against the original question)
- When you are building an automated evaluation pipeline that runs after every code change
- When you want to compare two different versions of the same agent
You do not need the bridge if you are only using Ragas with a simple LangChain chain (no LangGraph state).
How to Use It in LangGraph ?
Option 1: The Official Ragas Helper (Recommended)
Ragas 2026 has built-in support for LangGraph via convert_to_ragas_messages:
from ragas.integrations.langgraph import convert_to_ragas_messages # Run your graph final_state = graph.invoke({"question": "What is LangGraph?", "contexts": [], "messages": []}) # Convert using official helper ragas_data = convert_to_ragas_messages( messages=final_state["messages"], user_input=final_state["question"], contexts=final_state["contexts"] ) # Now you have a format Ragas understands!
Option 2: The Custom Bridge (Full Control)
If you need more control or want to understand what’s happening under the hood:
def convert_langgraph_to_ragas(final_state: dict, original_question: str, ground_truth: str = None) -> dict: """ Converts LangGraph state to Ragas evaluation format. This is the bridge that every beginner needs. """ # Extract the final answer from the last message messages = final_state.get("messages", []) if messages: # IMPORTANT: Extract .content from BaseMessage objects final_answer = messages[-1].content if hasattr(messages[-1], 'content') else str(messages[-1]) else: final_answer = "" # Extract retrieved contexts (guarantee they're strings) contexts = final_state.get("contexts", []) # Ensure each context is a string contexts = [str(c) for c in contexts] # Format for Ragas eval_dict = { "user_input": [original_question], "retrieved_contexts": [contexts], # List of lists "response": [final_answer], } if ground_truth: eval_dict["reference"] = [ground_truth] return eval_dict
Using the Bridge
# Run your graph initial_state = {"question": "What is LangGraph?", "contexts": [], "messages": []} final_state = graph.invoke(initial_state) # Convert to Ragas format eval_data = convert_langgraph_to_ragas( final_state=final_state, original_question="What is LangGraph?", ground_truth="LangGraph is a library for building stateful, multi-actor LLM applications." ) # Create dataset from datasets import Dataset ragas_dataset = Dataset.from_dict(eval_data)
Important: The contexts Must Be Strings!
If your contexts are lists of dictionaries, the Dataset.from_dict() will fail with a PyArrow schema error. Use this to fix it:
# If contexts are documents with .page_content contexts = [doc.page_content for doc in retrieved_docs] # If contexts are anything else contexts = [str(c) for c in contexts]
7. Running Your First Evaluation
Now that you have your data in the right format, let’s run the evaluation.
Step 1: Set Up the Judge Model
Ragas uses an LLM to judge your agent’s output. By default, it uses OpenAI. You can customize it:
from ragas.llms import LangchainLLMWrapper from langchain_openai import ChatOpenAI # Option 1: Use GPT-4o-mini (recommended for quality) judge_llm = LangchainLLMWrapper(ChatOpenAI(model="gpt-4o-mini")) # Option 2: Use a larger model for better scoring judge_llm = LangchainLLMWrapper(ChatOpenAI(model="gpt-4o")) # Option 3: Use a local model (less reliable, but cheaper) # from langchain_community.llms import Ollama # judge_llm = LangchainLLMWrapper(Ollama(model="llama3:70b"))
Step 2: Run Evaluation
from ragas import evaluate from ragas.metrics import faithfulness, answer_relevancy, context_precision metrics = [faithfulness, answer_relevancy, context_precision] scores = evaluate( dataset=ragas_dataset, metrics=metrics, llm=judge_llm # Optional: override the default judge ) print(scores.to_pandas())
Example Output:
user_input faithfulness answer_relevancy context_precision 0 What is LangGraph? 0.95 0.88 0.90
Interpreting Your Scores :
| Score | Faithfulness | Answer Relevancy | Context Precision |
|---|---|---|---|
| 🟢 Good | >0.85 | >0.85 | >0.85 |
| 🟡 Acceptable | 0.70-0.85 | 0.70-0.85 | 0.70-0.85 |
| 🔴 Needs work | <0.70 | <0.70 | <0.70 |
8. Common Beginner Mistakes and Fixes :
Mistake 1: Forgetting to Save Contexts in State
The Problem: Your retrieval node doesn’t save contexts to the state, so Ragas has nothing to evaluate.
The Fix:
def retrieval_node(state: GraphState): # ... retrieve documents return {"contexts": retrieved_docs} # SAVE IT!
Mistake 2: Wrong Data Format for Ragas
The Problem: Ragas expects retrieved_contexts as a list of lists of strings. You pass a list of strings.
The Fix:
# ❌ Wrong ragas_input = { "retrieved_contexts": ["doc1", "doc2"] # List of strings } # ✅ Correct ragas_input = { "retrieved_contexts": [["doc1", "doc2"]] # List of lists }
Mistake 3: Evaluating with Local LLMs
The Problem: Ragas uses LLM-as-a-judge. Small local models often return NaN scores because they can’t follow the complex evaluation prompt.
The Fix: Use a larger model for evaluation. GPT-4o-mini works well.
from ragas.llms import LangchainLLMWrapper from langchain_openai import ChatOpenAI judge_llm = LangchainLLMWrapper(ChatOpenAI(model="gpt-4o-mini")) scores = evaluate( dataset=ragas_dataset, metrics=[faithfulness, answer_relevancy], llm=judge_llm # Override the default judge )
Mistake 4: Not Using Ground Truth
The Problem: You skip reference (ground truth) but want to measure context precision.
The Fix: Context Precision requires ground truth. If you don’t have it, stick to Faithfulness and Answer Relevancy.
# Faithfulness and Answer Relevancy don't need ground truth metrics = [faithfulness, answer_relevancy] # Context Precision needs reference metrics = [faithfulness, answer_relevancy, context_precision] # Then include "reference" in your eval data
Mistake 5: MESSAGE_COERCION_FAILURE (The Most Common Error)
The Problem: You pass a BaseMessage object (like HumanMessage or AIMessage) to Ragas instead of its .content string.
The Fix: Always extract .content before passing to Ragas.
# ❌ Wrong: Passing BaseMessage object final_answer = state["messages"][-1] # This is a BaseMessage object # ✅ Correct: Passing .content string final_answer = state["messages"][-1].content # This is the actual text # What the error looks like: # ValueError: Message dict must contain 'role' and 'content' keys # The error happens because Ragas sees the BaseMessage object and tries to serialize it
How to fix it in your bridge function:
def convert_langgraph_to_ragas(final_state: dict, original_question: str) -> dict: messages = final_state.get("messages", []) if messages: # ✅ ALWAYS extract .content final_answer = messages[-1].content if hasattr(messages[-1], 'content') else str(messages[-1]) else: final_answer = "" # ... rest of function
Mistake 6: PyArrow Schema Error with Dataset.from_dict()
The Problem: Dataset.from_dict() fails because retrieved_contexts contains elements that aren’t strings.
# ❌ Wrong: contexts is a list of dictionaries contexts = [{"text": "doc1", "score": 0.9}, {"text": "doc2", "score": 0.8}] ragas_input = {"retrieved_contexts": [contexts]} # PyArrow error! # ✅ Correct: contexts is a list of strings contexts = ["doc1", "doc2"] ragas_input = {"retrieved_contexts": [contexts]} # Works!
Fix it:
# Convert documents to strings contexts = [doc.page_content for doc in retrieved_docs] # or contexts = [str(doc) for doc in retrieved_docs]
9. Agent-Specific Metrics (Beyond Basic RAG)
Ragas 2026 now supports metrics specifically for full agents (tool-using, multi-turn):
Tool Call Accuracy
What it checks: Did your agent call the right tools at the right time?
When to use: When your agent has multiple tools and needs to select the correct one.
Agent Goal Accuracy
What it checks: Did your agent achieve the user’s stated goal?
When to use: For task-oriented agents (e.g., “Book a flight” or “Order a pizza”).
Topic Adherence
What it checks: Did your agent stay on topic or go off on tangents?
When to use: For customer support or domain-specific agents.
Multi-Turn Evaluation (MultiTurnSample)
What it checks: How does your agent perform across a conversation with multiple turns?
When to use: For conversational agents that need to remember context across exchanges.
10. Production Tips :
Tip 1: Build a Quality Test Dataset
For reliable evaluation, build at least 100-200 curated examples with ground truth. Focus on edge cases and common failure modes.
Tip 2: Use a Real Vector Database
In production, replace the mock retrieval with a real vector store:
# Example with Chroma from langchain_chroma import Chroma from langchain_openai import OpenAIEmbeddings embeddings = OpenAIEmbeddings() vector_store = Chroma(collection_name="docs", embedding_function=embeddings) def retrieval_node(state: GraphState): docs = vector_store.similarity_search(state["question"], k=5) contexts = [doc.page_content for doc in docs] return {"contexts": contexts}
Tip 3: Track Metrics Over Time
import pandas as pd def track_evaluation(scores): """Store scores for trend analysis.""" df = pd.DataFrame(scores) df["timestamp"] = pd.Timestamp.now() df.to_csv("eval_history.csv", mode="a", header=False)
Tip 4: Set Quality Thresholds
# Alert if your agent is hallucinating too much if scores["faithfulness"] < 0.8: print("⚠️ Agent faithfulness dropped below 0.8! Needs attention.")
Tip 5: Handle Multi-Turn and Agentic Behavior
Original query vs rewritten query: Evaluate against the original user query
Multiple retrieval passes: Combine all retrieved contexts into one list
Agent refusal: Don’t penalize refusals if they’re correct behavior
11. Limitations of Ragas :
Ragas Uses LLMs as Judges
Ragas relies on LLMs to evaluate your agent’s output. This has three significant costs:
Cost: Running a judge model (even GPT-4o-mini) across 100 test cases costs money
Latency: Each metric requires multiple LLM calls, adding seconds to your evaluation
Bias: The judge model’s preferences can influence scores
Local Small Models Are Unreliable
Small local models (<7B parameters) often produce NaN scores or inconsistent results. For reliable scoring, use GPT-4o-mini or larger.
Timeliness
Ragas scores can vary between runs with the same dataset. This is inherent to LLM-as-judge evaluation. To minimize this:
Use temperature=0
Run multiple times and average scores
Focus on trends rather than absolute numbers
Dataset Requirements
Ragas works best with:
50+ examples for meaningful signal (100-200 recommended)
Diverse examples covering edge cases, not just easy ones
Ground truth for Context Precision and Recall metrics
12. FAQs :
What’s the difference between Faithfulness and Answer Relevancy?
Faithfulness catches hallucinations (“Did you make stuff up?”). Answer Relevancy catches dodging (“Did you actually answer the question?”). An answer can be faithful to the context but completely irrelevant to the question.
Do I need ground truth for Ragas?
Not for Faithfulness and Answer Relevancy. Yes for Context Precision and Context Recall.
Why are my Ragas scores NaN?
You’re likely using a small local model as the judge. Use GPT-4o-mini or larger for evaluation.
What’s the correct format for retrieved_contexts?
retrieved_contexts must be a list of lists of strings. Each inner list is the contexts for one example.
# ✅ Correct format { "retrieved_contexts": [ ["doc1", "doc2"], # Example 1's contexts ["doc3", "doc4"] # Example 2's contexts ] }
How do I handle agent loops in Ragas?
Run your agent to completion, then evaluate the final state with the original user question. Combine all retrieved contexts from each loop into a single list.
What’s a good score?
| Metric | Good | Acceptable | Needs Work |
|---|---|---|---|
| Faithfulness | >0.85 | 0.70-0.85 | <0.70 |
| Answer Relevancy | >0.85 | 0.70-0.85 | <0.70 |
| Context Precision | >0.85 | 0.70-0.85 | <0.70 |
| Context Recall | >0.85 | 0.70-0.85 | <0.70 |
Can I use Ragas with local LLMs?
Yes, but use a capable model like Mixtral or Llama-3-70B. Small models (<7B) often produce unreliable scores.
What’s MESSAGE_COERCION_FAILURE?
It happens when you pass a BaseMessage object (like HumanMessage) instead of its .content string. Always extract .content before passing to Ragas.
Why is my Dataset.from_dict() failing?
Make sure retrieved_contexts is a list of lists of strings. If your contexts are documents, extract .page_content first.
13. The Bottom Line :
Your LangGraph agent needs an inspector. Ragas is that inspector.
The Bridge Function is Everything: LangGraph gives you messages, Ragas needs user_input, response, and contexts. Write the bridge function once and reuse it everywhere.
The Three Key Metrics: Faithfulness (hallucinations), Answer Relevancy (dodging), Context Precision (noise). Master these three and you’ll catch 90% of failures.
The Golden Rules:
Always extract
.contentfrom BaseMessage objectsAlways convert contexts to strings
Always wrap
retrieved_contextsin an extra listUse GPT-4o-mini as the judge for reliable scores
Ragas turns your LangGraph agent from “it feels like it works” into “I can prove it works.”
