10. Qualitätsprüfung
Die Qualitätsprüfung verhindert, dass ein generierter Beitrag direkt in die Review-Queue oder in den Veröffentlichungsprozess gelangt, obwohl er technisch oder inhaltlich ungeeignet ist.
Das Quality-Gate prüft nicht, ob ein Beitrag brillant ist. Es prüft, ob er veröffentlichbar ist.
PostDraft
↓
harte Ausschlussregeln
↓
Zeichenlimit
↓
Link-Prüfung
↓
Wiederholungsprüfung
↓
Tonalitätsprüfung
↓
Verkaufsdruck-Prüfung
↓
draft bleibt draft oder wird quality_failed
10.1 Harte Ausschlussregeln
Harte Ausschlussregeln laufen ohne KI. Sie sind deterministisch.
Ein Beitrag wird abgelehnt, wenn:
Text leer ist
Text nur aus Leerzeichen besteht
Plattform unbekannt ist
Beitragstyp unbekannt ist
Pflichtbild fehlt
Pflichtlink fehlt
verbotene Formulierung enthalten ist
Text zu lang ist
Text technische Platzhalter enthält
Text interne Markierungen enthält
Beispiele für verbotene Fragmente:
Jetzt kaufen
Unbedingt lesen
Tauchen Sie ein
Meisterwerk
Ein Muss
Lassen Sie sich verzaubern
{{
}}
TODO
Lorem ipsum
Python:
class HardRejectRule:
def __init__(self) -> None:
self.blocked_fragments = [
"jetzt kaufen",
"unbedingt lesen",
"tauchen sie ein",
"meisterwerk",
"ein muss",
"lassen sie sich verzaubern",
"{{",
"}}",
"todo",
"lorem ipsum",
]
def validate(self, draft: "PostDraft") -> None:
text = draft.text.strip()
if text == "":
raise QualityGateError("Post text is empty.")
platform_names = [
"linkedin",
"instagram",
"facebook",
]
if draft.platform not in platform_names:
raise QualityGateError("Unsupported platform: " + draft.platform)
post_types = [
"book_intro",
"quote",
"workshop",
"blog_summary",
"comment",
]
if draft.post_type not in post_types:
raise QualityGateError("Unsupported post type: " + draft.post_type)
lowered_text = text.lower()
for blocked_fragment in self.blocked_fragments:
if blocked_fragment in lowered_text:
raise QualityGateError("Blocked fragment found: " + blocked_fragment)
if draft.platform == "instagram" and draft.image_path is None:
raise QualityGateError("Instagram post requires image_path.")
Fehlerklasse:
class QualityGateError(Exception):
pass
10.2 Generik-Erkennung
Generische Beiträge sind formal korrekt, aber wertlos.
Typische Muster:
Spannende Einblicke
Ein zeitloses Werk
Ein Klassiker, der nichts von seiner Aktualität verloren hat
Perfekt für alle Literaturfreunde
Dieses Buch nimmt Sie mit auf eine Reise
Deterministische Prüfung:
class GenericPhraseValidator:
def __init__(self) -> None:
self.generic_fragments = [
"zeitloses werk",
"nichts von seiner aktualität verloren",
"spannende einblicke",
"literaturfreunde",
"nimmt sie mit auf eine reise",
"faszinierende geschichte",
"unvergessliche figuren",
"perfekt für",
"für alle fans von",
]
def validate(self, text: str) -> None:
lowered_text = text.lower()
for fragment in self.generic_fragments:
if fragment in lowered_text:
raise QualityGateError("Generic phrase found: " + fragment)
Zusätzlich kann OpenAI eine zweite Prüfung übernehmen.
Prompt:
Du prüfst, ob ein Social-Media-Beitrag generisch klingt.
Beitrag:
{{ post_text }}
Kriterien:
- allgemeine Floskeln
- austauschbare Buchwerbung
- keine konkrete Aussage
- kein eigener Gedanke
- kein erkennbarer Bezug zu Buch, Verlag oder Thema
Antworte ausschließlich im JSON-Format.
Schema:
GENERIC_CHECK_SCHEMA: dict[str, object] = {
"type": "object",
"properties": {
"is_generic": {
"type": "boolean"
},
"reason": {
"type": "string"
}
},
"required": [
"is_generic",
"reason"
],
"additionalProperties": False
}
10.3 Wiederholungsprüfung
Wiederholung entsteht auf zwei Ebenen:
identischer Text
inhaltlich gleicher Gedanke
Deterministische Prüfung gegen bereits veröffentlichte Beiträge:
class ExactDuplicateValidator:
def validate(self, new_text: str, previous_texts: list[str]) -> None:
normalized_new_text = self._normalize(new_text)
for previous_text in previous_texts:
normalized_previous_text = self._normalize(previous_text)
if normalized_new_text == normalized_previous_text:
raise QualityGateError("Exact duplicate found.")
def _normalize(self, text: str) -> str:
lowered_text = text.lower()
collapsed_text = " ".join(lowered_text.split())
return collapsed_text
Einfache Ähnlichkeitsprüfung ohne externe Bibliothek:
class SimilarityValidator:
def validate(self, new_text: str, previous_texts: list[str], max_similarity: float) -> None:
new_words = self._words(new_text)
for previous_text in previous_texts:
previous_words = self._words(previous_text)
similarity = self._jaccard(new_words, previous_words)
if similarity >= max_similarity:
raise QualityGateError("Similar post found.")
def _words(self, text: str) -> set[str]:
lowered_text = text.lower()
cleaned_text = ""
for character in lowered_text:
if character.isalnum() is True or character == " ":
cleaned_text = cleaned_text + character
else:
cleaned_text = cleaned_text + " "
words = set(cleaned_text.split())
return words
def _jaccard(self, left_words: set[str], right_words: set[str]) -> float:
if len(left_words) == 0 and len(right_words) == 0:
return 1.0
intersection = left_words.intersection(right_words)
union = left_words.union(right_words)
return len(intersection) / len(union)
Empfohlener Schwellenwert:
0.65
Für spätere Versionen kann OpenAI zusätzlich prüfen, ob der Gedanke trotz anderer Formulierung identisch ist.
10.4 Link-Prüfung
Links dürfen nicht blind übernommen werden.
Zu prüfen:
Link vorhanden, wenn erforderlich
Link beginnt mit https://
Link enthält keine Leerzeichen
Link gehört zu erlaubter Domain
Link ist nicht offensichtlich kaputt
Für Social-Media-Posts reicht zunächst eine formale Prüfung.
from urllib.parse import urlparse
class LinkValidator:
def __init__(self, allowed_domains: list[str]) -> None:
self.allowed_domains = allowed_domains
def validate_optional(self, url: str | None) -> None:
if url is None:
return
self._validate_url(url)
def validate_required(self, url: str | None) -> None:
if url is None:
raise QualityGateError("Required URL is missing.")
self._validate_url(url)
def _validate_url(self, url: str) -> None:
cleaned_url = url.strip()
if cleaned_url == "":
raise QualityGateError("URL is empty.")
if " " in cleaned_url:
raise QualityGateError("URL contains spaces.")
parsed_url = urlparse(cleaned_url)
if parsed_url.scheme != "https":
raise QualityGateError("URL must use https.")
if parsed_url.netloc == "":
raise QualityGateError("URL host is missing.")
if parsed_url.netloc not in self.allowed_domains:
raise QualityGateError("URL domain is not allowed: " + parsed_url.netloc)
Beispiel:
validator = LinkValidator(
allowed_domains=[
"xyz.de",
"www.xyz.de",
"abc.com",
"www.abc.com",
]
)
HTTP-Prüfung kann später ergänzt werden. Für ein schnelles MVP reicht die formale Prüfung, weil sie keine Netzwerkfehler erzeugt.
10.5 Zeichenlimit-Prüfung
Plattformlimits sollten absichtlich niedriger gesetzt werden als die technischen Maximallimits.
Empfohlene interne Limits:
LinkedIn: 900 Zeichen
Instagram: 700 Zeichen
Facebook: 1.200 Zeichen
Kommentar: 500 Zeichen
Antwort auf Nutzerkommentar: 600 Zeichen
Python:
class CharacterLimitValidator:
def __init__(self) -> None:
self.platform_limits = {
"linkedin": 900,
"instagram": 700,
"facebook": 1200,
}
self.post_type_limits = {
"comment": 500,
"comment_reply": 600,
}
def validate(self, platform: str, post_type: str, text: str) -> None:
limit = self._get_limit(platform, post_type)
if len(text) > limit:
raise QualityGateError(
"Text exceeds character limit. Limit: "
+ str(limit)
+ ", actual: "
+ str(len(text))
)
def _get_limit(self, platform: str, post_type: str) -> int:
post_type_limit = self.post_type_limits.get(post_type)
if post_type_limit is not None:
return post_type_limit
platform_limit = self.platform_limits.get(platform)
if platform_limit is None:
raise QualityGateError("Missing character limit for platform: " + platform)
return platform_limit
10.6 Tonalitätsprüfung
Die Tonalitätsprüfung kann zweistufig erfolgen:
harte Phrase-Prüfung im Code
semantische Prüfung per OpenAI
Deterministische Prüfung:
class ToneValidator:
def __init__(self) -> None:
self.rejected_fragments = [
"unglaublich",
"fantastisch",
"wunderschön",
"magisch",
"bezaubernd",
"atemberaubend",
"einzigartig",
"sensationell",
]
def validate(self, text: str) -> None:
lowered_text = text.lower()
for fragment in self.rejected_fragments:
if fragment in lowered_text:
raise QualityGateError("Rejected tone fragment found: " + fragment)
OpenAI-Prüfung:
Du prüfst die Tonalität eines Social-Media-Beitrags für einen kleinen deutschen E-Book-Verlag.
Gewünschte Tonalität:
- sachlich
- professionell
- kultiviert
- nicht anbiedernd
- nicht künstlich emotional
- nicht werblich
- keine Floskeln
Beitrag:
{{ post_text }}
Antworte ausschließlich als JSON.
Schema:
TONE_CHECK_SCHEMA: dict[str, object] = {
"type": "object",
"properties": {
"passed": {
"type": "boolean"
},
"tone_score": {
"type": "number"
},
"reason": {
"type": "string"
}
},
"required": [
"passed",
"tone_score",
"reason"
],
"additionalProperties": False
}
Validator für das Ergebnis:
from typing import Any
class ToneCheckResultValidator:
def validate(self, payload: dict[str, Any]) -> None:
passed = payload.get("passed")
tone_score = payload.get("tone_score")
reason = payload.get("reason")
if isinstance(passed, bool) is False:
raise QualityGateError("tone_check.passed must be boolean.")
if isinstance(tone_score, int) is False and isinstance(tone_score, float) is False:
raise QualityGateError("tone_check.tone_score must be number.")
if isinstance(reason, str) is False:
raise QualityGateError("tone_check.reason must be string.")
if passed is False:
raise QualityGateError("Tone check failed: " + reason)
10.7 Verkaufsdruck-Prüfung
Verkaufsdruck beschädigt die Verlagsstimme. Die Prüfung sollte streng sein.
Blockierte Muster:
Jetzt kaufen
Hier bestellen
Nur heute
Sichern Sie sich
Greifen Sie zu
Ein Muss für alle
Unbedingt lesen
Python:
class SalesPressureValidator:
def __init__(self) -> None:
self.blocked_fragments = [
"jetzt kaufen",
"hier bestellen",
"nur heute",
"sichern sie sich",
"greifen sie zu",
"ein muss für alle",
"unbedingt lesen",
"kaufen sie",
"bestellen sie",
]
def validate(self, text: str) -> None:
lowered_text = text.lower()
for fragment in self.blocked_fragments:
if fragment in lowered_text:
raise QualityGateError("Sales pressure found: " + fragment)
Zulässig sind dezente Hinweise:
Die Ausgabe ist im Shop verfügbar.
Mehr dazu im Blog.
Weitere Titel stehen im Verlagsprogramm.
Bei Kommentaren sollte jeder Verkaufslink ausgeschlossen werden.
class CommentSalesLinkValidator:
def validate(self, post_type: str, text: str) -> None:
if post_type != "comment" and post_type != "comment_reply":
return
lowered_text = text.lower()
if "https://" in lowered_text:
raise QualityGateError("Comments must not contain links.")
if "shop" in lowered_text:
raise QualityGateError("Comments must not contain shop references.")
10.8 Beispiel: quality_gate.py
Datei:
src/quality_gate.py
from dataclasses import dataclass
from typing import Any
from urllib.parse import urlparse
class QualityGateError(Exception):
pass
@dataclass(frozen=True)
class QualityGateResult:
passed: bool
reason: str
checks: dict[str, bool]
@dataclass(frozen=True)
class PostDraft:
id: str
source_type: str
source_id: str
platform: str
post_type: str
status: str
text: str
image_path: str | None
target_url: str | None
prompt_name: str
prompt_version: str
model: str
quality_result: dict[str, Any] | None
created_at: str
updated_at: str
class HardRejectRule:
def __init__(self) -> None:
self.blocked_fragments = [
"jetzt kaufen",
"unbedingt lesen",
"tauchen sie ein",
"meisterwerk",
"ein muss",
"lassen sie sich verzaubern",
"{{",
"}}",
"todo",
"lorem ipsum",
]
def validate(self, draft: PostDraft) -> None:
text = draft.text.strip()
if text == "":
raise QualityGateError("Post text is empty.")
platform_names = [
"linkedin",
"instagram",
"facebook",
]
if draft.platform not in platform_names:
raise QualityGateError("Unsupported platform: " + draft.platform)
post_types = [
"book_intro",
"quote",
"workshop",
"blog_summary",
"comment",
"comment_reply",
]
if draft.post_type not in post_types:
raise QualityGateError("Unsupported post type: " + draft.post_type)
lowered_text = text.lower()
for blocked_fragment in self.blocked_fragments:
if blocked_fragment in lowered_text:
raise QualityGateError("Blocked fragment found: " + blocked_fragment)
if draft.platform == "instagram" and draft.image_path is None:
raise QualityGateError("Instagram post requires image_path.")
class CharacterLimitValidator:
def __init__(self) -> None:
self.platform_limits = {
"linkedin": 900,
"instagram": 700,
"facebook": 1200,
}
self.post_type_limits = {
"comment": 500,
"comment_reply": 600,
}
def validate(self, platform: str, post_type: str, text: str) -> None:
limit = self._get_limit(platform, post_type)
if len(text) > limit:
raise QualityGateError(
"Text exceeds character limit. Limit: "
+ str(limit)
+ ", actual: "
+ str(len(text))
)
def _get_limit(self, platform: str, post_type: str) -> int:
post_type_limit = self.post_type_limits.get(post_type)
if post_type_limit is not None:
return post_type_limit
platform_limit = self.platform_limits.get(platform)
if platform_limit is None:
raise QualityGateError("Missing character limit for platform: " + platform)
return platform_limit
class LinkValidator:
def __init__(self, allowed_domains: list[str]) -> None:
self.allowed_domains = allowed_domains
def validate_optional(self, url: str | None) -> None:
if url is None:
return
self._validate_url(url)
def validate_required(self, url: str | None) -> None:
if url is None:
raise QualityGateError("Required URL is missing.")
self._validate_url(url)
def _validate_url(self, url: str) -> None:
cleaned_url = url.strip()
if cleaned_url == "":
raise QualityGateError("URL is empty.")
if " " in cleaned_url:
raise QualityGateError("URL contains spaces.")
parsed_url = urlparse(cleaned_url)
if parsed_url.scheme != "https":
raise QualityGateError("URL must use https.")
if parsed_url.netloc == "":
raise QualityGateError("URL host is missing.")
if parsed_url.netloc not in self.allowed_domains:
raise QualityGateError("URL domain is not allowed: " + parsed_url.netloc)
class GenericPhraseValidator:
def __init__(self) -> None:
self.generic_fragments = [
"zeitloses werk",
"nichts von seiner aktualität verloren",
"spannende einblicke",
"literaturfreunde",
"nimmt sie mit auf eine reise",
"faszinierende geschichte",
"unvergessliche figuren",
"perfekt für",
"für alle fans von",
]
def validate(self, text: str) -> None:
lowered_text = text.lower()
for fragment in self.generic_fragments:
if fragment in lowered_text:
raise QualityGateError("Generic phrase found: " + fragment)
class ExactDuplicateValidator:
def validate(self, new_text: str, previous_texts: list[str]) -> None:
normalized_new_text = self._normalize(new_text)
for previous_text in previous_texts:
normalized_previous_text = self._normalize(previous_text)
if normalized_new_text == normalized_previous_text:
raise QualityGateError("Exact duplicate found.")
def _normalize(self, text: str) -> str:
lowered_text = text.lower()
collapsed_text = " ".join(lowered_text.split())
return collapsed_text
class SimilarityValidator:
def validate(self, new_text: str, previous_texts: list[str], max_similarity: float) -> None:
new_words = self._words(new_text)
for previous_text in previous_texts:
previous_words = self._words(previous_text)
similarity = self._jaccard(new_words, previous_words)
if similarity >= max_similarity:
raise QualityGateError("Similar post found.")
def _words(self, text: str) -> set[str]:
lowered_text = text.lower()
cleaned_text = ""
for character in lowered_text:
if character.isalnum() is True or character == " ":
cleaned_text = cleaned_text + character
else:
cleaned_text = cleaned_text + " "
return set(cleaned_text.split())
def _jaccard(self, left_words: set[str], right_words: set[str]) -> float:
if len(left_words) == 0 and len(right_words) == 0:
return 1.0
intersection = left_words.intersection(right_words)
union = left_words.union(right_words)
return len(intersection) / len(union)
class ToneValidator:
def __init__(self) -> None:
self.rejected_fragments = [
"unglaublich",
"fantastisch",
"wunderschön",
"magisch",
"bezaubernd",
"atemberaubend",
"einzigartig",
"sensationell",
]
def validate(self, text: str) -> None:
lowered_text = text.lower()
for fragment in self.rejected_fragments:
if fragment in lowered_text:
raise QualityGateError("Rejected tone fragment found: " + fragment)
class SalesPressureValidator:
def __init__(self) -> None:
self.blocked_fragments = [
"jetzt kaufen",
"hier bestellen",
"nur heute",
"sichern sie sich",
"greifen sie zu",
"ein muss für alle",
"unbedingt lesen",
"kaufen sie",
"bestellen sie",
]
def validate(self, text: str) -> None:
lowered_text = text.lower()
for fragment in self.blocked_fragments:
if fragment in lowered_text:
raise QualityGateError("Sales pressure found: " + fragment)
class CommentSalesLinkValidator:
def validate(self, post_type: str, text: str) -> None:
if post_type != "comment" and post_type != "comment_reply":
return
lowered_text = text.lower()
if "https://" in lowered_text:
raise QualityGateError("Comments must not contain links.")
if "shop" in lowered_text:
raise QualityGateError("Comments must not contain shop references.")
class QualityGate:
def __init__(self) -> None:
self.hard_reject_rule = HardRejectRule()
self.character_limit_validator = CharacterLimitValidator()
self.link_validator = LinkValidator(
allowed_domains=[
"xyz.de",
"www.xyz.de",
"abc.com",
"www.abc.com",
]
)
self.generic_phrase_validator = GenericPhraseValidator()
self.exact_duplicate_validator = ExactDuplicateValidator()
self.similarity_validator = SimilarityValidator()
self.tone_validator = ToneValidator()
self.sales_pressure_validator = SalesPressureValidator()
self.comment_sales_link_validator = CommentSalesLinkValidator()
def check(self, draft: PostDraft, previous_texts: list[str]) -> QualityGateResult:
checks: dict[str, bool] = {
"hard_reject": False,
"character_limit": False,
"link": False,
"generic": False,
"duplicate": False,
"similarity": False,
"tone": False,
"sales_pressure": False,
"comment_sales_link": False,
}
try:
self.hard_reject_rule.validate(draft)
checks["hard_reject"] = True
self.character_limit_validator.validate(
platform=draft.platform,
post_type=draft.post_type,
text=draft.text,
)
checks["character_limit"] = True
self.link_validator.validate_optional(draft.target_url)
checks["link"] = True
self.generic_phrase_validator.validate(draft.text)
checks["generic"] = True
self.exact_duplicate_validator.validate(draft.text, previous_texts)
checks["duplicate"] = True
self.similarity_validator.validate(
new_text=draft.text,
previous_texts=previous_texts,
max_similarity=0.65,
)
checks["similarity"] = True
self.tone_validator.validate(draft.text)
checks["tone"] = True
self.sales_pressure_validator.validate(draft.text)
checks["sales_pressure"] = True
self.comment_sales_link_validator.validate(
post_type=draft.post_type,
text=draft.text,
)
checks["comment_sales_link"] = True
return QualityGateResult(
passed=True,
reason="passed",
checks=checks,
)
except QualityGateError as exception:
return QualityGateResult(
passed=False,
reason=str(exception),
checks=checks,
)
Beispiel: Aufruf
from src.quality_gate import PostDraft
from src.quality_gate import QualityGate
def main() -> None:
draft = PostDraft(
id="draft_20260512_001",
source_type="manual_topic",
source_id="topic_001",
platform="linkedin",
post_type="workshop",
status="draft",
text="Gemeinfreie Texte sind frei verfügbar, aber nicht automatisch verlagsfertig. OCR-Korrektur, Typografie und Metadaten bleiben echte Arbeit.",
image_path=None,
target_url="https://www.xyz.de",
prompt_name="workshop_note",
prompt_version="1.0.0",
model="gpt-4.1-mini",
quality_result=None,
created_at="2026-05-12T09:00:00+02:00",
updated_at="2026-05-12T09:00:00+02:00",
)
previous_texts = [
"Gemeinfreie Literatur ist frei verfügbar, aber daraus entsteht noch keine gute digitale Ausgabe."
]
quality_gate = QualityGate()
result = quality_gate.check(draft, previous_texts)
print(result)
if __name__ == "__main__":
main()
Ergebnis dieses Kapitels
Das Quality-Gate prüft jetzt:
harte Ausschlussregeln
Generik
Wiederholung
Links
Zeichenlimits
Tonalität
Verkaufsdruck
Kommentarlinks
Damit wird aus einem rohen KI-Entwurf noch kein veröffentlichter Beitrag, sondern erst ein technisch und stilistisch geprüfter Kandidat für die Review-Queue.
0 Kommentare