LLM safety is defense in depth. No single check catches everything, so
you layer multiple detectors:
User Input
│
├── [1] Input Length Check (instant, catches overflow)
├── [2] Injection Detection (most critical, runs first)
├── [3] Toxicity Screening (content policy)
├── [4] PII Detection/Redaction (privacy protection)
│
▼
LLM Inference
│
├── [5] Output Toxicity Check (catches model-generated harm)
├── [6] Output PII Redaction (catches leaked data)
├── [7] Groundedness Check (catches hallucination)
├── [8] Hallucinated Contact Check (catches fake emails/URLs)
│
▼
User Output
from src.input_filter import InputFilter
from src.output_filter import OutputFilter
input_guard = InputFilter()
output_guard = OutputFilter()
def safe_chat(user_message: str) -> str:
# Check input
input_result = input_guard.check(user_message)
if not input_result.allowed:
return input_result.rejection_message
# Call your LLM
response = your_llm(input_result.filtered_text)
# Check output
output_result = output_guard.check(response)
if not output_result.safe:
return output_result.filtered_text # fallback message
return output_result.filtered_textfrom src.policy_engine import PolicyEngine, POLICY_PRESETS
# Choose a preset: "default", "strict", or "permissive"
engine = PolicyEngine(POLICY_PRESETS["default"])
def safe_chat(user_message: str, model_response: str) -> str:
input_decision, output_decision = engine.evaluate_conversation(
user_message=user_message,
model_response=model_response,
)
if input_decision.action == "block":
return "I can't process that request."
if output_decision.action == "block":
return "I'm sorry, I can't provide that response."
# Use the processed text (PII redacted if applicable)
return output_decision.processed_text| Preset | Use Case | False Positive Rate | Protection Level |
|---|---|---|---|
| default | General chatbots, internal tools | Low | Moderate |
| strict | Healthcare, finance, children, legal | High | Maximum |
| permissive | Creative writing, research tools | Very Low | Minimal |
The presets cover 80% of use cases. Create a custom policy when:
The most common false positives:
| Pattern | False Positive | Solution |
|---|---|---|
| Phone numbers | Product SKUs, order numbers | Raise confidence_threshold to 0.70 |
| SSN format | Version numbers, build IDs | Context-aware — penalizes "version" prefix |
| Email addresses | example.com addresses | Already handled — low confidence for test domains |
| IP addresses | Localhost, documentation ranges | Already handled — penalizes 127.x, 192.0.2.x |
False positives usually come from:
Solutions:
risk_threshold to 0.60-0.70 for technical-audience applicationsThe keyword-based approach misses:
For production systems with high stakes, combine this with a
transformer-based toxicity classifier.
| Vector | Detection Layer | Coverage | ||
|---|---|---|---|---|
| Direct override ("ignore instructions") | Known patterns | High | ||
| Role reassignment ("you are now DAN") | Known patterns + structural | High | ||
| Delimiter injection (`< | system | >`) | Known patterns | High |
| Conversation injection (fake dialogue) | Structural analysis | Medium | ||
| Base64 encoded payloads | Encoding detection | Medium | ||
| Unicode homoglyph substitution | Encoding detection | Medium | ||
| Zero-width character hiding | Encoding detection | High | ||
| Context overflow (padding attacks) | Anomaly detection | Low-Medium |
| PII Type | Pattern | Confidence |
|---|---|---|
| Email addresses | RFC-compliant regex | 95% base |
| US phone numbers | Multiple formats | 70-90% |
| International phones | +CC format | 80% base |
| Credit cards | Regex + Luhn validation | 60-90% |
| SSN (US format) | xxx-xx-xxxx with restrictions | 75-95% |
| IPv4 addresses | Standard dotted notation | 60% base |
| Passport numbers | Letter+digits pattern | 50% base |
| Category | Detection | Limitations |
|---|---|---|
| Threats/violence | Keyword patterns | Misses implicit threats |
| Self-harm | Keyword patterns | Cannot assess intent |
| Harassment | Pattern matching | Misses subtle bullying |
| Hate speech | Pattern matching | Limited cultural coverage |
| Profanity | Keyword matching | Configurable severity |
The policy engine logs every decision. Set up structured logging:
import logging
# Configure JSON logging for production
logging.basicConfig(
level=logging.INFO,
format='{"time":"%(asctime)s","level":"%(levelname)s","msg":"%(message)s"}',
)Every PolicyDecision includes:
timestamp — when the decision was madepolicy_name — which policy was appliedaction — allow/redact/flag/blocktriggered_rules — which rules fireddetails — per-rule scores and metadataAll checks run on Python stdlib with regex-based detection. Typical latency:
| Check | Latency (1KB input) | Notes |
|---|---|---|
| PII detection | < 1ms | Regex is fast |
| Injection detection | < 2ms | 4 layers, all regex |
| Toxicity detection | < 1ms | Keyword matching |
| Groundedness check | < 5ms | Entity extraction |
| Full policy evaluation | < 10ms | All checks combined |
For comparison, a single LLM inference call takes 200ms-5s. Safety
checks add negligible overhead.
1. No ML models: This toolkit uses heuristics, not trained classifiers.
Accuracy is lower than transformer-based approaches but latency is
orders of magnitude better.
2. English-centric: Patterns are designed for English text. Other
languages need custom keyword lists.
3. Evolving attacks: New jailbreak techniques appear weekly. Update
the known patterns regularly.
4. Context blindness: The detectors analyze text in isolation. They
don't understand conversation history or user intent.
5. Not a replacement for alignment: Safety guardrails are a
defense layer, not a substitute for proper model training and RLHF.