
最近实验室要求所有毕设初稿必须先过一轮内网AIGC痕迹筛查。 不能接公网不能上传学生隐私数据找了一圈现成工具踩了一堆坑。 之前试过GPTZero、原创力、团象AI检测、易撰、知网研学自带的筛查模块。 统统不好用要么要联网传数据要么本地部署动辄几G依赖完全满足不了需求。 得就干脆自己撸代码写个轻量版跑在实验室旧服务器上刚好能用。很多人以为AIGC痕迹检测是靠抓固定关键词其实根本不是那回事。 商用检测工具的核心逻辑是基于LLM的困惑度和突发熵两个维度做综合判定。 AI生成的文本每一个后续token的生成概率分布太平均困惑度普遍低于人类写作。 人类写东西经常跳脱会有长句突然接短句某段用词熵值突然飙升的情况。 我们做离线版不需要接入大模型直接用开源小参数量预训练模型就能算无GPU也能跑。我这次选的底座模型是开源124M参数版中文GPT2整个模型文件加起来才500MB不到。 最低配置只要2G内存CPU就能完成推理实验室2018年的旧戴尔服务器都能扛。 依赖库总共就4个transformers、torch、numpy、jieba连CUDA都不需要强制安装。环境安装命令如下pip install transformers4.28.1 torch numpy jieba0.42.1这里有个我踩过的大坑transformers别装4.29以上的版本。 新版对GPT2的分词器逻辑做了改动老CPU跑起来的速度直接慢3倍以上。 我一开始图新鲜装了4.35版本算一篇8000字的论文要15分钟人都等麻了。 退回到4.28稳定版之后相同配置下耗时直接降到2分钟以内效率提升非常明显。很多网上的开源教程计算困惑度直接用逐token求和的逻辑会有严重的溢出问题。 我之前跑长文本的时候算到3000字以上就报RuntimeError: exp overflow的错卡了快一小时。 后来翻了HuggingFace官方的示例代码才知道要把概率取对数再累加最后再取指数。 这个方案能100%避免数值溢出的问题长文本跑起来非常稳。核心的困惑度计算代码如下import jieba import torch import numpy as np from transformers import GPT2LMHeadModel, GPT2Tokenizer # 自动下载中文预训练GPT2到本地离线缓存 model_name uer/gpt2-chinese-cluecorpussmall tokenizer GPT2Tokenizer.from_pretrained(model_name) model GPT2LMHeadModel.from_pretrained(model_name) model.eval() def calc_ppl(text: str) - float: # 限制最大token长度不超过1024避免低配置机器内存溢出 tokens tokenizer.encode(text, return_tensorspt, truncationTrue, max_length1024) with torch.no_grad(): outputs model(tokens, labelstokens) # 直接取框架返回的损失值计算困惑度跳过手动求和步骤 loss outputs.loss ppl np.exp(loss.item()) return round(ppl, 2)这里要特别提醒一句千万别用英文预训练的GPT2来跑中文文本。 用英文底座算中文困惑度结果偏差能超过40%根本没有任何参考价值新手特别容易踩这个坑。 我选的是UER团队用中文CLUE语料微调的GPT2版本对中文的特征适配度非常高。但光靠困惑度这一个指标误报率会高到30%以上完全没法用。 我专门找了20篇纯AI生成、20篇学生手写的毕设初稿做对照测试结果发现两者的数值区间有大量重叠。 AI生成的内容ppl基本在25-65之间人类手写的内容ppl分布在70-200之间中间交叉的样本特别多。 这时候就必须引入第二个核心判定指标突发熵Burstiness。 AI生成的内容每句话的长度基本都稳定在15-25字之间方差小得离谱。 人类写作的句子长度分布极不稳定刚写完30字的长定语从句下一句可能就是5字的短总结。 这个指标算起来非常简单直接把文本按标点拆成单句统计每句的字数计算变异系数即可。突发熵的实现代码如下def calc_burstiness(text: str) - float: # 按中文常用句末标点拆分完整句子 sentences [s for s in text.replace(\n, ).split(。) if s.strip()] sentences [s for s in sum([s.split() for s in sentences], []) if s.strip()] sentences [s for s in sum([s.split() for s in sentences], []) if s.strip()] # 过滤掉句子数量不足的异常短文本 len_list [len(s) for s in sentences] if len(len_list) 2: return 0.0 # 归一化结果用 方差/均值 避免不同篇幅文本的数值差异过大 mean_len np.mean(len_list) var_len np.var(len_list) burstiness round(var_len / mean_len, 2) return burstiness两个指标都拿到之后我用100篇标注好的样本做阈值校准最终定的判定规则非常简单。 如果文本的ppl小于60且burstiness小于12直接判定为高概率AI生成。 如果ppl在60-100之间burstiness在12-25之间判定为混合生成建议人工复核。 剩下的所有情况全部判定为人类原创。 这套规则跑我手上的测试集整体准确率能到93%完全满足实验室的日常筛查需求。处理动辄几万字的完整毕设时千万不要直接把整篇论文塞进函数里计算。 整文本混算会抹平局部段落的异常特征就算某几章全是AI写的也容易被稀释成正常结果。 正确的做法是按1000字左右的窗口做滑动切片逐段计算最后把所有段的结果做加权平均。 这样还能精准定位到具体哪一个段落存在AI生成痕迹不用通篇人工排查。我写了个按段落边界切分的工具函数不会把完整的长句强行拆碎def split_text(text: str, window_size: int 1000) - list: # 先按换行拆分原生段落 paragraphs [p for p in text.split(\n) if p.strip()] chunks [] current_chunk for p in paragraphs: if len(current_chunk) len(p) window_size: current_chunk p \n else: chunks.append(current_chunk.strip()) current_chunk p \n if current_chunk.strip(): chunks.append(current_chunk.strip()) return chunks写完核心逻辑之后我直接用FastAPI包了个极简的上传接口前端用原生HTML写了个简单页面。 整个服务打包起来才800MB用Docker一键部署不需要任何额外的配置步骤。 所有上传的论文文件不会落盘到服务器硬盘直接在内存里读完就做处理处理完立刻清空内存。 完全符合实验室要求的「学生敏感数据绝对不能外传」的合规规定。 我自己跑了一周的测试处理100篇平均8000字的毕设总共耗时不到3小时。 之前问过商用的同类型离线筛查系统一年授权费要大几万我们这个自研版本零成本就能跑。最后说几个我踩过的冷门坑大家写代码的时候可以直接避开。 第一别把参考文献、公式块、图片说明这些内容算进统计范围。 这些部分的文本格式非常固定比如参考文献全是[1][2]开头句子都很短burstiness数值极低。 我专门加了个前置过滤逻辑把开头是方括号序号的行、全是英文的行全部跳过不计算。 第二学生手动修改的AI生成内容根本躲不开这套检测逻辑。 很多人以为把GPT写的内容逐字改30%的词就能混过去其实整体的句子结构、token分布特征还是AI生成的。 之前有个学生把AI写的段落改了快一半用词测出来的ppl还是只有42直接触发高风险警报。 第三不要用这套默认配置去检测古汉语、小语种等特殊领域的文本。 我们用的底座是通用中文语料训练的对垂直领域的特征适配度不够结果偏差会比较大。 这类场景只要拿对应的领域语料微调几十步GPT2底座就能把准确率拉到可用水平。很多人一提到AIGC检测就觉得必须得上百亿参数大模型搞集群分布式部署。 其实针对小体量的内网学术筛查场景这种轻量自研方案反而更实用。 完全不需要花冤枉钱买昂贵的商用授权全程数据不出内网性能也完全够小团队用。 最近有内网论文合规校验需求的朋友完全可以把这套代码拉下来改改直接跑。