AI 生成内容已经进入媒体生产流程,这已经不是一个预测,而是一个正在发生的现实。最近,Semafor 的一则调查报道展示了一组具体数字:310 篇署名专栏中,有 50 篇被检测出含有 AI 痕迹。这个比例约 16.1%,比很多人预想得高,但它真正值得技术人关注的,不是“哪些文章作弊了”,而是“机器是怎么判断出一段文字有 AI 痕迹的”。如果把这 50 篇当作检测工具的产出,那么检测器依据的文本特征、统计指标和人工复核流程,才是可以复用的知识。
这篇文章会用一套可运行的最小检测方案,把“AI 痕迹检测”从抽象概念拆成具体步骤:先理解 AI 文本为什么会有统计规律,再准备 Python 环境,接着用句长方差、n-gram 重复率和困惑度等特征构造一个检测器,最后讨论怎样用类似流程去做抽样调查,以及为什么单看检测分数容易误判。
1. 先理解 AI 痕迹:Semafor 调查背后的技术问题
1.1 这项调查到底在检测什么
很多人在看到“50 篇含 AI 痕迹”时,第一反应是把“含 AI 痕迹”等同于“作者用 AI 作弊”。这个理解不准确。检测工具能看到的不是作者是否打开过某个 AI 工具,而是文本本身是否带有机器生成的特征。
AI 痕迹可以理解为:一段文本的用词分布、句子长度、连接词密度、重复片段比例等统计指标,与 AI 语言模型的生成结果高度相似。检测器的工作,就是计算一段文本在这些指标上离“人类写作分布”有多远。距离越远,越容易被标记为疑似 AI 生成。
所以在调查语境里,50 篇是一批被检测工具标记为“疑似”的文章,而不是已经有证据证明“一定是机器直接生成”的文章。这个区别非常重要,因为它决定了后续的人工复核、阈值校准和结果报道方式。
1.2 AI 生成文本为什么有规律性
主流大语言模型在生成文本时,本质上是在做“逐 token 预测”。模型根据前文计算下一个词的概率分布,然后按某种采样策略挑选一个词。为了生成通顺的内容,模型会倾向于选择概率较高、搭配常见、语境符合的词汇组合。
这种机制带来的结果是:AI 生成的文本整体非常“顺滑”。它很少出现人类写作中常见的突然换主语、句子断在奇怪位置、口语化插入语、长短句极端交错等情况。AI 的句子长度通常被控制在比较均匀的范围,段落结构高度模板化,观点推进方式也常常是“提出观点—给出论据—总结强化”三段式。
当一段文本的“顺滑度”高到一定程度,人类读者可能觉得内容清晰,但检测系统会从统计上看出一致性异常:句长标准差偏低,高频 n-gram 重复率偏高,连接词使用密度稳定,几乎没有笔误和意识流表达。
1.3 检测工具判定的是“相似度”,不是“事实”
一个常见的误解是:AI 检测工具会给出类似“这段文本 99% 是 AI 生成的”这样的结论,就表示工具在文本里发现了某种确凿证据。实际上,大多数检测工具输出的是一个概率或分数,这个分数表示“文本与训练集中 AI 生成样本的相似程度”。
这意味着检测存在两类风险。一类是误报:人类写的法律文书、产品说明书、教科书节选,因为句式固定、逻辑严密,容易被判定为 AI 生成。另一类是漏报:AI 生成的内容经过人工润色、加入个人经历、调整标点和换词后,统计特征会向人类写作偏移,检测器可能发现不了。
采样调查中,检测工具更像是一个“初筛器”。它的作用是缩小范围,让人工复核人员把精力集中在可疑文本上,而不是直接代替人工下结论。
1.4 检测结果 50/310 的正确理解方式
310 篇里检出 50 篇,约 16.1%。这个数字本身不能直接解读为“有 16% 的专栏作者在偷偷使用 AI”。它依赖几个前提:
- 检测工具的阈值设置在哪里;
- 检测工具在目标领域的误报率有多高;
- 是否做了人工复核;
- 样本是否随机、是否覆盖不同写作风格。
如果阈值调得宽松,更多人类写的正常文章会被标记;如果阈值调得严格,又会漏掉很多经过润色的 AI 文本。因此,调查中的 50 篇应当被看作“在特定工具、特定阈值、特定样本下的疑似数”,而不是一个可以推广到所有专栏文章的事实结论。
对技术人来说,这个案例更值得复用的价值在于:如何设计一套检测流程,使得结果可以追溯、可以解释、可以在不同团队之间复现。
2. 准备环境和样本:从最小特征开始复现一次检测
2.1 技术选型与依赖
为了快速跑通,本文分别提供两套检测方案:
- 轻量统计版:只使用 Python 标准库,计算句长标准差、重复 n-gram 比例和连接词密度。优点是依赖少、速度快、离线可跑;缺点是只能捕捉非常粗粒度的 AI 痕迹。
- 模型版:使用 Hugging Face Transformers 加载预训练语言模型,计算文本困惑度。优点是更能反映语言模型对文本的“熟悉程度”;缺点是需要下载模型权重,首次运行耗时较长。
建议先跑通轻量统计版,理解特征含义,再决定是否接入模型版。环境要求如下:
| 项目 | 建议要求 | 说明 |
|---|---|---|
| Python | 3.9 或更高 | 代码没有使用非常新的语法,3.9 即可 |
| 依赖 | 仅模型版需要 transformers、torch | 轻量版只用内置 re、math、collections |
| 硬件 | CPU 即可 | 模型版推理对小文本 CPU 也可运行 |
| 文本格式 | UTF-8 纯文本 | 中文样本建议使用 UTF-8 保存 |
创建虚拟环境并安装模型版依赖:
python -m venv .venv source .venv/bin/activate pip install transformers torch如果网络条件或机器资源不允许安装大体积依赖,可以暂时跳过模型版,先运行轻量统计版。
2.2 项目目录与输入格式
建议按下面的目录结构组织代码和样本:
ai_trace_detector/ ├── detector.py ├── run_detector.py ├── samples/ │ ├── human_01.txt │ ├── ai_01.txt │ └── unknown_01.txt └── requirements.txtsamples目录存放待检测文本,每篇一个文件。detector.py存放特征函数,run_detector.py负责读取文件并输出报告。这样拆分的好处是:特征逻辑可以单独测试,命令行入口保持干净。
2.3 准备对照样本
为了验证检测函数是否有效,至少需要准备三类样本:
- 人类写作样本:找一篇带有明显个人风格、有长短句变化的文章;
- AI 生成样本:用自己认可的 AI 工具生成一段说明性文字,或者直接使用公开的 AI 文本样本;
- 未知待测样本:可以是专栏摘录、论文段落或任意一段真实文本。
下面是一对示例文本。第一段偏人类写作风格,第二段偏 AI 模板风格,仅用于验证检测函数:
# samples/human_01.txt 昨天下午我去了一趟老城区。路边的店拆了好几家,也添了几家新的。走到巷子口的时候,突然想起小时候在这里买过糖画,那种甜味到现在还记得。天快黑了,我没多待,坐地铁回去了。# samples/ai_01.txt 首先,城市更新是一个复杂而漫长的过程。它涉及基础设施改造、产业结构调整以及社区居民生活方式的转变。因此,需要从多维度进行规划。其次,政府应当制定长期的战略目标。此外,公众参与也是至关重要的一环。总之,城市更新需要多方协同推进。注意:示例文本严格来说并不代表所有 AI 生成内容,但足够用来观察算法输出的差异。
2.4 环境自检
完成目录创建后,运行一个简单的 Python 命令,确认轻量版代码可以正常读取文件并输出文本长度:
python -c "from pathlib import Path; print(len(Path('samples/human_01.txt').read_text(encoding='utf-8')))"能输出一个数字,说明目录和编码没有问题。如果使用 Windows,文件编码建议统一为 UTF-8,避免gbk解码报错。
3. 用统计特征实现第一版 AI 痕迹识别
3.1 句长标准差:AI 写不出人类句式的“参差感”
人类写作时,句子长度往往不均匀。口语化文本里可能出现一个词组成的短句,也可能出现一个长句把多个条件堆在一起。AI 生成文本为了稳定输出,通常会保持句子长度相对均匀,避免出现极端的短句或超长句。
因此,句子长度标准差是一个很有解释力的特征。标准差越小,文本的句式越平稳,AI 痕迹越明显。实现时可以按中文句号、问号、叹号和英文句点切分句子,然后计算句长:
import re import math def split_sentences(text): parts = re.split(r'(?<=[。!?!?.])\s*', text.strip()) return [p.strip() for p in parts if p.strip()] def sentence_std(text): sentences = split_sentences(text) if len(sentences) < 3: return 0.0 lengths = [len(s) for s in sentences] avg = sum(lengths) / len(lengths) variance = sum((x - avg) ** 2 for x in lengths) / len(lengths) return math.sqrt(variance)这里直接使用字符数作为句长。虽然字符数不是完美的语言度量,但在中文场景下已经能体现文本节奏差异。对于很短的文本,句长统计不稳定,所以函数里限制至少 3 个句子。
3.2 重复 n-gram:AI 容易在措辞上“原地打转”
AI 生成文本的另一个特点是局部表达重复。因为模型在生成时依赖前文上下文,很容易反复使用相同的短语组合,尤其是三元组和四元组。检测重复 n-gram 的思路是:把所有连续 n 个词作为一个组合,统计这些组合中有多少是重复出现的。
重复率越高,文本越像机器拼装出来的。实现时不需要分词器,直接用正则提取中英文、数字即可:
from collections import Counter def ngram_repetition(text, n=3): words = re.findall(r'[\u4e00-\u9fffA-Za-z0-9]+', text.lower()) grams = [tuple(words[i:i+n]) for i in range(len(words) - n + 1)] if not grams: return 0.0 counts = Counter(grams) repeated = sum(1 for count in counts.values() if count > 1) return repeated / len(grams)这个比例会存在一定噪声。比如短文本中很多单词只出现一次,重复率为 0,但这不代表它是人类写的。因此,重复率需要和句长标准差、连接词密度一起看。
3.3 标点与连接词特征:AI 的书面语惯性
AI 在写中文说明文时,习惯使用“首先”“其次”“此外”“因此”“总之”等逻辑连接词。人类写作虽然也会用,但密度通常更低,除非是在写论文或报告。可以把常见连接词的出现次数归一化成密度:
def connector_density(text): connectors = ["然而", "此外", "总之", "因此", "首先", "其次", "最后", "这意味着", "需要注意的是"] count = sum(text.count(c) for c in connectors) return count / max(len(text), 1)连接词密度并不是越少越好,而是当密度明显偏高时,文本更像结构化写作的产物。这个特征在生产环境中需要根据领域调整,因为法律、技术文档等领域天然高频使用连接词。
3.4 把特征合并成一个可解释的痕迹分
把三个特征合并成一个 0 到 1 之间的分数,便于快速排序。这里使用启发式权重,不是经过训练的模型:
def ai_style_score(text): std = sentence_std(text) rep3 = ngram_repetition(text, 3) conn = connector_density(text) # 句长标准差越小,认为越像 AI;这里用 20 作为“人类句式波动”的经验参考值 std_score = max(0.0, 1.0 - min(std / 20.0, 1.0)) # 重复率本身已经是比例,越大越像 AI rep_score = rep3 # 连接词密度做一次缩放,避免文本较短时出现极端值 conn_score = min(conn * 50, 1.0) return 0.3 * std_score + 0.4 * rep_score + 0.3 * conn_score这个分数没有绝对意义,它的价值在于排序:把一批文本按分数从高到低排列,分数最高的那些最值得人快速浏览复核。权重可以根据自己的样本调整,但建议先保持默认值,跑一批人工标注数据后再修改。
4. 进阶:用预训练语言模型计算困惑度
4.1 困惑度的含义:模型对文本有多“意外”
困惑度(Perplexity)是语言模型领域常用的指标,用来衡量模型对一段文本的“意外程度”。逻辑是:如果一段文本与模型训练语料的分布很接近,模型能比较准确地预测下一个词,那么这段文本的困惑度就低;如果文本包含大量人类特有的表达、词汇组合或句式,模型的预测难度变大,困惑度就会升高。
AI 生成文本由模型采样而来,通常落在模型熟悉的分布区间,因此困惑度往往比人类文本更低。这也是许多检测工具把困惑度作为核心信号的原因。但要注意:人类写的简单邮件、短通知也可能有低困惑度,因为它们同样常见;而 AI 写的创意诗歌也可能有较高困惑度。所以困惑度仍然只是一个概率信号。
4.2 基于 GPT-2 的最小实现
以英文 GPT-2 为例,加载模型后可以直接计算损失,再取指数:
from transformers import GPT2LMHeadModel, GPT2Tokenizer import torch import math MODEL_NAME = "gpt2" tokenizer = GPT2Tokenizer.from_pretrained(MODEL_NAME) model = GPT2LMHeadModel.from_pretrained(MODEL_NAME) model.eval() def perplexity_of_text(text, max_length=512): encodings = tokenizer( text, return_tensors="pt", truncation=True, max_length=max_length ) input_ids = encodings.input_ids with torch.no_grad(): outputs = model(input_ids, labels=input_ids) return math.exp(outputs.loss.item())第一次运行时需要下载模型权重,实际耗时取决于网络和机器性能。在 CPU 上执行小段文本推理通常不会太久,但如果要批量分析 310 篇文章,建议使用 GPU,并增加批处理逻辑。
调用方式:
text = "This is a short example generated by a language model." ppl = perplexity_of_text(text) print(ppl)4.3 中文文本的坑:通用英文模型不能直接套用
GPT-2 的默认 tokenizer 主要面向英文,中文文本会被切分成单字或奇怪片段,计算出的困惑度可信度不高。要检测中文文本,需要换成中文预训练语言模型,例如在中文语料上继续训练过的 GPT-2 结构模型或 Bloom 结构模型。
具体替换方式很简单:把MODEL_NAME改成目标模型名,并把 tokenizer 和模型都从同一个模型名加载。不同模型的 max length 可能不同,需要阅读模型卡片确认。这里不指定具体模型名,是因为模型资源更新很快,而且团队的访问策略可能不同。落地前可以先在几篇人工标注的中文人写文本和 AI 文本上校准,确认困惑度分布有明显区分,再应用到批量场景。
4.4 特征融合与阈值设定
困惑度可以和统计特征一起使用。例如:
def hybrid_score(text, ppl): stat_score = ai_style_score(text) ppl_score = max(0.0, 1.0 - min(ppl / 100.0, 1.0)) return 0.4 * stat_score + 0.6 * ppl_score这里把 100 作为困惑度的经验参考上限,困惑度越低,ppl_score越高。实际使用时,应该用一批已知样本画出困惑度分布,再选择能把人类和 AI 文本分开的阈值。没有一个阈值适合所有领域,因为诗歌、技术文档、新闻稿的困惑度分布差异很大。
5. 回到 Semafor 式调查:如何设计一套可复用的判定流程
5.1 媒体调查的典型流程
如果要在自己的内容库中做一次类似的 AI 痕迹调查,推荐按以下流程操作:
- 明确样本范围:选择哪些文章、时间范围、作者群体,避免只挑容易出问题的样本。
- 统一文本清洗:去掉页眉页脚、作者简介、广告链接等非正文内容。
- 多特征打分:使用统计特征和困惑度等信号,对每篇文章生成一个分数。
- 设置初筛阈值:根据人工标注样本选定一个较高阈值,只对超过阈值的文章进入复核。
- 人工复核:由至少两个复核人分别查看可疑段落,记录判断依据。
- 输出结果:同时报告检测阈值、模型版本、误报风险和人工复核结论。
这样得到的“50 篇”才有方法论支撑,而不是一个孤立的数字。
5.2 采样偏差与工具误报
抽样调查中最容易犯的错误是样本不随机。如果选取的 310 篇专栏都来自同一类型,比如科技评论,那么这些文章本身可能就带有固定的句式结构,检测工具的误报率会明显升高。另一个问题是检测工具的训练数据来源,如果工具主要用英文新闻训练,中文专栏文本的分数就不一定有参考价值。
因此,任何调查报告都应该写明使用了什么工具、什么版本、什么阈值,以及是否有人工复核。缺少这些信息,读者无法判断 50/310 到底说明什么。
5.3 人工复核环节不能省
检测工具擅长排序,不擅长解释。人工复核时,可以重点看以下几点:
- 文本中是否有真实的个人经历或具体日期细节;
- 是否有作者特有的口头禅、写作偏好;
- 是否有不符合 AI 模板的反常表达;
- 是否存在检测工具标记为疑似的段落,但上下文支持它是人类写作。
人工复核的目的不是验证工具是否“抓到了作弊”,而是给标记结果提供更可靠的证据链。
5.4 可复用调查流程清单
| 阶段 | 关键动作 | 输出 |
|---|---|---|
| 抽样 | 明确样本来源、时间范围、类型 | 样本清单 |
| 清洗 | 去除页眉页脚、作者简介、超链接 | 清洗后纯文本 |
| 初筛 | 用统计特征和困惑度打分 | 每篇一个分数 |
| 阈值 | 用人工标注样本确定阈值 | 阈值记录 |
| 复核 | 两人独立复核可疑文章 | 复核记录 |
| 报告 | 附上工具版本、阈值、误报风险 | 调查报告 |
这个清单可以直接迁移到内容审核、媒体编辑和内部内容质量评估场景。
6. 运行验证:用已知样本检查检测结果
6.1 构造测试集
为了验证ai_style_score是否有效,准备一个小的测试脚本,读取samples目录下的文件并计算分数:
from pathlib import Path from detector import ai_style_score for file_path in sorted(Path("samples").glob("*.txt")): text = file_path.read_text(encoding="utf-8") score = ai_style_score(text) print(f"{file_path.name}: {score:.3f}")将上一节的人类写作样本和 AI 风格样本分别保存到samples/human_01.txt和samples/ai_01.txt,运行脚本后应看到 AI 风格样本的分数明显更高。
6.2 执行检测并输出报告
命令:
python run_detector.py可能的输出:
ai_01.txt: 0.812 human_01.txt: 0.351 unknown_01.txt: 0.622这里的数字是示例。实际结果会因为样本内容不同而变化。关键不是数字大小,而是相对排序是否符合预期。
6.3 结果怎么解读
如果人工智能生成的样本分数低于人类样本,说明当前权重或特征没有校准好。常见原因是文本太短,句长标准差无法体现差异。此时可以增加样本长度,或者调整权重,提高重复率和连接词密度的占比。
对于未知样本,不建议只凭一个分数下结论。正确做法是先按分数排序,然后对前 10% 到 20% 的文本做人工复核。分数可以作为筛选器,而不是裁决器。
6.4 阈值调整与评估指标
要评估检测效果,需要一组带有标签的样本。把标签分为“人类写作”和“AI 生成”,然后计算:
| 指标 | 含义 |
|---|---|
| 精确率 | 标记为 AI 的文本中,确实为 AI 的比例 |
| 召回率 | 真正的 AI 文本中,被成功标记的比例 |
| F1 | 精确率和召回率的调和平均 |
调整阈值时,阈值越高,精确率通常越高,但召回率会下降;阈值越低,能抓到更多 AI 文本,但误报也会增加。业务场景不同,选择也不同。内部审核更看重精确率,因为误伤成本高;公开调查更看重召回率,因为希望尽量不漏掉可疑案例。
7. 真实场景中的误判与排查路径
7.1 人类短句被误判为 AI
现象:一段只有两三句话的产品介绍被检测工具标记为“AI 生成”。
原因:短文本中句长样本太少,标准差计算不稳定;产品介绍本身用词固定,连接词和重复三元组比例可能偏高。
排查方式:检查脚本中的sentence_std是否因为句子数小于 3 而返回 0。如果是,则说明该特征在短文本中失效,分数完全由重复率和连接词决定。
处理建议:对短文本单独设规则。如果句子数少于 5 句,不建议使用统计特征直接判定,应增加“最短文本长度”限制。
7.2 AI 文本被人工改写后漏报
现象:AI 生成的文章经过作者加入个人案例、调整标点后,检测分数显著下降,检测器没有标记。
原因:人工改写打破了 AI 文本的统计规律,尤其是句长标准差和重复率会明显变化,导致检测器失去信号。
排查方式:对比改写前后的检测分数,确认是哪个特征下降最多。通常句长标准差上升,重复率下降。
处理建议:不要把检测作为唯一防线。应结合文本修订记录、作者访谈、发布渠道和内容审核流程综合判断。检测工具更适合做批量筛查,不适合做“审判”。
7.3 检测工具被提示词优化欺骗
现象:有人通过修改提示词,要求 AI“避免使用首先其次等连接词”“每句话长度保持 15 到 30 个字”,生成一篇在某检测工具下分数很低的文本。
原因:提示词优化实际上是在约束模型输出的统计分布,让它更接近人类文本的特征。检测器是基于统计特征的,所以可以被人为规避。
排查方式:如果同一作者的多篇文章都表现出异常的“均匀分布”,即使检测分数不高,也值得人工查看是否存在风格突变。
处理建议:在内容治理场景中,加入“作者历史风格一致性”检查。如果某作者过去三年来从未写过类似句式,但最近一个月的所有文章都切换成了非常统一的模板,这本身就是一个需要关注的现象。
7.4 从现象到根因的排查表
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 人类文章被判 AI | 短文本、固定模板、误报 | 查看单句样本数和特征值 | 提高文本长度下限,降低阈值 |
| AI 文章未被检测 | 人工润色、提示词规避 | 对比历史写作风格 | 引入风格一致性检查 |
| 分数波动大 | 特征权重和文本长度影响 | 用同一批样本重复测试 | 固定输入长度,使用滑动窗口 |
| 模型版加载慢 | 模型权重未缓存 | 检查缓存目录和网络 | 使用本地缓存或替换为更容易获取的模型 |
8. 生产落地与最佳实践
8.1 内容平台接入 AI 检测前要想清楚的问题
内容平台一旦接入 AI 检测,通常会面临三个问题:
- 误伤成本:正常作者被误判后会产生投诉,需要在产品设计上给出申诉机制。
- 对抗成本:检测器上线后,很快会有专门的提示词绕过方案,检测器需要持续迭代。
- 业务目标:平台是想减少低质内容,还是想识别机器批量发布内容。目标不同,检测口径完全不同。
最好把 AI 检测设计成“内容质量分”的一部分,而不是一个单独的否定按钮。低分内容可以被限流,但不应该被直接判定为违规。
8.2 多工具投票与人工复核机制
建议不要只依赖一个检测器。可以让多个检测工具并行打分,再进行投票:
- 两个以上工具超过阈值:标记为高风险;
- 一个工具超过阈值:标记为待复核;
- 所有工具均低于阈值:正常流转。
高风险内容进入人工审核队列,由审核人员在页面上看到检测分数、可疑段落和理由说明。这样既能降低误报,也便于责任追溯。
8.3 从检测到治理的演进路径
检测只是治理的第一步。对团队而言,更实际的路径是:
- 先用统计特征做快速批量筛查。
- 再用模型困惑度提升识别精度。
- 加入领域文本微调,针对自己的内容类型训练分类器。
- 建立持续更新机制,定期用新样本重新评估检测器。
- 将检测结果融入作者侧和运营侧的数据看板,形成闭环。
这样做的价值是:即使不能百分百识别 AI 文本,也能把成本最高的处理环节集中到最可疑的内容上。
8.4 团队可以立刻采用的检查清单
发布任何 AI 痕迹检测功能前,建议逐项确认:
- [ ] 是否有带人工标签的评测集;
- [ ] 是否记录检测工具版本和模型版本;
- [ ] 是否设置了文本长度下限;
- [ ] 是否评估误报率和漏报率;
- [ ] 是否设计人工复核流程;
- [ ] 是否保留检测原始记录;
- [ ] 是否允许作者申诉;
- [ ] 是否对高分组内容做抽样人工验证;
- [ ] 是否明确检测结果不直接作为违规处罚依据;
- [ ] 是否计划定期更新模型和特征。
AI 痕迹检测不会因为工具的增多而变得更简单,真正的难点始终是定义清楚“痕迹”的含义,以及愿意为误判付出多大代价。对开发者来说,先跑通一个统计检测器,再用人工标注数据迭代,比直接迷信某种工具更可靠。Semafor 那次调查的真正启示,不是 16.1% 这个数字,而是它提醒所有内容平台:AI 文本正在成为常态,检测与治理也需要从“能不能用”走向“怎么用才可信”。