2024 年我做一个 AI 客服系统,用户问一句,系统调大模型生成一段回答,展示给用户。把模型的回答展示出去这件事,我压根没多想。第一版我做得很省事:调用大模型,不就是把它返回的那段文字,原样展示给用户?调一次 call_llm(),拿到 resp.text,return 出去,就完事了。本地开发时——真不错:我问几个正常问题,模型回得又得体又专业,文字稳稳显示在页面上,几行代码搞定。我心里很踏实:"调模型嘛,它回什么就显什么?"可等这个系统真正上线、面对成千上万个真实用户,一串问题冒了出来。第一种最先把我打懵:模型在某次回答里生成了一段不合规、带冒犯性的内容,被我原样展示给了用户,截图传开,成了一次事故。第二种最难缠:有用户反复套话、诱导模型,模型被绕进去、说了一堆不该说的话,我也照单全收地显示了出去。第三种最头疼:模型自顾自地"幻想"出一个承诺——跟用户说"这单给你全额退款",可这事我们业务上根本不允许,用户拿着这句话来理论。第四种最莫名其妙:我后来加了审核,可一上流式输出,回答一个字一个字往外蹦,审核还没跑完,用户已经把违规内容看在眼里了。我盯着这一连串问题想了很久才彻底想明白,第一版错在一个根本的认知上:我以为"调用大模型,就是把它返回的文字原样展示给用户"。这句话把"模型的输出"当成了一件"可以直接信任、可以直接交付"的最终成品。可它不是。我脑子里,模型的输出就像我自己写好的一段文案——是我可控的、得体的、对业务负责的,拿来就能展示。可模型的输出根本不是这种东西。模型是一个根据概率生成文字的系统,它没有"业务红线"的概念,不知道你这家公司哪句话能说、哪句话是事故;它也守不住,它会被用户精心构造的话术诱导着越界;它还会"幻想",一本正经地编出一个你根本给不了的承诺。换句话说,模型的输出,是一个"不完全受你控制、不对你的业务负责"的来源产生的内容——它和用户上传的文件、填写的表单一样,在到达终端用户之前,是一段不能预先信任、必须先过一道关的"半成品"。我第一版所有的麻烦,根上都是同一件事:我把一段"半成品",当成"成品"直接交付了出去。真正做对 AI 应用的输出,核心不是"模型回什么就展示什么",而是把模型的输出当作"必须先过审核关的半成品"来对待:在它到达用户之前,要过一道独立于模型的内容审核——本地红线挡掉业务绝不能出现的措辞,内容安全服务挡掉有害类别,命中了就拦下、换成兜底话术、并留痕。这篇文章就把 AI 应用的内容审核梳理一遍:为什么"模型回什么就展示什么"是错的、审核为什么是独立的一关、审什么、输入要不要审、流式输出怎么审,以及命中后的兜底、留痕、审核服务降级这些把输出审核真正做扎实要避开的坑。
问题背景
先把那串问题的现象和我的误判讲清楚,后面所有的设计都是冲着纠正这个误判去的。
现象:一套"模型回什么就展示什么"的 AI 问答,在真正面对海量真实用户后冒出一串问题:模型某次生成了不合规、带冒犯性的内容,被原样展示给用户,成了事故;有用户诱导套话,模型被绕进去说了不该说的话,也照样显示;模型"幻想"出一个"全额退款"的承诺,可业务上根本不允许;后来补了审核,可一上流式输出,审核还没跑完,违规内容已被用户看见。
我当时的错误认知:"调用大模型,就是把它返回的文字,原样展示给用户。"
真相:这个认知错在它把"模型的输出"当成了一件"可信的、对业务负责的成品"。在我脑子里,模型生成的回答,就像我自己审过的一段文案——措辞我把过关、承诺我兜得住、红线我守得牢,拿来直接展示天经地义。可模型的输出完全不是这种东西。模型是个按概率生成文字的系统:它不知道你公司的业务红线,分不清哪句话是日常、哪句话是事故;它守不住边界,会被用户构造的话术诱导着越界;它还会"幻想",煞有介事地编出一个你给不了的承诺。这意味着模型的输出,是一个"不完全受你控制、不对你业务负责"的来源产出的内容。开头那四个问题,根上全是"把不受控的半成品当成品交付":展示了不合规内容,是因为我信了模型不会生成有害内容;展示了被套出的越界言论,是因为我信了模型守得住边界;展示了乱给的承诺,是因为我信了模型懂我的业务红线;流式输出漏出违规内容,是因为我把审核当成了可有可无的事后补丁,没给它在"展示"前留出位置。问题的根子清楚了:这不是"模型不够聪明"的小毛病,而是要换一个根本的认知——模型的输出是一段必须先过审核关的半成品,做对它,就是要在它到达用户之前,亲自替它把一道独立的内容审核守住。
要把 AI 应用的输出审核做对,需要几块认知:
- 为什么"模型回什么就展示什么"是错的——输出是半成品,不是成品;
- 审核是独立的一关——本地红线挡业务措辞,内容安全服务挡有害类别;
- 不只审输出——用户的输入本身也要审,它可能有害或在诱导越界;
- 流式输出怎么审——攒够一个整句再审,而不是一个字一个字往外放;
- 命中之后怎么办——拦下、换兜底话术、把命中内容留痕;
- 审核服务降级、审核留痕这些工程坑怎么处理。
一、为什么"模型回什么就展示什么"是错的
先把这件最根本的事钉死:"模型回什么就展示什么"错在它脑子里有一幅错误的图景——它把模型,想象成一个"和你立场一致、靠得住的同事":你把问题转给他,他写好回答,这份回答自然是得体的、守规矩的、不会给公司惹麻烦的,你只管原样转交给用户。这幅图景之所以危险,是因为它把"模型"和"一个对你业务负责的人"划上了等号。可模型不是人,更不是你的人。要理解这一点,得想清楚模型到底是什么:它是一个在海量文本上训练出来的、按概率续写文字的系统。它的目标是"生成看起来合理、流畅的下文",而不是"生成对你这家公司安全、合规、负责的下文"。这两个目标大多数时候碰巧重合,所以它平时看着很得体——但它们本质上是两回事。模型不知道你公司的客服绝不能承诺退款,因为这条红线只存在于你的业务规则里,不在它的训练数据里;模型守不住对话边界,因为一个足够巧妙的提问能把它的概率分布带偏到你不希望的方向;模型会"幻觉",因为它生成的是"概率上合理的文字"而非"事实"。所以正确的图景是:模型是一个能力很强、但立场中立、不对你业务负责、还可能被诱导的"外部内容源"。从一个外部内容源拿到的东西,在交付给你的终端用户之前,必须由你——这个真正对业务负责的人——再审一道。把"模型是我靠得住的同事"换成"模型是个我必须复核其产出的外部内容源",你才算站到了做对输出审核的起点上。
下面这段代码,就是我那个"本地问几句没事、上线就出事"的第一版:
# 反面教材:把模型返回的文字,当成可以直接交付的最终成品
def answer(question):
resp = call_llm(question) # 调用大模型,拿到它生成的回答
return resp.text # 破绽:模型生成什么,就原样甩给用户什么 —— 中间没有任何一道关
这段代码在本地开发时表现不错,因为本地我问的问题,其实是"善意而普通"的——是我自己想出来的几个正常问题:产品怎么用、订单在哪查。我亲手扮演了一个温和的用户,我的提问既不带恶意、也不去诱导模型越界,于是模型生成的回答也恰好都得体、都安全。代码恰好一路平安,你看不出任何破绽。它的问题不在某一行语法上——call_llm()、resp.text、return,语法都对——而在它对"模型的输出"这件东西,做了一个彻底的、不该有的信任:它信模型不会生成有害内容,信模型守得住边界、不被套话,信模型懂得并尊重业务红线。本地我自己问,问题温和又普通,这三个信任恰好都没被辜负;一上线、面对海量真实用户里那些恶意的、爱钻空子的人,它们会被逐一击穿。问题的根子清楚了:做对输出审核,第一步不是换个模型,而是承认"模型的输出是不受你控制的半成品",然后在 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:审核本身要可观测。要能随时看到:审核命中率多少、命中的都是哪类、审核服务的延迟和失败率多少。审核是个需要长期盯着的系统,不是上线就不管的一次性代码。
关键概念速查
| 概念 / 手段 | 说明 |
|---|---|
| 模型输出是半成品 | 来自不受你控、不对业务负责的外部源,展示前必须复核 |
| 回什么显什么的错 | 把半成品当成品,信模型不越界、懂业务红线 |
| 审核是独立一关 | 不靠模型自律,应用层有自己独立的审核环节 |
| 本地红线层 | 纯本地零延迟,挡业务专属、绝不能出现的措辞 |
| 内容安全服务层 | 挡暴力仇恨色情等通用有害类别,覆盖广 |
| 输入也要审 | 入口拦有害提问与诱导话术,比输出端更早更省 |
| 流式以整句为单位 | 整句是既能审出语义又不破坏流式体验的平衡点 |
| 兜底话术 | 命中后给用户一句得体回复,不暴露内部机制 |
| 命中内容留痕 | 供复盘、申诉举证、优化红线,是审核运营的数据基础 |
| 故障即拦截 | 审核服务挂掉时,高风险场景宁可错拦不可错放 |
避坑清单
- 把模型输出当作必须先过审核关的半成品,不是可直接展示的成品。
- 审核要独立于模型,不把交付安全押在模型自己的对齐自律上。
- 审核分两层:本地红线挡业务专属措辞,内容安全服务挡通用有害。
- 本地红线放在最前,零成本挡掉一批,再花网络往返调远程服务。
- 不只审输出,用户输入也要审,入口拦掉有害提问和诱导话术。
- 流式输出以整句为单位审核,审过一句才放一句给用户。
- 流式收尾别漏审最后那段不足一句、没有句末标点的残余。
- 命中后给用户得体的兜底话术,不让用户对着空白、不暴露机制。
- 命中内容连同提问、命中原因完整留痕,供复盘申诉与优化红线。
- 审核服务会挂,高风险场景故障即拦截,红线要持续运营迭代。
总结
回头看那串"展示了不合规内容、展示了被套出的越界言论、展示了乱给的承诺、流式漏出违规内容"的问题,以及我后来在内容审核上接连踩的坑,最该记住的不是某一个审核函数的写法,而是我动手前那个想当然的判断——"调用大模型,就是把它返回的文字,原样展示给用户"。这句话错在它把"模型的输出"当成了一件可信的、对业务负责的成品。我以为把模型生成的回答接过来、显示出去,这件事就办成了。可我忽略了一件最要紧的事:模型不是我那个立场一致、靠得住的同事,而是一个能力很强、却立场中立、不对我的业务负责、还可能被用户诱导的外部内容源。它按概率生成"看起来合理"的文字,但它不知道我公司的业务红线,守不住对话的边界,还会煞有介事地"幻想"出我根本给不了的承诺。它生成的那段回答,是一段不完全受我控制的"半成品"。我第一版的错,就是把这段"半成品",当成可以直接交付的"成品",中间没有设任何一道关。这个错配,本地开发时根本看不出来——因为本地提问的"用户"就是我自己,我亲手问的都是温和而普通的问题,模型恰好回得得体又安全,代码恰好一路平安;它只会在真正上线、面对海量真实用户里那些恶意的、爱钻空子的人时,以一次内容事故的方式爆出来。
所以做对 AI 应用的输出,真正的功夫不在"调用模型、拿到回答"那几行上。调模型本身不难。真正的功夫,在于你要从一开始就承认"模型的输出是必须先过审核关的半成品",然后在它到达用户之前,亲手替它把一道独立的审核守住:你不能信模型懂你的业务红线,就用本地红线挡掉那些业务上绝不能出现的措辞;你不能信模型不生成有害内容,就用内容安全服务挡掉通用的有害类别;你不能只审输出,就在入口也审一道,拦掉有害的提问和诱导的话术;你不能让流式输出把违规内容漏给用户,就攒够一个整句、审过了再放;而到了命中后的兜底、留痕、审核服务降级这些边角上,你还要处处守住,别让违规内容又从某个角落漏出去。这篇文章的几节,其实就是顺着这套规矩展开的:先想清楚"模型回什么就展示什么"为什么错,再讲审核的两层、输入也要审、流式怎么审、命中后怎么办,最后是审核降级、误杀、可观测这几个把审核守扎实的工程细节。
你会发现,内容审核这件事,和现实里"一家电视台怎么对待一段还没播出的录像"完全相通。一个不靠谱的电视台会怎么做?随便哪个外来的供片方送来一盘带子,它看都不看,直接接进信号、播了出去。它默认那盘带子里的内容一定是合规的、得体的、不会惹麻烦的——可那盘带子是别人做的,做带子的人既不了解这家电视台的播出规范,也不对它的牌照负责,带子里夹了什么不该播的画面、不能说的话、乱给的承诺,全都顺着信号直接进了千家万户。而一个靠谱的电视台怎么做?它再信任供片方,也绝不会让一盘带子不经审看就直接播出——它有一道独立的、属于自己的审片关(这就是独立审核);它的审片既照着通行的播出标准看(有没有暴力、不雅),也照着自己台里专门的红线看(有没有违规广告、不当承诺)(这就是内容安全服务 + 本地红线两层);它不光审外来的带子,连观众打进来的来电也要先过滤一道再接进直播(这就是输入审核);遇到直播,它会用一个几秒的延时,留出审看和掐断的窗口(这就是流式的边出边审);真要掐掉一段,它立刻垫上一段备用画面、绝不留黑屏,还会把掐掉的那段存档备查(这就是兜底话术和留痕)。同样是把内容送到观众面前,不靠谱的电视台把供片方当成自己人、送什么播什么,靠谱的电视台始终记得"这盘带子是外面做的、我必须替观众和我的牌照亲自审过一遍"——差别不在"把信号播出去这件事本身难不难",只在电视台心里有没有"在播出之前,必须有一道属于我自己的审片关"这根弦。
最后想说,AI 应用的输出审核做没做对,差距永远不会在"本地开发、自己问几句测一测"时暴露——本地那个提问的"用户"就是你自己,你亲手问的都是温和而普通的问题,模型恰好回得既得体又安全,你那段"接收、展示"的代码恰好把每一个盲目的信任都赌赢了,回答稳稳显示在页面上,你自然觉得"调模型嘛,回什么显什么"一点问题都没有。它只在真实的、面对海量用户、其中总有人恶意构造提问的环境里才显形。那时候它会用最难堪的方式给你结账:做不好,你会因为模型生成的一段不合规内容,被原样展示给用户、截图传开、酿成一次事故,会因为用户的几句套话,让模型说出的越界言论直接显示出去,会因为模型乱给的一个承诺,被用户拿着来理论;而做对了,你的每一段模型输出都过了本地红线、过了内容安全服务,输入端拦掉了有害提问,流式输出一句一句审过才放,命中的内容被兜底话术接住、被完整留痕,无论用户怎么精心构造,每一段到达用户眼前的内容都被规规矩矩地审过、拦得干干净净。所以别等"一段违规内容被截图传开"那一刻找上门,在你写下展示模型回答的第一行代码时就该想清楚:这段输出过审核了吗、本地红线我定了吗、有害类别我挡了吗、输入我审了吗、流式输出会不会漏、命中了我兜底和留痕了吗,这一道道关口,我是不是都替这段不受控的半成品守住了?这些问题有了答案,你交付的才不只是一套"本地问几句看着对"的代码,而是一个无论用户构造了多坏的提问、模型生成了什么,每一段内容都被牢牢审住的、让人放心的系统。
—— 别看了 · 2026