检测降AI一体落地踩坑:别再搞分开的两套脚本了

上周组里接的学术内容批量润色项目,卡在内审环节卡了整整三天。之前我们一直把检测和降重分成两个完全独立的环节跑,流程碎得离谱,直到出了连续3篇文档AI率超标的事故,才逼着我们把检测降AI一体的流水线提上迭代优先级。

最开始的思路特别想当然,找个开源检测库扫完全文,AI率高就直接把全文丢给大模型全量改写,觉得一步到位省事儿。结果跑出来的结果能把人气死,本来人工写的、完全没问题的段落,被大模型改完之后反而带上了浓重的AI生成特征,检测率直接飙上去,属于越改越烂。

# 旧版分开检测改写的垃圾脚本 from detect_gpt import Detector from openai import OpenAI detector = Detector() client = OpenAI() def old_flow(doc_path: str): with open(doc_path, 'r', encoding='utf-8') as f: content = f.read() # 全量检测一次 full_score = detector.score(content) if full_score < 0.6: return "pass" # 全量直接改写 resp = client.chat.completions.create( model="gpt-3.5-turbo", messages=[{"role":"user", "content": f"改写以下内容降低AI概率:{content}"}] ) rewritten = resp.choices[0].message.content # 再全量检测 return detector.score(rewritten)

我当时对着这个脚本跑出来的结果愣了半小时,有一篇原始AI率只有42%的报告,全量改写之后直接冲到87%,内审打回的时候我还以为是检测库抽风,反复校验了三四次才发现问题根因。

你想啊,整文档全量丢给大模型改写,动辄几千上万字的内容,模型根本顾不上之前的段落风格,统一用它那套“首先、其次、综上所述”的模板化表达,本来低风险的人工内容直接被污染,相当于把好人也拉去陪绑了。

最坑的是全量检测完直接全量改写,你根本不知道哪一段是高风险、哪一段是安全的,所有调整都是盲操作,出了问题根本回溯不了,改出来的内容逻辑连贯性还特别差。

段落级检测降AI一体的调度逻辑才是核心

我们当时拍板推翻了之前的全量处理逻辑,改成先把整文拆成独立的语义块,逐块做检测,只针对分数超标的高风险块做定向改写,改完立刻验证本段的分数,过了就留,没过就迭代改两次,这样全程每一步的状态都是可回溯的。

很多人拆段落图省事按换行拆,最后踩一堆坑,我们前前后后测了两百多份文档,摸出来最优的拆分方式是先按句子粒度打散,再合并成300-500字的语义块,这个长度的片段既不会因为过短导致检测结果波动大,也不会因为过长导致改写成本飙升,分数误差基本能控制在5%以内,很少有公开资料提这个实测出来的阈值。

# 段落级核心调度逻辑 import re from nltk.tokenize import sent_tokenize def split_semantic_block(content: str, target_len: int=400) -> list[str]: # 按句子拆分后合并为接近目标长度的语义块 sentences = sent_tokenize(content) blocks, current_block = [], "" for sent in sentences: if len(current_block) + len(sent) > target_len and len(current_block) > 100: blocks.append(current_block.strip()) current_block = "" current_block += sent if current_block.strip(): blocks.append(current_block.strip()) return blocks def new_flow(doc_path: str, threshold: float=0.6) -> tuple[str, float]: with open(doc_path, 'r', encoding='utf-8') as f: content = f.read() blocks = split_semantic_block(content) processed_blocks = [] for block in blocks: score = detector.score(block) # 低风险块直接保留 if score < threshold: processed_blocks.append(block) continue # 高风险块定向改写,最多迭代2次 current_rewrite = block for _ in range(2): current_rewrite = rewrite_low_ai(current_rewrite) new_score = detector.score(current_rewrite) if new_score < threshold: break processed_blocks.append(current_rewrite) final_content = "\n".join(processed_blocks) # 最后全量校验一次总分 final_score = detector.score(final_content) return final_content, final_score

这里的rewrite_low_ai函数的prompt也有讲究,不能随便用网上那种泛泛的降重提示词,我们调了十多版,最后留的版本是要求改写时100%保留所有专业术语、数据结果、公式编号和引用标记,只调整语序、替换非核心的书面化表达,绝对不能改动原意,不然改出来的内容逻辑跑偏,后续人工校对的成本反而更高。

我们之前踩过个特别蠢的坑,提示词没写清楚不能改专业名词,模型直接把“残差网络的跳层连接”改成“残差网络的跳跃式链路”,提交给客户之后被专业评审打回,说内容表述不专业,白忙活了一下午。

顺着这个调度逻辑我们又补了好几个小优化,比如加了本地缓存,同一个语义块之前改过、测过合格的话,就直接把结果存到本地KV库,下次碰到完全相同的片段直接读缓存,不用重复调用大模型,光是这一项就把API调用成本干下去70%。

还加了异常拦截规则,要是某段改写之后的AI检测率比原始块高了20%以上,直接放弃本次改写,回退到原始内容,避免大模型抽风输出的内容把整段的风险拉高,后续排查都没法排查。

所有调度逻辑跑通之后我把攒的三十多份测试样例批量丢到团象AI检测里跑一遍,确认全量输出的通过率稳定在95%以上才同步给组里其他同学用。

测完批量样例我们还发现了个很有意思的小细节,之前大家都怕加多余的内容会改变原文意思,其实在高风险块里随机插入1-2个没有实际语义负担的连接词,比如“其实”“换句话说”“值得一提的是”这类完全不影响专业内容的词,AI检测率能悄咪咪降5%-10%,效果比改好多句子都明显。

原理也很简单,现在主流的AI生成内容检测模型,训练集里的大模型输出基本都是极度精简、没有冗余连接词的规整表达,你人工加一两个这种完全随意的词,直接就能打破大模型生成内容的特征分布,属于成本极低但收益极高的小trick。

还有个很少有人注意的点,大模型天生爱写排比类的规整句式,什么“第一、第二、第三”分点论述,这种结构的检测特征特别明显,AI检测模型一抓一个准。我们后来在调度逻辑里加了个10行不到的正则规则,碰到连续三个并列的规整分点,就随机把其中一两个分点的开头改成更随意的表述,比如把“第一”改成“这里要额外说下”,改完之后的段落检测率普遍能再降一截。

现在这套流程跑了快两周,效果比我们最开始预估的还要好,之前跑100份文档的全流程要3个多小时,现在全链路自动化跑下来40分钟就能出结果,人工只需要最后抽10%左右的样做逻辑校验,整体人力成本降了80%。

之前最头疼的全量改写带来的次生高风险问题基本消失了,现在100份文档里最后全量检测不达标的占比不到3%,比之前37%的超标率好太多,这周已经把之前攒的积压文档全部清完了。

昨天刚试了个新的小规则,把整句长度超过80字的长句,随机找个逻辑断点拆成两个短句,目前跑了20份样例看整体AI率还能再往下压2到3个百分点,等这周攒够100份测试数据,确认没有副作用之后就把这段逻辑补到主流程里。