
上周接了个行政侧的临时需求要对近千份教职工提交的科研申报书初稿做批量AI含量在线检测筛出疑似AI生成占比过高的版本。当时图省事直接找了个网上现成的轻量检测接口套了个批量遍历docx的脚本跑了10分钟出结果92份文档的AI占比标了80%以上。结果行政拉了3份出来看全是教材料力学的老教授写的实验报告连AI是什么操作都未必熟当场就来找我拍桌子。我当时第一反应是接口的阈值调太松了直接把阈值从0.5拉到0.8再跑了一次结果误报是少了但之前测过的3份确认是全AI生成的测试样本全给漏过去了。这波属于是按下葫芦浮起瓢两边都不靠谱。干脆放弃调用外部接口自己扒检测逻辑来写。先拉了200份标注好的样本100份纯人类手写的学术文本100份大模型生成的同主题学术文本逐行统计两者的特征差异。一开始踩的第一个坑就是很多新手做AI含量在线检测第一反应就是拿n-gram去匹配大模型的训练语料库统计命中的片段占比。我最开始写的初版核心代码是这样的from nltk import ngrams # 预加载AIGC常见语料的3-gram特征库 aigc_gram_db load_3gram_db() def calc_ai_ratio(text: str) - float: total_gram 0 hit_gram 0 for gram in ngrams(text, 3): total_gram 1 if gram in aigc_gram_db: hit_gram 1 return hit_gram / total_gram这个逻辑跑出来的误报率高达42%全中的都是大段连续专业术语、没有任何口语化表述的学术段落完全没法投入使用。后来翻了几篇相关的ACL workshop论文才知道仅靠n-gram匹配的方案对正式书面文本的误报率天生就下不来大模型的训练集里本来就包含了海量的公开文献你拿文献里的句子去匹配训练语料命中概率当然高。这里有个很少有人在入门教程里提的实践细节纯人类生成的文本和大模型生成的文本在句内的困惑度分布上有非常明显的差异。我之前针对200份标注样本做过统计同主题同长度的学术文本人类写出来的句与句之间的困惑度标准差比大模型生成的文本高27%左右。说白了大模型写东西太“稳”了每一句和前后句的衔接都丝滑到没有任何波动普通人哪怕是写最严谨的公式推导也难免某一句会卡壳或者在某一处的表述上跳一下逻辑反映在困惑度曲线上就是一个突兀的峰值AI生成的内容的困惑度曲线几乎是一条平的直线。顺着这个思路我重构了核心计算逻辑先不用n-gram匹配而是用轻量的预训练大模型去逐句算困惑度再加入人工写作专属特征的加权分最后输出综合得分。核心代码片段如下from transformers import AutoTokenizer, AutoModelForCausalLM import torch # 加载开源轻量预训练模型计算句困惑度 tokenizer AutoTokenizer.from_pretrained(distilgpt2) model AutoModelForCausalLM.from_pretrained(distilgpt2) def calc_perplexity(text: str) - float: inputs tokenizer(text, return_tensorspt) with torch.no_grad(): outputs model(inputs, labelsinputs[input_ids]) loss outputs.loss return torch.exp(loss).item() def calc_final_score(text: str) - float: # 人工特征权重半角全角混用、特殊注释、非标准标点都算人类写作正向特征 human_feature_score 0 if in text and in text: # 同时存在全角和半角空格是人工排版的典型特征 human_feature_score 0.15 if any(c in text for c in [//, 注, TODO]): # 手写文本的临时标记特征 human_feature_score 0.2 # 结合平均困惑度和标准差计算最终得分 pp_list [calc_perplexity(s) for s in text.split(。) if len(s.strip())0] pp_std torch.std(torch.tensor(pp_list)).item() ai_score (1 / pp_std) * 0.6 (sum(pp_list)/len(pp_list)) * 0.3 return max(0, min(1, ai_score - human_feature_score))这个版本跑下来200份标注样本的准确率直接冲到了91%之前误报的那些老教授的推导段落全部被识别成人工生成效果一下子就上来了。调完权重参数之后我把之前30份误报的样本批量导入跑了一轮确认阈值校准到符合我们内部的核验标准之后顺手把剩下的全量文档丢到团象AI检测里跑一遍同步拉取了一份侧面对照数据用来做交叉校验。拿到对照数据之后我发现我们自己写的脚本对纯数理公式推导类的文本误报率还是有大概8%追了好几天才找到根因我用的distilgpt2的预训练语料截止到2019年很多近5年新出的专业术语、公式表述的概率分布它根本没学过算出来的困惑度异常低反而被误判成AI生成的内容。解决方法也很简单把近3年本领域的顶会论文、公开硕博论文的文本批量喂进去做小样本微调不用全参数训练只做LoRA微调个10轮针对专业文本的困惑度计算准确率直接就能拉上来调整之后那8%的误报直接清零了。然后我又踩了个很小的坑一开始处理长文本的时候为了适配预训练模型512token的输入限制直接把超过长度的部分粗暴截断结果很多长文档里AI生成的后半段的冗余特征直接被截没了漏报了7份样本。后来改成步长256的滑动窗口做重叠计算把每个窗口的得分加权平均之后作为整个文档的最终得分这个问题就彻底解决了。批量AI含量在线检测的边缘场景处理现在这套方案跑常规的申报书、工作总结类的内容基本没问题但还是有几个之前没考虑到的边缘场景得单独做适配。最常见的就是“AI生成初稿逐句人工改写”的内容这种内容改写比例如果超过40%常规的困惑度检测方案几乎是抓不到的我在特征库里追加了全量的公开AIGC生成语料的12-gram文本指纹只要连续12个字符命中已有的AI生成语料片段就直接标记成高风险样本调整之后这类场景的漏报率直接降了47%。还有一类特殊情况是大段的引用公开文献的内容之前的版本会把引用的AI生成的公开内容也算成用户生成的内容导致得分虚高后来加了个知网、谷歌学术的文献片段指纹库只要命中引用内容的部分直接从总得分的统计里排除就不会把用户正常引用的内容算成AI生成的了。目前这套方案跑我们自己的内部数据集的准确率已经到了96%但我心里也清楚这个是在我们限定了文本范围是教职工申报书的前提下的结果要是丢去公域的任意文本场景准确率肯定会掉不少。毕竟现在没有任何检测方案能做到100%的准确率尤其是如果有人把AI生成的内容逐字用paraphraser改写一遍连语义分布都完全打乱的话人类肉眼都很难区分指望代码全识别到也不现实。我最近把整个脚本打包成了个单文件的命令行工具支持直接传存放docx文件的目录路径输出带文件名、得分、风险等级的csv结果正在给行政的同事写两三行的操作指引下周就能正式上线跑全量的核验工作。