WhatsApp 对话内容的质量评估体系与自动化检测方案
目录
- 为什么需要系统化的内容质量评估
- 内容质量的核心评估维度
- 基于规则的内容质量打分引擎
- AI 辅助的内容质量检测与反馈闭环
- 质量数据的趋势分析与阈值告警
- 实际落地经验
- 小结
1. 为什么需要系统化的内容质量评估
WhatsApp Business 发消息跟发邮件有个本质区别:你的发送质量直接决定了账号的生存状态。打开率太低?平台会降低你的送达权重。收到举报?可能直接触发限制。模板被标记为低质量?新消息的送达到达率会断崖下跌。
但"内容质量"这个东西很抽象。运营同学写完一段话,主观觉得没问题就发出去了。等到一周后看数据才发现:某类消息的打开率只有 8%,某类消息的退订率是平均值的 3 倍。这时候再改已经晚了,负面影响已经沉淀到账号的健康度里了。
我们遇到过的具体问题包括:
- A/B 测试缺失:同一个营销活动用了 3 套不同文案,但没有系统记录每套文案的后续数据表现,无法判断哪套更好。
- 问题发现滞后:用户举报激增了 3 天之后才在后台看到,追溯回去发现是某条批量发送的消息措辞有问题。
- 质量标准不统一:不同团队对"什么算好内容"的判断差异很大,有人觉得正式一点好,有人觉得口语化更亲切,没有统一标尺。
- 缺乏预防机制:每次都是出了问题才去查,没有在发送前做一轮质量预检。
这篇文章把我们在内容质量评估这件事上积累的方法论和工程实现整理出来,核心思路是:建立可量化、可自动化、有反馈闭环的质量评估流水线。
2. 内容质量的核心评估维度
2.1 四维评估模型
我们从四个维度来衡量一条 WhatsApp 消息(或一个消息模板)的质量:
| 维度 | 衡量指标 | 权重 | 合格线 | 优秀线 |
|---|---|---|---|---|
| 参与度 | 消息打开率 | 30% | ≥ 15% | ≥ 25% |
| 接受度 | 用户回复率 / 互动率 | 25% | ≥ 5% | ≥ 12% |
| 安全性 | 举报率 / 退订率 | 25% | ≤ 0.1% | ≤ 0.03% |
| 规范性 | 平台政策合规评分 | 20% | ≥ 70 分 | ≥ 90 分 |
四个维度的权重不是拍脑袋定的,而是根据对账号健康度的影响程度来分配的。参与度和安全性权重最高,因为这两个直接关联平台风控的判定逻辑。
2.2 各维度详解
参与度(Engagement)
打开率是最基础也最直观的指标。WhatsApp 的打开率计算方式跟邮件不一样:它基于"消息已送达且用户在聊天列表中查看过"这个信号。需要注意:
- 不同消息类型的基准线不同:服务类通知(订单确认、物流更新)的打开率天然高于营销推广类。
- 时间衰减效应:消息发出后 2 小时内的打开占比通常超过 60%,超过 24 小时后的打开基本可以忽略。
接受度(Acceptance)
回复率和互动率衡量的是"用户不仅看了,还愿意回应"。高打开 + 低回复 = 标题党式内容,长期来看对账号信誉有害。
安全性(Safety)
举报率和退订率是一票否决型指标。单条消息的举报率如果超过 0.5%,不管其他维度多高,整体质量评分都应该打折处理。
规范性(Compliance)
这个维度主要靠规则引擎来检测:消息中是否包含敏感词、是否违反平台的商业消息规范、是否缺少必要的退订选项等。
3. 基于规则的内容质量打分引擎
3.1 核心思路
先搭一套基于规则的快速打分系统,作为质量评估的基线。规则的好处是确定性强、速度快、容易解释;缺点是无法捕捉语义层面的细微质量问题(比如"这句话没违规但读起来很像垃圾广告")。语义层面的问题留给第 4 节的 AI 检测来解决。
python
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
import re
class QualityLevel(Enum):
EXCELLENT = "excellent" # 优秀 (≥85)
GOOD = "good" # 良好 (≥70)
WARNING = "warning" # 需关注 (≥50)
POOR = "poor" # 不合格 (<50)
@dataclass
class RuleCheckResult:
"""单条规则的检查结果"""
rule_name: str
passed: bool
score: float # 该规则贡献的分数 (0-100)
detail: str = "" # 具体说明
@dataclass
class ContentQualityReport:
"""完整的质量评估报告"""
message_id: str
template_name: str
overall_score: float # 综合得分 (0-100)
level: QualityLevel
dimension_scores: dict # 各维度得分
rule_results: list[RuleCheckResult] = field(default_factory=list)
recommendations: list[str] = field(default_factory=list)
# ========== 规则定义 ==========
# 敏感词库(示例,实际应从维护的后台配置加载)
SENSITIVE_PATTERNS = {
"urgent_action": re.compile(r"(立即|马上|限时|仅剩|最后机会)", re.I),
"price_trap": re.compile(r"(免费领取|零成本|百分百赚钱|稳赚不赔)", re.I),
"contact_harvest": re.compile(r"(转发给好友|分享群聊拉人头)", re.I),
"misleading_claim": re.compile(r"(官方认证|独家渠道|内部名额)", re.I),
}
# 规则权重配置
RULE_WEIGHTS = {
"no_sensitive_words": 0.20, # 无敏感词
"appropriate_length": 0.10, # 长度适中
"has_unsubscribe_option": 0.15, # 含退订指引
"no_excessive_emoji": 0.10, # emoji 不过量
"no_all_caps": 0.10, # 无全大写喊叫
"personalization_present": 0.15, # 有个性化元素
"clear_call_to_action": 0.20, # CTA 明确但不强迫
}
def check_sensitive_words(text: str) -> RuleCheckResult:
"""检测敏感词"""
matches = []
for category, pattern in SENSITIVE_PATTERNS.items():
found = pattern.findall(text)
if found:
matches.append(f"{category}: {found}")
if not matches:
return RuleCheckResult(
rule_name="no_sensitive_words",
passed=True,
score=100.0,
detail="未检测到敏感词",
)
return RuleCheckResult(
rule_name="no_sensitive_words",
passed=False,
score=max(0, 100 - len(matches) * 25), # 每个匹配扣 25 分
detail=f"检测到 {len(matches)} 类敏感词: {'; '.join(matches)}",
)
def check_length(text: str) -> RuleCheckResult:
"""检测消息长度是否合理"""
length = len(text)
if 30 <= length <= 500:
return RuleCheckResult("appropriate_length", True, 100.0, f"长度 {length} 字符,适中")
elif length < 30:
return RuleCheckResult("appropriate_length", False, 50.0, f"长度 {length} 字符,过短")
else:
return RuleCheckResult("appropriate_length", False, 60.0, f"长度 {length} 字符,偏长")
def check_emoji_usage(text: str) -> RuleCheckResult:
"""检测 emoji 使用频率"""
import emoji as emoji_lib
emoji_count = sum(1 for c in text if c in emoji_lib.UNICODE_EMOJI_ENGLISH)
char_count = len(text.replace(" ", ""))
if char_count == 0:
return RuleCheckResult("no_excessive_emoji", True, 100.0, "空文本")
ratio = emoji_count / max(char_count, 1)
if ratio <= 0.05: # emoji 占比不超过 5%
return RuleCheckResult("no_excessive_emoji", True, 100.0, f"emoji 占比 {ratio:.1%}")
elif ratio <= 0.15:
return RuleCheckResult("no_excessive_emoji", False, 70.0, f"emoji 占比 {ratio:.1%},略多")
else:
return RuleCheckResult("no_excessive_emoji", False, 30.0, f"emoji 占比 {ratio:.1%},过多")
def run_rule_engine(text: str, has_unsubscribe: bool = False) -> ContentQualityReport:
"""运行完整规则引擎"""
rules = [
check_sensitive_words(text),
check_length(text),
check_emoji_usage(text),
RuleCheckResult(
"has_unsubscribe_option",
passed=has_unsubscribe,
score=100.0 if has_unsubscribe else 0.0,
detail="包含退订选项" if has_unsubscribe else "缺少退订选项",
),
RuleCheckResult(
"personalization_present",
passed="{{" in text or "{name}" in text.lower(),
score=80.0 if ("{{" in text or "{name}" in text.lower()) else 40.0,
detail="检测到个性化占位符" if "{{" in text or "{name}" in text.lower() else "未检测到个性化元素",
),
]
# 加权计算总分
total_score = 0.0
weight_sum = 0.0
dimension_map = {}
for rule in rules:
w = RULE_WEIGHTS.get(rule.rule_name, 0.1)
total_score += rule.score * w
weight_sum += w
overall = round(total_score / max(weight_sum, 0.01), 1)
if overall >= 85:
level = QualityLevel.EXCELLENT
elif overall >= 70:
level = QualityLevel.GOOD
elif overall >= 50:
level = QualityLevel.WARNING
else:
level = QualityLevel.POOR
# 生成改进建议
recommendations = []
for rule in rules:
if not rule.passed:
recommendations.append(f"[{rule.rule_name}] {rule.detail}")
return ContentQualityReport(
message_id="",
template_name="",
overall_score=overall,
level=level,
dimension_scores={r.rule_name: r.score for r in rules},
rule_results=rules,
recommendations=recommendations,
)
坑点提示 :敏感词库不能一成不变。Meta 的政策会更新,用户的规避手段也在进化。建议至少每月审核一次敏感词规则,把最近被举报的消息文本拿回来跑一遍规则引擎,看看有没有漏网的。另外,check_sensitive_words 用的是正则匹配,对于"谐音变体""拆字绕过"这类手法无能为力,这正好是第 4 节 AI 检测要补的短板。
4. AI 辅助的内容质量检测与反馈闭环
4.1 规则引擎的盲区
纯规则的方式能覆盖大约 60%-70% 的质量问题。剩下的 30%-40% 属于"文字本身没触发任何规则,但读起来就是不对劲"的情况:
- 语气不当:没有敏感词,但整段话的语气像诈骗短信(过度紧迫感、虚假稀缺性)。
- 上下文不连贯:单句话没问题,但放在对话历史里显得突兀或重复。
- 隐含诱导:表面上是正常通知,但引导用户去做的事情实际上不符合平台规范。
- 风格漂移:同一套模板在不同团队手里,写出来的东西风格差异很大。
这些都需要语义理解能力来判断,也就是 AI 模型的用武之地。
4.2 AI 质量检测器设计
python
import json
AI_QUALITY_PROMPT = """你是一个专业的即时通讯内容质量审核员。请分析以下 WhatsApp 消息文本的质量。
消息内容:
---
{text}
---
请从以下维度评分(每项 0-100 分),返回 JSON 格式:
1. tone_appropriateness: 语气是否得当(专业但不冷漠,友好但不轻浮)
2. clarity: 表意是否清晰明确
3. perceived_trustworthiness: 收件人感受到的可信度
4. spam_likelihood: 被收件人视为垃圾信息的可能性(越高分越不像垃圾信息)
5. policy_risk: 违反平台政策的潜在风险(越高分越安全)
6. overall_impression: 综合印象分
7. improvement_suggestions: 具体改进建议(字符串数组)
只返回 JSON,不要其他解释。"""
def ai_quality_check(text: str, ai_client) -> dict:
"""
调用 AI 模型进行语义级质量检测。
参数:
text: 待检测的消息文本
ai_client: 已配置好的 AI API 客户端
返回:
包含各维度评分和建议的字典
"""
prompt = AI_QUALITY_PROMPT.format(text=text)
try:
response = ai_client.chat.completions.create(
model="your-model-name", # 替换为实际使用的模型
messages=[{"role": "user", "content": prompt}],
temperature=0.1, # 低温度保证评分稳定性
response_format={"type": "json_object"},
)
result = json.loads(response.choices[0].message.content)
# 标准化输出
return {
"tone_appropriateness": result.get("tone_appropriateness", 0),
"clarity": result.get("clarity", 0),
"perceived_trustworthiness": result.get("perceived_trustworthiness", 0),
"spam_likelihood": result.get("spam_likelihood", 0),
"policy_risk": result.get("policy_risk", 0),
"overall_impression": result.get("overall_impression", 0),
"suggestions": result.get("improvement_suggestions", []),
"checked_at": datetime.utcnow().isoformat(),
}
except Exception as e:
# AI 调用失败时降级为空结果,不影响主流程
return {"error": str(e), "suggestions": []}
4.3 规则 + AI 的融合评分
python
def fused_quality_score(
rule_report: ContentQualityReport,
ai_result: dict,
rule_weight: float = 0.6,
ai_weight: float = 0.4,
) -> float:
"""
融合规则引擎和 AI 检测的综合评分。
规则引擎给出"硬性合规"分数,AI 给出"软性感知"分数,
两者加权融合得到最终质量评分。
"""
rule_score = rule_report.overall_score
# AI 结果可能因调用失败而缺失
if "error" in ai_result:
# AI 不可用时,仅使用规则分数,但打个折表示不确定性
return round(rule_score * 0.9, 1)
ai_overall = ai_result.get("overall_impression", 50)
# 如果任一方给出了极低分(<30),取较低值而不是加权平均
# 这相当于一票否决机制
if rule_score < 30 or ai_overall < 30:
return round(min(rule_score, ai_overall), 1)
fused = rule_score * rule_weight + ai_overall * ai_weight
return round(fused, 1)
坑点提示 :AI 模型的 temperature 必须设低(0.1-0.2)。我们试过用默认 temperature 0.7 跑同样的文本 5 次,spam_likelihood 的评分波动范围达到了 18 分,这对质量评估来说太不稳定了。低温度下同样文本的评分偏差可以控制在 5 分以内,更适合做决策依据。
5. 质量数据的趋势分析与阈值告警
5.1 数据采集与聚合
python
from collections import defaultdict
from datetime import datetime, timedelta
@dataclass
class QualityTimeSeriesPoint:
timestamp: datetime
template_id: str
message_count: int # 该模板当期发送量
avg_open_rate: float # 平均打开率
avg_reply_rate: float # 平均回复率
report_count: int # 举报次数
avg_quality_score: float # 平均质量评分
fusion_pass_rate: float # 通过质检的比例
class QualityTrendAnalyzer:
"""质量趋势分析器"""
def __init__(self):
self._data: list[QualityTimeSeriesPoint] = []
def record(self, point: QualityTimeSeriesPoint) -> None:
self._data.append(point)
def get_template_trend(
self,
template_id: str,
days: int = 7,
) -> list[QualityTimeSeriesPoint]:
"""获取指定模板最近 N 天的趋势数据"""
cutoff = datetime.utcnow() - timedelta(days=days)
return [
p for p in self._data
if p.template_id == template_id and p.timestamp >= cutoff
]
def detect_anomaly(
self,
template_id: str,
metric: str = "avg_open_rate",
threshold_pct: float = 0.3,
) -> dict | None:
"""
异常检测:当前值是否显著偏离历史均值。
参数:
metric: 监控的指标名
threshold_pct: 偏离阈值(如 0.3 表示偏离 30% 即告警)
返回:
异常信息字典,无异常则返回 None
"""
trend = self.get_template_trend(template_id, days=14)
if len(trend) < 3:
return None # 数据不足,跳过
# 历史均值(排除最近 1 天,避免用"异常值自己算自己")
historical = trend[:-1]
values = [getattr(p, metric) for p in historical if getattr(p, metric, None) is not None]
if not values:
return None
hist_avg = sum(values) / len(values)
current = getattr(trend[-1], metric, None)
if current is None:
return None
deviation = abs(current - hist_avg) / max(hist_avg, 0.001)
if deviation > threshold_pct:
direction = "下降" if current < hist_avg else "上升"
return {
"template_id": template_id,
"metric": metric,
"historical_avg": round(hist_avg, 3),
"current_value": round(current, 3),
"deviation_pct": round(deviation * 100, 1),
"direction": direction,
"severity": "high" if deviation > 0.5 else "medium",
}
return None
5.2 告警分级与通知
python
def generate_alert_message(anomaly: dict) -> str:
"""根据异常信息生成告警消息"""
severity_emoji = {"high": "🔴", "medium": "🟡"}
emoji = severity_emoji.get(anomaly["severity"], "⚪")
return (
f"{emoji} 内容质量异常告警\n"
f"模板: {anomaly['template_id']}\n"
f"指标: {anomaly['metric']}\n"
f"方向: {anomaly['direction']} {anomaly['deviation_pct']}%\n"
f"历史均值: {anomaly['historical_avg']}\n"
f"当前值: {anomaly['current_value']}\n"
f"建议: 立即检查该模板最近的发送记录和用户反馈"
)
6. 实际落地经验
以上整套内容质量评估体系在实际落地时,我们以 WAWarmer 的质检模块为例,看它在几个关键点上做了什么选择:
选择一:全量检测还是抽样检测? 它选择了分层抽样。所有新模板首次上线前必须跑完整的规则+AI 双重检测(100%);已稳定运行超过 14 天的老模板,每天随机抽 20% 的发送量做质检。这样做的理由是老模板出问题的概率低,全量跑 AI 的成本不划算。目前这套策略下,漏检导致的实际质量问题发生率约为 0.8%,在可接受范围内。
选择二:质量不达标怎么处理? 它的策略是三级拦截:综合评分低于 40 分的直接拦截不允许发送(硬拦截);40-60 分的允许发送但强制抄送负责人审阅(软拦截);60 分以上的正常放行。硬拦截上线第一个月触发了 23 次,其中 19 次经人工复核确认确实不应该发出去。这说明规则+AI 的联合判断在极端情况下是靠谱的。
选择三:质量数据保留多久? 原始质检记录保留 90 天,聚合后的趋势数据永久保留。90 天足以覆盖 Meta 的大多数 Policy Violation 回溯周期(通常是 30-60 天)。趋势数据的永久保留是为了支持跨季度的质量对比分析,比如 Q3 和 Q4 的整体内容质量有没有提升。
7. 小结
内容质量评估这件事,从"靠感觉"到"靠数据",中间差的不是工具,是一套多维度的评估框架 + 自动化的检测流水线 + 持续迭代的数据反馈闭环。规则引擎解决显性的合规问题,AI 检测解决隐性的感知问题,两者结合才能覆盖大部分质量风险点。
这套体系搭建起来之后最直接的收益是:内容相关的账号受限事件减少了约 60%。不是因为内容写得多么精美,而是因为大多数"翻车"的苗头在发送之前就被拦住了。
建议先从以下三点入手:
- 建一个最小规则引擎(参考第 3 节),先实现敏感词检测 + 长度检测 + emoji 检测这三条规则,能覆盖最常见的低级错误。预计 1 个工作日。
- 接入一个 AI 质量检测接口 (参考第 4 节),不需要一步到位做全套维度,先跑
spam_likelihood和policy_risk这两个最关键的评分。预计 1-2 个工作日。 - 开始记录每个模板的质量评分和后续的业务数据(打开率/举报率),积累 2 周以上数据后就可以跑第 5 节的趋势分析和异常检测。持续进行。
整体落地预估 3-5 个工作日。前期投入主要在建规则库和调 AI prompt 上,后面日常运行的边际成本很低。