All Articles
guide

How to Build a Moderation API Without Any Labelled Data

Use an LLM as a teacher to generate candidate training data, then evaluate a moderation classifier without requiring a historical customer dataset.

Peter Dobson10 July 20269 min read

TL;DR

You do not need to start with thousands of hand-labelled examples to evaluate a content moderation classifier. An LLM can generate synthetic examples offline; Sparkient's controlled synthetic content-moderation run reported 0.900 macro F1, 91.5% accuracy, and 41ms batch-average time per item. Define the policy, generate or add data, train, evaluate, and deploy only if it clears the project's safety thresholds.

The Problem: No Data, No Model, No Time

You're building a platform with user-generated content. You know you need moderation. But you have a cold-start problem:

  • No labelled data. Your platform is new, or you've never systematically tagged content.
  • No ML team. You're a product engineer, not a data scientist.
  • No time. You need moderation working this quarter, not next year.

A traditional ML path can require collection, annotation, training, deployment, and monitoring. A live LLM avoids some setup but adds model latency, token usage, and a provider dependency to every request. Measure those costs on the actual project rather than assuming a universal timeline or bill.

You need a third option: generate the training data synthetically, train a fast classifier, and deploy it at production speeds.

The "LLM as Teacher" Approach

The core insight is simple: an LLM already knows how to moderate content. It's been trained on enough internet to understand sarcasm, coded language, harassment patterns, and edge cases. You just can't afford to call it on every message.

So instead of using the LLM on every production request, use it offline to help teach a smaller model:

  1. Define your moderation policy — what gets approved, what needs review, what gets rejected
  2. Write rules for obvious cases — blocklists, rate limits, known patterns
  3. Use the LLM to generate synthetic examples — diverse, edge-case-heavy training data
  4. Train a classifier on that synthetic data — a small model that encodes the LLM's judgement
  5. Deploy the compiled model — sub-100ms decisions in production

The LLM is the teacher. The classifier is the student. Once the student graduates, the teacher goes home.

Step 1: Define Your Moderation Policy

Before generating any data, you need clarity on what you're moderating. This means defining:

  • Decision options: What actions can the system take?
  • Input schema: What data does the system see?
  • Rules: What hard constraints always apply?

For a typical UGC platform:

python
moderation_config = {
    "name": "content-moderation",
    "description": "Moderate user-generated posts on a community platform",
    "options": ["approve", "review", "reject"],
    "input_schema": {
        "type": "object",
        "properties": {
            "text": {"type": "string", "description": "The post content"},
            "user_id": {"type": "string"},
            "account_age_days": {"type": "integer"},
            "previous_violations": {"type": "integer"}
        },
        "required": ["text"]
    }
}

A three-way policy (approve/review/reject) can be more useful than binary allow/block when the operation has a real human-review path. The review bucket creates a pressure valve for ambiguous content, but its threshold and review capacity still need evaluation.

Step 2: Write Rules for Obvious Cases

Some moderation decisions don't need ML at all. A message containing a phone number in a dating app should be flagged. A user with 5 previous violations posting a link should be reviewed. These are business rules, not pattern recognition.

CEL (Common Expression Language) rules handle this layer:

cel
// Auto-reject if user has too many violations
ctx.previous_violations >= 5 ? "reject" : null

// Auto-review new accounts posting links
ctx.account_age_days < 7 && ctx.text.contains("http") ? "review" : null

// Auto-approve very short, clean content from established users
ctx.account_age_days > 90 && size(ctx.text) < 50 ? "approve" : null

Rules usually execute in under 1ms and handle deterministic cases. The classifier handles cases that do not match a rule.

Step 3: Generate Synthetic Training Data

This is where the LLM earns its keep. Instead of hand-labelling thousands of examples, you ask the LLM to generate realistic content for each category.

The DIY approach — calling the LLM yourself:

python
import google.generativeai as genai

genai.configure(api_key="YOUR_GEMINI_KEY")
model = genai.GenerativeModel("gemini-2.5-flash")

prompt = """Generate 50 realistic examples of user-generated content for
a community platform. For each example, provide the text and the correct
moderation decision (approve, review, or reject).

Guidelines:
- "approve": Normal discussion, questions, opinions, humor that isn't harmful
- "review": Borderline content — mild insults, possible spam, ambiguous intent
- "reject": Clear harassment, hate speech, explicit threats, obvious spam

Include edge cases: sarcasm, coded language, passionate-but-acceptable debate,
subtle manipulation, and context-dependent content.

Format as JSON array with "text" and "label" fields.
"""

response = model.generate_content(prompt)

When automatic generation is enabled, Sparkient builds examples for every configured outcome. The appropriate dataset size is task-specific; use a fixed held-out set to decide whether more examples help.

What makes good synthetic data:

  • Diversity: Multiple tones, lengths, topics, and communication styles
  • Edge cases: Content that sits on the boundary between categories
  • Class balance: Roughly equal numbers per category (you can oversample rare classes)
  • Realism: Content that sounds like real users, not textbook examples

The automated approach — Sparkient handles all of this. You define the decision type, and the pipeline generates, labels, augments for rare classes, and validates the data automatically:

python
import httpx

# Create the decision type
response = httpx.post(
    "https://api.sparkient.ai/api/v1/decision-types",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "name": "content-moderation",
        "description": "Moderate user-generated posts on a community platform",
        "options": ["approve", "review", "reject"],
        "input_schema": {
            "type": "object",
            "properties": {
                "text": {"type": "string"},
                "user_id": {"type": "string"},
                "account_age_days": {"type": "integer"},
                "previous_violations": {"type": "integer"}
            },
            "required": ["text"]
        }
    }
)
decision_type_id = response.json()["id"]

Step 4: Train the Classifier

With synthetic data in hand, you train a fast classifier. The model architecture matters: you need something that handles text well but runs in milliseconds, not seconds.

The DIY approach — manual pipeline with scikit-learn:

python
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
import json

# Load your synthetic data
with open("synthetic_data.json") as f:
    data = json.load(f)

texts = [d["text"] for d in data]
labels = [d["label"] for d in data]

# Simple TF-IDF + gradient boosting
vectorizer = TfidfVectorizer(max_features=5000, ngram_range=(1, 2))
X = vectorizer.fit_transform(texts)
X_train, X_test, y_train, y_test = train_test_split(X, labels, test_size=0.2)

clf = GradientBoostingClassifier(n_estimators=200)
clf.fit(X_train, y_train)
print(classification_report(y_test, y_train))

This gets you a baseline, but TF-IDF misses semantic meaning. "You're so smart" (genuine) and "You're so smart" (sarcastic) look identical.

What Sparkient does differently: The training pipeline uses a semantic text encoder to create high-dimensional embeddings, then feeds those into a gradient-boosted classifier with automated hyperparameter tuning. This captures semantic features beyond word frequency. In the published synthetic content-moderation run, the result was 0.900 macro F1 and 91.5% accuracy.

Trigger training with a single API call:

python
response = httpx.post(
    f"https://api.sparkient.ai/api/v1/decision-types/{decision_type_id}/train",
    headers={"Authorization": "Bearer YOUR_API_KEY"}
)
# Training runs asynchronously — check status via the training endpoint

Step 5: Deploy and Call

Once training completes, the model is compiled to ONNX and deployed automatically. You call it like any API:

python
response = httpx.post(
    "https://api.sparkient.ai/api/v1/decide",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "decision_type": "content-moderation",
        "input": {
            "text": "Check out this amazing product! Click here for 90% off!!!",
            "user_id": "user_456",
            "account_age_days": 2,
            "previous_violations": 0
        }
    }
)

result = response.json()
# {
#     "decision": "review",
#     "confidence": 0.87,
#     "latency_ms": 38,
#     "stage": "classifier"
# }

The response tells you exactly what happened:

  • decision: The moderation verdict
  • confidence: How sure the model is (0-1)
  • latency_ms: Measured end-to-end decision time; the compiled path targets under 100ms, while escalated requests are model dependent
  • stage: Which pipeline stage made the call (rules, classifier, or escalation)

What About Accuracy?

The natural concern: "Synthetic data can't be as good as real data, right?"

It depends on the task. For content moderation, Sparkient's benchmarks show:

| Metric | Compiled Model | Best ML baseline | |--------|---------------|------------------| | Macro F1 | 0.900 | 0.606 | | Accuracy | 91.5% | 67.9% | | Batch-average time per item | 41ms | Controlled synthetic run; not per-request p95 |

This run validates an improvement of 29.4 F1 points over the best traditional ML baseline. It does not prove parity with a live LLM or a universal saving. Route uncertain or high-risk cases according to a policy you test explicitly.

Improving Over Time

Synthetic data gets you from zero to production. Real data makes you better. As your moderation system runs:

  1. Log decisions — Sparkient stores every decision with confidence scores
  2. Review low-confidence calls — Human review can turn selected outcomes into corrected examples
  3. Retrain periodically — Add real examples to your training set and retrain
  4. Tighten rules — As you spot patterns, add CEL rules for instant handling

Retraining can improve accuracy when the reviewed examples are representative and correctly labelled, but improvement is not automatic; compare every candidate model on a fixed evaluation set before deployment.

FAQ

How many synthetic examples do I need? Start with the configured default, inspect per-class coverage and errors, and increase the target only when the evaluation supports it. Sparkient's gap analysis can generate examples for underrepresented patterns, but more synthetic data is not guaranteed to improve the model.

What if my moderation policy is unusual? That's actually where this approach shines. Generic moderation APIs enforce someone else's policy. With the teacher approach, you describe your specific policy — maybe you allow strong language but not personal attacks, or you need to catch financial spam specifically. The LLM generates data matching your rules.

Can I add my own examples alongside synthetic ones? Yes. The Sparkient API lets you upload reviewed examples alongside generated data. Add representative difficult cases, retrain, and use the fixed evaluation set to determine whether quality actually improves.

What happens when the model isn't confident? If optional escalation is enabled, decisions below the configured confidence threshold can call the LLM in real time. Measure combined quality, escalation rate, latency, and credits on representative content instead of assuming a standard percentage.


You don't need labelled data to build accurate content moderation. You need a clear policy and a good teacher.

Start with the free tier — 5,000 credits, no credit card required. Define the policy, train a candidate, and compare it with representative held-out content.

Ready to get started?

Start with 5,000 free credits and 250 decisions. No credit card required.

Start Free