Under the Sun with Paddy

第 85 期 · 2026年10月13日

超越想象——掉san的AI生图结果

一次国产 S60 加速卡

bd-suiyuan-node-0001: 模力方舟

上的 FLUX 生图事故复盘,顺便研究一下生图模型的原理。

这篇文章要讲的,就是我亲眼看着一张AI生成图片把我的 san 值按在地上摩擦的经历。

一、事故现场

先说背景。我在做一项国产 AI 加速卡(燧原 S60)的模型部署认证考试,其中一题是:把 FLUX 生图模型跑起来,按考题描述出一张图

考题原文(70 字长句):一只可爱的橘色小猫坐在窗台上,窗外是夕阳余晖洒满的城市天际线,温暖的光线透过玻璃照在猫咪的毛发上,营造出宁静祥和的氛围。高清摄影风格,细节丰富。
参数:1024×1024,30 步,guidance 3.5,seed=42

一句人畜无害的提示词。我预期得到一只毛茸茸的橘猫。然后我得到了这个:

事故现场一:说好的猫呢

一张冷色调的城市街道广角——视角就站在马路中央,朝着马路延伸的方向望去,行人和车辆热热闹闹,一切都很”合理”。唯独没有猫。 我盯着它看了很久,脑子里冒出一个不太舒服的念头:我要的是一只猫,它给了我一座城市——那我的猫呢?

会不会再生成几次直接出来一个血淋淋的

不死心,我把提示词删改成一句 20 字短句(一只可爱的橘色小猫坐在窗台上高清摄影风格,细节丰富。,参数不变)又跑了一次。得到了这个:

事故现场二:丛林里的人形生物

如果说第一张只是”错愕”,这一张是真的恐怖了。一种 80 年代《西游记》老剧的质感:丛林里,一个脸上长毛的人形生物,怀里抱着一只站在石头上的生物——好像是兔子,又好像是猫,那张脸扭曲到无法辨认。它不是”画错了”,它是”长错了”:构图、光线、氛围全部在线,唯独主角落在人类认知的裂缝上。

太掉san了。它是画出了一幅”几乎正确”的图——构图合理、光影正常、氛围到位,唯独每个细节都在尖叫”这个世界不对”。恐怖谷效应的完全体。

二、乱码文字无所谓,但图片很吓人

这里有个值得琢磨的不对称现象。

如果 LLM(大语言模型)输出一堆乱码,扫一眼,”哦,模型抽风了”,关掉重跑,情绪成本为零。文字的错乱是抽象的——你的大脑处理语言时本来就带一层符号抽象,符号坏了,意义层直接过滤掉它。

但生图模型输出乱掉的内容,冲击力完全是另一个量级。因为人脑的视觉系统是硬编码的:人脸检测、肢体比例检测、光影一致性检测,这些模块不走”理性分析”通道,是毫秒级自动触发的。

一张五官错位的脸不是”错误的符号”,它是一次直接的感官入侵——你的视觉系统在报告”这里有个东西,它长得不对”,而你没办法闭眼。

而要理解这些幻觉从哪来,得先知道生图模型到底在干什么。

三、生图模型原理

以这次事故的主角 FLUX 为例(Stable Diffusion 3、SDXL 同族)。先看全景——它生成一张图,就是这条流水线:

flowchart LR P["prompt 提示词"] --> C1["CLIP-L 编码器<br/>77-token 窗口"] P --> C2["T5-XXL 编码器<br/>512-token 窗口"] SD["seed<br/>初始随机噪声"] --> D["DiT 去噪主干 Transformer<br/>迭代 30 步"] C1 --> D C2 --> D D -->|每步去掉一点噪声| D D --> V["VAE 解码器<br/>8 倍上采样"] V --> O["1024×1024 像素图"]

下面把四个关键部件逐一拆开。每一节配的都是真实实验图——这台 S60 机器上跑出来的原片,不是示意图。

3.1 它不是”画”图,是”去噪”

生图模型不是从白纸开始画,而是从一团纯随机噪声出发,迭代地把它”擦”成图片。程序员的类比:一个迭代求解算法——初值是噪声张量,目标是”符合提示词的图”,每步往目标挪一点:

flowchart TB A(["t = 30,最噪"]) --> B["DiT 预测方向 v(x_t, t, 条件)"] B --> C["小步挪动:x_t → x_(t-1)"] C --> Q{"t = 0 了吗?"} Q -->|否| B Q -->|是| Z(["x_0 → 交给 VAE 解码"])

FLUX 的具体 formulation 是流匹配(flow matching):把噪声和图片看作一条直线两端,模型预测从任意中间点走向图片端的方向。记住”迭代式地从混沌收敛到图像”就够了。真实轨迹长这样(同一张图从噪声到成形的 6 个切片,768²、gs 3.5、seed 42、双语提示词):

去噪轨迹:step 1/5/10/15/20/30

步数(num_inference_steps)就是迭代次数:只跑 4 步,语义已放对位置但高频细节全失——像一张失焦的实拍(见第五节实验 2 的 exp_steps4.png);30 步是常规工作点。

3.2 它在”潜空间”里干活,不在像素上

被迭代的不是 1024×1024×3 的像素,而是一个小 64 倍的潜空间张量(latent)——VAE(变分自编码器)训练出来的压缩表示:

flowchart LR P["像素 H×W×3"] -->|VAE 编码 ÷8| L["潜空间 (H/8)×(W/8)×16"] L -->|2×2 打包| T["token 网格 (H/16)²×64<br/>DiT 实际看到的形状"] T -->|30 步迭代全在这里| T T -->|解包还原| L2["潜空间"] L2 -->|VAE 解码 ×8| O["像素"]

这就是为什么 1024 的图算得动——迭代发生在 1/64 的数据量上。左下是 768² 生成走到最后一步时潜空间的前三个通道,右边是它经 VAE 解码出的像素图——模型全程在压缩空间里工作,直到最后一刻才”显影”:

潜空间与像素的对照

3.3 文字是怎么进来的:两个编码器,两套语言能力

这是本次事故的案发第一现场。FLUX 用两个文本编码器并行读你的提示词:

编码器窗口角色脾气
CLIP-L77 token管风格、关键词、构图英文为中心,词表是字节级 BPE
T5-XXL512 token管语义、细节、指令遵循32k sentencepiece 词表

“token”是 NLP 版的字符串切分 + 查表:文字切成片段 → 查词表得整数 id → 经 embedding 矩阵(一张 32000×4096 的查找表)变成向量。**模型从不直接看到文字,只看到这些向量。**两个编码器读”同一句话的两种写法”的真实下场(红 = 乱码/未知,绿 = 正常词,橙 = 截断):

token 对比:纯中文 vs 英文双语

词表和 embedding 都是训练时定死的:CLIP-L 的语料几乎全是英文,T5 的 32k 词表对中文覆盖极其有限。喂它词表外的语言会发生什么——第四节全程取证。

3.4 引导力度:guidance_scale

去噪的每一步都在”任意合理图片”(先验)与”贴提示词”(条件)之间拿捏,guidance_scale 是往条件方向用力的系数(FLUX 把这个机制蒸馏进了权重)。三个真实样本,同一提示词、同 seed,只有 guidance 不同:

guidance 对比:0 / 3.5 / 15

推荐值 3.5。拧到 0 并不会”无视提示词”——左图仍是一只正常的猫,只是细节留了瑕疵;拧到 15 则毛发边缘起高光,AI 感明显。

3.5 seed:平行世界的钥匙

seed 决定初始噪声。同参数 + 同 seed = 逐字节复现同一张图(事故两张已验证,见第五节实验 1);换 seed = 同一”条件”掷出的另一个平行世界。同一句读不懂的短句、两个相邻的种子:

seed 对比:同提示词 seed=0 vs seed=1

流水线讲完了。现在回到案发现场。

四、模型它到底看到了什么

事故当天我的第一反应也是最常见的猜测:提示词太长被截断了(CLIP 只看前 77 个 token,而中文在字节级 BPE 下会被拆得很碎)。

但这次复盘,我决定取证到底——直接调用模型自己的 tokenizer,按流水线的真实路径(padding 到 max_length、允许截断)编码那句中文提示词,把模型实际收到的东西打印出来。结果比猜测有趣得多:

== 事故短句(20 字 → 丛林图的源头)==
  T5-XXL 实际内容 : ['', '<unk>', ',', '<unk>']
                    ↑ 两个未知符 + 一个逗号,这就是那张恐怖图的全部条件
  CLIP-L         : raw=70 → kept=69,未截断,但 69 个 token 全是字节碎片 '�'

== 事故长句(70 字 → 街道图的源头)==
  T5-XXL 实际内容 : ['', '<unk>', ',', '<unk>', ',', '<unk>', ',', '<unk>', ',', '<unk>']
                    ↑ 5 个 <unk> 夹 4 个逗号
  CLIP-L         : raw=192 → 77 窗口截断(当晚日志的 "184 > 77" 警告即此,
                    慢/快分词器口径差几个 token)

两个发现,一个比一个狠:

发现一:截不截断根本不重要。 事故短句在 CLIP 里只有 70 个 token(<77,**没有截断**);长句 192 个 token(>77,截掉了大半)。但看下面——截断与否,读不懂就是读不懂。当晚日志里那行 184 > 77 把我的注意力引向了”截断”。

发现二:送进去了,但模型读不懂。 看清楚那两段输出:

  • 在 T5 这边(FLUX 的语义主力):词表里压根没有这些常用汉字,整段汉字序列坍缩成了 <unk>(unknown)。短句整句只剩”两个 夹一个逗号”,长句剩”五个 夹四个逗号”。逗号恰好在中英文里共用词表项,是唯一幸存的”语义”。
  • 在 CLIP 这边:每个汉字被字节级 BPE 拆成 UTF-8 原始字节碎片(显示为 �,实际类似 <0xE4><0xB8><0x80>)。这些字节 token 在 CLIP 的英文语料里出现频率趋近于零——embedding 几乎没被训练过,对模型而言近似随机噪声向量。

所以真相是:“猫”这个概念,从头到尾没有到达过模型。 生成那张丛林恐怖图时,T5 收到的全部指令是 <unk> , <unk>;CLIP 收到的是 69 个乱码碎片。条件约等于真空。

条件空白时,去噪主干在干什么?它在做它唯一还会做的事:从训练先验里采样一张”平均意义上合理”的图片。FLUX 见过海量 LAION 级别的网络图片,其中最常见的类别是什么?街景、行人、招牌、森林、人物。于是你得到黄昏街道或者森林老人——不是它画错了猫,是它压根不知道你要猫,它只是按全球图片的平均分布随机抽了一张。伪汉字招牌尤其讽刺:模型见过几百万块招牌,知道”招牌上有字”,但字长什么样它没学会——于是它把训练分布里”字的统计形状”直接渲染了出来。这玩意儿比画错更掉san,因为它证明模型的世界里”文字”只是一个视觉纹理概念。

顺带,对照组也一样说了实话。双语提示词(英文在前、中文在后):

== 双语版 == CLIP-L 77 token 窗口实际内容:
  ['<|startoftext|>', 'a', 'cute', 'orange', 'tabby', 'cat', 'sitting', 'on',
   'a', 'windo', 'ws', 'ill', ',', 'city', 'skyline', ..., 'rich', 'details',
   '�', '�', ... 中文部分碎成字节碎片 ...]

  T5-XXL 实际内容:
  ['A', 'cute', 'orange', 'tab', 'by', 'cat', 'sitting', ..., 'rich', 'details', '', '<unk>']

英文部分在两个编码器里都完整、干净、语义明确(cat、tabby、windowsill 全是高频 token,embedding 训得很好);中文尾巴在 CLIP 侧被 77 窗口截掉、在 T5 侧缩成一个 <unk>,但此时已经无所谓——语义主力换成了英文。结果:

对照组:双语提示词,猫回来了

猫在窗台上,夕阳,城市天际线,高清摄影感。同一台机器、同一个模型、同一秒都没差,唯一的变化是提示词的语言。

这就是”双语提示词”这个生图圈常识的底层原理,也是我此前”截断论”笔记的更正:不是窗口装不下,是词表里没有你的语言。 取证要到 tokenizer 层才算到底。

token 处理——两个编码器实际收到的全量 token(红 = 乱码/未知,绿 = 正常词,橙 = 截断):

token 处理对比:纯中文 vs 英文双语

去噪过程——30 步迭代去噪实录,每一步都是当时潜向量经 VAE 解码出的真实画面:

去噪过程动画:纯中文 vs 英文双语

两路画面在第 10 步左右开始分道扬镳:左路长成街景,右路长出猫。语义有没有抵达,画面从不撒谎。

技术上的做法(可复现):diffusers 的 FluxPipeline 支持逐步回调,callback_on_step_end 里可以拿到每一步的潜向量;FLUX 的 transformer 工作在 2×2 打包后的潜空间上(形状是 2304×64 而不是 16×96×96),解码前要先 _unpack_latents 还原、再除以 scaling_factor 加 shift_factor、过 VAE。

五、边界实验:把旋钮拧到极端,结果意外地体面

理解了原理,就可以故意制造失败了。我在一台新的算力实例上重跑了一套边界实验(同一模型、同一提示词体系,每次只改一个参数)。

事先声明结果:这些实验几乎没有一张是恐怖的——极端参数伤害的是”质感”,制造不了恐怖。真正的恐怖在下一节。但正因如此,这组对照才有趣:它标出了哪些旋钮只会让图”变差”,而不是”变怪”。

实验 1:精确复现事故

在新机器上用事故当晚的两句原话、原参数(1024²、30 步、guidance 3.5、seed 42)重跑:

两张都是逐字节复现:PNG 字节数与当晚 Notebook 落盘的完全一致(1614369 / 1620791),与本地保存的事故图像素平均差 6.3 / 4.4——就是同一张图。所谓”灵异事件”,其实是读不懂的提示词 + 固定 seed = 一台可以无限重放的老虎机。而一旦换掉 seed,同一个提示词又会掷出完全不同的世界(见六.五的掷骰实验)。

实验 2:去噪步数不足(30 步 → 4 步)

提示词换成正常的英文版,只把迭代次数从 30 砍到 4:

4 步:像失焦的实拍

预期中的”半成品怪物”没有出现——实际效果像一张没对焦的实拍照片:猫是正常的,画面甚至颇有氛围感。迭代步数砍到 4/30,模型依然把”猫”这个语义放对了位置,只是放弃了高频细节。低步数 ≠ 崩坏,更像是”失焦”。

实验 3:引导力度拧到两端(3.5 → 0 / 15)

guidance_scale=0:

gs=0:床边的小猫

我原本以为它会输出”无视提示词的抽象噪声”——结果它给了一张床边的小猫:对焦正常、质感真实,唯一的破绽是尾巴末梢多出一小团莫名其妙的增生毛团。这纠正了一个想当然的理解:FLUX 的引导是蒸馏进模型权重的,gs=0 并不等于”关闭条件”,提示词依然生效,极端值只在细节处留下瑕疵。

guidance_scale=15(推荐值的四倍多):

gs=15:边缘高光的猫

猫还是那只猫,但毛发边缘出现一圈高光,AI 感明显。引导过度不产生怪物,只产生”太用力”的塑料感。

实验 4:乱码提示词

提示词:焱燚雥龘靐齾爩鱻麤龗齉纞爨盨 stare into the 爩

tokenizer 招供的结果:T5 收到 <unk> stare into the <unk>——这串乱码的全部有效语义是英文里的 “stare into the(凝视……)”。我满心以为会得到全系列最恐怖的一张:

乱码提示词:更像专辑封面

结果一点都不恐怖——氛围阴郁、构图讲究,很像一张另类音乐的专辑封面。语义残片没有催生怪物,只贡献了一点忧郁的气质。模型的”自由发挥”远比人类想象得克制。

实验 5:提示词只有一个句号

提示词:。

只有一个句号:古典静物

一幅技法成熟的古典静物:玻璃杯、无花果、深色织物。完全无指令时,模型从先验里随机抽了一个”艺术史平均品味”。你别说,还怪好看的。

实验 6:极端画幅(512×1408 竖幅)

正常英文提示词,但分辨率拉成 1:2.75 的细长条:

512x1408:完全正常的猫

又一个预期落空:猫完全没有形变,构图正常,画面呈柔光质感,AI 感甚至比 gs=15 那张更弱。位置编码对分布外画幅的容忍度比想象中高得多——极端长宽比伤不了它。

六、失败模式 × 流水线环节对照表

把所有实验排在一起,你会发现每种掉san都精准对应流水线上一个环节的失效:

流水线环节它负责什么怎么弄坏它坏掉的样子
Tokenizer / 编码器把文字变成有意义的向量用词表外的语言(纯中文、乱码)内容随机漂移:街景、丛林人形——不可预测,且可能很恐怖
去噪步数迭代收敛的次数砍到 4 步像失焦的实拍:主体正常,高频细节丢失
引导力度(CFG)条件强度的蒸馏系数拧到 0 或 15gs=0 仅细节留瑕疵;gs=15 边缘高光、塑料感
随机种子初始噪声换数字同一失败模式的平行世界变体
位置编码 / 画幅空间坐标系先验极端长宽比意外地稳:无拉伸,柔光
VAE 解码器潜空间 → 像素(分布外潜向量)细节涂抹、纹理伪影

反过来读这张表,就是生图模型的调试手册:图不对题 → 查编码器和 tokenizer;图没成形 → 加步数;图太假 → 降 guidance;构图怪 → 换画幅/seed。 同时它揭示了本篇最重要的一个观察:参数层面的失败(步数、引导、画幅)只会让图”变差”,不会让图”变怪”。真正制造恐怖谷的,是语义层的事故。

六.五、回到案发现场:恐怖是一次随机抽样

先坦白一个走弯路的尝试:本文初稿里,我曾设计 12 个”埋了恐怖配方”的正常提示词(人形×动物×模糊物种×老剧质感)跑了 24 张,想主动复刻丛林人形——没有一张是恐怖的。用提示词去”点单”恐怖,此路不通。恐怖不是提示词的属性。要搞清楚它到底是什么,只能回到案发现场。

现场还原

好在证据齐全:Notebook 记录、服务端日志、还有当时与 AI 助手的完整对话存档(用”和猫一点关系没有”当关键词从本地数据库里翻出来的)。时间线:

时间动作结果
23:48卸载 xformers(先被 PEP 668 拦一道,补 --break-system-packages 成功)—
~23:57Run1:考题 70 字长句,1024²/30 步/gs3.5/seed42日志现 184 > 77 截断警告 → 城市街道(提示词1)
~00:01Run2:删改成 20 字短句,同参数无任何警告 → 丛林人形(提示词2)
00:07把现场贴给 AI:”问题是其生成出来的图片我看了一下,和猫一点关系没有”开始归因

注意那个反转:最恐怖的一张,出自更短、看起来更无害的那句。token 取证已经解释了两句为什么都读不懂;至于”读不懂之后会掷出什么”,靠猜是没用的——我猜短句更容易出恐怖,掷骰结果把这个猜测也推翻了。

掷骰实验:同一句读不懂的话,掷 14 次

五组批量采样,每组只动提示词和 seed,其余参数固定。逐组给出组图与参数:

长句组——事故 70 字长句,1024²、30 步、gs 3.5,seed 0–7:

长句组:seed 0-7

短句组——事故 20 字短句,1024²、30 步、gs 3.5,seed 0–7:

短句组:seed 0-7

句号组——提示词仅为一个句号”。”,768²、25 步、gs 3.5,seed 0–5:

句号组:seed 0-5

其中新的恐怖D3(seed3)单独放这里了(总不能只吓到我自己吧)

dot_D3:句号 prompt,seed=3

中英组——英文核心 + 中文原文(对照,模型读得懂),768²、25 步、gs 3.5,seed 1–2:

中英组:seed 1-2

把 token 证据和掷骰结果并排放:22 张全新采样里,恐怖只出现了 1 次(D3),而且出自条件最空的那组——提示词只有一个句号,连”语义”这个概念都不存在。

加上事故本身(短句 seed42,T5 只读成 <unk> , <unk>),两次恐怖全部落在”条件近真空”的区间。样本很小,不谈概率,但方向与机制一致:强条件把采样压在常见区域;条件越空,采样越靠近纯先验;而纯先验的角落里,躺着一些没人标注过的形体。

它们平时被强条件压着出不来,只有当你的话模型一个字都听不懂时,才有机会被 seed 掷出来——这批实验里二十分之一的频率。

所以那晚真正发生的事情是:一句 T5 只能读成 <unk> , <unk> 的 20 字中文,把条件强度推到了接近真空;seed 42 恰好掷进了先验分布里那片长毛的脸。

这也给”AI 又画恐怖图了”的都市传说一个冷酷的注脚:绝大多数”灵异事件”不是模型黑化,而是一次可复现的、平庸的随机抽样——只是抽中的那位用户,不知道自己的 seed 是多少。

七、写在最后

回到开头的那个不对称:LLM 生成乱码你无感,生图模型生成乱图你掉san。

做完这一轮实验我觉得原因还有更深一层——乱码文字暴露的是模型的”无知”,而乱掉的图片暴露的是模型的”库存”。

它不知道你要猫,但它见过全世界的街景,于是给你一座热闹的城市,让你的猫下落不明;而当条件彻底读不懂时,它就从记忆的最深处随手捞起一张——有时候,那是一张长毛的脸。

某种程度上这暴露的是模型深处的记忆~~

附:复现参数

实验环境:燧原 S60 加速卡容器(torch 2.8.0 + torch_gcu 2.8.0+3.6.0.23 + diffusers 0.32.2 + transformers 4.57.3),FLUX.1-Krea-dev(diffusers FluxPipeline,bfloat16,max_sequence_length=512)。

全量参数:产生的全部生成图的参数下载: generation_log.csv(点此下载)
记录了每张图的精确提示词、分辨率、步数、guidance、seed、所用脚本,以及对应的 原图下载链接(cos_url 列)。

广告位
广告位
广告位
广告位
广告位
广告位

读者来信(1)

  1. 2broear 的头像

    卧槽,,汗毛都竖起来了。这场景。。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注