DEV Community

Cover image for Regex, Classifiers, and RLHF: The Three Layers of LLM Content Moderation, Explained
nicknick80
nicknick80

Posted on

Regex, Classifiers, and RLHF: The Three Layers of LLM Content Moderation, Explained

Every chat app built on an LLM ships with some form of content moderation. But "moderation" isn't one thing — it's usually two or three separate systems stacked on top of each other, built at different times, using completely different techniques. If you're building a chat product and reach for "just add a moderation API call," you're only covering one of three layers, and it's worth knowing which one.

This post breaks down the three layers, how each actually works, where each one fails, and what "uncensored model" really means from an engineering standpoint (hint: it's not "no filters," it's "no RLHF").

Layer 1: Input filtering (regex / keyword blocklists)

This is the oldest and cheapest layer, and it's still in production everywhere because it's fast and free to run.

import re

BLOCKLIST = [
    r"\bhow to (make|build) a bomb\b",
    r"\bcredit card number generator\b",
]

def blocklist_check(text: str) -> bool:
    text = text.lower()
    return any(re.search(pattern, text) for pattern in BLOCKLIST)
Enter fullscreen mode Exit fullscreen mode

Why it's still used: it runs in microseconds, costs nothing, and needs no model inference. For known bad phrases (CSAM keywords, obvious violence requests, PII patterns like SSNs or credit card numbers), a well-maintained list catches a huge share of clearly bad traffic before it ever reaches the model.

Where it fails: paraphrase and typo evasion. "how to bake a bmob," "h0w d0 i bulid an eksplosive," or just asking the same question in a different language walks straight through a regex list. It also produces false positives — a security researcher asking about SQL injection, or a nurse asking about drug dosages, can get blocked by an overly broad keyword.

Regex filtering is a floor, not a ceiling. Almost every production system pairs it with something smarter.

Layer 2: Classifier-based moderation

This is a separate, purpose-trained model — usually much smaller than the chat model itself — that scores text across categories like violence, self-harm, sexual content, and hate speech. It runs as an independent pass on both the input (before the prompt reaches the LLM) and the output (before the response reaches the user).

import requests

def classify_moderation(text: str, api_key: str) -> dict:
    resp = requests.post(
        "https://api.openai.com/v1/moderations",
        headers={"Authorization": f"Bearer {api_key}"},
        json={"input": text},
    )
    return resp.json()["results"][0]["category_scores"]
Enter fullscreen mode Exit fullscreen mode

Why it's better than regex: it's trained on labeled examples, not exact strings, so it generalizes to paraphrasing, typos, and novel phrasing it's never seen verbatim. It outputs a continuous score per category rather than a binary match, so you can set different thresholds for different contexts (a roleplay app might tolerate a higher "violence" score than a customer support bot).

Where it fails: classifiers inherit the biases and blind spots of their training data. They tend to over-flag reclaimed language, medical/educational content, and creative writing that discusses dark themes without endorsing them. They also add latency (an extra network round-trip, typically 100–300ms) and cost, since it's a second inference call on top of the main generation.

This is the layer most third-party "add moderation to your app" tutorials are actually describing. It's necessary, but it's still bolted on after the fact — the base model has no idea this check exists.

Layer 3: RLHF / alignment tuning baked into the model

This is the layer people usually mean when they say a model is "censored" or "aligned," and it's fundamentally different from the first two because it isn't a filter at all — it's part of the model's weights.

During RLHF (Reinforcement Learning from Human Feedback), human raters score model outputs, and the model is fine-tuned to produce more of the responses raters preferred and fewer of the ones they didn't — including refusals for requests judged unsafe. The refusal behavior isn't a separate system checking the output; it's a learned pattern baked directly into how the model predicts its next token. When an aligned model refuses, there's no external check being run — the model has simply learned that "I can't help with that" is the highest-reward continuation for that kind of prompt.

This is why "uncensored" doesn't mean "no filters." An uncensored/base model is one that either skipped this fine-tuning stage or was fine-tuned on a dataset without refusal examples. It's not that the safety layer was removed from a censored model — it's that the alignment training that would have created the refusal behavior never happened. Structurally, an "uncensored" open-weight model (something like a base Llama or Mistral checkpoint before instruction/safety tuning) and its "aligned" counterpart can have an identical architecture and near-identical weights outside the fine-tuning delta. The difference lives entirely in that last training stage.

Where it fails: RLHF alignment is famously brittle to distribution shift. Prompt structures the fine-tuning data didn't cover — role-play framing, hypothetical/fictional framing, translation into a low-resource language, token-level obfuscation — can slip past learned refusals because the model is pattern-matching against training examples, not reasoning about intent from first principles. This is the entire premise behind "jailbreak" prompting: finding an input distribution the alignment training didn't anticipate.

How production systems actually stack these

A serious chat platform doesn't pick one layer — it runs all three, in this order:

  1. Regex/keyword pass on the input — cheap, catches the obvious stuff, zero latency cost.
  2. Classifier pass on the input — catches paraphrased and novel-phrasing violations before they reach the model.
  3. The model's own RLHF-trained behavior — the model may refuse independent of the above two layers.
  4. Classifier pass on the output — because even an aligned model can be walked into an unsafe completion through clever prompting, so the output gets scored again before it's shown to the user.

Each layer catches what the others miss. Regex catches known-bad exact strings for free. The input classifier catches paraphrases regex can't match. The model's alignment training catches things neither prior layer flagged because the request only becomes unsafe in the context of a full conversation. The output classifier is the last line of defense for cases where the model got jailbroken despite the alignment training.

What this means if you're building on top of an LLM

If you're building a chat product, the practical takeaway is that "moderation" is an architecture decision, not a single API call:

  • If you're using a hosted, aligned model (GPT, Claude, Gemini), you're getting layer 3 for free, but you should still add layer 2 on your own input/output if you're accepting arbitrary user text, because no alignment training is jailbreak-proof.
  • If you're using an open-weight base model without safety tuning, you have none of layer 3 by default — all of your safety has to come from layers 1 and 2, which means your classifier thresholds need to be stricter and your regex list needs to be actively maintained, because you no longer have the model's own training as a backstop.
  • Latency budgets matter: a regex check is free, a classifier call adds a network round trip, and running that twice (input + output) doubles it. For real-time chat, that's often the difference between a snappy product and a sluggish one.

Curious what other builders are doing here — are you running your own classifier layer on top of a hosted model, or leaning entirely on the base model's alignment? And for anyone working with open-weight/uncensored models directly, what's your actual moderation stack look like?

Top comments (0)