Ragas developer on retainer: ragas.evaluate() faithfulness 0.87 on 200 GPT-4o-generated test questions — same GPT-4o model generates answers and evaluates them via ragas.llms.LangchainLLM — circular evaluation inflates score — test set drawn from 15% of document corpus — production faithfulness 0.61 on real user queries — Ragas developer on monthly retainer
ragas 0.1.14 • langchain 0.2.14 • Python 3.11 • GPT-4o generator • GPT-4o evaluator (same model) • 200 synthetic test questions • test set covers 15% of corpus • faithfulness 0.87 synthetic • production user satisfaction 54% • Claude-3.5-Sonnet evaluator on 100 real queries: faithfulness 0.61 • fix: independent evaluator LLM + production query sampling + corpus stratification • 3.5h retainer engagement
In October 2026, Thornfield AI Research Partners deployed a RAG pipeline to production after their Ragas evaluation showed a faithfulness score of 0.87 on a 200-question test set. Within two weeks, users began filing complaints: answers were frequently inaccurate, cited documents that did not support the claims being made, and failed to address questions about older research in the corpus. An internal satisfaction survey showed only 54% of users rating answers as accurate or trustworthy. The Ragas retainer developer was engaged to determine why the evaluation metric had predicted a high-quality system that was failing in production. The investigation traced two compounding root causes: a circular evaluation setup where GPT-4o was being used both to generate answers and to evaluate those same answers via ragas.llms.LangchainLLM, and a test set construction method that had inadvertently sampled questions only from the 15% of the corpus most recently ingested, leaving 85% of the document corpus — with substantially worse chunking quality — completely untested. The total engagement was 3.5 hours.
Thornfield AI Research Partners operates an internal research knowledge base: approximately 1,000 documents comprising academic papers, technical reports, grant summaries, and internal methodology notes, spanning work from 2019 through October 2026. The RAG pipeline used langchain 0.2.14 with a Pinecone vector store, GPT-4o as the answer-generation LLM, and a standard retrieval chain that fetched the top-5 context chunks per query. The pipeline had been in development for three months. Before deployment, the team ran a Ragas evaluation to measure pipeline quality. They constructed a 200-question test set by prompting GPT-4o over a sample of their documents: for each sampled document, GPT-4o was asked to produce “three questions a researcher might ask that this document answers.” The 200 resulting questions were then fed through the RAG pipeline to generate answers and retrieve context, and ragas.evaluate() was called on the resulting dataset. The Ragas faithfulness metric returned 0.87. The team interpreted this as a strong signal that the pipeline was reliably grounding answers in retrieved context and deployed to their 40-person research team.
The original evaluation setup looked like this:
# evaluation/run_ragas_eval.py — Thornfield AI Research Partners
# ragas 0.1.14, langchain 0.2.14, Python 3.11
# October 2026 — pre-deployment evaluation
from ragas import evaluate
from ragas.metrics import faithfulness, context_precision, answer_relevancy
from ragas.llms import LangchainLLM
from langchain_openai import ChatOpenAI
from datasets import Dataset
import pandas as pd
# RAG pipeline components (same GPT-4o used for answer generation)
# generator_llm = ChatOpenAI(model="gpt-4o", temperature=0)
# Evaluator LLM — PROBLEM: same model family as generator
evaluator_llm = LangchainLLM(ChatOpenAI(model="gpt-4o", temperature=0))
# Configure metrics to use GPT-4o as evaluator
faithfulness.llm = evaluator_llm
context_precision.llm = evaluator_llm
answer_relevancy.llm = evaluator_llm
# Load the 200-question synthetic test set
# Generated by prompting GPT-4o over 150 of 1,000 corpus documents
# All 150 sampled documents ingested in last 90 days (Q3 2026)
eval_df = pd.read_json("data/synthetic_testset_200q.json")
eval_dataset = Dataset.from_dict({
"question": eval_df["question"].tolist(),
"answer": eval_df["answer"].tolist(), # GPT-4o RAG answers
"contexts": eval_df["contexts"].tolist(), # retrieved Pinecone chunks
# ground_truth omitted — context_recall not computed
})
result = evaluate(
dataset=eval_dataset,
metrics=[faithfulness, context_precision, answer_relevancy],
)
print(result)
# faithfulness: 0.8712
# context_precision: 0.8340
# answer_relevancy: 0.8901
# Overall score: 0.8651
The 0.87 faithfulness score was real in the sense that the computation completed without error and the number was mathematically correct given the inputs and evaluation model. The problem was that the number was measuring something different from what the team believed it was measuring. The Ragas faithfulness metric works by decomposing each answer into individual factual claims and then prompting the evaluator LLM to judge, for each claim, whether the retrieved context chunks contain sufficient information to support it. When GPT-4o evaluates the faithfulness of GPT-4o’s own outputs, the judgment process is not independent. GPT-4o generated the answer by reasoning over retrieved context; when it later judges whether each claim in that answer is supported by the context, it is drawing on the same implicit knowledge and paraphrase norms it used during generation. It recognizes its own summary patterns, its own way of bridging inferred information with explicitly stated facts, and its own calibration for what constitutes a “reasonable inference” from a source. A different evaluator model — one that had not generated the answer and did not share the same instruction-following conventions — would apply a stricter standard to claims that are inferred but not explicitly stated in the retrieved chunks. The result is that faithfulness scores from same-model evaluation are systematically inflated relative to the scores an independent evaluator would assign.
The second root cause was invisible from the faithfulness score alone. The retainer developer examined the metadata of the 150 documents used to generate the 200 synthetic test questions. Every document in the sample had an ingestion date after July 1, 2026 — Q3 2026 content. Of Thornfield’s 1,000-document corpus, approximately 150 documents (15%) had been ingested in the past 90 days. The remaining 850 documents (85%) represented work from 2019 through June 2026, ingested during a migration from an older SharePoint-based system in early 2024. That migration had used a bulk ingestion pipeline that chunked documents at a hard 4,096-token boundary with no sentence or paragraph boundary detection. A research report that described a methodology in a section beginning mid-chunk would have its context split at an arbitrary token boundary, with the section header in one chunk and the methodology detail in the next. Retrieval quality on these legacy documents was substantially lower than on recently ingested content, which used a 512-token chunking strategy with sentence boundary splitting and semantic overlap.
Because the test set contained zero questions about 2019–2023 research, and because the RAG pipeline reliably retrieved good-quality context chunks for questions about Q3 2026 documents, the 0.87 faithfulness score reflected a pipeline that was genuinely performing well — on 15% of its corpus. When real users asked questions about earlier work — “what methodology did we use in the 2021 grant deliverables?”, “how does the 2022 climate risk model compare to our current approach?” — the pipeline retrieved poorly chunked legacy context, and GPT-4o generated answers that either hallucinated bridging details the context chunks did not contain or gave generic responses that failed to answer the question. The faithfulness score for those answers would have been substantially lower, but those question types were entirely absent from the evaluation dataset.
The retainer developer ran a diagnostic by re-evaluating the existing synthetic answers with Claude-3.5-Sonnet as the evaluator LLM, and separately by running 100 real user questions from production query logs through the same pipeline and evaluating with Claude-3.5-Sonnet:
# diagnosis/rerun_with_independent_evaluator.py
# ragas 0.1.14, langchain 0.2.14, Python 3.11
# Step 1: Re-evaluate existing synthetic answers with Claude-3.5-Sonnet
from ragas import evaluate
from ragas.metrics import faithfulness, context_precision, answer_relevancy
from ragas.llms import LangchainLLM
from langchain_anthropic import ChatAnthropic
from datasets import Dataset
import pandas as pd
# Independent evaluator: Claude-3.5-Sonnet (different model family from GPT-4o generator)
independent_evaluator = LangchainLLM(
ChatAnthropic(model="claude-3-5-sonnet-20241022", temperature=0)
)
faithfulness.llm = independent_evaluator
context_precision.llm = independent_evaluator
answer_relevancy.llm = independent_evaluator
# Same 200 synthetic questions, same RAG answers — only evaluator LLM changed
eval_df = pd.read_json("data/synthetic_testset_200q.json")
synthetic_dataset = Dataset.from_dict({
"question": eval_df["question"].tolist(),
"answer": eval_df["answer"].tolist(),
"contexts": eval_df["contexts"].tolist(),
})
synthetic_result = evaluate(
dataset=synthetic_dataset,
metrics=[faithfulness, context_precision, answer_relevancy],
)
print("Synthetic test set — Claude-3.5-Sonnet evaluator:")
print(synthetic_result)
# faithfulness: 0.6308 (was 0.8712 with GPT-4o evaluator)
# context_precision: 0.7891 (was 0.8340 with GPT-4o evaluator)
# answer_relevancy: 0.8412 (was 0.8901 with GPT-4o evaluator)
# Step 2: Sample 100 real user queries from production logs (October 1–18, 2026)
# and run through pipeline to get answers + contexts
real_query_df = pd.read_json("data/production_queries_oct2026_100sample.json")
# These include questions about legacy 2019–2023 corpus documents
real_dataset = Dataset.from_dict({
"question": real_query_df["question"].tolist(),
"answer": real_query_df["answer"].tolist(), # GPT-4o RAG answers on real queries
"contexts": real_query_df["contexts"].tolist(),
})
real_result = evaluate(
dataset=real_dataset,
metrics=[faithfulness, context_precision, answer_relevancy],
)
print("\nReal production queries — Claude-3.5-Sonnet evaluator:")
print(real_result)
# faithfulness: 0.6103
# context_precision: 0.6720
# answer_relevancy: 0.7890
# Summary of the gap:
# Synthetic / GPT-4o evaluator: faithfulness 0.87
# Synthetic / Claude-3.5-Sonnet evaluator: faithfulness 0.63 (evaluator bias: -0.24)
# Real queries / Claude-3.5-Sonnet: faithfulness 0.61 (corpus coverage gap: -0.02 additional)
# Total gap from reported to production: 0.87 → 0.61 = -0.26
The diagnostic separated the two root causes cleanly. The evaluator model swap alone — switching from GPT-4o to Claude-3.5-Sonnet while keeping the same 200 synthetic questions and the same GPT-4o-generated answers — dropped faithfulness from 0.87 to 0.63. This 0.24-point gap was attributable entirely to circular evaluation: GPT-4o had been judging its own outputs and applying a more lenient standard to inferred claims than an independent model would. When Claude-3.5-Sonnet evaluated the same answers against the same context, it flagged claims that the generated answer had presented as supported but that were, in fact, logical inferences bridging two context chunks rather than statements directly present in either. Real queries with Claude-3.5-Sonnet as evaluator showed an additional small drop to 0.61, attributable to the corpus coverage gap: questions about legacy documents with poor chunking produced lower-quality context, which in turn produced answers with more unsupported claims even accounting for the evaluator difference.
The retainer developer also inspected the 100 real user queries to characterize the vocabulary and structure difference between synthetic and real questions. The synthetic questions, generated by prompting GPT-4o to produce “questions a researcher might ask,” tended to mirror the language of the source document closely: if a document described a “spatiotemporal hazard correlation model,” the synthetic questions asked about “the spatiotemporal hazard correlation model.” Real user questions used paraphrases, asked from a user need rather than a document topic, and frequently asked about things the document corpus did not directly address, requiring the RAG pipeline to either aggregate across multiple documents or acknowledge the coverage gap. GPT-4o’s RAG pipeline — which had been prompted to always attempt an answer — generated plausible-sounding responses to these queries even when the retrieved context was insufficient. An evaluator using the same generation conventions found these responses faithful; an independent evaluator did not.
The fix required three coordinated changes. First, the evaluator LLM was replaced with Claude-3.5-Sonnet across all Ragas metrics. Second, the test set was rebuilt from production query logs combined with a new round of synthetic questions stratified across the full corpus. Third, a re-chunking job was scheduled for the 850 legacy documents to bring them up to the same 512-token sentence-boundary chunking standard used for recent documents:
# evaluation/run_ragas_eval_fixed.py — corrected evaluation setup
# ragas 0.1.14, langchain 0.2.14, Python 3.11
# October 2026 — post-diagnosis fix
from ragas import evaluate
from ragas.metrics import faithfulness, context_precision, answer_relevancy
from ragas.llms import LangchainLLM
from langchain_anthropic import ChatAnthropic
from langchain_openai import ChatOpenAI
from datasets import Dataset
import pandas as pd
import random
# FIX 1: Independent evaluator LLM — Claude-3.5-Sonnet, not GPT-4o
# GPT-4o is the generator; evaluator must be a different model family
independent_evaluator = LangchainLLM(
ChatAnthropic(model="claude-3-5-sonnet-20241022", temperature=0)
)
faithfulness.llm = independent_evaluator
context_precision.llm = independent_evaluator
answer_relevancy.llm = independent_evaluator
# FIX 2: Rebuild test set from production query logs + stratified synthetic questions
# 100 real questions from production logs (October 1–18, 2026)
real_queries = pd.read_json("data/production_queries_oct2026_100sample.json")
# 100 synthetic questions generated over stratified corpus sample
# Stratification: proportional to document count by ingestion year
# 2019–2021: 30 docs sampled → 30 synthetic questions
# 2022–2023: 40 docs sampled → 40 synthetic questions
# 2024 (migration batch): 20 docs sampled → 20 synthetic questions
# 2025–2026 Q2: 10 docs sampled → 10 synthetic questions
# (recent Q3 2026 already covered by real queries)
synthetic_stratified = pd.read_json("data/synthetic_stratified_100q.json")
# Combine into 200-question mixed test set
combined_df = pd.concat([real_queries, synthetic_stratified], ignore_index=True)
combined_df = combined_df.sample(frac=1, random_state=42).reset_index(drop=True)
# Run mixed test set through RAG pipeline to generate answers + contexts
# (pipeline call omitted for brevity — same langchain RAG chain as before)
eval_dataset = Dataset.from_dict({
"question": combined_df["question"].tolist(),
"answer": combined_df["answer"].tolist(),
"contexts": combined_df["contexts"].tolist(),
"source_type": combined_df["source_type"].tolist(), # "real" or "synthetic"
"corpus_year": combined_df["corpus_year"].tolist(), # year of source document(s)
})
result = evaluate(
dataset=eval_dataset,
metrics=[faithfulness, context_precision, answer_relevancy],
)
print("Corrected evaluation — Claude-3.5-Sonnet evaluator, mixed stratified test set:")
print(result)
# faithfulness: 0.6240 (honest baseline; target: 0.75+ after re-chunking)
# context_precision: 0.6890
# answer_relevancy: 0.7730
# FIX 3: Identify legacy documents with poor chunking for re-chunking
# Filter evaluation results by corpus_year to pinpoint low-scoring cohorts
result_df = result.to_pandas()
result_df["corpus_year"] = eval_dataset["corpus_year"]
faithfulness_by_year = result_df.groupby("corpus_year")["faithfulness"].mean()
print("\nFaithfulness by corpus year:")
print(faithfulness_by_year)
# 2019 0.481
# 2020 0.503
# 2021 0.519
# 2022 0.538
# 2023 0.561
# 2024 0.612 (mixed: some migration-chunked, some re-chunked early)
# 2025 0.718
# 2026 0.791
# Confirms: 2019–2023 cohort (850 migration-chunked docs) driving low faithfulness
The corpus-year faithfulness breakdown confirmed the chunking quality hypothesis precisely: documents from 2019 through 2023, all ingested via the 4,096-token bulk chunking migration, produced faithfulness scores in the 0.48–0.56 range. Documents from 2025–2026, ingested with the newer 512-token sentence-boundary chunking, scored 0.72–0.79. The 2024 cohort was intermediate, reflecting a mix of migration-era documents that had been re-chunked during a partial remediation effort. The pattern was unambiguous: the retrieval quality gap between new and legacy documents was the dominant driver of per-query faithfulness variation.
The retainer developer specified a re-chunking job to process the 850 legacy documents through the updated ingestion pipeline (512-token chunks, sentence boundary splitting using langchain.text_splitter.RecursiveCharacterTextSplitter with chunk_size=512, chunk_overlap=64, separators=["\n\n", "\n", ". ", " "]). The re-chunking job would delete the existing Pinecone vectors for the affected document IDs and re-index the new chunks. The projected faithfulness improvement from re-chunking alone was estimated at 0.08–0.12 points based on the 2024 partial remediation cohort’s score of 0.61 compared to the 2019–2023 cohort’s average of 0.52 — with the expectation that full 2019–2023 re-chunking would bring those documents up to the 2024 level or higher.
An evaluation policy document was prepared for the client specifying three standing requirements. The evaluator LLM independence requirement: ragas.evaluate() must always use a model from a different provider than the RAG pipeline generator — if GPT-4o generates answers, Claude or Gemini must evaluate; if Claude generates answers, GPT-4 must evaluate. The test set sourcing requirement: at minimum 40% of test questions must come from production query logs sampled within the prior 60 days, and the remaining synthetic questions must be stratified proportionally across the full document corpus by ingestion cohort year. The corpus coverage audit: before any test set is declared final, the covered document IDs must be checked against the full Pinecone index to confirm that at least 60% of document ingestion cohorts (by year) are represented in the retrieved contexts of at least one test question.
After the re-chunking job completed on October 27, the corrected evaluation was re-run on the same 200-question mixed stratified test set with Claude-3.5-Sonnet as evaluator. Faithfulness improved from 0.62 to 0.74, context_precision from 0.69 to 0.78. The team set a deployment gate of faithfulness ≥ 0.72 and context_precision ≥ 0.72 on the corrected evaluation setup, against which future RAG pipeline changes would be gated before any production deployment.
Total retainer engagement: 3.5 hours (1.5h root cause: circular evaluation hypothesis testing by re-running ragas.evaluate() with Claude-3.5-Sonnet on the existing synthetic answers, GPT-4o vs Claude-3.5-Sonnet evaluator score comparison confirming 0.87→0.63 drop, document metadata analysis of the 200-question test set source documents against full corpus ingestion dates confirming 15% corpus coverage, legacy chunking quality audit confirming 4,096-token boundary splits in 2019–2023 migration batch, real query sampling and pipeline execution on 100 production queries; 0.5h impact: user satisfaction survey timeline correlation with pipeline deployment, faithfulness-by-corpus-year breakdown quantifying legacy document contribution to production quality gap, scope of re-chunking required; 1.0h fix: Claude-3.5-Sonnet LangchainLLM evaluator configuration, production query log sampling script, corpus stratification logic for synthetic question generation, re-chunking job specification for 850 legacy documents with RecursiveCharacterTextSplitter parameters, Pinecone delete-and-reindex pipeline design; 0.5h policy: evaluator LLM independence policy document, test set sourcing requirements, corpus coverage audit script, deployment gate threshold calibration against post-re-chunking evaluation run). Retainer hours logged to HourTab with ragas version, the circular GPT-4o evaluation and corpus coverage gap root causes, and the evaluator independence and test set stratification policy, so the client could audit the full 3.5-hour engagement at the shared URL without logging in.
Related retainer posts
- LangSmith developer on retainer — single project mixes environments
- Arize Phoenix developer on retainer — OTEL span truncation hides retrieval context
- TruLens developer on retainer — retainer work
Track retainer hours without a client portal
HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.
See pricing →