Turn Real Support Tickets into Automated RAG Regression Tests

Your RAG system can pass a beautiful synthetic evaluation suite and still fail in production.

The synthetic tests ask clean questions like, “What is the refund policy?” Real users ask, “I was charged twice and the button does nothing, help???” They include typos, missing context, account-specific edge cases, and phrasing that nobody on your team predicted.

The uncomfortable truth is that your best RAG regression tests already exist. They are sitting in your support queue.

Support tickets contain the actual language users use, the product areas that hurt, and the consequences of getting an answer wrong. The problem is that tickets are messy, private, and often missing the clean “expected answer” structure you need for automated testing.

The job is not to paste tickets into a spreadsheet and call it an eval set. The job is to build a pipeline that turns real support tickets into deterministic, reviewable, automated RAG regression tests.

📌 TL;DR

  • Real support tickets are better than synthetic queries for discovering RAG regressions because they contain real user language and real failure modes.
  • Don’t test tickets as raw text. Transform them into sanitized, versioned test cases with retrieval and answer expectations.
  • Split RAG tests into layers: retrieval, context assembly, grounding, answer quality, and abstention.
  • Start with deterministic retrieval tests before adding LLM-based judges.
  • Every failing test should tell you whether the problem is retrieval, ranking, context assembly, generation, or missing knowledge.
  • Keep the test suite alive by continuously mining new tickets, retiring stale tests, and tracking product-language drift.

📋 Table of Contents


1. The Support Queue Is the Eval Suite You Keep Ignoring

Scenario: You ship a new chunking strategy. Your offline eval says retrieval recall improved by three points. Then support starts getting tickets like, “Why does the assistant say my plan has no exports when I’m on Pro?” The eval did not catch it because nobody wrote a synthetic test for that exact phrasing, plan type, and feature interaction.

Synthetic RAG evaluations are useful, but they have a structural weakness: they are written by people who already understand the product.

Support tickets are written by people who do not.

That difference is valuable because RAG systems fail at the boundary between user intent and knowledge representation. Users do not use your internal terminology. They describe symptoms, not features. They mix billing, permissions, onboarding, and bugs into one sentence. They ask questions that depend on plan, region, account state, or product version.

A support ticket often contains several testing signals at once:

  • The user’s actual query phrasing.
  • The product area involved.
  • The business consequence of a wrong answer.
  • The knowledge article or internal note that resolved it.
  • Whether the issue was common, severe, or repeated.
  • Whether the assistant previously gave a bad answer.

That is a richer starting point than a handcrafted question like, “How do exports work?”

But tickets are not automatically good tests. A ticket may contain:

  • Private customer data.
  • A confused or incorrect user assumption.
  • A support agent workaround that is not official policy.
  • A bug that should be escalated, not answered from docs.
  • A knowledge gap that no RAG system can solve.
  • A resolved issue that is now obsolete.

So the goal is not “use tickets directly.” The goal is to create a controlled pipeline:

support tickets
  -> filter for value
  -> sanitize
  -> normalize into test case
  -> human review
  -> automated RAG regression test
  -> CI execution
  -> failure triage
  -> test suite maintenance

This turns support pain into engineering feedback instead of recurring incidents.

2. Mine Tickets for Failure Value Not Volume

Scenario: You export 5,000 support tickets and turn them into tests. Your CI suite becomes slow, noisy, and dominated by password-reset questions. Meanwhile, the dangerous billing and permission failures remain untested.

The biggest mistake teams make is treating ticket volume as test value. A thousand nearly identical “Where is my invoice?” tickets may be important operationally, but they may only require one or two regression tests.

You want tickets with high failure value.

Failure value comes from a combination of severity, complexity, recurrence, and RAG relevance.

A useful scoring model looks like this:

from dataclasses import dataclass


HIGH_RISK_TAGS = {
    "billing",
    "payments",
    "permissions",
    "security",
    "data-export",
    "account-deletion",
    "compliance",
}


@dataclass
class Ticket:
    ticket_id: str
    status: str
    csat: int | None
    tags: set[str]
    reopened: bool
    linked_kb_article: str | None
    assistant_involved: bool
    contains_error_message: bool


def ticket_failure_value(ticket: Ticket) -> int:
    score = 0

    if ticket.status == "resolved":
        score += 1

    if ticket.csat is not None and ticket.csat <= 2:
        score += 3

    if ticket.reopened:
        score += 2

    if ticket.tags & HIGH_RISK_TAGS:
        score += 3

    if ticket.assistant_involved:
        score += 4

    if ticket.contains_error_message:
        score += 2

    if ticket.linked_kb_article:
        score += 2

    return score

This is not meant to be a perfect model. It is a filter.

Tickets worth converting into RAG regression tests often have one or more of these traits:

  • The user phrased the question in a way the assistant misunderstood.
  • The correct answer depends on plan, role, region, or account state.
  • The issue was resolved by a specific knowledge article.
  • The issue involved a dangerous action, such as deletion or billing.
  • The user received a wrong answer from the assistant before escalating.
  • The ticket was reopened because the first answer was incomplete.
  • The ticket contains a common misconception that the assistant should correct.
  • The correct answer is “we don’t know” or “contact support,” which is useful for abstention testing.

You also need negative examples.

If your RAG system should not answer medical, legal, financial, or security-sensitive questions without escalation, then tickets showing those boundaries are valuable. A good RAG test suite does not only test what the system should know. It tests what it should refuse, qualify, or escalate.

A practical starting point is a small but sharp corpus:

Ticket categoryWhy it matters
Resolved with KB linkGives you expected evidence
Reopened ticketsReveals incomplete or misleading answers
Low-CSAT assistant answersReveals user-visible failures
Billing and permission issuesHigh business risk
“Not enough info” ticketsGood abstention tests
Duplicate clustersReveals common phrasing patterns
Tickets after a releaseReveals regression patterns

I would rather start with 150 well-chosen tickets than 5,000 uncurated ones. The smaller set is easier to sanitize, review, and keep reliable.

3. Sanitize the Ticket Before It Becomes a Test

Scenario: You convert a real ticket into a regression test. It works beautifully. Then someone clones the repository, runs the eval suite, and now customer names, email addresses, order IDs, and internal account IDs are sitting in CI logs, test artifacts, and maybe a third-party evaluation dashboard.

This is where many teams quietly kill the idea. They know tickets are valuable, but they do not want the compliance headache. That is fair. The answer is not to avoid tickets. The answer is to sanitize aggressively and deliberately.

Before a ticket becomes a test case, it should go through several transformations:

  1. Remove direct identifiers.
  2. Remove or generalize account-specific details.
  3. Replace sensitive values with stable placeholders.
  4. Keep only the fields needed for testing.
  5. Store the original ticket reference separately with restricted access.
  6. Get legal, privacy, and security policies involved early.

A basic redaction function might look like this:

import re

EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
ORDER_RE = re.compile(r"\bORD-\d{6,}\b", re.IGNORECASE)
ACCOUNT_RE = re.compile(r"\bACCT-\d{6,}\b", re.IGNORECASE)
PHONE_RE = re.compile(r"\b\+?\d[\d\-\s()]{7,}\b")


def redact_ticket_text(text: str) -> str:
    text = EMAIL_RE.sub("[EMAIL]", text)
    text = ORDER_RE.sub("[ORDER_ID]", text)
    text = ACCOUNT_RE.sub("[ACCOUNT_ID]", text)
    text = PHONE_RE.sub("[PHONE]", text)
    return text

But do not pretend regex is a full privacy program.

Regex can catch obvious patterns. It will miss names, addresses, free-form account descriptions, screenshots, attachments, and unusual customer identifiers. For production use, combine redaction with:

  • A policy defining which fields may enter the eval set.
  • A review step before tests are committed.
  • A restricted mapping table from test ID to original ticket ID.
  • Synthetic rewriting for highly sensitive tickets.
  • Deletion rules when retention expires.
  • Access controls on raw ticket exports.

Sometimes the best transformation is not redaction but rewriting.

Original ticket:

I'm Maria from Northwind Traders. Our account ACCT-88213 was charged twice
for order ORD-551902 and Finance is angry. We need this fixed before Friday.

Sanitized test query:

Our company was charged twice for the same order. How do we request a refund?

That rewritten query is still valuable. It preserves the intent without preserving the private details.

⚠️ Gotcha: if your test suite includes expected answers copied from support agents, sanitize those too. Agent replies often contain names, internal tool links, account IDs, or unofficial workarounds.

4. Turn One Messy Ticket into One Executable Assertion

Scenario: A support ticket says, “The assistant told me refunds are impossible after 30 days but our enterprise contract says 45 days.” That is a great test candidate. But if you only store the raw ticket text, your test cannot fail in a useful way.

A RAG regression test needs structure. It needs:

  • A user query.
  • Optional context constraints.
  • Expected retrieval evidence.
  • Expected answer constraints.
  • Severity and ownership.
  • A link back to the source ticket, without exposing the raw ticket.
  • A knowledge-base version or release marker.

A practical test case format:

{
  "test_id": "ticket-10432-enterprise-refund-window",
  "source_ticket_id": "TCK-10432",
  "created_from": "support_queue",
  "kb_release": "kb-2026.02.10",
  "query": "Our enterprise contract says we have 45 days for refunds. Is that true?",
  "context_constraints": {
    "plan": "enterprise",
    "locale": "en-US"
  },
  "expected_retrieval": {
    "must_include": [
      "kb/billing/refunds.md#enterprise-refund-window"
    ],
    "must_not_include": [
      "kb/billing/legacy-refunds.md"
    ],
    "max_rank_for_must_include": 5
  },
  "expected_answer": {
    "must_include_facts": [
      "standard refund window",
      "enterprise refund window",
      "45 days"
    ],
    "must_not_include_facts": [
      "refunds are never available after 30 days"
    ],
    "requires_citation": true,
    "should_abstain": false
  },
  "severity": "high",
  "tags": ["billing", "refunds", "enterprise"]
}

This format is intentionally boring. It is meant to be reviewed, diffed, and executed.

The most important part is the distinction between expected retrieval and expected answer.

If you only assert the final answer, you will not know why the test failed. Did the retriever miss the right document? Did the prompt ignore the retrieved evidence? Did the knowledge base change? Did the model hallucinate?

If you assert retrieval separately, you can localize the failure.

Also include abstention tests.

{
  "test_id": "ticket-11873-custom-legal-question",
  "query": "Can you confirm whether our data processing agreement satisfies German procurement law?",
  "expected_answer": {
    "should_abstain": true,
    "must_include_facts": [
      "contact legal or support"
    ],
    "must_not_include_facts": [
      "legal confirmation",
      "guarantee compliance"
    ]
  },
  "severity": "high",
  "tags": ["compliance", "legal", "abstention"]
}

A RAG system that confidently answers questions it should not answer is not smart. It is a liability.

When converting tickets, avoid making the test too narrow. If the original ticket says, “We are on Enterprise Gold Plus EMEA,” the test should usually use the relevant constraint, not every irrelevant detail. The test should capture the condition that changes the answer.

That is the difference between replaying a ticket and learning from it.

5. Test Retrieval Grounding and Generation Separately

Scenario: A test fails because the final answer says refunds are limited to 30 days. The team immediately blames the language model. But when you inspect the trace, the right document was never retrieved. The model never had a chance.

RAG failures are often described as “the LLM hallucinated,” but that phrase hides too much.

A RAG answer can fail because:

  1. The right knowledge was not retrieved.
  2. The right knowledge was retrieved but ranked too low.
  3. The right knowledge was retrieved but truncated during prompt assembly.
  4. The right knowledge was included but the model ignored it.
  5. The model used the evidence but produced an unsupported interpretation.
  6. The knowledge base does not contain the needed fact.
  7. The knowledge base contains conflicting facts.
  8. The query requires account-specific data that RAG cannot see.
  9. The system should have abstained or escalated.

If your regression tests only check the final answer, you will spend hours guessing which of these happened.

A better test suite is layered.

Test layerWhat it checksDeterministic?Example failure
Retrieval recallDid required chunks appear?Mostly yesRefund policy not in top 10
Retrieval rankingDid required chunks appear early enough?Mostly yesRight doc at rank 18
Retrieval safetyDid forbidden chunks appear?Mostly yesDeprecated policy retrieved
Context assemblyWas retrieved evidence included in the prompt?Yes, if loggedChunk retrieved but truncated
GroundingDoes the answer use only provided evidence?PartiallyAnswer invents a 60-day policy
Fact coverageDoes the answer include required facts?PartiallyMisses enterprise exception
AbstentionDoes the system refuse when appropriate?PartiallyAnswers legal question confidently
Tone and policyIs the answer safe and non-misleading?NoPromises guaranteed refund

The first few layers should be as deterministic as possible. The later layers may need an LLM judge, but only after the deterministic layers are in place.

This layering also changes how you improve the system.

If retrieval recall fails, improve indexing, chunking, metadata, embeddings, query expansion, or filters.

If retrieval recall passes but grounding fails, improve prompt constraints, evidence ordering, citation requirements, or model choice.

If grounding passes but fact coverage fails, improve answer synthesis, context length, or chunk granularity.

If the knowledge base lacks the fact, no amount of model tuning will fix it. That is a content problem.

A RAG regression suite should not merely say “bad answer.” It should point at the broken subsystem.

6. Build Deterministic Retrieval Regression Tests First

Scenario: You start with LLM-as-judge tests because they feel flexible. Two weeks later, half your tests are flaky, nobody trusts the judge, and every failure becomes a philosophical debate about whether the answer was “close enough.”

Start with retrieval tests. They are cheaper, faster, easier to debug, and more stable.

A retrieval test asks: for this user query, which chunks should be retrieved?

Define a simple interface:

from dataclasses import dataclass
from typing import Protocol


@dataclass(frozen=True)
class RetrievedChunk:
    chunk_id: str
    doc_id: str
    score: float


class Retriever(Protocol):
    def retrieve(self, query: str, top_k: int) -> list[RetrievedChunk]:
        ...

Then write a pytest-style regression test:

def test_enterprise_refund_window_retrieval(rag_retriever: Retriever):
    query = (
        "Our enterprise contract says we have 45 days for refunds. "
        "Is that true?"
    )

    hits = rag_retriever.retrieve(query, top_k=10)
    chunk_ids = [hit.chunk_id for hit in hits]

    assert "kb/billing/refunds.md#enterprise-refund-window" in chunk_ids[:5]
    assert "kb/billing/legacy-refunds.md" not in chunk_ids

That is simple, but it catches a huge class of regressions.

You can also measure retrieval quality across the suite:

def recall_at_k(gold_ids: set[str], retrieved_ids: list[str], k: int) -> float:
    if not gold_ids:
        return 1.0
    return len(gold_ids.intersection(retrieved_ids[:k])) / len(gold_ids)


def reciprocal_rank(gold_ids: set[str], retrieved_ids: list[str]) -> float:
    for rank, chunk_id in enumerate(retrieved_ids, start=1):
        if chunk_id in gold_ids:
            return 1.0 / rank
    return 0.0

Then aggregate:

def retrieval_metrics(cases, retriever: Retriever, top_k: int = 10):
    recalls = []
    mrrs = []

    for case in cases:
        query = case["query"]
        expected = case.get("expected_retrieval", {})
        gold = set(expected.get("must_include", []))

        if not gold:
            continue

        hits = retriever.retrieve(query, top_k=top_k)
        retrieved_ids = [hit.chunk_id for hit in hits]

        recalls.append(recall_at_k(gold, retrieved_ids, top_k))
        mrrs.append(reciprocal_rank(gold, retrieved_ids))

    return {
        "recall_at_k": sum(recalls) / len(recalls),
        "mrr": sum(mrrs) / len(mrrs),
        "cases": len(recalls),
    }

This gives you a cheap regression signal you can run on every pull request that touches:

  • Chunking logic.
  • Embedding code.
  • Metadata filters.
  • Retrieval configuration.
  • Query rewriting.
  • Knowledge-base content.
  • Vector index rebuilds.
  • Reranker settings.

Retrieval tests also make it easier to review knowledge-base changes. If someone edits kb/billing/refunds.md, you can run all billing retrieval tests and see whether expected chunks still appear.

A subtle but important point: expected chunk IDs need to be stable.

If your chunk IDs are random UUIDs regenerated on every index rebuild, retrieval assertions become painful. Use deterministic chunk identifiers derived from the document, section, and knowledge-base release. For example:

kb/billing/refunds.md#enterprise-refund-window

Or:

doc_1234:section_enterprise_refund_window

If chunk boundaries change, tests may need updating. That is not a flaw. It is the system telling you that retrieval behavior changed.

💡 Practical note: for high-severity tests, assert both presence and absence. Retrieving an old, deprecated policy can be just as dangerous as missing the new one.

7. Use LLM Judges Only Where Determinism Runs Out

Scenario: Your retrieval tests pass. The correct chunk is in the top three. But the final answer still says, “Refunds are always limited to 30 days.” Deterministic checks can detect that the required phrase is missing, but they cannot fully judge whether the answer is misleading, contradictory, or unsafe.

This is where an LLM judge becomes useful.

But use judges carefully. They are powerful, expensive, and capable of being confidently wrong.

A good judge evaluates a narrow question, not “Is the answer good?”

For RAG regression tests, useful judge questions include:

  • Is the answer supported by the retrieved evidence?
  • Does the answer include all required facts?
  • Does the answer include unsupported claims?
  • Does the answer correctly qualify uncertainty?
  • Does the answer incorrectly promise something?
  • Does the answer fail to abstain when it should?

Define the judge output as structured data:

from typing import Literal, TypedDict


class JudgeResult(TypedDict):
    verdict: Literal["pass", "fail", "unknown"]
    missing_facts: list[str]
    unsupported_claims: list[str]
    reasoning: str

The judge prompt should be restrictive:

You are evaluating a customer-support answer produced by a RAG system.

Evaluate only whether the answer is supported by the provided evidence and
satisfies the required constraints.

Do not use outside knowledge.
Do not reward style if facts are wrong.
If the evidence is insufficient to judge, return "unknown".

Return JSON matching this schema:
{
  "verdict": "pass" | "fail" | "unknown",
  "missing_facts": string[],
  "unsupported_claims": string[],
  "reasoning": string
}

User query:
{query}

Retrieved evidence:
{evidence}

Required facts:
{required_facts}

Forbidden claims:
{forbidden_claims}

Assistant answer:
{answer}

The important part is the unknown verdict.

Many teams force judges into pass/fail. That creates false confidence. If the evidence is ambiguous, the judge should say so. Then the test framework can mark the case for human review instead of silently failing or passing.

When using judges in CI, do not treat them like unit tests with hard boolean semantics unless the test is critical and well-calibrated.

A safer policy:

Test typeCI behavior
Deterministic retrieval testsHard fail
Security and compliance answer testsHard fail
Critical billing or legal testsHard fail after judge confirmation
General answer-quality testsTrack trend, alert on regression
Exploratory mined ticketsNightly report, not PR blocker

Also record the judge input and output for every run. If a judge-based test fails, you want to see:

  • The query.
  • The retrieved evidence.
  • The final answer.
  • The judge prompt version.
  • The judge model version.
  • The structured judgment.
  • The test case version.
  • The knowledge-base release.

Without that, judge failures become impossible to debug.

One more warning: do not use the same model for generation and judging without validation. It can work, but it can also create blind spots. If your generator and judge share the same weaknesses, the judge may approve plausible but incorrect answers. Calibrate the judge on a small human-labeled slice before trusting it.

8. Make Failures Debuggable with a RAG Autopsy

Scenario: CI says twelve RAG tests failed. The PR author sees a wall of red and immediately asks, “Are these flaky?” If your test output only says “answer did not match,” the conversation ends there.

A good RAG regression failure should look like an autopsy report.

For each failing test, capture:

test_id
source_ticket_id
kb_release
pipeline_version
query
context_constraints
retrieved_chunks
chunk_scores
prompt_context
generated_answer
deterministic_checks
judge_result
failure_class

Then classify the failure.

A practical classifier:

def classify_rag_failure(
    case: dict,
    retrieved_ids: list[str],
    context_doc_ids: set[str],
    judge: JudgeResult | None,
) -> str:
    expected = case.get("expected_retrieval", {})
    must_include = set(expected.get("must_include", []))
    max_rank = expected.get("max_rank_for_must_include", 5)

    if must_include and not must_include.intersection(retrieved_ids):
        return "retrieval_miss"

    if must_include:
        best_rank = min(
            retrieved_ids.index(chunk_id)
            for chunk_id in must_include
            if chunk_id in retrieved_ids
        )

        if best_rank >= max_rank:
            return "ranking_miss"

    if must_include and not must_include.issubset(context_doc_ids):
        return "context_assembly_miss"

    if judge is not None and judge["verdict"] == "fail":
        if judge["unsupported_claims"]:
            return "unsupported_generation"
        if judge["missing_facts"]:
            return "missing_facts"

    if judge is not None and judge["verdict"] == "unknown":
        return "ambiguous_evidence"

    return "unclassified"

Now failures become actionable.

Failure classLikely causeFirst place to look
retrieval_missChunk missing, wrong metadata, bad embedding, filter issueIndex and chunking
ranking_missRelevant chunk retrieved too lateReranker, scoring, query expansion
context_assembly_missRetrieved but not included in promptPrompt builder, truncation
missing_factsEvidence present but answer incompletePrompt, model, evidence ordering
unsupported_generationModel invented or overclaimedGrounding constraints, model choice
ambiguous_evidenceDocs conflict or lack clarityKnowledge base
knowledge_gapNeeded fact does not existContent team
abstention_failureAnswered when it should escalatePolicy routing, guardrails

This changes the team’s relationship with the test suite.

Instead of “RAG tests are flaky,” you get:

“Three failures are retrieval misses after the chunk-size change, two are context truncation issues, and one is a knowledge-base conflict.”

That is a bug report, not a vibe.

It also helps you decide which failures should block a deploy.

A retrieval_miss on a high-severity billing test should block.

An ambiguous_evidence result on a low-severity mined ticket may open a content review task instead.

A ranking_miss where the required chunk moved from rank 2 to rank 6 may be acceptable if the final answer is still correct, depending on your product requirements.

The key is that the test suite should produce engineering decisions, not just red builds.

9. Keep the Test Suite Alive as Product Language Drifts

Scenario: Your test suite was built from tickets six months ago. Since then, the product renamed “Exports” to “Data Delivery,” added a new enterprise tier, and changed the refund policy. The suite still passes, but it no longer represents what users ask or what the product does.

RAG regression tests decay.

They decay because:

  • Product terminology changes.
  • Policies change.
  • Knowledge articles move.
  • Chunk IDs change.
  • User phrasing evolves.
  • New features create new failure modes.
  • Old tickets become irrelevant.
  • Judges and models change.
  • Retrieval infrastructure changes.

A living pipeline is better than a frozen golden dataset.

A good maintenance loop looks like this:

weekly:
  - mine new high-value tickets
  - identify duplicate failure clusters
  - propose new test cases

per release:
  - run full regression suite
  - compare retrieval metrics against baseline
  - review new failures by failure class

monthly:
  - retire stale tests
  - validate judge calibration
  - review abstention tests
  - check for test duplication
  - update expected evidence after approved KB changes

per quarter:
  - audit PII in test assets
  - review retention rules
  - refresh severity labels
  - test the test suite itself

That last item matters. Test the test suite.

For example:

  • Mutate a knowledge article and verify the related tests fail.
  • Remove a required chunk from the index and verify retrieval tests catch it.
  • Insert a deprecated policy and verify negative retrieval tests catch it.
  • Change the answer prompt to remove citations and verify citation tests fail.
  • Replace a correct answer with an unsupported one and verify the judge catches it.

If your suite cannot detect deliberate regressions, it is not a regression suite. It is a collection of hopeful assertions.

Also track query-language drift.

Support tickets are useful not only as tests but as a signal of how users talk about the product. If users suddenly start saying “data delivery” instead of “export,” your retrieval system may need query expansion, synonyms, updated metadata, or new docs.

You can mine this without turning every ticket into a test:

def new_phrase_candidates(old_terms: set[str], ticket_texts: list[str]):
    candidates = {}

    for text in ticket_texts:
        for term in extract_noun_phrases(text):
            term = term.lower().strip()
            if term and term not in old_terms:
                candidates[term] = candidates.get(term, 0) + 1

    return sorted(
        candidates.items(),
        key=lambda item: item[1],
        reverse=True,
    )

The exact phrase extraction method can be simple or sophisticated. The point is operational: your test suite should evolve with user language.

Finally, do not let the suite become only historical. Real tickets are great for regressions, but they under-test future features and adversarial behavior. Combine them with:

  • Synthetic edge cases.
  • Policy boundary tests.
  • Prompt-injection attempts.
  • Unsupported-question abstention tests.
  • Multilingual variations if you support them.
  • Permission-scoped queries.
  • Conflicting-document tests.
  • Version-specific product questions.

Real tickets tell you what already hurt. Synthetic and adversarial tests help you anticipate what might hurt next.


The RAG Regression Pipeline I Would Build

If I were turning support tickets into automated RAG regression tests for a production assistant, I would not try to build a perfect evaluation platform on day one. I would build a boring, reviewable pipeline.

Start here:

  1. Export resolved, high-value support tickets.
  2. Filter for tickets with clear resolution evidence and business risk.
  3. Redact or rewrite sensitive details before anything enters the test repo.
  4. Convert each ticket into a structured test case.
  5. Add expected retrieval evidence before adding answer judgments.
  6. Run deterministic retrieval tests in CI.
  7. Add narrowly scoped LLM judges for grounding and safety.
  8. Log full RAG traces for every failure.
  9. Classify failures into retrieval, ranking, context, generation, and knowledge gaps.
  10. Mine new tickets continuously and retire stale tests deliberately.

The production checklist looks like this:

  • Every test case has a stable ID and source ticket reference.
  • No raw PII exists in the test repository or CI artifacts.
  • Expected evidence is tied to a knowledge-base release.
  • Retrieval assertions run before answer-quality checks.
  • High-severity tests fail builds.
  • Low-severity mined tests produce reports, not noise.
  • Judge results include unknown, not only pass/fail.
  • Every failure captures retrieved chunks, prompt context, answer, and judgment.
  • The suite is tested against deliberate regressions.
  • Test ownership is clear: engineering owns the harness, support owns ticket selection, content owners review knowledge expectations.

The deeper point is that support tickets are not just incidents to close. They are examples of what users actually need the system to understand.

A RAG system that only passes synthetic tests is fragile. A RAG system tested against real support language, sanitized evidence, and layered failure checks becomes something much more useful: a system you can change without fear.

Post a Comment

Previous Post Next Post