---
title: xiaoyu-chinese-ai-detect
canonical_url: "https://www.modelscope.cn/models/fuyi163/xiaoyu-chinese-ai-detect"
md_url: "https://www.modelscope.cn/models/fuyi163/xiaoyu-chinese-ai-detect.md"
repository: fuyi163/xiaoyu-chinese-ai-detect
last_updated: 2026-10-24
license: apache-2.0
pipeline_tag: text-classification
tasks:
  - text-classification
model_type:
  - bert
architectures:
  - BertForSequenceClassification
base_model_relation: finetune
library_name:
  - pytorch
  - transformer
  - safetensors
frameworks:
  - pytorch
language:
  - zh
downloads: 32
stars: 3
tags:
  - ai-generated-text-detection
  - ai-text-detection
  - chinese
  - bert
---

# xiaoyu-chinese-ai-detect

> xiaoyu-chinese-ai-detect - fuyi163 在 ModelScope 开源的模型。一个中文「AI 生成 / 人写」判别模型。输入一段文本，输出一个整篇AI概率，以及每句话的AI概率（仅供参考）。

fuyi163/xiaoyu-chinese-ai-detect 是 ModelScope 魔搭社区上的text-classification模型，采用 apache-2.0 许可。

- **Repository**: fuyi163/xiaoyu-chinese-ai-detect
- **License**: apache-2.0
- **Tasks**: text-classification
- **Tags**: ai-generated-text-detection, ai-text-detection, chinese, bert
- **Downloads**: 32
- **Stars**: 3
- **Last updated**: 2026-10-24

Source: https://www.modelscope.cn/models/fuyi163/xiaoyu-chinese-ai-detect

---

# 晓语 · 中文 AI 检测

**当前模型版本：V3.3**

一个中文「AI 生成 / 人写」判别模型。输入一段文本，输出**一个整篇AI概率**，以及**每句话的AI概率**（仅供参考）。

底座为中文 RoBERTa（BERT-base 结构，12 层 / 768 hidden / 词表 21128），单 logit 输出，  
`sigmoid` 后得到「这段文本像 AI 写的」概率。

同一份权重打包成 **三档体积**，从 390 MiB 到 60 MiB。**int8 与 fp32 判别力基本一致；int4 概率排序仍然保持，但边界样本的概率会明显漂移**——具体读数见 `quant/bench_readout.json`（那份在仓库内可复现）。

| 档    | 文件                     | 体积        | 你怎么加载它                 | sha256                                                             |
| ---- | ---------------------- | --------- | ---------------------- | ------------------------------------------------------------------ |
| fp32 | `model.safetensors`    | 390.1 MiB | ✅ `from_pretrained` 一行 | `75e56acd525cc7452f70741c898d4ad42dd9304d5ad6df8f45a4726f237d6ea7` |
| int8 | `quant/int8_packed.pt` | 101.0 MiB | ⚠️ 先跑 10 行解包代码        | `bf6456105bbdd92c38a41671cf53882d90731129c24a4fbb6a79197a0a8b9687` |
| int4 | `quant/int4_packed.pt` | 60.2 MiB  | ⚠️ 先跑 10 行解包代码        | `80a2505fbc922ab33cc4e9875dd87b626225daba83bf980316c1ad07b045f2d9` |

> **三档都已经打包好、直接放在仓库里了，你不需要自己打包。** 差别只在**加载方式**：fp32 一行
> `from_pretrained` 就完事；int8 / int4 要先跑几行代码把打包件还原成 state_dict，再灌进模型。
>
> **它们不是 GGUF / GPTQ / AWQ**，是本仓库自带的打包格式，只认仓库里的 `pack_quant.py`。  
> 图省事就用 fp32 那件；要小体积就照「量化档」那节的示例解包，代码可以直接复制。

> **int8 是平衡档，int4 是轻量档**：更高的档位（如 int16）没有随仓库发布，**这是唯一需要你自己跑一次打包的情况**：  
> `python pack_quant.py --src . --bits 16 --out /tmp/q16`  
> （输出目录别设为仓库自己，脚本会在输出目录写一份 `manifest.json`，混进仓库容易误提交）

---

## 输出怎么读

三档判决，只此一套线：

| AI生成概率    | 判决          |
| --------- | ----------- |
| ≥ 90%     | **AI特征明显**  |
| 50% – 90% | **疑似AI参与**  |
| < 50%     | **AI特征不明显** |

**显示的数字和判定用的线必须是同一个标准**，不要自己另设阈值，否则会出现「45% 却判疑似 AI 参与」这种看不懂的输出。

> 概率会饱和，logit 不会。区分两篇「都很像 AI」的稿子，看 logit 差值比看 4 位小数的概率可靠得多。

---

## 长文本：整篇概率怎么算

模型硬上限 512 token（中文约 600 汉字），超过这个长度必须切窗再合并。本仓库提供两种合并方式：

- 500 字 / 窗，步长 400（重叠 100 字，避免跨窗切断语义）
- 窗级 `sigmoid` 再聚合，**两种口径可选**：

| 方式       | 算法                | 说明             |
| -------- | ----------------- | -------------- |
| `eq` 等权  | Σp / n            | 每窗一票，**默认**    |
| `len` 加权 | Σ(len · p) / Σlen | 按窗口字数加权，末窗短就少说 |

**不满 500 字符**的文本切不出第二个窗，自动退化为单窗——此时整段一次前向（超 512 token 会被截断），两种口径给出同一个数。到 500 字符以上才开始切第二个窗，所以「单窗 / 多窗」的分界是 **500 字符**，不是 512 token。

参考实现（与下方精度读数同一套口径，可直接抄）：

```python
import torch

FULL_WIN, FULL_STRIDE, MAXLEN = 500, 400, 512

def build_windows(text):
    """滑窗：每窗 500 字，步长 400（相邻窗重叠 100 字）。"""
    segs, i, n = [], 0, len(text)
    while i < n:
        seg = text[i:i + FULL_WIN]
        if len(seg.strip()) < 30:      # 尾部残片太短，整段丢弃
            break
        segs.append(seg)
        if i + FULL_WIN >= n:          # 已经到文末，不再切
            break
        i += FULL_STRIDE
    return segs

def score_long(text, tok, model, how='eq'):
    segs = build_windows(text)
    if len(segs) <= 1:                 # 切不出第二个窗：整段一次前向
        with torch.no_grad():
            enc = tok(text, return_tensors='pt', truncation=True, max_length=MAXLEN)
            return torch.sigmoid(model(**enc).logits.float()).item()
    lens = [len(s) for s in segs]      # len = 该窗的【字符数】
    ps = []
    with torch.no_grad():
        for s in segs:
            enc = tok(s, return_tensors='pt', truncation=True, max_length=MAXLEN)
            ps.append(torch.sigmoid(model(**enc).logits.float()).item())
    if how == 'len':
        return sum(l * p for l, p in zip(lens, ps)) / float(sum(lens))
    return sum(ps) / len(ps)
```

`len` 口径里的 `len` 是**窗口字符数，不是 token 数**；相邻窗重叠的 100 字在每个窗里各计一次，不做去重——所以要加权就直接按上式算，别自己先合并重叠段再算。

> **两种口径的概率刻度不同，不要跨口径比大小。** 把「认出 99% 的 AI 稿」这件事对齐时，
> 两种口径需要的线分别约 0.52 与 0.68；拿同一个 0.5 去比两边，比出来的是刻度差异，不是判别力差异。
>
> 长文本上**不要把开头当成整篇**：只取前 512 token 打分，长稿上它测的是「起手像不像 AI」，  
> 不代表全文。要么用本仓库的切窗聚合，要么明确说明你只评了开头。

---

## 精度

内部混合测试集（人工撰写文本 + 多来源 AI 生成文本）上的读数。

> **请把这组数字当「作者标定记录」，不是「可独立验证的评测结果」。** 该测试集的构成与规模不公开、不随仓库发布，  
> 因此**这几个数在仓库内无法复现**。仓库内能复现的只有 `quant/bench_readout.json`（量化档与 fp32 的一致性）。  
> 若要据以下判断，请在自己的语料上重跑一遍——本模型的分数在不同体裁、不同长度上分布不同，直接迁移阈值会失真。

| 聚合方式     | 冤枉真人稿的比例 | 认出 AI 稿的比例 | 要求认出 99% 的 AI 稿时的冤枉比例 |
| -------- | -------- | ---------- | --------------------- |
| `eq` 等权  | 1.36%    | 99.47%     | 1.18%                 |
| `len` 加权 | 1.52%    | 99.45%     | **0.78%**             |

三列的白话意思。**前两列的「线」都设在 50%**，意思是「AI生成概率 ≥ 50% 就算作判为像 AI」，  
也就是**至少落到上面「疑似AI参与」这一档**（50%–90%），而不是只算「AI特征明显」那一档（≥90%）：

- **冤枉真人稿的比例**——拿人写的稿子来测，有多少篇分数摸到 50% 及以上，被判成「疑似AI参与」或更重。  
  **这个数字越低越好。**
- **认出 AI 稿的比例**——拿 AI 写的稿子来测，有多少篇分数摸到 50% 及以上，被认出来。  
  **这个数字越高越好。**
- **要求认出 99% 的 AI 稿时的冤枉比例**——这一列绕了一层，但它是三种算法之间唯一公平的比法。

  换个问法就清楚了：不问「0.5 这条线怎么样」，而问「**如果要保证 100 篇 AI 稿里至少 99 篇被认出来，  
  这条线得放到多松？放宽之后，会连带冤枉多少篇真人稿？**」

  之所以非得这么比：等权和加权算出来的概率**刻度不一样**，同一个 0.5 对两者不是同一件事。  
  直接拿 0.5 去比「冤枉率」，比出来的是刻度差异，不是判别力差异。只有先把「认出 AI 的能力」  
  拉齐到同一水平（都是 99%），再比「为此付出多少冤枉真人的代价」，才是在比算法本身。  
  要认出 99% 的 AI 稿，等权需要把线压到约 0.52，加权需要把线压到约 0.68。  
  这两个线同样来自上面那批内部标定数据，**不是你机器上测出来的**，换语料要重新标定。

还有两点请留意：

- **前两列取的是最宽的判法。** 三档判决里要 ≥ 90% 才叫「AI特征明显」；只按 90% 这条线统计，  
  冤枉率会比 1.36% 更低。这里故意用宽口径，不往好看的方向挑线。
- **前两列的分母不是同一批样本**（一列全是真人稿，一列全是 AI 稿），各自在自己那批上统计，  
  所以两列不要相减或相加。

怎么选：

- **默认用等权**。50% 这条常用线下它冤枉真人更少（1.36% vs 1.52%）。
- **加权要配合重标阈值才有优势**：加权口径的刻度偏松，实测把线从 50% 提到约 68%、把认出率拉齐到 99% 之后，  
  冤枉率降到 0.78%（等权同口径是 1.18%）。若沿用 50% 直接用加权，反而比等权差。
- 两者认出 AI 稿的能力基本持平（99.4%+）。

误判与漏放的代价不对称：**把真人稿判成 AI 的代价高于放过 AI**。边界样本建议报「疑似AI参与」，  
不要直接说「是 AI 写的」。

---

## 句头概率（仅供参考）

除了整篇分，本模型还能给出每句话的句头概率——以该句为中心取上下文窗喂给模型得到的分数。

**它衡量的是「这一句所在的上下文读起来像不像 AI」，不是「这句由 AI 生成」。**

必须知道的限制：

1. **不能用它在篇内排序定位。** 句级信号在长篇里很弱，靠句分排 top-k 接近随机。
2. **真人写的稿子也可能有句子被标注AI特征。**
3. 整篇分与句头分是两套不同的统计量，不要混着当同一个结论用。

一句话：**句头是给人看的辅助线索，不是证据，也不是定位器。**

---

## 快速开始

### fp32（最省事）

```python
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from modelscope import snapshot_download

d = snapshot_download('fuyi163/xiaoyu-chinese-ai-detect')   # 或直接传本地目录
tok = AutoTokenizer.from_pretrained(d)
model = AutoModelForSequenceClassification.from_pretrained(d).eval()

text = '把要检测的中文文本放这里。'
with torch.no_grad():
    enc = tok(text, return_tensors='pt', truncation=True, max_length=512)
    logit = model(**enc).logits.float().item()

p = torch.sigmoid(torch.tensor(logit)).item()
print('AI生成概率 %.2f%%' % (100 * p))
```

### 量化档（int4 / int8）

> **这不是 GGUF / GPTQ / AWQ，是本仓库自带的打包格式。**
> `AutoModel.from_pretrained()` 不能直接加载 `quant/*.pt`；vLLM / ollama / llama.cpp 也都不认。
> **打包这一步已经做完了**——`quant/*.pt` 就是成品，直接下载用，不要自己再跑一遍打包。
> 你只需要照下面这段把成品**解包**成 state_dict 再灌进模型（`unpack_blob` 来自仓库里的 `pack_quant.py`）。
> **不想抄代码就用上面 fp32 那段**——那是标准 safetensors，`from_pretrained` 一行就加载。

量化件不是完整模型，是 `pack_quant.py` 的分组量化打包格式（group size 128，对称量化），
下载后需要解包回 state_dict 再灌进模型：

```python
import os, sys, torch
from modelscope import snapshot_download
from transformers import AutoConfig, AutoModelForSequenceClassification

d = snapshot_download('fuyi163/xiaoyu-chinese-ai-detect')   # 与上一段同一份
sys.path.insert(0, d)                      # 让 pack_quant 可被 import
from pack_quant import unpack_blob

bits = 4                                   # 4 / 8
blob = torch.load(os.path.join(d, 'quant', 'int%d_packed.pt' % bits),
                  map_location='cpu', weights_only=False)
sd = unpack_blob(blob)

# 只读 config.json 建空壳再灌权，避免为了跑 int4 先把 390 MiB 的 fp32 读进来
cfg = AutoConfig.from_pretrained(d)
model = AutoModelForSequenceClassification.from_config(cfg).eval()
missing, unexpected = model.load_state_dict(sd, strict=False)
assert not missing and not unexpected, (missing, unexpected)
```

> 打包件是全键完整的，正常情况 `missing` 和 `unexpected` 都应为空。有一项非空就说明打包件与  
> `config.json` 不配套，别继续用。

---

## 能做什么、不能做什么

**能：** 判一篇文章整体「像不像 AI 写的」，给一个概率和一个三档判决；逐句给出句头概率作辅助线索。  
用来做稿件自检、批量筛查、给编辑当参照。

**不能（重要，请勿误用）：**

1. **不能定位「哪几句是 AI 写的」。** 句级信号弱，篇内定位基本等于瞎猜。整篇分是倾向，不是证据。
2. **真人写的稿子也会被标红。** 措辞上只能说「这几句读起来像 AI」，不能说「这几句是 AI 写的」。
3. **不能当仲裁者。** 遇到人机混写、深度改写的稿子，本模型的分数不足以证明作者身份。
4. **跨体裁直接比大小没意义。** 论文体、演讲体、短评体的分数分布不同，阈值应分体裁分长度桶标定；  
   真人短稿有时比 AI 更像 AI。

---

## 文件说明

```
config.json               模型结构配置
model.safetensors         fp32 权重（标准 safetensors，可直读）
vocab.txt                 词表
tokenizer_config.json     分词器配置
special_tokens_map.json   特殊 token 映射
pack_quant.py             打包/解包工具（用 int8/int4 时要 import 它的 unpack_blob）
quant/int4_packed.pt      int4 打包件（60 MiB，轻量档，非标准格式）
quant/int8_packed.pt      int8 打包件（101 MiB，平衡档，非标准格式）
quant/bench_readout.json  量化一致性评估读数
LICENSE                   Apache License 2.0 全文
NOTICE                    底座出处与署名
SHA256SUMS.txt            全文件校验和
MANIFEST.json             构建信息（量化参数、逐文件哈希）
```

**必须先校验完整性再用**，尤其量化件（文件头损坏时 `torch.load` 报的错往往很有误导性）：

```bash
sha256sum -c SHA256SUMS.txt
```

Windows 上若没有 `sha256sum`，可用 Git Bash 执行同一条命令，或对单个文件用  
`certutil -hashfile <文件名> SHA256` 比对 `SHA256SUMS.txt` 里记的值。

---

## 环境

- Python ≥ 3.9，`torch` ≥ 2.0，`transformers` ≥ 4.40
- **`modelscope` ≥ 1.9**——上面两段示例用 `snapshot_download` 拉权重，缺了它第一行就 `ModuleNotFoundError`。  
  若权重已经在本地目录，可以不装 modelscope：把 `d` 直接设成那个路径即可
- 纯 CPU 可跑；有 GPU 更好，批量打分尤其建议上 GPU
- **速度随机器差异很大**，下面是 8 线程 CPU、batch=1 的实测参考值（不是承诺值，同机连续测的抖动约 ±15%）：

  | 场景 | 参考耗时 |
  | ---- | ---- |
  | 加载权重（一次性） | 约 1 秒 |
  | 512 token 一次前向 | 约 0.6 秒 |
  | 1200 字（3 窗） | 约 2 秒 |
  | 1 万字（约 25 窗） | 约 16 秒 |

  核数少、降频、或与其他任务抢 CPU 时，可能慢 3–10 倍；请以自己的实测为准。
- 内存：加载后约 1.5 GB。**下载 int4 只省磁盘和流量，运行内存与 fp32 相同**——  
  反量化后仍是 fp32 张量

---

## 许可与出处

权重基于开源的 `chinese-roberta-wwm-ext`（Apache-2.0）微调而来，本模型同样以 Apache-2.0 发布。  
详见同目录 `NOTICE`。

使用时请保留出处，并**不要把这个概率当作「作者身份」的判定依据**——它只是一个统计倾向，  
误判代价由使用方承担。
