AI重复惩罚:这次怎么落地的

批量生成博客草稿时,同一个 prompt 连跑十几遍,头几次结构还正常,后面就开始出现整段复制:「首先…其次…最后」的骨架固定下来,连举例子的产品名都懒得换。长对话项目里我也见过模型自己接自己话茬,越聊越窄。

根因是生成时高概率 token 被反复强化,最后掉进自循环。这篇记我从粗暴加 penalty 到按场景调参的过程。

起因:为什么又要研究重复惩罚?

前几天在优化我的博客生成流程时,发现一个很奇怪的现象:同一个 prompt 让 GPT-4o 写技术文章,第一次生成质量还不错,连续跑了十几次后,输出的内容开始变得重复。

具体表现是

  • 段落结构和措辞开始固化
  • 同样的技术解释在多个地方反复出现
  • 甚至有些例子直接复制粘贴

这不是偶然。我在之前的对话式 AI 项目中也遇到过类似问题——模型在长对话中容易陷入"自循环"。

问题本质:AI 模型在生成时倾向于选择概率高的 token,而某些组合(尤其是开头和结尾)会因为贝叶斯增强效应变得越来越强,最终形成"死循环"。

这篇文章就是记录我如何从"简单粗暴"到"精细调优"的过程,把重复惩罚机制从理论落实到实践。

需求:到底要解决什么?

先明确一下实际场景和限制条件:

实际场景

  1. 批量生成:需要连续生成 50-100 篇类似主题的文章
  2. 长文档生成:单次生成 5000-10000 字的技术文档
  3. 对话场景:连续 20-30 轮对话,保持内容新鲜度

现实限制

  1. API 成本:不能无限制调参测试
  2. 生成质量:不能为了去重牺牲内容的连贯性
  3. 响应速度:后处理方案不能太慢
  4. 兼容性:需要适配不同模型(GPT、Claude、本地模型)

核心诉求

  • 控制重复率:将 n-gram 重复率控制在 10% 以下
  • 保持连贯性:不能因为强制去重导致句子逻辑断裂
  • 性能可接受:增加的延迟不超过 20%

初版方案:直接用 repetition_penalty

最直接的方案是使用模型自带的 repetition_penalty 参数。

基本原理

graph LR A[原始概率分布] --> B[检测重复 n-gram] B --> C{是否重复?} C -->|是| D[对重复 token 的概率打折扣] C -->|否| E[保持原始概率] D --> F[重新归一化] E --> F F --> G[采样]

Python 实现

from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

def generate_with_repetition_penalty(model, tokenizer, prompt, penalty=1.1):
    inputs = tokenizer(prompt, return_tensors="pt")
    outputs = model.generate(
        **inputs,
        max_length=500,
        repetition_penalty=penalty,
        temperature=0.7,
        top_p=0.9
    )
    return tokenizer.decode(outputs[0], skip_special_tokens=True)

# 测试不同惩罚值
penalties = [1.0, 1.1, 1.2, 1.5, 2.0]
results = {}

for p in penalties:
    text = generate_with_repetition_penalty(model, tokenizer, test_prompt, p)
    results[p] = calculate_repetition_rate(text)

第一次踩坑

直接用默认值 repetition_penalty=1.1,问题出现了:

现象:文章确实不重复了,但…

  • 词汇变得"生僻",为了避开常用词开始用奇怪的表达
  • 句子之间缺乏过渡词,读起来像"翻译腔"
  • 技术术语被强行替换(如把"函数"改成"运算单元")

根本原因:简单地对所有重复 token 统一打折扣,没有区分:

  • 结构性重复(如"首先…然后…最后…")——需要保留
  • 内容性重复(如重复解释同一个概念)——需要惩罚

第二版方案:N-gram 检测 + 分层惩罚

意识到问题后,我改用了更精细的 n-gram 检测策略。

实现思路

  1. 分层检测:不同长度的 n-gram 用不同惩罚力度
  2. 位置感知:距离越近的重复惩罚越重
  3. 词性过滤:忽略虚词、标点等结构性元素

代码实现

import numpy as np
from collections import defaultdict

class SmartRepetitionPenalty:
    def __init__(self,
                 n_gram_weights={2: 0.8, 3: 1.2, 4: 1.5},
                 distance_decay=0.1,
                 stop_words=None):
        self.n_gram_weights = n_gram_weights
        self.distance_decay = distance_decay
        self.stop_words = stop_words or {'的', '了', '是', '在', '和', '或', '但'}

    def calculate_penalty(self, text_tokens, current_pos, token):
        total_penalty = 1.0

        for n, weight in self.n_gram_weights.items():
            # 检查以当前 token 结尾的 n-gram 是否重复
            if current_pos >= n - 1:
                n_gram = tuple(text_tokens[current_pos - n + 1: current_pos + 1])

                # 检查历史出现位置
                positions = self.n_gram_history.get(n_gram, [])
                for pos in positions:
                    # 计算距离衰减
                    distance = current_pos - pos
                    decay = np.exp(-self.distance_decay * distance)

                    # 累加惩罚
                    total_penalty += weight * decay

        return total_penalty

    def update_history(self, text_tokens):
        for n in self.n_gram_weights.keys():
            for i in range(len(text_tokens) - n + 1):
                n_gram = tuple(text_tokens[i:i + n])
                if n_gram not in self.n_gram_history:
                    self.n_gram_history[n_gram] = []
                self.n_gram_history[n_gram].append(i)

效果对比

方案重复率连贯性评分生成速度
原始25%8.5/101.0x
简单惩罚(1.1)12%6.0/101.0x
分层惩罚8%7.8/101.3x

踩坑记录:几个容易被忽略的问题

问题1:中英文混杂场景

现象:中文文章中的英文技术名词(如 “API”, “REST”, “JSON”)被错误惩罚。

原因:英文单词通常是单个 token,而中文是字级 token,导致检测粒度不统一。

解决:添加语言检测层,对不同语言用不同 n-gram 设置。

def detect_token_type(token):
    # 简单检测:主要是英文
    if token.encode('utf-8', errors='ignore').decode() == token:
        return 'en'  # 英文
    return 'zh'  # 中文

# 英文用 2-4 gram,中文用 4-8 gram
lang_ngrams = {'en': [2, 3, 4], 'zh': [4, 6, 8]}

问题2:技术术语被迫重写

场景:解释"HTTP 状态码"时,第二次提到变成"网络响应标识",读者理解成本增加。

解决:建立术语白名单,允许专业术语在一定范围内重复。

# 术语白名单(允许重复的词汇)
term_whitelist = {
    'HTTP', 'API', 'REST', 'JSON', 'XML',
    '函数', '变量', '类', '接口', '方法'
}

def is_term(token):
    return token in term_whitelist or any(
        token.startswith(t) for t in term_whitelist
    )

问题3:性能瓶颈

实测:用上面的分层检测,生成长文本时延迟增加了 30%。

优化方向

  1. 滑动窗口:只检测最近的 500 tokens
  2. 增量计算:只更新受影响的 n-gram 计数
  3. 向量化:用 numpy 向量操作代替循环
# 滑动窗口优化
WINDOW_SIZE = 500

def calculate_penalty_sliding(self, text_tokens, current_pos, token):
    start = max(0, current_pos - WINDOW_SIZE)
    window_tokens = text_tokens[start:current_pos]

    # 只在窗口内检测
    total_penalty = 1.0
    for n, weight in self.n_gram_weights.items():
        if current_pos >= n - 1:
            n_gram = tuple(text_tokens[current_pos - n + 1: current_pos + 1])
            # 检查窗口内出现次数
            count = window_tokens.count(n_gram)
            total_penalty += weight * count

    return min(total_penalty, 2.0)  # 封顶

最终方案:混合策略

经过多轮迭代,我最终采用了混合策略:

1. 生成前:Prompt Engineering

# 添加多样性指导
system_prompt = """
你是一个技术写作专家。在写作时请注意:
- 使用多样化的表达方式,避免重复用词
- 同一个概念可以用不同的角度解释
- 允许专业术语重复,但避免冗余描述
- 保持段落结构的多样性
"""

2. 生成时:轻量级惩罚

final_config = {
    'repetition_penalty': 1.05,  # 轻度惩罚
    'temperature': 0.75,  # 提高一点温度
    'top_p': 0.92,  # 稍微放宽采样范围
}

3. 生成后:后处理去重

def post_process_dedup(text, threshold=3):
    """后处理:检测并合并重复段落"""
    paragraphs = text.split('\n\n')
    processed = []
    seen_hashes = set()

    for para in paragraphs:
        # 简单的段落指纹
        para_hash = hash(tuple(para.split()[:5]))

        if para_hash not in seen_hashes:
            processed.append(para)
            seen_hashes.add(para_hash)
        else:
            # 重复段落:保留但标记
            processed.append(f"[已合并相似段落: {para[:20]}...]")

    return '\n\n'.join(processed)

实际效果:数据说话

测试场景

  • 任务:批量生成 50 篇技术文章
  • 模型:GPT-4o
  • 长度:每篇约 2000 字

对比数据

指标原始方案简单惩罚混合策略
平均重复率22%11%7%
可读性评分8.2/106.5/108.0/10
生成时间基准基准+5%
API 成本基准基准基准
人工审核率35%20%12%

下表数据可视化后,三种方案在「降重复」与「保可读」之间的取舍一目了然:简单惩罚重复率降了但可读性明显下滑,混合策略两者兼顾。

批量生成 50 篇技术文章:原始方案、简单惩罚与混合策略的重复率及可读性对比

混合策略把重复率压到 7% 的同时,可读性评分仍维持在 8.0/10,是成本与质量最平衡的选择。

质量评估示例

原始输出(有重复):

API 是应用程序编程接口。API 允许不同应用程序之间通信。
API 使用 REST 或 GraphQL 等协议。API 需要认证和授权。

简单惩罚(生硬):

API 是应用程序编程接口。应用程序编程接口让各异软件程序互通。
软件程序运用 REST 亦或 GraphQL 等规范。软件程序需要鉴证及许可。

混合策略(自然):

API 是应用程序编程接口,它允许不同应用之间无缝通信。
现代服务通常采用 REST 或 GraphQL 协议,并通过 API Key 或 OAuth 进行访问控制。

结语:没有银弹

折腾了几个月重复惩罚,我的最大感悟是:“去重"和"流畅"是一对矛盾统一体

简单粗暴的惩罚虽然能降低重复率,但会牺牲内容的自然性。精细调优虽然效果好,但复杂度难以维护。

最终,我选择了"轻干预"的混合策略:

  • 生成前通过 Prompt 指导(成本低)
  • 生成时用轻度惩罚(平衡点)
  • 生成后简单过滤(兜底)

这个方案不是完美的,但在我的场景下达到了成本、质量和性能的最佳平衡。

给你的建议

  1. 先测再调:不同模型对重复惩罚的敏感度差异很大
  2. 关注上下文:短生成和长生成需要不同的策略
  3. 留人工空间:完全自动化的去重难免有瑕疵,保留人工审核环节

如果你也在做批量生成或长文本生成,希望这些经验能帮你少走一些弯路。


本文记录了我在优化 AI 生成流程中的实践,如果你有更好的方案或遇到了其他问题,欢迎交流讨论。

版权声明: 本文首发于 指尖魔法屋-AI重复惩罚:这次怎么落地的https://blog.thinkmoon.cn/post/352-ai-repetition-penalty-loop-fluent-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!