BXUP Track progress & test yourself — free
Data Science & ML/AI · Playbook · Advanced · ~5 min read
DATA SCIENCE & ML/AI · PLAYBOOK · ADVANCED

Production RAG Health: Failure-Mode Triage and Ingestion at Scale

Two disciplines keep a shipped RAG system trustworthy: a trace-driven triage method that turns "the bot gave a wrong answer" into a specific component diagnosis (retrieval-miss vs grounding-miss vs generation-miss), and an ingestion pipeline that keeps the index true to a living corpus — incremental upserts, dedup, document versioning, and drift monitoring.

Tier: Advanced · Workflow area: RAG Operations & Observability

This playbook installs both; they meet in the middle, because a large share of "model problems" are actually stale-index problems.

When to use

  • A RAG system is in front of users and bad answers arrive as anecdotes with no diagnostic path.
  • The corpus changes (docs updated, added, deleted) and the current process is "re-index everything sometime."
  • You inherited someone else's RAG app and need a systematic way to find where it's broken.

Prerequisites

  • Tracing on every query: question, rewritten query, retrieved chunk IDs + scores, final context, answer, model/prompt/index versions. Arize Phoenix (self-hosted OSS) or Langfuse OSS both do this with a few lines of instrumentation.
  • The pb-21 metrics harness and grounding set; the ingestion source of truth (doc store, wiki API, S3 bucket) with change timestamps.
  • 30–60 logged production failures (or deliberately broken cases) to run the clinic on.

The workflow

  1. Adopt the triage taxonomy. Every bad answer gets exactly one primary label, assigned by walking the trace top-down:

    LabelTest on the traceFix lives in
    Retrieval-missGold/needed evidence absent from retrieved setpb-22/23/24/25; or ingestion (below)
    Grounding-missEvidence retrieved but answer ignores/contradicts itGeneration prompt, context formatting, reranker order
    Generation-missEvidence present and used; answer still wrong (bad reasoning, wrong synthesis)Model tier, prompt, decomposition
    Ingestion-missEvidence not in the index at all (stale, deleted, never chunked)This playbook, steps 4–6
    Refusal-missQuestion unanswerable; system answered anyway (or vice versa)Abstention prompt + hard-negative evals
  2. Run the clinic. For each failure: open the trace, apply the tests in order, label, and file. Automate the first test — a script that checks whether any retrieved chunk contains the expected evidence (RAGAS context_recall per-case does this). After 30–60 cases you have a Pareto: most systems concentrate 60–80% of failures in one or two labels, which sets the roadmap. Re-run the distribution monthly; it shifts as you fix things.

    # per-case component check with ragas
    from ragas import SingleTurnSample
    from ragas.metrics import LLMContextRecall, Faithfulness
    sample = SingleTurnSample(user_input=q, retrieved_contexts=ctx,
                              response=ans, reference=gold_answer)
    # low context_recall → retrieval/ingestion-miss; high recall + low faithfulness → grounding-miss
    
  3. Wire failures back into evals. Every triaged case gets minimized and appended to the pb-21 grounding set's hard slice (same loop as pb-20 step 6). The clinic is not a one-off audit; it is the intake valve for your eval set.

  4. Make ingestion incremental and idempotent. Diff the source of truth against index state on a schedule; upsert only changes:

    for doc in source.changed_since(last_run):
        chunks = chunk(doc)                                 # pb-22 winner
        point_ids = [uuid5(NAMESPACE, f"{doc.id}:{i}:{doc.version}") for i in range(len(chunks))]
        qdrant.upsert("corpus", points=embed_and_wrap(chunks, point_ids, doc))
        qdrant.delete("corpus", filter=Filter(must=[
            FieldCondition(key="doc_id", match=MatchValue(value=doc.id)),
            FieldCondition(key="version", match=MatchValue(value=doc.prev_version))]))
    

    Deterministic point IDs (doc + position + version) make reruns idempotent. Delete-then-orphan is the classic bug: deleting the doc's old chunks must be part of the same transaction-shaped step as upserting the new ones, or searches interleave versions.

  5. Dedup and version at ingestion. Near-duplicate chunks (boilerplate, mirrored docs, template pages) poison retrieval diversity — apply the pb-17 FAISS dedup pass at ingestion, keeping counts as metadata. Carry version and effective_date in payloads (pb-25 schema) so retrieval can prefer current versions and answer "as of March" questions where required.

  6. Monitor corpus and query drift. Weekly job: (a) index freshness — max(source updated_at − index updated_at), alert over threshold; (b) embedding-space drift — centroid shift / distribution distance of new chunks vs the corpus, which flags new topics your eval set doesn't cover; (c) retrieval health — score distributions of top-1 hits trending down means queries are outrunning the corpus; (d) unanswered/low-confidence rate by topic cluster (pb-17 on recent queries) to find what users need that the corpus lacks.

  7. Schedule the full-reindex escape hatch. Chunking, embedding-model, or schema changes (pb-22/23/25 decisions) require full re-embedding: blue-green it — build the new index alongside, run the pb-21 scorecard against both, cut over on green, keep the old index until the bake period ends.

Remember:

Before touching any prompt or model in response to a bad answer, check the trace for whether the evidence was retrieved — and check the index for whether the evidence was ever ingested. Stale-index bugs wear a hallucination costume.

Tool variants

  • Tracing/observability: Arize Phoenix (OTel/OpenInference, self-host) and Langfuse OSS — both free and local; MLflow tracing if you standardized on it in pb-20; LangSmith as the managed option.
  • Orchestration: Airflow/Dagster for the incremental jobs — Dagster's asset model maps cleanly onto corpus→chunks→embeddings→index lineage; a cron + state table is a legitimate v1.
  • Vector-store upserts: Qdrant/Weaviate/pgvector all support upsert-by-ID and delete-by-filter; verify your store's delete semantics under concurrent search before trusting the version-swap step.
  • Eval integration: RAGAS per-case metrics inside the triage script; the pb-20 gate blocks prompt "fixes" that regress other slices.

Verification

  • 30+ failures triaged with the taxonomy; Pareto chart produced; the top failure label has a named owner and a fix ticket.
  • Ingestion idempotency test: run the sync twice, index state identical; update a doc, confirm old-version chunks are gone and new ones searchable within the SLA.
  • Orphan audit: zero chunks whose doc_id no longer exists in the source of truth.
  • Drift dashboard live with at least freshness + retrieval-score trends, and one alert threshold tested by simulation.

Pitfalls

  • Triaging from answers instead of traces. Without retrieved-context logs you cannot distinguish the failure modes — instrumentation precedes diagnosis, always.
  • Multi-label cop-out. Forcing one primary label per case is what makes the Pareto actionable; "a bit of everything" analyses produce no roadmap.
  • Re-embedding the world nightly. Full reindexes as routine hygiene burn money and mask ingestion bugs; incremental with a scheduled full rebuild (quarterly, or on config change) is the discipline.
  • Deleting by doc_id without version awareness. Racing deletes and upserts on the same doc_id interleave versions in live search results; deterministic IDs + ordered steps fix it.
  • Fixing the component that's easiest to change. Prompt tweaks are cheap, so grounding-misses get "fixed" while the dominant retrieval-miss bucket sits untouched. The Pareto, not convenience, sets the order.

Restricted-environment variant

  • Phoenix/Langfuse self-hosted keep traces (which contain user queries and client text) entirely in the engagement environment — traces are among the most sensitive artifacts a RAG system produces; never ship them to a hosted observability SaaS without explicit clearance.
  • Triage judge calls (context_recall per-case) run on the local model; human labeling covers what the local judge can't.
  • The ingestion pipeline is local by nature; drift monitoring adds no external calls.

Success criteria

  • A failure-triage practice: taxonomy applied, Pareto produced, top bucket fixed, and the fix's effect visible in the next month's distribution.
  • Incremental ingestion with idempotent upserts, version-safe deletes, ingestion-time dedup, and a tested blue-green full-reindex path.
  • Drift monitoring that has caught at least one real staleness or coverage gap before a user did.
  • Every production failure ends up as an eval case — the system gets harder to break every week it runs.
This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in