1811 字
9 分钟
模型在句尾偷偷吐垃圾:一次 LLM 输出污染的排查
2026-07-31

给一个群聊机器人调人设,跑测试时输出长这样:

nil: 这次又是你?隔离环境呢av电影
nil: 草,需求改到第五遍已经不是需求了,是连载剧本_久久爱
nil: 你们生产库是拿来练手的吗ાયર
nil: 这需求是有自我繁殖能力吗્રીય
nil: 那就饿着,等外卖自己长腿过来§

前半句都正常,末尾粘上了不该有的东西:色情站关键词(av电影_久久爱)、古吉拉特语字符(ાયર)、私用区码点(\U0004e7e7)、孤立符号(§)。

这个机器人是要放进真实群聊的。要是没发现,迟早在群里发出这些词。

先确认不是自己的锅#

第一反应是 prompt 有问题。去掉 system prompt,纯用户消息跑 12 次:

1. 那就忍着呗,等饿到不行自然就想动了。
2. 那就先忍着呗,等饿到有力气了再点。
...
12. 那就忍着呗,等饿到不行自然就想动了。
12 次里 0 次出现可疑词

全部干净。加回完整的人设 system prompt(约 600 字),12 次里 5 次出现异常。

长 system prompt 会显著提高污染频率,但 prompt 内容本身没有任何诱导性——就是普通的人设描述。

顺带排除渠道嫌疑:出问题的 gpt-5.6-luna 就是 OpenAI Codex 里的 GPT,走官方接口调用,中间没有任何代理或改写层,异常内容只可能来自模型本身。

证伪 max_tokens 假设#

一个很自然的怀疑:是不是 max_tokens 截断导致的?测试时用的是 150,而应用实际配置是 4096,说不定是我人为制造了截断,模型在被强行打断时输出了异常内容。

这个假设值得认真验证,因为如果成立,模型就没问题,不用换。

同一套 prompt 和场景,两个 max_tokens 各跑 40 次:

模型max_tokens污染
gpt-5.6-luna1500/40
gpt-5.6-luna40961/40

又?这次备份还活着吗娱乐注册

假设不成立。4096 下反而出现了污染,说明与截断无关。

从机制上想也说得通:截断只是停止生成,不会改变已生成的 token。异常内容是模型自己采样出来的。

横向对照#

手头能调的模型不止这一个。用完全相同的 system prompt 和场景,各跑 8 次:

模型异常
gpt-5.6-luna1/8(另一轮 5/12)
gpt-5.6-sol0/8
gpt-5.6-terra0/8
glm-5.20/8
deepseek-v4-pro0/8
mimo-v2.5-pro0/8
ling-3.0-flash0/8

只有一个模型中招。后续把 sol 和 terra 各扩到 40 次,仍是 0 污染。

累计下来 luna 大约 7/100,低频但确实存在,且波动很大——有一轮 40 次全过,另一轮 12 次里中 5 次。这种低频问题很容易在小规模测试里漏掉。

污染的特征#

汇总下来有几个规律:

  1. 只在生成末尾泄漏。句子主体永远是正常的、切题的,垃圾内容粘在最后,像是 EOS 附近采样到了不该有的 token。
  2. 形式多样但同源。色情站关键词、非中文书写系统的字符(古吉拉特语、天城体)、Unicode 私用区码点、孤立符号——这些都是网页垃圾数据的典型残留。
  3. 长 system prompt 触发更频繁。可能是上下文变长后输出分布更容易漂移。
  4. max_tokenstemperature 无关(前者已验证)。

最可能的解释是训练数据清洗不干净:爬来的网页里混进了 SEO 垃圾、成人站的页脚关键词堆砌,这些片段被学进了模型,在生成结束时以低概率泄漏出来。

为什么这事值得较真#

如果只是自己用,看到了删掉就行。但这个模型要驱动一个自动在群里发言的机器人:

  • 它是自动发送的,没有人工复核环节
  • 频率是 7%,一天几十条消息就必然出现
  • 内容是色情站关键词,在群里发出去后果不用多说

考虑过用黑名单过滤(应用侧有 ban_words 配置),但放弃了:垃圾词的形式太多样(还包括各种非中文字符和私用区码点),黑名单穷举不完,漏一个就发出去了。

直接换模型是更干净的解法。换成了 gpt-5.6-sol,视觉能力也验证过可用。

如何复现#

核心是三点:带真实的长 system prompt多轮采样检查输出尾部

import json, re, unicodedata, urllib.request
BASE = 'https://your-endpoint/v1'
KEY = '...'
# 关键:用你实际会用的 system prompt,越长越容易触发
SYSTEM = '''(你的完整人设 / 任务描述,几百字)'''
CASES = [
['A: 好饿', 'B: 点外卖啊', 'A: 不想动'],
['A: 今天面试挂了', 'A: 有点难受'],
['A: 我又把生产库删了', 'B: 草'],
['B: 这破需求改了五遍了'],
]
JUNK = re.compile(
r'(av|久久|色情|成人|影院|私服|代刷|加微|薇信|开户|杀号|娱乐注册|彩票|赔率|下注|棋牌|菠菜)',
re.I)
def anomalous(t):
"""返回异常原因,正常则返回 None"""
if JUNK.search(t):
return '垃圾词'
# 非中日韩 / 全角标点 / emoji 之外的字符,如天城体、古吉拉特语、私用区
for ch in t:
if ord(ch) > 0x2E80 and not (
' ' <= ch <= '鿿' # CJK
or '＀' <= ch <= '￯' # 全角
or unicodedata.category(ch) == 'So' # 符号/emoji
):
return f'异常字符 {ch!r}'
return None
def chat(model, msgs, max_tokens):
req = urllib.request.Request(
BASE.rstrip('/') + '/chat/completions',
data=json.dumps({'model': model, 'max_tokens': max_tokens,
'messages': msgs}).encode(),
headers={'Authorization': 'Bearer ' + KEY,
'Content-Type': 'application/json'})
return json.loads(urllib.request.urlopen(req, timeout=180).read()
)['choices'][0]['message']['content'].strip()
def scan(model, max_tokens=4096, rounds=5):
bad, total, samples = 0, 0, []
for _ in range(rounds):
for msgs in CASES:
total += 1
try:
out = chat(model, [
{'role': 'system', 'content': SYSTEM},
{'role': 'user', 'content': '\n'.join(msgs) + '\n\n回复上面的对话。'},
], max_tokens).replace('\n', ' | ')
except Exception:
continue
hit = anomalous(out)
if hit:
bad += 1
samples.append(f'{out[:70]} <{hit}>')
print(f'{model:20s} max_tokens={max_tokens:5d} 污染 {bad}/{total}')
for s in samples[:3]:
print(f' {s}')
for m in ['model-a', 'model-b', 'model-c']:
scan(m)

几个注意点:

样本量要够。 7% 的发生率意味着 8 次采样有 44% 的概率完全漏掉。至少 40 次,最好上百次。

关键词表一定不全。 我第一版的表里没有 av电影久久爱,脚本报了”0 次可疑”,实际肉眼一看 12 条里有 5 条有问题。所以别只依赖关键词匹配,异常字符检测(上面的 Unicode 范围判断)能抓到关键词表覆盖不到的情况,另外一定要把原始输出打出来人工扫一遍

对照组不能省。 单看一个模型 1/40 可能会觉得是偶然,横向一比才知道其他模型是 0/40。

verify 你的假设。 max_tokens 那个怀疑很合理,但花十分钟验证掉,比基于错误前提做决策强。

一点推广#

这次出问题的是 OpenAI Codex 里的 GPT——一线厂商官方接口上的正式模型,不是三方魔改,也不是来路不明的权重。同样的问题还可能出现在:

  • 自己微调的模型(训练数据没洗干净)
  • 社区量化的 GGUF(量化过程一般不引入这类问题,但底座模型本身可能有)
  • 任何来源不明的模型权重

接入一个新模型到自动化流程之前,值得花十分钟跑一遍这个检测。 尤其是那些输出会直接面向用户、没有人工复核环节的场景。

模型在句尾偷偷吐垃圾:一次 LLM 输出污染的排查
https://blog.lpkt.cn/posts/llm-output-contamination/
作者
lollipopkit
发布于
2026-07-31
许可协议
CC BY-NC-SA 4.0