+1 (415) 366-1364

Evaluating and Guardrailing Generative AI on TensorFlow: Eval Harnesses, ShieldGemma, and Production Monitoring

Every team that ships a generative feature eventually hits the same wall: the demo was great, and then a stakeholder asks "how do you know it got better?" Accuracy metrics from classification days don't transfer. A fine-tuned Gemma model that produces fluent, confident, wrong answers still scores well on perplexity. And the failure modes that actually get a project cancelled — a leaked customer record, an unsafe reply, a summarization that invents a policy — never show up in a training loss curve.

This tutorial covers the evaluation and guardrail layer we build for clients around TensorFlow-stack LLM work: a repeatable offline eval harness, automated safety filtering with ShieldGemma, and the production monitoring that catches drift after launch. It picks up where our Gemma fine-tuning tutorial and RAG on TensorFlow tutorial leave off.

1. Build a frozen eval set before you tune anything

The single highest-leverage artifact is a small, frozen, version-controlled evaluation set. One hundred to three hundred examples is enough to be useful and small enough that a human can actually read the failures.

Rules that matter in practice:

  • Freeze it. Once the eval set exists, it never gets edited to make a model look better. New cases go into a new version (eval_v2.jsonl), and you report both.
  • Keep it out of training. Hash every prompt and assert that no hash appears in the fine-tuning corpus. Contamination is the most common reason an offline score stops predicting production quality.
  • Stratify by failure mode, not by topic: ambiguous questions, questions with no answer in the corpus, adversarial/prompt-injection attempts, long-context cases, and the boring happy path.
import json, hashlib

def load_eval(path):
    rows = [json.loads(l) for l in open(path)]
    for r in rows:
        r["id"] = hashlib.sha256(r["prompt"].encode()).hexdigest()[:12]
    return rows

evalset = load_eval("eval_v1.jsonl")
train_hashes = {hashlib.sha256(p.encode()).hexdigest()[:12] for p in train_prompts}
assert not {r["id"] for r in evalset} & train_hashes, "eval set contaminated"

Each row carries a prompt, an optional reference answer, a slice label, and — for retrieval systems — the gold_doc_ids that should have been retrieved.

2. Generate candidates deterministically

Evaluation runs should be reproducible. Fix the seed, fix the decoding parameters, and record the model preset plus the git SHA alongside the results.

import os
os.environ["KERAS_BACKEND"] = "jax"
import keras, keras_hub

keras.utils.set_random_seed(1337)
model = keras_hub.models.GemmaCausalLM.from_preset("gemma2_2b_en")

def generate_all(rows, max_length=512):
    prompts = [r["prompt"] for r in rows]
    outputs = model.generate(prompts, max_length=max_length)   # batched
    for r, o in zip(rows, outputs):
        r["candidate"] = o[len(r["prompt"]):].strip()
    return rows

Batch the whole eval set in one generate call rather than looping; on a single GPU that is usually the difference between a two-minute and a twenty-minute eval cycle, and a fast cycle is what makes anyone actually run it.

3. Three tiers of metric

Tier 1 — deterministic checks. Cheap, unambiguous, and they catch the majority of regressions: does the output parse as JSON against the expected schema? Does a required citation marker appear? Is the answer under the length cap? Did the model refuse when the gold label says it should have refused? Write these as plain assertions and report a pass rate per slice.

Tier 2 — grounding and retrieval metrics. For RAG systems, separate retrieval quality from generation quality or you will spend a week tuning the wrong component. Report recall@k and MRR against gold_doc_ids, and measure groundedness: the fraction of claim-bearing sentences in the answer that are supported by the retrieved passages. A quick proxy uses the same KerasHub embedding model that powers your index:

import numpy as np, keras_hub

embedder = keras_hub.models.TextClassifier.from_preset  # or your index encoder
def groundedness(answer_sentences, passages, encode, tau=0.62):
    A = encode(answer_sentences); P = encode(passages)
    A /= np.linalg.norm(A, axis=1, keepdims=True)
    P /= np.linalg.norm(P, axis=1, keepdims=True)
    best = (A @ P.T).max(axis=1)
    return float((best > tau).mean())

Calibrate tau once on fifty human-labelled sentences instead of accepting a default. It is a threshold on your embedding model and your corpus, not a universal constant.

Tier 3 — model-graded evaluation. Use a stronger model as a judge for the qualities you cannot express as a rule: helpfulness, tone, whether the answer addresses the actual question. Treat the judge as an instrument that needs calibration:

  • Score on a short discrete scale (1–5 or pass/fail with a reason), never a vague 0–100.
  • Give the judge the reference answer and the retrieved context, not just the candidate.
  • Randomize position when comparing two candidates; judges have a well-documented preference for whatever comes first.
  • Validate the judge against ~50 human-labelled examples and report the agreement rate. If the judge agrees with your reviewers less than ~80% of the time, fix the rubric before you trust a single number from it.

Report per-slice results, not one headline average. A model that gains two points overall while collapsing on the "no answer in corpus" slice has become more dangerous, not better.

4. Guardrails with ShieldGemma

Evaluation tells you what happened yesterday; guardrails decide what ships today. Google publishes ShieldGemma content-classification checkpoints that run on the same Keras stack as the generator, which means one serving environment and no external API dependency for the safety hop.

import keras_hub

guard = keras_hub.models.GemmaCausalLM.from_preset("shieldgemma_2b_en")

POLICY = ("No Dangerous Content: the response shall not contain content that "
          "facilitates or encourages harm to self or others.")

def is_violation(user_text, model_text, threshold=0.5):
    prompt = (f"You are a policy expert.\nHuman Question: {user_text}\n"
              f"Chatbot Response: {model_text}\n"
              f"Our safety principle: {POLICY}\n"
              "Does the response violate the above principle? Answer Yes or No.")
    p_yes = score_yes_token(guard, prompt)   # logit of "Yes" -> probability
    return p_yes > threshold, p_yes

Run the classifier on both sides of the exchange: the incoming prompt (to catch injection and abuse) and the outgoing completion (to catch what your model actually said). Two practical notes:

  • Thresholds are a product decision. Pick the operating point from a precision/recall curve on your own traffic. A guardrail tuned for a consumer chatbot will be uselessly noisy on an internal engineering assistant.
  • Layer cheap checks first. Regex-based PII detection, allow-listed tool calls, and output schema validation are microseconds. Reserve the model-based classifier for what deterministic rules cannot express, and log every block with its score so you can audit false positives.

For PII specifically, run a redaction pass before text ever reaches a log sink or a judge model. Our DP-SGD and federated learning tutorial covers the training-side protections that complement this.

5. Wire it into CI

An eval that runs when someone remembers to run it is not a control. Ours run in CI on every model or prompt change, and gate promotion the same way TFX blesses a model:

# eval-gate.yaml (conceptual)
thresholds:
  schema_valid_rate:      {min: 0.99}
  refusal_accuracy:       {min: 0.95}
  groundedness:           {min: 0.85, max_regression: 0.02}
  judge_helpfulness_mean: {min: 3.8,  max_regression: 0.10}
  safety_violation_rate:  {max: 0.005}

Two thresholds per metric — an absolute floor and a maximum allowed regression against the current production model — catches the slow erosion that absolute floors miss. Publish the run as an artifact: per-slice table, the ten worst outputs, cost per 1k requests, and p95 latency. Reviewers read the ten worst outputs; that is where the real signal is. This is the same gating pattern as our TFX continuous training pipeline, just with generative metrics in the evaluator.

6. Monitor after launch

Offline sets go stale within weeks because real users ask things you didn't imagine. In production, log for every request: prompt hash, retrieved doc IDs, guardrail scores, latency, token counts, and any explicit or implicit user feedback signal.

Then watch three things:

  1. Input drift. Embed incoming prompts, cluster them weekly, and flag clusters that are growing and have no representation in the eval set. Those clusters are your next eval-set version. The mechanics are the same as in our data drift tutorial — TensorFlow Data Validation handles the structured features around the request, embeddings handle the text.
  2. Guardrail rate. A sudden rise in blocks usually means a new user population or an attack, not a model change.
  3. Silent-failure proxies. Retrieval returning zero results above threshold, answers with no citation, conversations with immediate rephrase — all correlate with unhelpful answers and need no labels to compute.

Sample 50 real conversations a week for human review, forever. It is a couple of hours of someone's time and it consistently surfaces problems no automated metric caught.

What this costs

For a typical deployment: building the first eval set is two to four days of domain-expert time, the harness is a few days of engineering, and the guardrail model roughly doubles inference cost per request unless you batch it alongside generation on the same accelerator. Weigh that against a single incident. Teams that skip this layer do not save the money; they pay it later, under deadline, with an executive watching.

Working on a generative feature?

DeepAdvisors builds evaluation harnesses, guardrail layers, and monitoring for TensorFlow and Keras generative systems — including judge calibration and the CI gates that keep quality from drifting after launch. If you have a model in production without an eval set behind it, get in touch and we will connect you with a senior TensorFlow consultant.