LoRA大模型微调实战:低秩适配原理与显存优化指南

最近在折腾大模型微调,第一眼看到 LoRA 这个缩写的时候,我和大多数人一样,先想到的是"这会不会又是什么玄学优化技巧"。等我真正把原理读懂、代码跑通、模型训起来之后才发现,它解决的问题非常实在:如何在显存有限、数据有限的条件下,把一个几十亿参数的大模型按自己的需求拧成想要的形状。先把名词对齐:本文说的 LoRA 是 Low-Rank Adaptation,也就是大模型微调里的低秩适配方法。如果你去搜 Lora 这个词,大概率会搜到另一个同名的无线通信技术,那是完全不同的技术路线,别搞混。

这篇笔记就是我整个学习过程的整理,适合两类人看:一类是刚接触 LoRA、只知道它"省显存"但说不清原理的初学者;另一类是已经用 LoRA 训过一两次,但效果不稳定、总搞不清该调哪个参数的朋友。我会从论文核心思想讲起,一直讲到我在训练和部署过程中踩过的具体坑,尽量把每个"为什么"都讲明白。

1. 为什么全参数微调越来越"奢侈",LoRA 却能用不到 1% 的参数干活

1.1 一次被显存卡死的微调尝试

我最早想对一个开源的基座模型做指令微调,让它更符合我业务里的问答习惯。第一反应就是全参数微调——把整个模型的权重都设为可训练,丢进训练框架里跑。结果实验刚开始没几步就弹出显存相关的错误,任务直接被打回原点。

我当时粗略算了一笔账:假设模型有 70 亿参数,用半精度存储参数本身大约占 14GB 显存。全参数微调意味着需要为每个参数保存梯度,还要保存优化器的状态(像 AdamW 这种优化器需要额外保存一阶动量和二阶动量,通常以单精度存储)。这样算下来,光是优化器状态和梯度就要占掉几十 GB,再加上前向计算过程中每一层还要保存激活值用于反向传播,训练显存轻松破百 GB。普通开发环境里单卡根本扛不住,多卡分布式也不是每个人都有条件搭。

这个现实让我意识到,需要换一种思路:能不能不更新所有参数,只更新其中一小部分,同时还能保持模型效果?

1.2 LoRA 的解题思路:冻结主干,只学"增量"

LoRA 的做法可以用一句话概括:把预训练好的权重矩阵 W 完全冻结,另外在旁边挂上两个小矩阵 A 和 B,让它们的乘积 BA 去模拟"微调过程中权重需要发生的变化",也就是增量 ΔW。训练时只更新 A 和 B,推理时再把 ΔW 合并回 W。

为什么这个方案省钱?因为 W 的参数量是 d×k,而 A 和 B 的参数量分别是 r×k 和 d×r。当 r 远小于 d 和 k 时,额外参数量只有原来的几十分之一甚至几千分之一。我实际跑过一个 70 亿参数的基座模型,设置 r=16 并且只挂 q、v 两个投影矩阵,可训练参数大概在八百万到一千万这个量级,占比连 1% 都不到。优化器状态、梯度、激活值都只针对这一小撮参数产生,显存占用自然大幅下降,一张消费级显卡也能跑得起来。

1.3 省显存不是唯一价值

很多人只把 LoRA 理解为"省显存方案",这是把它看小了。它更大的价值在于改变了微调的部署形态:训练得到的是一份很小的适配器权重文件,而不是整个模型。基座模型保持不变,不同的任务可以各挂一个适配器,想切换能力时只需要替换这张"插件卡"。同一个基座、多个适配器并存,这在工程实践里比每个任务都存一份完整模型要灵活得多。

当然它也有边界。如果任务和预训练领域差异极大、需要大规模改写模型的知识结构,LoRA 可能达不到全参数微调的上限;数据太少时还容易过拟合。我的经验是:先用 LoRA 快速验证"这条任务路线是否可行",跑通之后如果效果仍有缺口,再考虑更大参数量的方案。它的定位不是万能药,而是把微调这件事的成本结构彻底改变了,让"试错"变得便宜。

2. 低秩分解的核心原理:从"信息压缩"的角度看懂秩

2.1 矩阵的秩是什么

第一次看到"低秩分解"四个字,我以为要补一堆线性代数。后来发现,工程师视角只需要理解一个关键直觉:秩描述的是一块信息里真正独立的"方向"有多少个。

举个例子,一张纯色白底的图片,只需要极少的信息就能完整描述;如果画面里有很多复杂纹理、相互独立的结构,描述它就需要更大的自由度。矩阵的秩差不多就是这个意思。一个 d×k 的矩阵,如果它的秩只有 r,那它本质上可以被压缩成两个小矩阵相乘的形式,乘积和原矩阵完全等价。这就是低秩分解的存在基础。

2.2 ΔW = BA 是怎么来的

放到微调场景里:模型从预训练权重 W 出发,经过微调得到新权重 W',两者之差 ΔW = W' - W 就是更新量。LoRA 的假设是:这个更新量虽然是一个巨大的矩阵,但它的"有效信息维度"并不高,可以用低秩近似表示。

于是前向计算从原来的 y = Wx 变成 y = Wx + (α/r)·BAx。这里的 A 和 B 就是那两个小矩阵,α/r 是缩放系数,后面专门讲。初始化时通常把 A 设成随机高斯分布,B 设成全零,这样训练刚开始时 BA = 0,模型行为和基座模型完全一致,训练过程足够平稳。这也是我在读源码时注意到的细节——很多实现里 A 和 B 的初始化顺序不是随便写的,它直接影响前几步训练是否稳定。

2.3 为什么微调产生的更新天然是低秩的

你可能想问:凭什么说 ΔW 的秩很低?这其实不是一个纯理论证明出来的结论,而是一个被大量实验观测验证的经验事实。预训练模型已经通过海量数据学会了通用的语言结构或视觉特征,微调新任务时,大多数情况下只会对极少数的特征方向做调整。这就像一个人已经有了很扎实的专业基础,学一个新软件时只需要在几个关键习惯上做微调,不需要把整个知识体系推倒重来。

也有相关研究讨论过微调的"内在维度"问题:在低维子空间里搜索就能达到与全参数微调相当的效果。LoRA 正是把这个现象落到了工程实现上。理解这一点之后,你就能解释很多奇怪现象——比如 LoRA 用很小的 r 就能在某些任务上逼近全参数微调,而一旦任务过于复杂,小 r 就明显带不动。这不是代码 bug,而是低秩假设在特殊情况下的边界。

3. 工具链落地:把 LoRA 接进训练流程

3.1 版本配套是最容易被忽略的第一道坎

理论上理解了原理,就轮到动手。主流做法是借助参数高效微调库(PEFT)配合主流的预训练模型框架来完成。这类库已经帮我们封装好了"冻结主干权重、注入低秩矩阵、只训练新增参数"的整套逻辑,不用手写反向传播。

但版本配套是个很现实的坑。模型框架、PEFT 库、加速库三者之间对版本有耦合,如果装了最新版框架加一个比较老的 PEFT 版本,很容易在加载带 LoRA 适配器的模型时报出各种对不上的错误。我的做法是:建独立的虚拟环境,安装时先把这几个核心库写进依赖清单锁定版本,再装其他辅助库。宁可多用几分钟在环境上,也不要中途查一晚上兼容性问题。

3.2 最小可运行的配置示例

下面这段代码是把 LoRA 接到一个自回归语言模型上的最小流程,也是我所有实验的起点:

from transformers import AutoModelForCausalLM, AutoTokenizer from peft import LoraConfig, get_peft_model, TaskType # 基座模型路径,按你自己的情况替换 model_path = "pretrained_model_path" model = AutoModelForCausalLM.from_pretrained(model_path, torch_dtype="auto") tokenizer = AutoTokenizer.from_pretrained(model_path) lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=16, lora_alpha=32, lora_dropout=0.05, target_modules=["q_proj", "v_proj"], bias="none", ) model = get_peft_model(model, lora_config) model.print_trainable_parameters()

把模型包进 get_peft_model 之后,原来的模型就只保留主干权重(被冻结),新增的 A/B 矩阵变成唯一可训练参数。print_trainable_parameters() 会直接输出可训练参数数量,我习惯把它作为每个实验第一步确认项:如果这里显示的参数量和预期差太多,说明 target_modules 大概率写错了。

注意:不要凭记忆填 target_modules。先用模型打印出的网络结构确认真实层名,再写进配置,能省掉后面一大半排查时间。

训练部分直接用常见的训练器即可,配合梯度检查点、混合精度来进一步压显存。如果单卡放不下 batch,就减小 batch 并用梯度累积来等效增大有效批次。这些手段组合起来,大部分 LoRA 微调场景都能在单卡上跑完。

3.3 训练完的保存、合并与上线

LoRA 默认保存的是适配器,不是整个模型,这也正是它"小"的体现。用 save_pretrained 保存后,目录里往往只有几十 MB 到一两百 MB 的内容,相当于整份基座模型的一个小补丁。上线部署时有两种方式:一是把适配器作为独立文件部署,推理时加载基座模型后再套适配器;二是先执行权重合并操作,把 ΔW 合回基座权重,保存成完整模型再部署。合并后推理不需要额外的支路计算,速度和基座模型本身完全一致。

我的建议是:实验和迭代阶段用第一种方式,方便反复调整;正式上线阶段用第二种方式,避免每次部署都要带两份文件,也少一层适配器加载的开销和出错点。

4. 关键超参数逐项拆解:r、alpha、dropout、target_modules

4.1 r(秩):决定容量也决定风险

r 是 LoRA 最核心的超参数,它直接决定了低秩分解的"自由度"。r 越大,A 和 B 能表达的信息越丰富,模型可调整的空间越大;但 r 过大也会带来副作用——尤其是数据量不足时,模型很容易把训练集里的噪声和偶然模式当成规律记住,过拟合风险随 r 增长而上升。

我实验下来大致经验如下表:

r 取值适用场景风险
4-8风格迁移、简单指令遵循,数据量小容量不足,复杂任务学不到位
16-32通用的指令微调、任务适配,最常用区间较均衡
64+领域迁移明显、需求复杂、数据充足显存和过拟合压力同时上升

4.2 alpha(缩放系数):本质上在调学习强度

lora_alpha 经常被误解为"越大效果越好"。实际生效的是 α/r 这个比值,它控制 BA 这一项对模型输出的影响程度。可以粗略理解为:在 r 固定的情况下,α 变大等于把 LoRA 参数的更新步伐放大,相当于变相提升了这部分参数的学习率。

所以调参时需要把 r 和 α 一起看。我踩过的一个典型误区是:把 r 从 16 改成 64,α 还停在 32,结果实际缩放从 2 变成了 0.5,模型学得极其缓慢,看起来"升级了容量"反而效果变差。常用的稳妥做法是让 α = 2r,或直接让 α = r,然后通过学习率去微调训练强度,而不是单独折磨 α。

4.3 dropout 与 target_modules:两个容易被无视的开关

lora_dropout 是加在 LoRA 支路上的随机丢弃,一般设 0.05 就够,太高会让新增参数的训练变得不稳定。target_modules 则决定了 LoRA 挂在模型的哪些层上,这一步对效果的敏感度比多数人想象得高。注意力层里的 q、v 投影是性价比最高的挂载点;如果追求更强表现,可以扩展到 k、o 以及前馈网络里的全连接层。代价是训练参数成倍增加,显存压力也随之上升。

4.4 一组可以照抄的起步配置

对于自回归语言模型的指令微调,我通常从下面这组配置起步:r=16,lora_alpha=32,lora_dropout=0.05,target_modules 覆盖注意力四个投影,学习率 2e-4,warmup 比例 5%,余弦退火,batch 大小视显存设在 4-16,配梯度累积。数据量几百到几千条,epoch 跑 3-5 个,同时时刻盯验证集损失。

如果模型学不动,优先加学习率或数据;如果开始过度重复训练集内容,优先降 r 或加数据。这个调参循环说起来简单,真正做起来需要耐心,好在 LoRA 单次实验成本低,多跑几轮对比不算肉疼。

5. 训练过程中我踩过的四个典型坑

5.1 坑一:r 和 α 的搭配失衡

第一次独立调参时,我把 r 提到 128 想要"更大的容量",α 保持 32,结果训练曲线看起来非常平稳地一路下滑,但生成结果几乎没有任何变化。排查很久才发现问题出在缩放系数变成 0.25 了——参数是多了,真正起作用的更新幅度反而被压小了。这个坑的通用教训是:改动任何一个超参数前,先想清楚它在公式里处在哪个位置,影响的是容量、强度还是稳定性。

5.2 坑二:数据集又小又重复,模型训成了"复读机"

有一轮实验为了赶进度,把一百来条样本反复喂了 10 个 epoch。训练损失降得漂亮,验证集上表现却一塌糊涂,生成内容翻来覆去就那几句话,明显是死记硬背而不是能力迁移。后来我把数据清洗了一遍:去掉重复和近似重复的内容,优先保质量,宁可只有两百条高质量样本也不硬凑一千条注水数据。同时调整早停策略,观察验证损失的拐点。这件事让我彻底信了一句话:LoRA 的高效来自模型底子,但如果数据不给力,再高效的方法也救不回来。

5.3 坑三:target_modules 写错,等于白训

还有一次,自认为对某个基座模型的命名很熟,直接按常见的 q_proj / v_proj 填了 target_modules。训练正常完成,参数量显示也在可接受范围,但推理结果和基座模型几乎没有差异。我把模型结构打印出来逐层核对,才发现那个基座模型的注意力层命名完全不同,我写的模块名根本没被命中。从那以后,每一轮实验我都会先执行 print_trainable_parameters() 并打印模型结构,确认挂载层真实存在。

5.4 坑四:部署时权重没合并,行为对不上

训练完成的适配器单独保存后,我曾经在部署环境忘记挂载适配器就启动推理,结果模型行为退回基座模型,业务方自然对不上。另一次则是在合并流程里不小心执行了两次合并操作,导致 ΔW 被重复叠加,输出明显异常。现在我的部署检查清单很简单:记录基座模型路径、适配器路径、合并次数三类信息;合并前后各跑几个固定 prompt 做一致性对比;上线前用脚本自动核对输出是否匹配。

提示:合并权重前先记录基座模型路径、适配器路径和合并次数,上线前用固定 prompt 做一遍对比验证,能省掉大量排查时间。

5.5 排查问题的一般思路

遇到 LoRA 效果异常,我的一般排查顺序是:先确认可训练参数量是否与预期一致;再检查训练/验证损失曲线是否存在明显分歧;接着对比加了 LoRA 和没加 LoRA 两个模型的输出差异大小;最后逐个回退超参数,用消融实验定位问题。整个过程不快,但能避免靠玄学调参浪费时间。

6. 给后来者的学习路径与我的个人体会

6.1 我推荐的学习顺序

如果你也想从零掌握 LoRA,我的建议顺序是:先花半小时读一遍原始论文,重点看方法部分和实验对比,不用啃证明;然后照着上面的最小代码跑通一个真实小实验;接着固定其他变量,只调整 r 和 α 做几组对比;再多读一遍 PEFT 的源码,搞清楚 get_peft_model 内部到底改了什么。这个顺序让我从"会用接口"进阶到"理解行为",两个阶段之间的差距很大。

6.2 用消融实验建立对超参数的"手感"

光看文档不亲手试,永远不知道 r 从 8 改成 32 对真实任务的影响有多大。我建议设计一张固定的实验表:同一份数据、同一个基座模型,变量只取一个。比如 r 取 4/8/16/32 四档,各跑一次,记录训练损失、验证损失、参数量和几个固定 prompt 的输出,最后对比选出性价比最高的一档。完成这套消融之后,你对 LoRA 的理解会比读十篇文章都深。

6.3 写在最后的个人经验

我真正用熟 LoRA 之后,最大的体会不是"省显存",而是"微调变成了一种可以反复快速试错的日常操作"。从前改一次模型要考虑资源成本、训练周期、部署体积,现在这些问题被大幅压缩,我可以更放心地去尝试不同思路。如果你也想在有限设备上把大模型改造成自己真正可用的工具,LoRA 会是最好的起点——先按这篇笔记跑通一次,再慢慢往深了钻,你会得到比网上零散教程完整得多的理解。

相关新闻

AI调度官真的能管好团队?从拆目标到团建的三周实测拆解

AI调度官真的能管好团队?从拆目标到团建的三周实测拆解

我一直对“让智能体管团队”这件事嗤之以鼻。市面上的AI工具我试过不少:有的能生成任务清单,有的会自动回复消息,可真让它去拆周目标、分派活、催进度、协调资源,它基本就成了一个“高级复读机”。开会、排期、团建这些事&#xf…

2026/10/21 5:31:11 阅读更多 →
智能体技能体系设计:从提示词到可复用能力单元

智能体技能体系设计:从提示词到可复用能力单元

1. 从"技能"这个词说起:为什么智能体需要一套技能体系第一次看到"agent-skills"这个命名,我脑子里冒出来的第一个念头是:这不就是把智能体当成一个"新员工"来培养吗?你招一个人进来,他光…

2026/10/21 5:31:11 阅读更多 →
GPT-4o API接入实战:认证、流式输出与工程优化全攻略

GPT-4o API接入实战:认证、流式输出与工程优化全攻略

2. 调用核心机制与参数配置其实说“解锁”,对于大部分开发者来说,真正要解决的是两件事:第一,怎么拿到合法的调用凭证;第二,怎么把请求封装得又快又稳。这一节我把这两块展开讲透。2.1 认证机制与请求流程G…

2026/10/21 5:31:11 阅读更多 →

最新新闻

iOS 15 ATT审核被拒两次?Xcode自动隐私清单解法揭秘

iOS 15 ATT审核被拒两次?Xcode自动隐私清单解法揭秘

1. 项目概述:一次被驳回两次的ATT权限申请,背后藏着什么真实逻辑? “苹果审核iOS 15版本App Tracking Transparency两次被驳回”——这个标题一出来,很多做iOS开发、运营或合规的同学第一反应是:又一个被ATT政策卡住的…

2026/10/21 6:12:29 阅读更多 →
多智能体动态事件触发共识控制的Matlab仿真与实现

多智能体动态事件触发共识控制的Matlab仿真与实现

多智能体系统的分布式协同控制,这些年无论是在学术界还是工业界都是个大热点。以前我们做多机器人编队、无人机集群、卫星星座协同这类课题时,最头疼的问题之一就是:怎么用最少的信息交互,还能保证所有智能体最终达到一致的状态&a…

2026/10/21 6:12:29 阅读更多 →
鸿蒙ArkUI开发:Slider滑块与Progress进度条从入门到工程实践

鸿蒙ArkUI开发:Slider滑块与Progress进度条从入门到工程实践

在鸿蒙应用开发的实际项目中,Slider和Progress这两个组件往往不会像Text、Button那样被单独拎出来讲,但它们恰恰是提升应用完成度与交互质感的关键细节。很多开发者在入门阶段容易忽略这两个控件,直到做播放器、设置页、上传下载状态展示时&a…

2026/10/21 6:12:29 阅读更多 →
ONVIF Java SDK实战:摄像头统一接入、RTSP取流与PTZ控制

ONVIF Java SDK实战:摄像头统一接入、RTSP取流与PTZ控制

简介:这份资源是一套基于Java的ONVIF协议开发SDK,面向需要对接网络摄像头、实现设备发现、鉴权与媒体流控制的开发者。包内共1157个文件,主体为1147个Java源码文件,另有少量jar依赖库(如commons-codec、javaWsDiscover…

2026/10/21 6:12:29 阅读更多 →
知识工作插件实战:搭建高效采集整理输出工作流

知识工作插件实战:搭建高效采集整理输出工作流

“知识工作插件”这几年被反复提起,但真要问它到底是什么——是一类浏览器扩展,还是编辑器里的组件,又或者某个知识管理工具的二次开发能力?我自己的答案是:都不是单独一个,而是围绕“输入→处理→沉淀→输…

2026/10/21 6:12:29 阅读更多 →
机器人对话系统地图模块设计:POI检索、路径规划与避坑实践

机器人对话系统地图模块设计:POI检索、路径规划与避坑实践

做机器人对话系统最头疼的一件事,不是意图识别,也不是自然语言生成,而是把真实世界接进来。用户问一句“我从现在的地方去服务中心怎么走”,你得在几百毫秒内完成坐标解析、路径计算、话术拼接,还得把路线图用卡片发出…

2026/10/21 6:11:29 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事:AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里,按宣传口径,本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”,而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起:用户到底在追求什么“封号重开”这四个字,放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流,背后是大量的时间和精力成本。正常情况下,没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”,而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时,就意识到:单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/21 4:27:17 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →