大模型疯狂重复输出 3 小时没人发现:我搭了一套主动巡检 + 三级熔断体系

上个月一个周六晚上,座舱语音助手接到一条用户指令。三方模型流式输出直接炸了:
"你好呀!我!你刚!你!你!你!你!你!你!你你!你!你!你!你!你!你!你!你!你!你!你!你!你!你!你!你!你!你!你!你!你!你!......"

一个"你"字,循环输出了几百次。用户坐在车里,看着屏幕上的字一个一个往外蹦,像卡碟了一样。

没有告警,没有截断,没有兜底。接口返回 200,延迟正常,状态码正常——模型"活着",但在说废话。

这件事让我彻底意识到:三方大模型最恶心的故障不是挂了,是"活着但在说废话"。传统监控只看状态码和延迟,对这种"软故障"完全瞎了。

这篇文章,我把后来搭的整套主动巡检 + 分级触发 + 兜底熔断体系完整写出来,代码可以直接抄。

先搞清楚:哪些输出算"重复"

动手之前必须先定义清楚检测目标,不然写出来的规则要么漏检要么误杀。

我踩了两周坑之后,把三方模型的重复输出归成了 6 类:

这 6 类对应的检测策略不一样,不能一把尺子量所有。

核心检测引擎:4 个量化指标

所有检测逻辑最终落到 4 个可计算的数字上:

def compute_repeat_metrics(text: str, history: str = "") -> dict:    """    计算文本重复度四维指标    - text: 当前模型输出    - history: 上一轮模型回答(用于上下文复述检测)    """    # 指标1:n-gram 重复度(最精准,核心指标)    trigrams = [text[i:i+3] for i in range(len(text)-2)]    tri_repeat = 1 - len(set(trigrams)) / max(len(trigrams), 1)    # 指标2:滑动窗口字符重复率    window, size = text[-120:], 120    char_repeat = 1 - len(set(window)) / max(len(window), 1)    # 指标3:上下文余弦相似度(检测复述)    ctx_sim = cosine_similarity(text, history) if history else 0.0    # 指标4:循环周期检测(固定片段出现次数)    cycle_count = detect_cycle_pattern(text)    return {        "trigram_repeat": tri_repeat,    # >0.6 判定异常        "char_repeat": char_repeat,      # >0.35 判定异常        "context_sim": ctx_sim,          # >0.85 判定复述        "cycle_count": cycle_count       # >=4 判定循环    }

这 4 个指标覆盖了所有 6 类重复场景。n-gram 重复度是主力,其余三个做交叉验证降低误判。

第一层防线:实时流式拦截——边输出边检测

这是用户感知最小、止损最前置的一层。

模型还在往外吐 token 的时候,检测引擎就已经在跑了。命中规则,立刻截断,用户看到的只是一句正常的收尾话术。

class StreamRepeatDetector:    """流式输出实时重复检测器"""    def __init__(self, window_size=120, tri_threshold=4):        self.buffer = ""        self.window_size = window_size        self.tri_threshold = tri_threshold  # 3-gram 连续循环阈值    def feed(self, token: str) -> bool:        """每收到一个 token 调用一次,返回 True 表示命中重复"""        self.buffer += token        if len(self.buffer) < self.window_size:            return False        window = self.buffer[-self.window_size:]        # 规则1:3-gram 连续循环 ≥4 次        if self._check_trigram_cycle(window):            return True        # 规则2:窗口内重复率 >35%        if 1 - len(set(window)) / len(window) > 0.35:            return True        # 规则3:单词语连续重复 >6 次        if self._check_word_stuck(window):            return True        return False    def _check_trigram_cycle(self, text: str) -> bool:        trigrams = [text[i:i+3] for i in range(len(text)-2)]        from collections import Counter        return Counter(trigrams).most_common(1)[0][1] >= self.tri_threshold    def _check_word_stuck(self, text: str) -> bool:        # 检测最后一个词是否连续出现超过6次        words = text.split()        if len(words) < 6:            return False        return len(set(words[-6:])) == 1

命中截断后,系统自动补一句柔和收尾:

"以上就是本次解答内容,如果还有其他问题随时问我~"

用户完全无感知。但后台已经记了一笔:该模型故障次数 +1。

第二层防线:主动巡检——不等用户投诉

靠用户投诉发现问题,等于把质量保障外包给了你的用户。

这是我从那次 3 小时无告警事故里学到的最痛的一课。主动巡检分 4 条链路:

链路 1:线上实时指标监控(小时级)

按模型维度单独统计,每家三方模型独立看板:

  • 每千次请求的重复触发次数
  • 各重复类型占比(循环短句 / 段落复读 / 上下文复述)
  • 分场景故障率(闲聊 / 问答 / 文案 / 座舱对话)
  • 时段波动(凌晨高负载时重复率是否上涨)

核心告警逻辑:

当某厂商模型近 1 小时重复报错频次环比上涨 50%,系统自动标记该模型服务异常,推送告警。

链路 2:每日离线批量评测(凌晨自动跑)

每天凌晨低峰,用标准化测试集批量调用所有三方模型,自动打分。

测试集专门设计来诱导触发重复:

跑完自动输出各家模型重复率日报,对比基线阈值,超标即告警。

链路 3:灰度小流量探测

新模型版本上线前,切 5% 流量灰度验证。灰度期间重点盯重复指标,劣化则立刻停止放量、回滚旧版本。

链路 4:用户反馈闭环

用户点"回答重复"按钮 → 系统聚合 → 绑定对应三方模型 → 单模型单日重复类反馈超阈值 → 自动纳入异常清单。

被动反馈,转化为主动发现。

三级触发条件:从轻到重,梯度处置

检测发现问题之后怎么处置?不能一有重复就熔断,也不能等死了才切。我设了三级阈值:

级别 1:预警(仅通知,不动流量)

满足任意一条:

  • 单模型 10 分钟内重复请求占比 ≥8%
  • 离线评测当日重复率较基线上涨 ≥30%
  • 单场景重复频次环比上升 40%

动作:企微/钉钉推送告警,人工核查。

级别 2:限流降权(切走部分流量)

满足任意一条:

    • 半小时内重复占比 ≥15%
    • n-gram 循环报错持续上涨无回落
    • 同一重复 bug 集中爆发

    动作:该模型调用权重从 40% 降至 10%,其他模型承接,观测 30 分钟,回落则恢复。

    级别 3:熔断下线(彻底切走)

    满足任意一条硬条件:

    • 连续 15 分钟重复占比 ≥25%
    • 流式输出大面积字符卡顿刷屏
    • 离线评测重复率超基线 2 倍
    • 客服侧集中收到大量重复投诉

    动作:标记不可用,全量切备用模型。

    8% 预警、15% 限流、25% 熔断——三个数字,是拿线上事故换来的。

    兜底机制:4 层拦截,杜绝恶劣体验

    触发之后怎么兜?层层递进:

    兜底 1:流式截断(最前置)

    命中重复 → 终止拉取后续 token → 补全柔和收尾话术 → 用户无感知。

    兜底 2:跨模型重试(请求级)

    def call_with_fallback(query, context, model_priority=["modelA", "modelB", "modelC"]):    """单请求跨模型重试,最多切2个模型"""    for i, model in enumerate(model_priority[:3]):        response = call_llm(model, query, context)        metrics = compute_repeat_metrics(response, context.get("last_answer", ""))        if metrics["trigram_repeat"] < 0.6 and metrics["context_sim"] < 0.85:            return response  # 正常,直接返回        log_repeat_event(model, metrics)  # 记录异常    # 所有模型均异常,走静态话术兜底    return get_static_fallback(query)

    首次异常换同厂商备用接口重试,依旧重复则切第二优先级模型,最多切 2 次。

    兜底 3:全局静态话术(终极兜底)

    所有三方模型全挂 → 关闭动态生成 → 调用本地预置话术库应答 → 待指标回落后自动切回。

    兜底 4:上下文清理(多轮复述专属)

    连续 2 轮检测到上下文复述 → 自动裁剪过长历史 → 重置 prompt 并追加防重复约束。

    前置 Prompt 约束:从源头减少重复

    每次调用三方模型,系统默认塞入防重复指令:

    约束要求:1. 禁止语句循环、段落重复、首尾内容一致2. 长文本保证句式多变,杜绝同质化堆砌3. 对话收尾简洁,不无限输出客套话术4. 问题宽泛时多角度精简作答,不循环凑字数

    这层不能根治问题,但能把重复概率压低 30%-40%。属于成本最低的预防手段。

    整套体系跑起来之后的效果

    上线这套体系一个月,几个核心数字:

    最让我安心的是:再也没出现过"模型复读了 3 小时没人知道"这种事。

    三方大模型最恶心的故障不是宕机,是"活着但在说废话"。你的监控如果只看状态码和延迟,等于没监控。

    2026 年多模型调度已经是标配,但大部分团队的容灾还停留在"接口超时重试"这一层。重复内容这种"软故障",不主动巡检、不分级熔断、不做流式截断,迟早会在某个周六晚上给你来一次 3 小时静默事故。

    你们接三方模型遇到过重复输出的坑吗?怎么发现的?评论区聊聊,我猜大部分人第一次都是被用户投诉才知道的。

    这篇有用的话,转给正在做多模型调度的同事——他们可能正在裸奔。

举报/反馈
分享到: 微博 QQ 空间
对本文内容有合作意向?
我们将在 1 个工作日内与您联系
留言咨询