1. 从一次显存告急说起:为什么要盯上多模态KV Cache
多模态大模型跑推理的时候,显存占用往往比纯文本模型更让人头疼。文本模型里,KV Cache 的大小基本只跟序列长度、层数、注意力头数、头维度这几个量挂钩,算起来很线性。但一旦把图像、视频、音频这些模态塞进同一个上下文,序列长度会瞬间膨胀——一张 448×448 的图,经过视觉编码器切 patch,动辄就是几百上千个 token;一段几秒的视频,token 数直接上万。这些视觉 token 和文本 token 一起进入解码器,KV Cache 就跟着一起涨。
我在一个多模态对话的模拟项目里就踩过这个坑。当时上下文里放了三张高分辨率图加一段长文本,单次推理的 KV Cache 占用直接顶到了几十 GB 量级,batch size 只能压到 1,吞吐惨不忍睹。更麻烦的是,多模态场景下 KV Cache 的“冗余”分布和纯文本完全不一样:文本 token 的 KV 往往语义密度高、重复度低,而视觉 token 里有大量背景 patch、相似 patch,它们的 KV 高度冗余,却和文本 token 混在同一个 cache 里被一视同仁地保存。
这就是 UniCache 这类统一多模态 KV Cache 压缩策略要解决的核心问题。它不是一个单纯的“砍 token”技巧,而是一套针对多模态混合上下文的统一压缩框架。我把它拆成三篇来写,这是第二篇,专门讲压缩策略本身——也就是“到底怎么压、压哪里、压多少、怎么保证不崩”。第一篇讲的是多模态 KV Cache 的构成与瓶颈分析,第三篇会讲工程落地和评测。这一篇是整套方案里最硬核的部分,适合已经在跑多模态推理、被显存和吞吐卡住、想动手改 cache 管理的同学。
先说清楚这篇要覆盖什么:统一的多模态 KV 表示、压缩的粒度选择、重要性评估、分层压缩策略、跨模态的预算分配,以及压缩后如何做质量兜底。每一块我都会给出为什么这么设计、参数怎么算、实操里怎么调。看完你应该能自己搭一个可用的多模态 KV 压缩流程,而不是只会调别人的开关。
2. 统一多模态KV表示:先想清楚“压的是什么”
2.1 多模态KV Cache的构成拆解
要压缩,先得知道 cache 里到底存了什么。在标准的 decoder-only 多模态架构里,KV Cache 是逐层、逐头保存的。对于第 $l$ 层第 $h$ 个注意力头,缓存的是:
- Key 张量:形状
[batch, num_heads, seq_len, head_dim] - Value 张量:形状同上
这里的seq_len就是多模态拼接后的总序列长度。假设文本部分有 $T$ 个 token,视觉部分有 $V$ 个 token,音频部分有 $A$ 个 token,那么 $seq_len = T + V + A$(再加上一些特殊分隔 token)。显存占用近似为:
$$ \text{CacheSize} \approx 2 \times L \times H \times (T+V+A) \times D \times \text{dtype_bytes} $$
其中 $L$ 是层数,$H$ 是 KV 头数,$D$ 是头维度,系数 2 是因为 Key 和 Value 各存一份。以一个 7B 级别、$L=32$、$H=32$、$D=128$ 的模型为例,用 fp16(2 字节)存储,如果 $T+V+A = 8192$,那么单条样本的 KV Cache 就是:
$$ 2 \times 32 \times 32 \times 8192 \times 128 \times 2 \approx 4.3\ \text{GB} $$
注意这只是单条样本。batch 一上去,显存直接爆炸。而多模态场景里 $V$ 往往远大于 $T$,视觉 token 成了 cache 占用的主要来源。所以压缩的第一直觉就是:视觉 token 的 KV 是重点对象。
2.2 为什么不能简单套用文本KV压缩方法
纯文本的 KV 压缩已经有不少成熟思路,比如基于注意力分数淘汰、基于滑动窗口保留近期 token、基于 token 合并(merging)等。这些方法直接搬到多模态上,往往会出问题,原因有几个。
第一,视觉 token 的注意力分布和文本不一样。文本里,重要的往往是少数关键词,注意力集中且稀疏;视觉里,注意力可能分散在整片区域上,尤其是需要全局理解的任务(比如“图里有几只猫”),你把背景 patch 全砍了,模型可能连物体边界都判断不准。
第二,视觉 token 之间存在强空间相关性。相邻 patch 的 KV 高度相似,简单按注意力分数淘汰,可能把一整片区域都误删,导致局部信息缺失。而文本 token 之间这种空间相关性很弱。
第三,跨模态对齐 token 极其敏感。文本里指代图像的词(比如“这个”“左边那个”),它的注意力会强烈指向特定视觉区域。如果压缩时把这些对齐关系破坏掉,模型就会“答非所问”。我实测过,粗暴地对视觉 token 做 50% 淘汰,在指代类问题上准确率能掉二十多个点。
所以 UniCache 的核心思路不是“对视觉和文本用同一套规则”,而是建立一个统一的表示和评估框架,让不同模态的 token 在同一个重要性尺度下被比较,再按模态特性分配压缩预算。
2.3 统一表示的关键:把异构KV映射到可比空间
统一表示要解决的是“怎么让视觉 token 和文本 token 的重要性可以放在一起比”。直接比原始注意力分数是不公平的,因为视觉 token 数量多,注意力被稀释,单 token 分数天然偏低;文本 token 少,分数天然偏高。
一个可行的做法是做模态内归一化。对每个模态内部的注意力分数先做 z-score 或 softmax 归一化,映射到 [0,1] 区间,再跨模态比较。这样视觉 token 里相对重要的(比如物体中心 patch)就能和文本里的关键词处在同一量级。
另一个维度是引入“跨模态注意力”作为额外权重。具体来说,统计文本 token 对视觉 token 的注意力总量,以及视觉 token 对文本 token 的注意力总量,把这种跨模态交互强度作为重要性的一部分。一个视觉 patch 如果被多个文本 token 强烈关注,它的重要性就应该被抬高,哪怕它在视觉模态内部的注意力分数不算顶尖。
我在实现时用的统一重要性分数大致是:
$$ s_i = \alpha \cdot \tilde{a}_i + \beta \cdot c_i + \gamma \cdot r_i $$
其中 $\tilde{a}_i$ 是模态内归一化注意力,$c_i$ 是跨模态交互强度,$r_i$ 是位置/新近度先验,$\alpha,\beta,\gamma$ 是可调权重。这个公式不是唯一解,但它把三个关键因素显式拆开了,调参时心里有数。实测下来 $\alpha$ 取 0.5、$\beta$ 取 0.35、$\gamma$ 取 0.15 是个不错的起点。
注意:归一化一定要按模态分别做,不要全局做。全局归一化会让文本 token 系统性压过视觉 token,压缩后模型会“只读文本不看图”。
3. 压缩粒度选择:token级、头级还是层级的取舍
3.1 三种压缩粒度的对比
压缩粒度决定了你在哪个维度上做裁剪或合并。常见的有三种,各有适用场景。
| 粒度 | 操作对象 | 压缩率上限 | 实现复杂度 | 对多模态的友好度 |
|---|---|---|---|---|
| Token 级 | 序列维度上的 KV 条目 | 高(可到 70%+) | 中 | 需处理跨模态对齐 |
| 头级 | 注意力头的 KV | 中(30%-50%) | 低 | 较友好,头功能分化明显 |
| 层级 | 整层的 KV | 低(10%-30%) | 低 | 风险高,易伤深层语义 |
Token 级压缩最直观,就是决定哪些位置的 KV 保留、哪些丢弃或合并。它的压缩率最高,但也是最容易出问题的,因为直接改变了序列结构。
头级压缩利用的是注意力头的功能分化。多模态模型里,有些头专门处理视觉、有些专门处理文本、有些负责跨模态对齐。把冗余的头(比如多个头学到相似模式)合并或裁剪,对整体影响较小。这个粒度实现简单,风险可控。
层级压缩最激进,直接砍掉某些层的 KV。但多模态任务对深层语义依赖强,砍层往往导致生成质量断崖式下跌。我一般只在极端显存受限时考虑,而且只砍最浅或最深的少数层。
3.2 为什么UniCache主推token级+头级混合
单用 token 级,压缩率高但风险大;单用头级,安全但压缩率不够。UniCache 的做法是混合:先做头级筛选,把明显冗余的头合并掉,降低 KV 的“宽度”;再在剩下的头上做 token 级压缩,降低“长度”。两个维度叠加,压缩率能到 60%-75%,同时质量损失可控。
具体顺序很重要。我试过先 token 后头,效果不如先头后 token。原因是头级合并会改变每个 token 的 KV 表示,如果先做 token 淘汰,淘汰依据(注意力分数)是基于原始头的,合并后这个依据就失真了。先做头级合并,再基于合并后的表示算 token 重要性,评估更准。
头级合并的具体做法:对同一层内的 KV 头做聚类,把余弦相似度高于阈值的头归为一组,组内 KV 做加权平均合并成一个头。阈值一般取 0.85-0.92。太低会误合并功能不同的头,太高压缩效果不明显。这个阈值需要按模型调,不同模型的头冗余度差别很大。
3.3 粒度选择的实操判断标准
怎么决定用哪种粒度?我给几个实操判断标准。
如果你的瓶颈是显存、且能接受一定质量损失,优先 token 级 + 头级混合。如果瓶颈是延迟、显存还够,优先头级,因为头级合并对序列结构无影响,推理时 kernel 更友好。
如果模型本身头数就少(比如 GQA 架构,KV 头只有 4-8 个),头级压缩空间有限,重点放 token 级。反过来,如果模型是标准 MHA、KV 头几十个,头级压缩收益很大。
还有一个经验:视觉 token 占比越高,token 级压缩的收益越大,但风险也越大。当视觉 token 超过总序列的 70% 时,我建议 token 级压缩率不要超过 50%,剩下的压缩额度靠头级补。
4. 重要性评估:怎么判断哪些KV该留
4.1 注意力分数不是唯一信号
很多人做 KV 压缩第一反应就是看注意力分数,分数低的淘汰。这在纯文本上勉强能用,在多模态上不够。原因前面说过,视觉 token 注意力被稀释,单看分数会误杀。
我在模拟项目里做过对比实验:只用注意力分数做评估,在图像描述任务上,压缩 50% 后 BLEU 掉了 8 个点;加入跨模态交互和位置先验后,同样压缩率只掉 2 个点。差距很明显。
所以重要性评估要多信号融合。除了注意力,还要看:这个 token 的 KV 是否被后续层反复读取(层间一致性)、它的 Value 是否和邻近 token 高度相似(冗余度)、它是否处在跨模态对齐的关键位置。
4.2 多信号融合的评分公式与参数
我把评分拆成四个分量,每个分量归一化后加权求和:
- 注意力分量$a_i$:该 token 收到的累计注意力,模态内归一化。
- 跨模态分量$c_i$:该 token 与另一模态 token 的交互强度。
- 冗余分量$d_i$:与邻近 token 的 KV 相似度,越相似说明越可压缩,作为负权重。
- 新近度分量$r_i$:位置越靠后权重越高,因为近期 token 对生成影响大。
综合分数:
$$ s_i = w_a \cdot a_i + w_c \cdot c_i - w_d \cdot d_i + w_r \cdot r_i $$
权重建议起点:$w_a=0.4$,$w_c=0.3$,$w_d=0.2$,$w_r=0.1$。冗余分量取负,是因为越冗余越应该被压掉。
这里有个细节:$d_i$ 的计算要用滑动窗口,只和前后各 $k$ 个 token 比相似度,$k$ 取 4-8。窗口太大,会把远处不相关 token 也算进来,冗余度估计失真。
4.3 分层评估:浅层和深层用不同策略
多模态模型的浅层和深层,KV 的角色不一样。浅层更多是低级特征(边缘、纹理、局部模式),冗余度高,可以激进压缩;深层是高级语义和跨模态融合,冗余度低,要保守。
我的做法是分层设置压缩率。假设总压缩目标是 60%,浅层(前 1/3 层)压 75%,中层(中间 1/3)压 60%,深层(后 1/3)压 45%。这样整体达标,又保护了深层语义。
评估信号也要分层调权重。浅层加大冗余分量权重($w_d$ 提到 0.3),因为浅层冗余确实多;深层加大注意力分量权重($w_a$ 提到 0.5),因为深层注意力更能反映语义重要性。
实操心得:分层压缩率不要设成硬阶梯,用平滑曲线过渡更好。硬阶梯会在层边界处造成表示突变,生成时可能出现不连贯。我一般用余弦退火式的平滑曲线,从浅层的高压缩率平滑过渡到深层的低压缩率。
5. 跨模态预算分配:视觉和文本各压多少
5.1 预算分配的核心矛盾
压缩总预算是有限的,视觉和文本怎么分,是个核心矛盾。视觉 token 多、冗余高,理应多压;但视觉信息又是多模态任务的关键,压狠了模型就“瞎”了。文本 token 少、冗余低,压多了直接伤语义。
我见过两种极端做法都不好。一种是按 token 数量比例分配压缩量,视觉 token 多就多压,结果视觉信息损失过大;另一种是平均分配压缩率,视觉文本各压 50%,结果文本被过度压缩,语义崩了。
合理的做法是按“信息密度”分配。视觉 token 信息密度低(大量冗余),可以承受更高压缩率;文本 token 信息密度高,压缩率要低。但具体数值要靠实验定。
5.2 基于信息密度的动态分配方法
我用的方法分三步。
第一步,估计每个模态的信息密度。用一个简单代理指标:模态内 KV 的平均成对相似度。相似度越高,说明冗余越多,信息密度越低。视觉模态这个值通常明显高于文本。
第二步,按信息密度反比分配压缩率。设视觉信息密度为 $\rho_v$,文本为 $\rho_t$,总压缩率为 $R$,则:
$$ R_v = R \cdot \frac{\rho_t}{\rho_v + \rho_t} \cdot k, \quad R_t = R \cdot \frac{\rho_v}{\rho_v + \rho_t} \cdot k $$
其中 $k$ 是归一化系数,保证加权平均后等于 $R$。这个公式让信息密度低的模态承担更高压缩率。
第三步,设上下限保护。视觉压缩率上限 80%,文本压缩率上限 40%。超过上限就截断,把多余额度转给另一模态。这个保护很重要,防止某一模态被压崩。
实测下来,视觉压 65%-75%、文本压 20%-30% 是个比较稳的区间,整体压缩率能到 55%-65%。
5.3 对齐token的特殊保护
跨模态对齐 token 要单独保护。这些 token 包括:文本里的指代词、视觉里的物体中心 patch、以及模态分隔符附近的 token。它们的压缩率要额外降低,我一般给它们打 0.5 的压缩折扣,也就是别人压 60%,它们只压 30%。
识别对齐 token 的方法:看跨模态注意力。文本 token 如果对某片视觉区域注意力集中,它就是对齐 token;视觉 patch 如果被多个文本 token 关注,也是对齐 token。用一个阈值筛,比如跨模态注意力占比超过 30% 就标记为对齐 token。
这部分 token 数量通常不多,占总序列 5%-15%,但保护它们对质量影响很大。我在指代任务上测过,保护对齐 token 后,压缩 60% 的准确率损失从 15 个点降到 4 个点。
6. 压缩执行:合并、淘汰还是量化
6.1 三种压缩操作的适用场景
确定保留哪些 KV 之后,还要决定怎么处理被压的部分。三种操作:淘汰(直接丢)、合并(多个合成一个)、量化(降低精度)。
淘汰最简单,但信息直接丢失。合并保留信息但引入混合表示,可能模糊细节。量化不改变结构,只降精度,对 kernel 友好但压缩率有限。
我的组合策略是:高冗余 token 用合并,低冗余但低重要性的用淘汰,保留下来的 KV 统一做低比特量化。三层叠加,压缩率最大化。
具体来说,冗余度 $d_i$ 高于 0.8 的 token 走合并,把一组相似 token 的 KV 加权平均成一个;$d_i$ 在 0.5-0.8 且重要性分数低于阈值的走淘汰;剩下的保留,KV 量化到 int8 甚至 int4。
6.2 合并操作的实现细节
合并不是简单平均。直接平均会让合并后的 KV 偏离原分布,注意力计算时可能失配。我用的加权平均,权重是该 token 的重要性分数归一化后的值。重要的 token 在合并结果里占比更大。
合并还有个坑:Key 和 Value 的合并策略要一致,否则 K 和 V 的对应关系错乱。我一开始只合并了 K 没合并 V,结果注意力分数算出来完全不对,排查了半天才发现。
合并后要重新归一化。因为加权平均后向量的模长会变,不归一化的话,后续注意力点积的尺度会漂移。用 L2 归一化把合并后的 KV 拉回原尺度。
6.3 量化与压缩的叠加
量化是压缩的最后一环,也是性价比很高的一环。KV 从 fp16 量化到 int8,显存直接减半,质量损失通常很小(1 个点以内)。量化到 int4,显存减到 1/4,但质量损失开始明显,需要配合更保守的 token 压缩率。
量化的关键是 scale 的选取。per-token 量化比 per-tensor 量化精度高,但开销大。我一般用 per-head 量化,在精度和开销之间取平衡。scale 用该头内 KV 的最大绝对值除以量化范围。
注意:量化和 token 压缩叠加时,要先做 token 压缩再量化。反过来先量化,token 重要性评估会受量化噪声影响,评估不准。
7. 质量兜底:压缩后怎么保证不崩
7.1 压缩质量的自检指标
压缩完不能直接上线,要自检。我盯三个指标:注意力分布偏移、生成困惑度、任务准确率。
注意力分布偏移:压缩前后,同一批输入的最后几层注意力分布做 KL 散度。KL 超过 0.1 就说明压缩过头了,要回调压缩率。
生成困惑度:在验证集上算困惑度,压缩后涨幅超过 5% 就要警惕。
任务准确率:在指代、描述、问答三类任务上各测一批,任何一类掉超过 5 个点都要回调。
这三个指标里,注意力分布偏移最灵敏,能提前预警。我一般先用它做快速筛查,再用后两个做确认。
7.2 动态回退机制
压缩率不该是固定的,要根据输入动态调。简单输入(单图短文本)可以激进压缩,复杂输入(多图长文本、密集指代)要保守。
我实现了一个动态回退:推理前先估计输入复杂度,用视觉 token 数、文本 token 数、跨模态对齐 token 比例三个量算一个复杂度分数。分数高就自动降低压缩率,分数低就提高。
复杂度分数公式:
$$ \text{complexity} = \log(V) + \log(T) + \lambda \cdot \frac{N_{align}}{T+V} $$
$\lambda$ 取 2.0 左右,因为对齐 token 比例对复杂度影响大。复杂度分数映射到压缩率区间 [0.4, 0.75],分数越高压缩率越低。
这个机制让平均压缩率比固定策略低 3-5 个点,但质量稳定性好很多,极端输入的崩坏率大幅下降。
7.3 常见崩坏模式与修复
压缩后常见的崩坏模式有三种,我都遇到过。
第一种是“视觉失明”:模型回答完全忽略图像内容。原因通常是视觉 token 压太狠,或者对齐 token 被误删。修复:降低视觉压缩率,提高对齐 token 保护折扣。
第二种是“语义漂移”:回答和问题相关但细节错乱。原因多是深层 KV 被过度压缩。修复:降低深层压缩率,或对深层禁用 token 淘汰只做量化。
第三种是“重复生成”:模型陷入重复循环。原因常是合并操作破坏了 KV 的多样性,注意力塌缩。修复:降低合并阈值,减少合并比例,或对合并后的 KV 加一点噪声扰动。
这三种崩坏我做了个速查表,排查时按表走,能快速定位。
| 崩坏模式 | 典型表现 | 最可能原因 | 首选修复 |
|---|---|---|---|
| 视觉失明 | 忽略图像 | 视觉压缩过度/对齐token丢失 | 降视觉压缩率,加对齐保护 |
| 语义漂移 | 细节错乱 | 深层KV过度压缩 | 降深层压缩率,深层禁淘汰 |
| 重复生成 | 循环输出 | 合并破坏多样性 | 降合并阈值,加噪声扰动 |
8. 参数调优:从默认配置到场景适配
8.1 默认配置与调参顺序
给一套我常用的默认配置,适合大多数多模态推理场景起步:
- 头级合并阈值:0.88
- 分层压缩率:浅 0.75 / 中 0.60 / 深 0.45
- 重要性权重:$w_a=0.4, w_c=0.3, w_d=0.2, w_r=0.1$
- 视觉/文本压缩率:0.70 / 0.25
- 对齐 token 折扣:0.5
- 量化:int8,per-head
调参顺序很重要。先调分层压缩率(影响最大),再调视觉/文本分配,再调重要性权重,最后调头级阈值。每调一个,用自检指标验证,别一次调多个,否则出了问题不知道是哪个引起的。
8.2 不同场景的参数偏移
场景不同,参数要偏。我列几个典型场景的偏移方向。
长视频理解:视觉 token 极多,视觉压缩率可以提到 0.75-0.8,但要对时间维度做特殊处理,相邻帧的相似 patch 优先合并。文本压缩率保持 0.25 不变。
密集文档问答:文本 token 多且信息密度高,文本压缩率要降到 0.15-0.2,视觉压缩率可以到 0.7。对齐 token 保护要加强,折扣降到 0.4。
实时对话:延迟敏感,头级压缩优先,token 级压缩保守。头级阈值可以降到 0.85 多合并一些,token 压缩率整体降 5 个点。
8.3 调参的常见误区
第一个误区是追求极限压缩率。我见过有人把压缩率拉到 85%,跑分看着还行,实际用起来各种崩。压缩率不是越高越好,找到质量和显存的平衡点才是目标。我的经验是,多模态场景下 60%-70% 是甜点区,超过 75% 风险陡增。
第二个误区是忽略 batch 维度。压缩策略在小 batch 上调好,大 batch 上可能不一样,因为 batch 内不同样本的复杂度不同,动态回退会互相影响。我建议调参时用实际部署的 batch size,别用 1。
第三个误区是不做回归测试。每次改参数都要在固定测试集上跑一遍,记录指标。我建了个简单的回归表,每次调参后填一行,时间长了就能看出哪些参数敏感、哪些不敏感。
9. 我在实操中踩过的几个坑
第一个坑是 KV 头合并后忘了同步更新注意力 mask。合并头之后,头的数量变了,但 mask 还是按旧头数生成的,导致注意力计算维度不匹配,直接报错。这个坑排查起来很费劲,因为报错信息指向的是矩阵乘法维度,不直接指向 mask。后来我养成了习惯,任何改变 KV 结构的操作,都先检查 mask 和相关索引。
第二个坑是动态回退的复杂度估计用了压缩前的 token 数,但压缩后 token 数变了,导致回退逻辑自相矛盾。修复方法是复杂度估计必须在压缩决策之前完成,用原始输入算,算完再决定压缩率。
第三个坑是量化 scale 用了全局最大值,结果某些头的 KV 动态范围小,量化后精度损失大。改成 per-head 后好了很多。这个坑的教训是,KV 不同头的数值分布差异很大,别用全局统计量。
第四个坑是分层压缩率的平滑曲线用了线性插值,但线性插值在层数少的时候过渡不够平滑。改成余弦插值后,生成连贯性有改善。层数越少,插值函数的选择越重要。
这些坑都不大,但每个都能让你卡半天。写出来是希望你别重复踩。
10. 一个可复现的最小实现骨架
最后给一个最小实现骨架,帮你快速搭起来。伪代码为主,重点是流程和关键参数。
def compress_kv_cache(kv_cache, config): # 1. 头级合并 kv_cache = merge_heads(kv_cache, threshold=config.head_threshold) # 2. 计算重要性分数 scores = compute_importance( kv_cache, weights=config.importance_weights, window=config.similarity_window ) # 3. 分层确定压缩率 layer_rates = cosine_schedule( config.shallow_rate, config.deep_rate, num_layers=kv_cache.num_layers ) # 4. 跨模态预算分配 modal_budget = allocate_budget( kv_cache, total_rate=config.total_rate, visual_cap=config.visual_cap, text_cap=config.text_cap ) # 5. 对齐token保护 scores = apply_alignment_protection( scores, kv_cache, discount=config.align_discount ) # 6. 执行压缩(合并+淘汰) kv_cache = execute_compression( kv_cache, scores, layer_rates, modal_budget, merge_threshold=config.merge_threshold ) # 7. 量化 kv_cache = quantize(kv_cache, bits=config.quant_bits, granularity='per_head') return kv_cache这个骨架里,每一步的参数都在前面章节讲过。你可以先按默认配置跑通,再逐项调优。跑通之后,用第 7 节的自检指标验证,用第 8 节的调参顺序优化。
我个人在实际操作中的体会是,多模态 KV 压缩最难的从来不是某个单点技术,而是各环节的配合。头级和 token 级的顺序、压缩和量化的叠加、静态策略和动态回退的衔接,这些配合关系决定了最终效果。单看每个环节都不复杂,但组合起来坑很多。建议你先把流程跑通,再逐个环节深入,别一上来就追求最优参数。先把 60% 压缩率稳定跑住,再往 70% 冲,这样每一步都有退路。