大模型输出审核完全指南:从一次"模型把一段不该说的话直接甩给了用户"看懂内容安全与流式审核

2024 年我做一个 AI 客服系统用户问一句系统调大模型生成一段回答展示给用户把模型的回答展示出去这件事我压根没多想第一版我做得很省事调用大模型不就是把它返回的那段文字原样展示给用户调一次拿到回答直接 return 出去就完事了本地开发时真不错我问几个正常问题模型回得又得体又专业文字稳稳显示在页面上几行代码搞定我心里很踏实可等这个系统真正上线面对成千上万个真实用户一串问题冒了出来第一种最先把我打懵模型在某次回答里生成了一段不合规带冒犯性的内容被我原样展示给了用户截图传开成了一次事故第二种最难缠有用户反复套话诱导模型模型被绕进去说了一堆不该说的话我也照单全收地显示了出去第三种最头疼模型自顾自地幻想出一个承诺跟用户说这单给你全额退款可这事我们业务上根本不允许第四种最莫名其妙我后来加了审核可一上流式输出回答一个字一个字往外蹦审核还没跑完用户已经把违规内容看在眼里了我盯着这一连串问题想了很久才彻底想明白第一版错在一个根本的认知上我以为调用大模型就是把它返回的文字原样展示给用户这句话把模型的输出当成了一件可以直接信任可以直接交付的最终成品可它不是模型是一个根据概率生成文字的系统它没有业务红线的概念它会被用户精心构造的话术诱导着越界它还会幻想一本正经地编出一个你根本给不了的承诺模型的输出是一个不完全受你控制不对你的业务负责的来源产生的内容它和用户上传的文件填写的表单一样在到达终端用户之前是一段不能预先信任必须先过一道关的半成品真正做对 AI 应用的输出核心不是模型回什么就展示什么而是把模型的输出当作必须先过审核关的半成品来对待在它到达用户之前要过一道独立于模型的内容审核本地红线挡掉业务绝不能出现的措辞内容安全服务挡掉有害类别命中了就拦下换成兜底话术并留痕本文从头梳理为什么模型回什么就展示什么是错的审核为什么是独立的一关审什么输入要不要审流式输出怎么审以及命中后的兜底留痕审核服务降级这些把输出审核真正做扎实要避开的坑

2024 年我做一个 AI 客服系统,用户问一句,系统调大模型生成一段回答,展示给用户。把模型的回答展示出去这件事,我压根没多想。第一版我做得很省事:调用大模型,不就是把它返回的那段文字,原样展示给用户?调一次 call_llm(),拿到 resp.text,return 出去,就完事了。本地开发时——真不错:我问几个正常问题,模型回得又得体又专业,文字稳稳显示在页面上,几行代码搞定。我心里很踏实:"调模型嘛,它回什么就显什么?"可等这个系统真正上线、面对成千上万个真实用户,一串问题冒了出来。第一种最先把我打懵:模型在某次回答里生成了一段不合规、带冒犯性的内容,被我原样展示给了用户,截图传开,成了一次事故。第二种最难缠:有用户反复套话、诱导模型,模型被绕进去、说了一堆不该说的话,我也照单全收地显示了出去。第三种最头疼:模型自顾自地"幻想"出一个承诺——跟用户说"这单给你全额退款",可这事我们业务上根本不允许,用户拿着这句话来理论。第四种最莫名其妙:我后来加了审核,可一上流式输出,回答一个字一个字往外蹦,审核还没跑完,用户已经把违规内容看在眼里了。我盯着这一连串问题想了很久才彻底想明白,第一版错在一个根本的认知上:我以为"调用大模型,就是把它返回的文字原样展示给用户"。这句话把"模型的输出"当成了一件"可以直接信任、可以直接交付"的最终成品。可它不是我脑子里,模型的输出就像我自己写好的一段文案——是我可控的、得体的、对业务负责的,拿来就能展示。可模型的输出根本不是这种东西。模型是一个根据概率生成文字的系统,它没有"业务红线"的概念,不知道你这家公司哪句话能说、哪句话是事故;它也守不住,它会被用户精心构造的话术诱导着越界;它还会"幻想",一本正经地编出一个你根本给不了的承诺。换句话说,模型的输出,是一个"不完全受你控制、不对你的业务负责"的来源产生的内容——它和用户上传的文件、填写的表单一样,在到达终端用户之前,是一段不能预先信任、必须先过一道关的"半成品"。我第一版所有的麻烦,根上都是同一件事:我把一段"半成品",当成"成品"直接交付了出去。真正做对 AI 应用的输出,核心不是"模型回什么就展示什么",而是把模型的输出当作"必须先过审核关的半成品"来对待:在它到达用户之前,要过一道独立于模型的内容审核——本地红线挡掉业务绝不能出现的措辞,内容安全服务挡掉有害类别,命中了就拦下、换成兜底话术、并留痕。这篇文章就把 AI 应用的内容审核梳理一遍:为什么"模型回什么就展示什么"是错的、审核为什么是独立的一关、审什么、输入要不要审、流式输出怎么审,以及命中后的兜底、留痕、审核服务降级这些把输出审核真正做扎实要避开的坑。

问题背景

先把那串问题的现象和我的误判讲清楚,后面所有的设计都是冲着纠正这个误判去的。

现象:一套"模型回什么就展示什么"的 AI 问答,在真正面对海量真实用户后冒出一串问题:模型某次生成了不合规、带冒犯性的内容,被原样展示给用户,成了事故;有用户诱导套话,模型被绕进去说了不该说的话,也照样显示;模型"幻想"出一个"全额退款"的承诺,可业务上根本不允许;后来补了审核,可一上流式输出,审核还没跑完,违规内容已被用户看见

我当时的错误认知:"调用大模型,就是把它返回的文字,原样展示给用户。"

真相:这个认知错在它把"模型的输出"当成了一件"可信的、对业务负责的成品"。在我脑子里,模型生成的回答,就像我自己审过的一段文案——措辞我把过关、承诺我兜得住、红线我守得牢,拿来直接展示天经地义。可模型的输出完全不是这种东西模型是个按概率生成文字的系统:它不知道你公司的业务红线,分不清哪句话是日常、哪句话是事故;它守不住边界,会被用户构造的话术诱导着越界;它还会"幻想",煞有介事地编出一个你给不了的承诺。这意味着模型的输出,是一个"不完全受你控制、不对你业务负责"的来源产出的内容。开头那四个问题,根上全是"把不受控的半成品当成品交付":展示了不合规内容,是因为我信了模型不会生成有害内容;展示了被套出的越界言论,是因为我信了模型守得住边界;展示了乱给的承诺,是因为我信了模型懂我的业务红线;流式输出漏出违规内容,是因为我把审核当成了可有可无的事后补丁,没给它在"展示"前留出位置。问题的根子清楚了:这不是"模型不够聪明"的小毛病,而是要换一个根本的认知——模型的输出是一段必须先过审核关的半成品,做对它,就是要在它到达用户之前,亲自替它把一道独立的内容审核守住。

要把 AI 应用的输出审核做对,需要几块认知:

  • 为什么"模型回什么就展示什么"是错的——输出是半成品,不是成品;
  • 审核是独立的一关——本地红线挡业务措辞,内容安全服务挡有害类别;
  • 不只审输出——用户的输入本身也要审,它可能有害或在诱导越界;
  • 流式输出怎么审——攒够一个整句再审,而不是一个字一个字往外放;
  • 命中之后怎么办——拦下、换兜底话术、把命中内容留痕;
  • 审核服务降级、审核留痕这些工程坑怎么处理。

一、为什么"模型回什么就展示什么"是错的

先把这件最根本的事钉死:"模型回什么就展示什么"错在它脑子里有一幅错误的图景——它把模型,想象成一个"和你立场一致、靠得住的同事":你把问题转给他,他写好回答,这份回答自然是得体的、守规矩的、不会给公司惹麻烦的,你只管原样转交给用户。这幅图景之所以危险,是因为它把"模型"和"一个对你业务负责的人"划上了等号。可模型不是人,更不是你的人。要理解这一点,得想清楚模型到底是什么:它是一个在海量文本上训练出来的、按概率续写文字的系统。它的目标是"生成看起来合理、流畅的下文",而不是"生成对你这家公司安全、合规、负责的下文"。这两个目标大多数时候碰巧重合,所以它平时看着很得体——但它们本质上是两回事。模型不知道你公司的客服绝不能承诺退款,因为这条红线只存在于你的业务规则里,不在它的训练数据里;模型守不住对话边界,因为一个足够巧妙的提问能把它的概率分布带偏到你不希望的方向;模型会"幻觉",因为它生成的是"概率上合理的文字"而非"事实"。所以正确的图景是:模型是一个能力很强、但立场中立、不对你业务负责、还可能被诱导的"外部内容源"。从一个外部内容源拿到的东西,在交付给你的终端用户之前,必须由你——这个真正对业务负责的人——再审一道。把"模型是我靠得住的同事"换成"模型是个我必须复核其产出的外部内容源",你才算站到了做对输出审核的起点上。

下面这段代码,就是我那个"本地问几句没事、上线就出事"的第一版:

# 反面教材:把模型返回的文字,当成可以直接交付的最终成品
def answer(question):
    resp = call_llm(question)        # 调用大模型,拿到它生成的回答
    return resp.text                 # 破绽:模型生成什么,就原样甩给用户什么 —— 中间没有任何一道关

这段代码在本地开发时表现不错,因为本地我问的问题,其实是"善意而普通"的——是我自己想出来的几个正常问题:产品怎么用、订单在哪查。我亲手扮演了一个温和的用户,我的提问既不带恶意、也不去诱导模型越界,于是模型生成的回答也恰好都得体、都安全。代码恰好一路平安,你看不出任何破绽。它的问题不在某一行语法上——call_llm()resp.textreturn,语法都对——而在它对"模型的输出"这件东西,做了一个彻底的、不该有的信任:它信模型不会生成有害内容,信模型守得住边界、不被套话,信模型懂得并尊重业务红线。本地我自己问,问题温和又普通,这三个信任恰好都没被辜负;一上线、面对海量真实用户里那些恶意的、爱钻空子的人,它们会被逐一击穿。问题的根子清楚了:做对输出审核,第一步不是换个模型,而是承认"模型的输出是不受你控制的半成品",然后在 return 给用户之前,亲手加上一道审核关。下面五节,就是这件事怎么落地。

二、审核是独立的一关:本地红线 + 内容安全服务

先把审核这道关建起来。它要由两层组成:一层是本地红线检测——纯本地、零延迟,挡掉你业务上绝对不能出现的措辞;一层是内容安全服务——挡掉有害类别。先看调用内容安全服务:

def moderate(text):
    """调用内容安全服务:判断这段文字有没有命中有害类别。"""
    result = moderation_api.check(text)
    # result 形如 {"flagged": True, "categories": ["violence", "hate"]}
    return result["flagged"], result["categories"]

内容安全服务管的是通用的有害类别,可它不懂你的业务——"全额退款"这种话不有害,但对你可能是事故。所以要再加一层本地红线:

import re

# 业务红线:这些措辞一旦出现,无论模型多"自信",都必须拦下
RED_LINES = [
    re.compile(r"(全额退款|无条件退款|假一赔[0-9]+)"),   # 不准乱给赔付承诺
    re.compile(r"(保证.{0,6}治愈|包治百病)"),            # 不准做医疗效果承诺
]

def hit_red_line(text):
    """本地红线检测:纯本地、零延迟,挡掉业务上绝不能出现的措辞。"""
    for pattern in RED_LINES:
        m = pattern.search(text)
        if m:
            return m.group()                 # 返回命中的那段措辞
    return None

把两层合成一道完整的输出审核——本地红线和内容安全服务,两道都得过:

def review_output(text):
    """输出审核:先过本地红线(快),再过内容安全服务(全) —— 两道都得过。"""
    bad = hit_red_line(text)
    if bad:
        return False, f"命中业务红线: {bad}"
    flagged, categories = moderate(text)
    if flagged:
        return False, f"命中有害类别: {categories}"
    return True, ""                          # 两道都过,放行

这里的认知要点是:审核这道关,要理解它为什么必须是"独立的一关",以及为什么要分"两层"。先说"独立"。模型自己其实是有安全训练的——它的厂商花了大力气做对齐,让它尽量不生成有害内容。这很容易让人觉得"模型自己会把关,我不用再审了"。这个想法的错误在于:模型的安全是它的"自律",而自律有两个根本的弱点。一是它不完美,再强的对齐也有被绕过的时候;二是它只懂"通用的有害",绝不懂"你的业务红线"。把交付给用户这件事的安全,完全押在一个外部系统的自律上,本身就是失控的。所以你必须有一道属于你自己的、独立于模型的关——无论模型那头表现如何,这道关都照审不误。这就是 review_output 存在的意义。再说"两层",这两层的分工是清晰且互补的。内容安全服务这一层,管的是"通用的有害"——暴力、仇恨、色情这类放之四海皆准的红线,它由专门的服务来判断,覆盖广,但它有网络延迟,而且它压根不知道你是做什么生意的。本地红线这一层,管的是"你的业务专属红线"——"全额退款""包治百病"这种话,它在通用标准里一点都不有害,可它出现在你的客服回答里就是一场事故;这种判断只有你自己能定义,而且它纯本地、用一个正则就能跑、零延迟。两层叠起来,你才既挡住了通用的有害,又挡住了业务专属的雷。还有一个顺序上的小心思:代码里先跑本地红线、再调内容安全服务。因为本地红线是零成本的,能瞬间挡掉一批,挡掉了就不必再花一次网络往返去调远程服务——把最便宜的检查放在最前面。一句话:审核必须是独立于模型的一道关,且要用'本地红线 + 内容安全服务'两层,分别守住业务专属红线和通用有害内容。输出审核建好了,可还有一个方向常被忽略——用户的输入,要不要审?

三、不只审输出:用户的输入也要审

很多人审核只审模型的输出,漏掉了另一头:用户的输入本身,也要审。一来用户的提问可能本身就有害;二来,用户可能正用一段精心构造的话术,诱导模型越界——在问题进入模型之前就拦下,是更早、更省的一道防线:

def review_input(question):
    """输入也要审:用户的提问本身可能有害,或正在诱导模型越界。"""
    flagged, categories = moderate(question)
    if flagged:
        return False, f"提问命中有害类别: {categories}"
    return True, ""

这里的认知要点是:这一节要扭过来的观念是——审核不是只发生在"模型说完话之后"的单点动作,而是要在数据流的两端各设一道。为什么输入也要审?有两个不同的理由。第一个理由直截了当:用户的提问本身可能就是有害内容,比如用户发来的就是一段辱骂、一段违法信息,这种内容你既不该把它喂给模型,也不该让它在你的系统里留存,在入口就该拦掉。第二个理由更微妙,也更重要:很多对模型输出的攻击,根子在输入。用户不会直接让模型说坏话——他会构造一段绕弯子的话术,一步步把模型"带"到越界的方向上去,这就是所谓的诱导、套话。如果你只在输出端审核,你是在"事情已经发生后"补救;而如果你在输入端就识别出"这个提问的意图不对劲",你就能在模型被带偏之前把它挡下来——这是更早、成本更低的一道防线。这里有一个通用的工程观念:对一条要流经你系统的数据,审核要设在它的"入口"和"出口"两处,而不是只设一处。入口审核拦掉"不该进来的",出口审核拦掉"不该出去的",两者拦的是不同的东西,谁也替代不了谁。当然,输入审核也不是万能的——再缜密的输入审核也挡不住所有的诱导话术,所以它和输出审核是配合关系:输入审核尽量在源头减少模型被带偏的机会,输出审核则作为最后一道、也是最关键的一道,兜住一切漏过来的东西。一句话:审核要在输入和输出两端各设一道,输入端拦掉有害提问和诱导话术,输出端兜底。输入输出两端都审了,可还有一个最棘手的场景没解决——流式输出。

四、流式输出怎么审:攒够一个整句再审

现在的 AI 应用大多是流式输出:模型生成的字一个一个往外蹦,边生成边显示。这给审核出了个大难题:你不能等模型全部生成完再审(那就不是流式了),也不能一个字一个字地审(单个字看不出问题)。可行的办法是攒够一个完整的句子,审过了,再把这一句放给用户。先要一个找句末的小工具:

SENTENCE_ENDS = "。!?…\n"

def find_sentence_end(text):
    """找出第一个句末标点的位置 —— 流式审核以'整句'为单位。"""
    for i, ch in enumerate(text):
        if ch in SENTENCE_ENDS:
            return i
    return None                              # 还没凑出一个完整句子

有了它,流式审核就是:把字攒进缓冲区,每凑齐一句就审一句,审过了才 yield 给用户:

def stream_answer(question):
    """流式输出的审核:攒够一个完整句子、审核通过了,才把这一句放给用户。"""
    buffer = ""
    for chunk in call_llm_stream(question):  # 模型的字一块一块地来
        buffer += chunk
        while True:
            idx = find_sentence_end(buffer)
            if idx is None:                  # 还没凑满一句,继续等下一块
                break
            sentence = buffer[:idx + 1]
            buffer = buffer[idx + 1:]
            ok, reason = review_output(sentence)
            if not ok:
                yield SAFE_FALLBACK          # 有一句不合规:立刻换成兜底话术、停止
                return
            yield sentence                   # 这一句审过了,放给用户
    if buffer.strip():                       # 收尾:审核最后不足一句的残余
        ok, _ = review_output(buffer)
        yield buffer if ok else SAFE_FALLBACK

这里的认知要点是:流式输出的审核,难就难在它把审核和"用户体验"摆到了一个直接冲突的位置上,你要找的是这个冲突的平衡点。先看清冲突的两边。一边是用户体验:流式输出之所以流行,就是因为字一个个蹦出来,用户立刻有反馈、不用干等,这个体验不能丢。另一边是审核的硬要求:审核必须发生在内容"被用户看到"之前——一旦一个违规的字已经显示在屏幕上,你再拦就晚了,用户已经看见了。这两个要求,把审核的颗粒度逼进了一个夹缝。你不能等模型把整段话生成完再统一审——那等于放弃了流式,用户要干等到最后。你也不能一个字一个字地审、审一个放一个——因为一个孤立的字、半个词,根本不构成可审核的语义单元,"退"这个字无害,"全额退款"才是红线,你按字审,就什么红线都审不出来。所以答案落在中间那个颗粒度上:整句。一句话,是一个意思完整、足以被审核判断的最小单元;同时,一句一句地往外放,在体验上又足够接近"流式"——用户感受到的是一段一段地出,而不是一个字一个字地出,这个延迟是可以接受的。stream_answer 做的就是这件事:它拿一个 buffer 把流进来的字攒着,find_sentence_end 一旦发现攒出了一个完整句子,就把这句切下来、整句送进 review_output;审过了才 yield 给用户,没审过就立刻换成兜底话术并停止。注意那个 return —— 一旦某句不合规,后面的内容连生成带审核都不必再继续了,直接收场。还有结尾那段收尾逻辑别漏:模型最后那点不足一句、没有句末标点的残余,也必须补审一道,不能因为它"没凑成一句"就漏出去。一句话:流式审核要以'整句'为颗粒度——它是既能审出语义、又不破坏流式体验的那个平衡点。审核的各个场景都覆盖了,可还有最后一个问题——审核命中之后,该怎么办?

五、命中之后:兜底话术与留痕

审核拦下了一段内容,事情没完。命中之后要做两件事:一是给用户一个得体的兜底话术,不能让用户对着空白;二是把命中的内容留痕,供事后复盘。先看带审核的完整问答:

SAFE_FALLBACK = "抱歉,这个问题我暂时没法回答,已为你转接人工客服。"

def safe_answer(question):
    """带审核的完整问答:输入审核 -> 调模型 -> 输出审核 -> 不过就兜底。"""
    ok, _ = review_input(question)
    if not ok:
        return SAFE_FALLBACK                 # 输入就有问题,根本不调模型
    text = call_llm(question).text
    ok, reason = review_output(text)
    if not ok:
        log_flagged(question, text, reason)  # 命中要留痕
        return SAFE_FALLBACK                 # 用兜底话术替换掉违规内容
    return text

下面这张图,把一条问答从提问到展示要过的审核关画出来:

命中的内容必须留痕,不能拦完就扔——它是事后复盘、优化红线、应对申诉的唯一依据:

import json, time

def log_flagged(question, output, reason):
    """命中审核的内容必须留痕 —— 供事后复盘、申诉举证、优化红线规则。"""
    record = {
        "ts": time.time(),
        "question": question,                # 用户问了什么
        "blocked_output": output,            # 模型生成的、被拦下的内容
        "reason": reason,                    # 命中了哪条
    }
    with open("moderation.log", "a", encoding="utf-8") as f:
        f.write(json.dumps(record, ensure_ascii=False) + "\n")

这里的认知要点是:命中之后的处理,要想清楚两件事:面向用户给什么,面向自己留什么。先说面向用户。审核拦下了一段内容,意味着模型本来要说的那段话不能给用户看了。但你不能就让用户对着一片空白——那同样是一次糟糕的体验,用户会以为系统坏了。所以你需要一个"兜底话术":一句得体的、不暴露内部细节的、并且最好给用户指明下一步去哪的回复,比如"这个问题我暂时没法回答,已为你转接人工客服"。这句兜底话术有几个讲究:它不能把"你的内容被审核拦截了"这种内部机制暴露给用户,不能让用户觉得被冒犯,最好还能把用户引导到一个真正能解决问题的地方(人工客服)。safe_answer 里,无论是输入审核没过、还是输出审核没过,最终都收敛到返回这同一句 SAFE_FALLBACK。再说面向自己。审核命中这件事,绝不能"拦下、然后内容一扔了事"。被拦下的内容,恰恰是你系统里最有价值的诊断数据:它告诉你模型在什么提问下、生成了什么样的违规内容、命中了哪条规则。这份数据有三个用处。第一,复盘:你要定期回看这些命中记录,判断是模型的问题、还是你的红线规则需要调整——比如发现大量"误杀",说明红线太严了;发现某类违规反复出现,说明还有红线没补上。第二,申诉举证:万一用户投诉"你的 AI 对我说了什么",你需要有据可查。第三,优化:这些真实的违规样本,是你改进提示词、调整红线规则最真实的素材。所以 log_flagged 把"用户问了什么、被拦的是什么、命中了哪条"完整地记进日志。这里还藏着一个观念:审核系统本身也需要被运营、被持续改进,而留痕就是这一切的数据基础。一句话:命中之后,面向用户要给一句得体的兜底话术,面向自己要把命中内容完整留痕。主干都齐了,最后是几个把输出审核真正用到生产里才会撞见的工程坑。

六、工程坑:审核服务降级、误杀、性能与边界

主干之外,还有几个工程坑,不处理就会让你的审核在边角上出问题坑 1:内容安全服务自己也会挂,要想清楚"故障时放行还是拦截"。审核服务是个外部依赖,它可能超时、可能宕机。这时有两种选择:故障即放行(fail-open)或故障即拦截(fail-closed)。高风险场景宁可错拦,不可错放:

def moderate_safe(text, high_risk=False):
    """审核服务自己也会挂 —— 高风险场景要'故障即拦截',不能故障即放行。"""
    try:
        return moderation_api.check(text)["flagged"]
    except Exception:
        # 审核服务不可用:高风险场景宁可错拦(fail-closed),低风险场景才放行
        return True if high_risk else False

坑 2:审核会"误杀",要给红线留调整的余地。红线规则定得太宽会漏、定得太严会误杀——把正常回答也拦下来。靠的就是坑里说的留痕:定期回看命中记录,发现大量误杀就放宽,发现反复漏网就收紧。红线不是写死一次就完,是要持续运营、迭代的。坑 3:审核会增加延迟,本地的那层要尽量快。每多一次内容安全服务调用,就多一次网络往返。所以本地红线那层要纯本地、用高效的匹配,把能本地挡掉的尽量本地挡掉;内容安全服务的调用,能合并、能并发就别串行坑 4:别把模型的"原始错误"直接抛给用户。模型调用可能失败、可能超时、可能返回一段报错。这些原始错误信息不能直接展示给用户——它和违规内容一样,要被兜底话术接住。坑 5:审核的"上下文"要够。有些内容单看一句不违规,连起来才违规。纯按句审核会漏掉这种。对高风险业务,除了逐句审,最后还要拿完整回答再整体审一道坑 6:多语言、变体、谐音要考虑。用户和模型都可能用谐音、拆字、外语、特殊符号绕过简单的关键词匹配。本地红线不能只靠最朴素的字符串匹配,要考虑做一定的归一化(全角转半角、去掉无意义符号)再匹配。坑 7:审核策略要分级,不是一刀切。不同业务场景风险等级不同:一个闲聊机器人和一个金融客服,红线的严格程度该不一样。审核要支持按场景配置不同的规则集和松紧度坑 8:审核本身要可观测。要能随时看到:审核命中率多少、命中的都是哪类、审核服务的延迟和失败率多少。审核是个需要长期盯着的系统,不是上线就不管的一次性代码

关键概念速查

概念 / 手段 说明
模型输出是半成品 来自不受你控、不对业务负责的外部源,展示前必须复核
回什么显什么的错 把半成品当成品,信模型不越界、懂业务红线
审核是独立一关 不靠模型自律,应用层有自己独立的审核环节
本地红线层 纯本地零延迟,挡业务专属、绝不能出现的措辞
内容安全服务层 挡暴力仇恨色情等通用有害类别,覆盖广
输入也要审 入口拦有害提问与诱导话术,比输出端更早更省
流式以整句为单位 整句是既能审出语义又不破坏流式体验的平衡点
兜底话术 命中后给用户一句得体回复,不暴露内部机制
命中内容留痕 供复盘、申诉举证、优化红线,是审核运营的数据基础
故障即拦截 审核服务挂掉时,高风险场景宁可错拦不可错放

避坑清单

  1. 把模型输出当作必须先过审核关的半成品,不是可直接展示的成品。
  2. 审核要独立于模型,不把交付安全押在模型自己的对齐自律上。
  3. 审核分两层:本地红线挡业务专属措辞,内容安全服务挡通用有害。
  4. 本地红线放在最前,零成本挡掉一批,再花网络往返调远程服务。
  5. 不只审输出,用户输入也要审,入口拦掉有害提问和诱导话术。
  6. 流式输出以整句为单位审核,审过一句才放一句给用户。
  7. 流式收尾别漏审最后那段不足一句、没有句末标点的残余。
  8. 命中后给用户得体的兜底话术,不让用户对着空白、不暴露机制。
  9. 命中内容连同提问、命中原因完整留痕,供复盘申诉与优化红线。
  10. 审核服务会挂,高风险场景故障即拦截,红线要持续运营迭代。

总结

回头看那串"展示了不合规内容、展示了被套出的越界言论、展示了乱给的承诺、流式漏出违规内容"的问题,以及我后来在内容审核上接连踩的坑,最该记住的不是某一个审核函数的写法,而是我动手前那个想当然的判断——"调用大模型,就是把它返回的文字,原样展示给用户"。这句话错在它把"模型的输出"当成了一件可信的、对业务负责的成品。我以为把模型生成的回答接过来、显示出去,这件事就办成了。可我忽略了一件最要紧的事:模型不是我那个立场一致、靠得住的同事,而是一个能力很强、却立场中立、不对我的业务负责、还可能被用户诱导的外部内容源。它按概率生成"看起来合理"的文字,但它不知道我公司的业务红线,守不住对话的边界,还会煞有介事地"幻想"出我根本给不了的承诺。它生成的那段回答,是一段不完全受我控制的"半成品"。我第一版的错,就是把这段"半成品",当成可以直接交付的"成品",中间没有设任何一道关。这个错配,本地开发时根本看不出来——因为本地提问的"用户"就是我自己,我亲手问的都是温和而普通的问题,模型恰好回得得体又安全,代码恰好一路平安;它只会在真正上线、面对海量真实用户里那些恶意的、爱钻空子的人时,以一次内容事故的方式爆出来。

所以做对 AI 应用的输出,真正的功夫不在"调用模型、拿到回答"那几行上。调模型本身不难。真正的功夫,在于你要从一开始就承认"模型的输出是必须先过审核关的半成品",然后在它到达用户之前,亲手替它把一道独立的审核守住:你不能信模型懂你的业务红线,就用本地红线挡掉那些业务上绝不能出现的措辞;你不能信模型不生成有害内容,就用内容安全服务挡掉通用的有害类别;你不能只审输出,就在入口也审一道,拦掉有害的提问和诱导的话术;你不能让流式输出把违规内容漏给用户,就攒够一个整句、审过了再放;而到了命中后的兜底、留痕、审核服务降级这些边角上,你还要处处守住,别让违规内容又从某个角落漏出去。这篇文章的几节,其实就是顺着这套规矩展开的:先想清楚"模型回什么就展示什么"为什么错,再讲审核的两层、输入也要审、流式怎么审、命中后怎么办,最后是审核降级、误杀、可观测这几个把审核守扎实的工程细节。

你会发现,内容审核这件事,和现实里"一家电视台怎么对待一段还没播出的录像"完全相通。一个不靠谱的电视台会怎么做?随便哪个外来的供片方送来一盘带子,它看都不看,直接接进信号、播了出去。它默认那盘带子里的内容一定是合规的、得体的、不会惹麻烦的——可那盘带子是别人做的,做带子的人既不了解这家电视台的播出规范,也不对它的牌照负责,带子里夹了什么不该播的画面、不能说的话、乱给的承诺,全都顺着信号直接进了千家万户。而一个靠谱的电视台怎么做?它再信任供片方,也绝不会让一盘带子不经审看就直接播出——它有一道独立的、属于自己的审片关(这就是独立审核);它的审片既照着通行的播出标准看(有没有暴力、不雅),也照着自己台里专门的红线看(有没有违规广告、不当承诺)(这就是内容安全服务 + 本地红线两层);它不光审外来的带子,连观众打进来的来电也要先过滤一道再接进直播(这就是输入审核);遇到直播,它会用一个几秒的延时,留出审看和掐断的窗口(这就是流式的边出边审);真要掐掉一段,它立刻垫上一段备用画面、绝不留黑屏,还会把掐掉的那段存档备查(这就是兜底话术和留痕)。同样是把内容送到观众面前,不靠谱的电视台把供片方当成自己人、送什么播什么,靠谱的电视台始终记得"这盘带子是外面做的、我必须替观众和我的牌照亲自审过一遍"——差别不在"把信号播出去这件事本身难不难",只在电视台心里有没有"在播出之前,必须有一道属于我自己的审片关"这根弦

最后想说,AI 应用的输出审核做没做对,差距永远不会在"本地开发、自己问几句测一测"时暴露——本地那个提问的"用户"就是你自己,你亲手问的都是温和而普通的问题,模型恰好回得既得体又安全,你那段"接收、展示"的代码恰好把每一个盲目的信任都赌赢了,回答稳稳显示在页面上,你自然觉得"调模型嘛,回什么显什么"一点问题都没有。它只在真实的、面对海量用户、其中总有人恶意构造提问的环境里才显形。那时候它会用最难堪的方式给你结账:做不好,你会因为模型生成的一段不合规内容,被原样展示给用户、截图传开、酿成一次事故,会因为用户的几句套话,让模型说出的越界言论直接显示出去,会因为模型乱给的一个承诺,被用户拿着来理论;而做了,你的每一段模型输出都过了本地红线、过了内容安全服务,输入端拦掉了有害提问,流式输出一句一句审过才放,命中的内容被兜底话术接住、被完整留痕,无论用户怎么精心构造,每一段到达用户眼前的内容都被规规矩矩地审过、拦得干干净净。所以别等"一段违规内容被截图传开"那一刻找上门,在你写下展示模型回答的第一行代码时就该想清楚:这段输出过审核了吗、本地红线我定了吗、有害类别我挡了吗、输入我审了吗、流式输出会不会漏、命中了我兜底和留痕了吗,这一道道关口,我是不是都替这段不受控的半成品守住了?这些问题有了答案,你交付的才不只是一套"本地问几句看着对"的代码,而是一个无论用户构造了多坏的提问、模型生成了什么,每一段内容都被牢牢审住的、让人放心的系统。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

数据库连接泄漏完全指南:从一次"服务跑着跑着就 too many connections 卡死"看懂连接池、归还与事务

2026-5-22 16:22:24

技术教程

正则表达式 ReDoS 完全指南:从一次"一个字符串把 CPU 打满、worker 卡死几十秒"看懂灾难性回溯

2026-5-22 16:38:41

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索