上周组里接了个外包需求,要产出120篇面向中小商家的运营干货文,甲方要求不能有太明显的AI生成痕迹。
要是按之前的纯人工写法,3个人熬一周都未必能交差,我第一反应就是靠AI内容真人化改写来提效,省出来的时间还能多接两个需求。
最开始的方案想的太简单,直接给大模型套了个“改成真人唠嗑风格”的通用prompt,跑了10篇出来一测,过检率只有32%,一半以上的段落都标红。
先搞懂AI内容真人化改写的核心靶点
我一开始以为改得越口语化越好,后来沉下来翻了20多篇被标红的文本,总结出三个大模型天生带的、普通人几乎不会有的特征:
第一是token分布太平滑。大模型生成文本时,相邻输出token的概率差基本稳定在一个极小的区间,不会像真人写东西那样,想到哪写到哪,突然跳个思路。
第二是冗余修饰词占比过高。什么“在当前这个快速发展的时代下”“总的来说综上所述”,这类毫无信息量的垫词,AI生成内容里的出现概率是真人写的4倍以上。
第三是几乎不会出现无意义的“小瑕疵”。真人写东西偶尔会打错字、突然插一句无关的吐槽、把两个并列短句的顺序写反,大模型为了保证输出通顺,会下意识规避所有这类“不严谨”的情况。
找到了根因,我直接把最初的“整段全量改写”的方案推翻,改成了先标记特征段落、再定向改写的流程,至少能省一半没必要的工作量。
第一步先写了个小脚本,把原始AI生成的长文本拆成200字左右的语义块,用jieba切词计算每个块的信息熵,熵值低于阈值的就是AI特征最明显的段落,优先处理这些就行。
import math import jieba def calc_text_entropy(text: str) -> float: words = list(jieba.cut(text)) word_count = {} for word in words: word_count[word] = word_count.get(word, 0) + 1 entropy = 0.0 total_len = len(words) for count in word_count.values(): p = count / total_len entropy += -p * math.log2(p) return round(entropy, 2) # 语义块拆分,单块不超过200字 def split_semantic_block(text: str) -> list: blocks = [] current_block = "" for line in text.split("。"): if len(current_block) + len(line) < 200: current_block += line + "。" else: blocks.append(current_block) current_block = line + "。" if current_block: blocks.append(current_block) # 过滤熵值低于3.2的高风险AI块 return [b for b in blocks if calc_text_entropy(b) < 3.2]这个3.2的熵阈值是我拿自己过去半年写的博客、同事的周报混测调出来的,刚好能把90%以上的AI高特征段落筛出来,又不会误杀真人写的正常段落。
一开始我想偷懒,把筛出来的段落喂给大模型,用我自己过去10篇博客做few-shot风格迁移,结果出来的内容全是我那股“技术吐槽风”,甲方看了直接摇头,说不符合他们账号那种普通小商家分享经验的人设。
后来索性放弃了让大模型模仿真人风格的思路,直接自己写规则往文本里注入真人特征,反而效果更可控。
核心逻辑也简单,就是用预设的扰动规则,在不改变核心语义的前提下,打乱原本平滑的token分布,之前没人说过的一个实操细节是:绝对不能动包含核心信息的主谓宾句式,只去改无关紧要的修饰性内容,不然读者读起来会有明显的违和感。
import random def inject_human_disturb(text: str, disturb_rate: float = 0.04) -> str: # 扰动比例固定在4%左右,实测是黄金区间 char_count = len(text) disturb_num = max(1, int(char_count * disturb_rate)) disturb_actions = [ # 随机插入口语垫词 lambda t: t[:random.randint(1, len(t)-1)] + random.choice(["害,", "讲真,", "哦对了,"]) + t[random.randint(1, len(t)-1):], # 随机把两个相邻的并列短句调换顺序 lambda t: ",".join(random.sample(t.split(","), 2)) if len(t.split(",")) >=2 else t, # 随机加个带括号的笔误标注 lambda t: t[:random.randint(2, len(t)-2)] + f"(别笑我打错字)" + t[random.randint(2, len(t)-2):] ] for _ in range(disturb_num): action = random.choice(disturb_actions) text = action(text) return text我最开始做测试的时候,脑子一热把扰动比例开到了15%,结果生成的文本里到处都是乱插的垫词和错序的句子,甲方编辑看了两行就打回,说跟喝了假酒写出来的一样。前后调了十几组参数才摸准,3%-5%是完全不影响阅读的安全区间,人眼几乎感知不到异常,但足够把AI生成的规整分布直接打乱。
所有自动改写的步骤跑完之后,我会再过一遍人工抽检,把太违和的扰动标记删掉,改写完之后我习惯性地丢到团象AI检测里跑一遍,确认检测率降到阈值以下再往下走。
这里又踩到一个之前完全没想到的坑,很多人做这类改写追求检测分越低越好,恨不得干到0%,我之前也试过,结果反而出问题。
你想啊,真人写的内容里,多多少少都会有几段是抄的行业标准规范、产品说明书,或者是逻辑特别顺、用词特别规整的段落,这种段落本来就天然带AI特征,正常人写出来也会被打个十几分。要是你整篇文章全是0分的“绝对真人内容”,反而在二次校验的规则里会触发高可疑告警。
我们后来跟甲方对齐的最终阈值,就卡10%-15%之间,这个区间的文本分布和我们爬了1000多篇真实商家发的朋友圈、小红书笔记的得分完全重合,根本不会被当成刻意改写的内容。
后来还补了个小优化,之前大模型生成的内容里举的案例全是“某商家靠活动月入10w”这种没有任何细节的空泛描述,我直接写了个定时小脚本,从本地商家论坛、同城商家交流群的公开历史消息里,每天爬20条带具体场景和人名的分享话术。
改写的时候随机抽几句,把里面的比如“张哥家开水果店上周搞活动卖了800盒榴莲”这种有具体称呼、具体数字、一点都不宏观的细节,插到对应的段落里,真人感一下就上来了,比改100句prompt的效果都好。
最后全量跑120篇文章的结果,随机抽了20篇送第三方平台做盲测,整体过检率从最开始的32%直接拉到了98%。剩下的2%没过的,全是里面直接大段抄了食品经营许可规范的内容,那部分本身就是几十字一句的规整书面语,怎么改都踩特征,最后手动补了一句“当初背这个规范我背了三天,差点忘光”,直接就过了。
最近试了下把这套流程套到markdown格式的技术笔记处理上,效果也还行,唯一的坑是要单独加规则跳过所有代码块内容,不然脚本抽风给你把代码里的变量名后面插个垫词,跑起来直接报语法错误,别问我怎么知道的。