用 Jev 做内容审核:一致、可解释的判定

更新于 适用版本 Jev 1.13

TL;DR: 内容审核不是一个难题,而是很多个小决策。用 choice 调用对扁平类别列表做分诊,置信度不足时对具体政策规则降级为 judgment 调用,每次删除都把 rationale 存成审计记录。一致性来自固定的问题格式,可解释性来自 rationale。下文给出流程、阈值与申诉路径。

先设计类别

分两层,刻意保持简单:

{"answer":"yes","confidence":0.97,"rationale":"该消息以付款为条件威胁人身伤害。"}

这与人审团队的实际工作方式一致:先分诊,难例逐条规则复核。同时每个问题都很小——这正是判断模型(judgment model)最喜欢的输入形态。

审核流程图

内容提交
   |
   v
Jev choice 调用(类别:spam / harassment / adult / violence / none)
   |
   +--> 类别 = none 且置信度高 --> 发布
   |
   +--> 置信度低或类别不确定
   |        |
   |        v
   |    Jev judgment 调用(针对具体规则:yes / no / unclear)
   |        |
   |        +--> 明确 yes / no --> 执行动作,存 rationale
   |        |
   |        +--> 仍不明确 --> 人工审核队列
   |
   +--> 明确违规 --> 删除,存 rationale,附带申诉链接通知用户

每次删除固定存储五元组:内容 id、类别、answer、confidence、rationale、模型版本。这就是你的审计记录(audit trail)。

最小实现

import os, json, requests

URL = "https://openrouter.ai/api/v1/chat/completions"
# Confirm the exact model slug on the OpenRouter model page.
MODEL = "typesafe/jev-1.13"
CATEGORIES = ["spam", "harassment", "adult", "violence", "none"]

def call(system: str, text: str) -> dict:
    resp = requests.post(
        URL,
        headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"},
        json={"model": MODEL, "temperature": 0, "messages": [
            {"role": "system", "content": system},
            {"role": "user", "content": text}]},
        timeout=30,
    )
    resp.raise_for_status()
    # 示例数据(example fixture,judgment):
    # {"answer":"yes","confidence":0.97,"rationale":"..."}
    return json.loads(resp.json()["choices"][0]["message"]["content"])

def moderate(text: str, low=0.85) -> dict:
    c = call("Pick exactly one category from: "
             + ", ".join(CATEGORIES) + ".", text)
    if c["answer"] != "none" and c["confidence"] >= low:
        return {"action": "remove", **c}
    if c["confidence"] >= low:
        return {"action": "publish", **c}
    j = call("Does this text violate the harassment policy? "
             "Answer yes, no, or unclear.", text)
    if j["answer"] == "unclear" or j["confidence"] < low:
        return {"action": "human-review", **j}
    return {"action": "remove" if j["answer"] == "yes" else "publish", **j}

官方 API 的端点与字段请以 typesafe.ai 官方文档为准。

阈值与申诉路径

阶段配置项建议
第一层 choice自动执行阈值删除与发布统一用 confidence >= 0.85
第二层 judgment边界规则复核第一层低置信度时触发;仅对明确的 yes/no 执行动作
人工队列触发条件任何 unclear 结果,或第二层后置信度仍低于阈值
审计记录一律存储内容 id、类别、answer、confidence、rationale、模型版本
申诉用户可见通知里附 rationale 摘要与一键申诉入口

为什么问题格式这么重要,见状态与问题(state and questions)指南;阈值调优见置信度与兜底(confidence and fallback)指南。多语言审核案例展示了这套两层设计在跨语言场景下的表现。

本文适用版本:Jev 1.13。

多级处置策略:放行、复审、拦截

两层调用只给出答案,处置策略决定对这个答案做什么。把”答案”与”动作”分开,是让审核系统可维护的关键一步。

三档动作与触发条件

动作触发条件执行内容
放行(publish)第一层 answer=none,confidence ≥ 0.85内容进入正常发布队列
拦截(remove)第一层 answer 为违规类别,confidence ≥ 0.85隐藏内容,向用户发送附 rationale 的通知
复审(human-review)任何 unclear,或两层后 confidence 仍低于阈值进入人工队列,附完整调用记录

不对称阈值的使用时机

拦截阈值和放行阈值不必对称。若平台对误删(false positive)的容忍度低,可将拦截阈值调高(如 0.92),而放行阈值保持 0.85——中间地带更多流向复审队列,而不是直接拦截。同理,成人内容与垃圾信息对用户伤害程度不同,可以为不同类别设置不同阈值。

示例数据(example fixture):{"category":"adult","threshold_remove":0.92,"threshold_publish":0.85}

阈值体系本身要版本化存储,和模型版本一同归档——这样回溯历史判定时,能复现当时的决策逻辑。关于置信度调优的更多方法,参见置信度与兜底指南。

边界样本处理

置信度接近阈值的内容(边界样本)是审核系统最容易出错的地方。以下是两种常见的处理路径。

路径一:针对规则的二次 judgment

当第一层 choice 返回低置信度时,不要直接进人工队列——先用 judgment 调用对最可能违规的规则做一次是/否确认。示例数据(example fixture):

{"answer":"no","confidence":0.91,"rationale":"该消息措辞强硬但未包含可识别的威胁要素。"}

这一步的成本远低于人工审核,能消化掉相当一部分模糊样本。

路径二:聚合边界样本用于策略迭代

对连续进入复审队列、但人工最终判定为”发布”的样本,定期做聚合分析。如果某个类别的边界样本中人工放行率持续偏高,说明该类别的提示词措辞或阈值需要重新校准,而不是让人工反复处理同类内容。

聚合时,只记录调用元数据(类别、answer、confidence、人工结论),不存储内容原文——这样既能用于策略迭代,也便于在数据留存政策到期时单独清除内容字段。

阈值与置信度联动

置信度数值本身不等于”准确率”,它是模型对自身判断的不确定性估计。这一节讨论如何把置信度接入系统逻辑,而不是只做一个静态截断。

动态阈值触发链

第一层 choice
   |
   +--> confidence ≥ 0.85,answer 明确 --> 直接执行动作
   |
   +--> 0.70 ≤ confidence < 0.85 --> 触发第二层 judgment
   |
   +--> confidence < 0.70 --> 直接进人工队列(跳过第二层)

第二层 judgment 调用只有在置信度处于中间区间时才有价值。极低置信度(< 0.70)时,再做一次 judgment 往往也返回 unclear,不如直接进人工队列节省延迟。具体分界值需根据实际流量调整,这里给出的仅为参考结构,不是固定数字。

多类别置信度分布监控

若平台流量足够,建议按类别分别追踪置信度分布(直方图),而不只看总体通过率。示例数据(example fixture):

[
  {"category":"spam",       "p50_confidence":0.96,"p10_confidence":0.88},
  {"category":"harassment", "p50_confidence":0.89,"p10_confidence":0.71},
  {"category":"adult",      "p50_confidence":0.94,"p10_confidence":0.82}
]

harassment 类别的 p10 置信度(0.71)明显低于其他类别,说明这个类别更容易产生边界样本,可以优先为它补充更细粒度的规则拆分或调整提示词措辞。

日志留存与审计记录

每次调用都要归档,不只是删除事件。日志是申诉流程、监管合规和策略迭代的共同基础。

最小日志结构

{
  "event_id": "mod-20261019-0042",
  "content_id": "post-8871",
  "layer": 1,
  "call_type": "choice",
  "answer": "harassment",
  "confidence": 0.91,
  "rationale": "消息包含针对具体个人的重复性贬损语言。",
  "model_version": "jev-1.13",
  "action": "remove",
  "ts": "2026-10-19T08:14:03Z"
}

示例数据(example fixture):上方 JSON 结构。

字段说明:layer 区分第一层 choice 与第二层 judgment,便于单独统计两层的调用量与置信度分布;model_version 与动作绑定,确保策略变更前后的判定可追溯;rationale 原文保留,申诉审核员可直接阅读,无需重新调用模型。

留存期与分级存储

日志中内容原文和 rationale 的留存期,需与平台隐私政策对齐。一种常见做法是分级存储:

数据留存策略本身应作为配置项版本化管理,而不是硬编码在业务逻辑里。关于状态与调用结构的更多讨论,参见三元基础指南。

FAQ

常见问题

+ 审核类别该怎么用 Jev 组织?

第一层用 choice 调用从扁平类别列表里选一个(如 spam、harassment、adult、violence、none);边界或低置信度情况再用第二层 judgment 调用针对具体规则提问,而不是塞进一个巨型多标签提示词。

+ 为什么每次判定都要保留 rationale?

rationale 就是审计记录。它向用户、申诉审核员和监管方解释这条内容为什么被删除,这是人工审核备注很难做到的一致程度。

+ 置信度低的时候怎么办?

先针对具体规则做一次廉价的 judgment 复核,能解决的边界案例当场处理;仍然不明确就连同 rationale 一起进人工审核队列。

继续阅读