How to Build an LLM Content Moderation Service
An LLM content moderation service sends each piece of text to the model with a system prompt that demands a JSON classification - safe, warning, or blocked - then wraps the result in a clean API. Here's how to build it.
The architecture
The service is a thin wrapper: FastAPI receives POST /api/moderate, calls the LLM
with a classifying system prompt, parses the JSON it returns, and sends back the
label plus a reasoning string. A second endpoint, POST /api/moderate/batch, runs
the same logic over a list of items.
The model does the judgment; your job is a clean request/response contract and error handling at every boundary.
The system prompt
The prompt is the product. It must tell the model exactly what labels to use and what format to return - otherwise you get prose instead of JSON:
SYSTEM_PROMPT = (
"You are a content moderator. Classify the user's content as one of: "
"safe, warning, or blocked. Respond with ONLY a JSON object of the form "
'{"classification": "safe|warning|blocked", "reasoning": "one short sentence"}. '
"No markdown, no prose, JSON only."
)
Calling the LLM and parsing the response
Use httpx to POST to the OpenAI-compatible endpoint, pull the model's reply, and
extract the JSON. Always strip markdown fences - models ignore "JSON only" sometimes:
import re, json, httpx
def classify(content: str) -> dict:
try:
with httpx.Client(timeout=60.0) as client:
r = client.post(
f"{LLM_API_URL}/v1/chat/completions",
json={
"model": "mock-llm",
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": content},
],
"temperature": 0,
"max_tokens": 100,
},
)
r.raise_for_status()
raw = r.json()["choices"][0]["message"]["content"]
except Exception as e:
return {"content": content, "classification": "safe",
"reasoning": f"moderator unavailable: {e}"}
# Strip fences, extract the first {...} block
m = re.search(r"\{.*\}", raw, re.DOTALL)
parsed = json.loads(m.group(0)) if m else {}
label = parsed.get("classification", "safe").lower()
if label not in {"safe", "warning", "blocked"}:
label = "safe"
return {"content": content, "classification": label,
"reasoning": parsed.get("reasoning", raw.strip())}
The endpoints
Wire both endpoints to the same classify helper:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
app = FastAPI()
class ModerationRequest(BaseModel):
content: str
class BatchModerationRequest(BaseModel):
items: List[str]
@app.post("/api/moderate")
def moderate_content(req: ModerationRequest):
return classify(req.content)
@app.post("/api/moderate/batch")
def moderate_batch(req: BatchModerationRequest):
return {"results": [classify(item) for item in req.items]}
Hardening the output
LLMs are inconsistent. After parsing, always validate the label against your allowed set and fall back to a default rather than passing unknown strings downstream. For production, add a cheap regex or keyword pre-filter before the LLM call - it handles obvious cases instantly and keeps inference costs down.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Writing a system prompt that forces structured JSON output from the LLM
- Calling an OpenAI-compatible LLM API with httpx and parsing the JSON response
- Building single and batch moderation endpoints with FastAPI and error handling
FAQ
How do I get an LLM to return a consistent JSON classification?
Put the exact JSON schema in the system prompt - label the allowed values and say 'JSON only, no markdown, no prose.' Then validate the label after parsing and fall back to a safe default if the model strays.
How do I build a batch moderation endpoint with an LLM?
Expose a POST endpoint that accepts a list of strings, iterate over the items calling the same single-item classify function for each, and return the results array. Keep it simple - parallel calls add complexity and the LLM is already the bottleneck.
What should I do when the LLM moderation call fails?
Wrap the httpx call in try/except and return a default classification (safe or an explicit error state) so a single upstream failure doesn't 500 the endpoint. Log the error and monitor the LLM failure rate separately.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →