第 18 章:内容审核与信任安全——Moderation and trust and safety
第 18 章:内容审核与信任安全——Moderation and trust and safety
内容审核是所有垂直场景里判断量最大的一个:每一条用户内容都要过一遍,坏内容又在不停变换写法逃逸。用昂贵的大模型逐条审,成本扛不住;用关键词表,逃逸一戳就破。本章讲 Jev 怎么用"分级审核 + 逐条 Noul + 批量吞吐"这套组合,把审核做成既便宜又扛得住对抗的判定层。
为什么是分级,而不是一个大分类器
新手做审核,第一反应往往是训一个大分类器把所有违规类型一次判完。这条路在真实对抗下有几个硬伤:类别会持续变化(新话术层出不穷),一个多分类模型对"同时踩两类"的内容给不出正确表达,更麻烦的是它的输出没有可直接用的概率,定不了阈值的分级处置。
于是成熟的做法都是分级 + 逐条:
- 分级:先跑便宜且确定的第一层(关键词、正则、本地格式规则),只把剩下的灰区内容往上级送。级别越高,判定越贵,送上去的量越小。
- 逐条:违规类型写成互相独立的
Noul问题,一条内容可能同时命中"垃圾信息"和"钓鱼链接",用多个布尔判定如实表达,而不是强迫它选一个类。
这套做法把"贵"和"准"错开了:绝大多数内容在第一层就被廉价地处理掉,只有真正模糊的才动用模型。
mastra-jev-moderation:一次请求,两级判定
CodeAlive-AI/mastra-jev-moderation 把分级审核压缩进了一次 Jev 请求。它是一个 Mastra 的输入处理器,对每条进来的消息问两个问题:一个 Boolean("这条消息是否必须被拦截?"),加一个类别 Choice,命中阈值就中止本回合。
生产数据很能说明问题:它拦截了 9/9 条敌对消息,同时 0/49 条正常消息被误拦,中位延迟约 0.4 秒,成本大约是一个 LLM 审核员的 1/4(约 4 倍便宜)。
更值得抄的是它的失败策略——fail-open,并且加超时和熔断。审核挂掉时它选择放行而不是全拦,因为全拦会让整个助手不可用。超时和熔断保证不会因为 Jev 拖慢而卡住主流程。审核场景里"坏内容漏过"和"正常服务瘫痪"是两种代价,这个项目明确选了不能瘫痪。
d = client.systemone(
state=message_text,
questions={
"block": {
"type": "noul",
"question": "这条消息是否必须被拦截(钓鱼、垃圾、社工)?"
},
"category": {
"type": "choice",
"question": "若需拦截,属于哪一类?",
"choices": ["phishing", "spam", "social_engineering", "none"]
}
},
instructions="你是输入审核器。务必拦截钓鱼与社工消息,正常闲聊一律放行。"
)
BLOCK_THRESHOLD = 0.7 # 到达 0.7 即中止本回合
try:
if d["block"].probability >= BLOCK_THRESHOLD:
abort_turn(reason=d["category"].choice)
except (TimeoutError, APIError):
pass # fail-open:审核不可用时不阻断主流程把 block 和 category 放在同一次请求里,还有一个隐性收益:类别判定能吃到与拦截判定相同的上下文,两者不会互相打架。
Jev Moderation Bot:四阶段升级阶梯
Jev Moderation Bot(brainstormity/Jev-Moderation-Bot)是个 Discord 机器人,它把"分级"做成了显式的四阶段 escalation ladder:对每条进来的消息,用 Jev 打分判断钓鱼、垃圾、社工风险,再按风险级别逐级升级处置——从静默记录,到警告,到限流,直到人工介入。
它有一个很聪明的细节:被"赦免"(判为正常)的消息会重新注入回上下文,作为"已验证安全"的先例。这样后续相似的消息就有了参照,判定更稳,也减少了同类误判的重复发生。这其实是在决策层之上加了一层很轻的记忆。
escalation ladder 的价值在于它把"审核"从一个二值动作变成了一个连续的动作空间。不是"删或留",而是"记录 / 警告 / 限流 / 移交人工",每级都有自己的代价。阈值定在哪一级,取决于社区对误伤的容忍度——技术社区容忍度低,就要把自动处置的档位往上抬。
severity = client.systemone(
state=message_text,
questions={
"is_phishing": {"type": "noul", "question": "是否为钓鱼链接或诱导点击?"},
"is_scam": {"type": "noul", "question": "是否为诈骗或社工话术?"},
"level": {
"type": "score",
"question": "该消息对社区的危害等级?",
"labels": ["无", "轻", "中", "重"]
}
},
instructions="你是社区审核员,按社区的公开规则判定。"
)
lv = severity["level"].score
if severity["is_phishing"].probability >= 0.8:
ban_and_log(message) # 最高档
elif lv >= 2.5:
throttle(message)
elif lv >= 1.5:
warn(message)
else:
log_only(message) # 最低档:只记录阶梯的每一档都写在代码里,if/elif 一眼看全,调整处置策略不需要动模型。
逃逸对抗:变换写法、隐语与混排
审核最难的从来不是"识别直白的脏话",而是识破变换。用户会把字母换成数字或形近字符(a55h0le),会把脏话拆成谐音或隐语(mike_hunt),会混在图片文字里,会在正常句子里夹一句攻击。
profanity-checker(4rays/profanity-checker)针对的就是变换写法这一层。它是个 Cloudflare Worker,对文本或用户名问两个 Noul:一个判字面的脏话,另一个专门判拼音或形近伪装(原文举的例子正是 a55h0le、mike_hunt)。两个判定用 max() 策略合并——任一命中即算违规。阈值、max() 策略、JSON 响应和 OpenAPI schema 全都写在 Worker 代码里,endpoint 还能通过 service binding 被其他 Worker 直接调用。
async function check(text: string, env: Env): Promise<{ flagged: boolean; p: number }> {
const d = await systemone({
state: text,
questions: {
literal: { type: "noul", question: "文本或用户名中是否含有字面的脏话?" },
disguised: { type: "noul",
question: "是否通过数字、形近字符或拼音谐音伪装成脏话(如 a55h0le、mike_hunt)?" }
},
instructions: "你是字符级审核器,只判断是否存在脏话,不判断语气。"
});
const p = Math.max(d.literal.probability, d.disguised.probability); // max 策略
return { flagged: p >= THRESHOLD, p };
}拆成"字面"和"伪装"两个问题,比合成一个问题更好调——你可以对伪装层单独设一个更严格的阈值,因为实际攻击大多走伪装这条路。
隐语是比字符伪装更难的一层:mike_hunt 这类谐音,只有知道它指什么才判得出来,纯字符串规则完全无能为力——而这正是语义判定的用武之地。你只要在题干里说清"是否通过谐音或圈内隐语表达脏话或攻击",它就能泛化到没见过的写法。代价是每一版新隐语都要过一次判定,所以审核量会随社区活跃度增长,这也是为什么上一层的关键词过滤不能省。
对抗的另一维度是多模态。social-risk-qwen-jev(TianJinWeiBoss520/social-risk-qwen-jev)的做法是把一个 Qwen3-VL LoRA 和一个 Jev 判定模型配对,去判断多模态的社媒风险内容——视觉部分交给 VL 模型,风险判定交给 Jev。文本改写法只是对抗的一半,图片里藏字、图文混排的组合是另一半,这类组合必须靠多模态前端加决策层后端一起覆盖。
MCP 形态的接入则让审核能力可以被 Agent 直接调用。jev-screen-mcp(jiawei686/jev-screen-mcp)是个单工具 MCP server,把屏幕内容过一遍 Jev,把 noul、choice、score 三种问题类型都作为一等公民暴露出来,还带一个确定性的 mock mode 和三个测试。
批量吞吐与成本控制
审核的成本模型很直接:量乘以单次判定成本。所以除了把判定本身压低,还要批处理。
Jev Chat for Twitch(ethanplusai/jev-chat-for-twitch)是个 Chrome 扩展,通过匿名 IRC WebSocket 读取 Twitch 频道聊天,每 20 条消息一批问 Jev 一个类别 Choice,只把匹配选定意图(helpful、questions、funny、feedback)的消息显示在第二列。它的成本数据很具体:每条消息约 504 个输入 token,在每秒 2 条消息的聊天里大约 $0.15/小时,涨到每秒 50 条时约 $0.76/小时。批处理让活跃聊天室的成本保持在可接受的量级。
Rot Guard(plusminushalf/rot-guard)是另一个批处理的例子,但复杂度更高——它对每个 YouTube 视频、X、Reddit、LinkedIn 页面问一个内容类别 Choice、一个"是否符合用户写的目标"的 Noul 和一个 0–4 的"浪费时间" Score,而对 X 帖子和 YouTube 缩略图按每批 10 条问 Choice 和 Noul。它的阈值还会随累计停留时间在 2、10、20 分钟三个节点逐步收紧——用得越久,拦截越严。这是把"用户注意力"这个成本维度写进了阈值策略。
其余的轻量实现也能看出这个思路:jev-slop-guard(三个并发上限、每条一次缓存)、profanity-checker(Cloudflare Worker 边缘执行)、jev_antispam_bot(backmeupplz/jev_antispam_bot,极简 grammY 机器人,配十个测试文件)、HN Jev Sorter(ritza-co/hn-jev-sorter,逐条给 HN 评论分类,约一秒一条)。
关于"逐条 Noul + 多标签优于一个大分类器",Sniff Test(DanRWilloughby/snifftest)提供了一个有说服力的对照。它的场景是文字审查,对每段问十个 Boolean 问题(堆叠的模糊表达、复述式结尾、not-X-but-Y 转折、光秃秃的成本数字),阈值 0.7。它的误报数据很亮眼:54 段干净文本里只有 1 段被误标;对照之下,用它自己的话说,Haiku 4.5 在同一批文本上误标了 37 段。拆成多条布尔判定、各自定阈值,比让一个模型给一个笼统的"合格/不合格"要准得多——误报少,用户体验就好。Perch(lakeday-org/perch)把这个思路用在了代码上:对每个方法(连同它的调用方和被调方)问一个"是否有 bug"的 Noul、一个"是哪类、在哪行"的 Choice、一个严重度 Score,再叠上按语言过滤的 CWE 判定和自定义规则,任一判断超过下限就让 CI 失败。
误报成本在审核里被严重低估。每一条正常内容被误拦,用户就少用一次产品;审核系统的核心指标不只是"抓到多少坏内容",还有"放过多少好内容"。这也是为什么这些项目普遍偏爱逐条判定 + 概率阈值 + 可调松紧,而不是一个黑箱的"合格分"。
阈值校准与回归
审核的阈值不能拍脑袋定,得当分类器来测。jev-spam-eval(bitnovus/jev-spam-eval)给的正是这个:用零样本(zero-shot)的 Jev Boolean 问题做垃圾信息分类,并拿它跟 TF-IDF 基线做对照。关键不在于"Jev 一定比 TF-IDF 强"——传统基线在固定词表上往往很能打——而在于你得有一个基线。如果 Jev 在这个环节并没有明显强过本地就能跑的 TF-IDF,那这部分或许根本不必发请求。
给一批带标注的样本、扫一遍阈值,就能画出误报/漏报的权衡:
import numpy as np
for t in np.arange(0.3, 0.91, 0.05):
pred = np.array(probs) >= t
fp = ((pred == 1) & (labels == 0)).sum() # 误报:正常内容被拦
fn = ((pred == 0) & (labels == 1)).sum() # 漏报:坏内容被放
print(f"阈值 {t:.2f} 误报 {fp:3d} 漏报 {fn:3d}")阈值落在哪儿,取决于你更怕哪一种错。CodeAlive-AI/mastra-jev-moderation 那组数据(0/49 误拦、9/9 敌对拦截)之所以可信,是因为背后有标注样本撑着;没有标注集,阈值只是一个感觉。
除了命中率,还有一个更细的指标值得盯:Brier 分数——概率预测的平方误差,它同时罚"判错"和"过度自信"。一个把每条判定都压在 0.95 的分类器,只要错一次,Brier 就比一个诚实报 0.6 的分类器还差。这就解释了为什么这些项目宁可让概率留在中间、把灰区交给人工,也不去把每个输出都刷到接近 1。
所以回归测试要把"阈值 + 标注集"一起钉死,每次改问题措辞或指令就重跑一遍。审核系统的改动很微妙——把题干从"垃圾信息"改成"垃圾或推广",误报可能立刻抬头。这类回归不做,就只能等用户投诉来发现问题。
一个分级审核管线
综合上面的做法,一条审核管线大概长这样:
flowchart TD
MSG["用户消息"] --> L1["第一层:本地规则
关键词 / 正则 / 格式"]
L1 -->|确定命中| BLOCK["直接拦截"]
L1 -->|通过| L2["第二层:cheap Jev
Noul 拦截 + Choice 类别"]
L2 -->|p ≥ 0.7| BLOCK
L2 -->|灰区| L3["第三层:升级判定
Score 危害 + 多 Noul 细分"]
L2 -->|p < 0.7| PASS["放行"]
L3 -->|高危害| ESC["人工 / 封禁"]
L3 -->|低危害| PASS
L3 -->|赦免| CTX["回注上下文
作为安全先例"]
BLOCK --> LOG["审计日志"]
ESC --> LOG
PASS --> LOG第一层几乎零成本地筛掉大部分内容;第二层做便宜的二值判定;只有灰区才升到更贵的细粒度判定。赦免的消息回注上下文当先例,是这个阶梯里很实用的一笔。
小结
- 审核的核心结构是分级 + 逐条:先跑便宜的确定性规则,把灰区往上级送;违规类型拆成互相独立的
Noul,而不是一个大分类器。 CodeAlive-AI/mastra-jev-moderation用一次请求同时问拦截Boolean和类别Choice,生产数据 9/9 敌对拦截、0/49 正常误拦、约 0.4 s、比 LLM 审核员便宜约 4 倍,并在超时和熔断下 fail-open。brainstormity/Jev-Moderation-Bot用四阶段 escalation ladder,并把赦免消息回注上下文作为安全先例。- 对抗变换靠拆分判定:
4rays/profanity-checker用"字面 + 伪装"两个Noul加max()策略;多模态靠TianJinWeiBoss520/social-risk-qwen-jev这类 VL + Jev 组合。 - 批量吞吐是成本的主战场:
ethanplusai/jev-chat-for-twitch每批 20 条、约 504 token/条($0.15–$0.76/小时);plusminushalf/rot-guard每批 10 条且阈值在 2/10/20 分钟逐步收紧。 - 误报是被低估的成本:
DanRWilloughby/snifftest十个布尔判定在 54 段干净文本上只误标 1 段(对照 Haiku 4.5 误标 37 段),说明逐条判定 + 各自阈值比一个笼统判定更准。 - 阈值要靠标注集和基线来定,不能拍脑袋:
bitnovus/jev-spam-eval用零样本Boolean对照 TF-IDF 基线;用误报/漏报权衡曲线加 Brier 分数选阈值,并把"阈值 + 标注集"钉进回归测试。
下一章进入游戏与实时交互,看决策层怎么在不生成一行文本的前提下驱动角色与裁决对局。