Qwen2指令微调入门:从数据准备到LoRA参数配置与推理验证

第一次跑 Qwen2 指令微调的时候,我心里预期很朴素:找一批问答数据,跑通脚本,看 loss 下降,然后得到一个“听话”的模型。结果真等训练结束才发现,能跑通只是开始,模型在验证集上的表现、对指令格式的敏感度、甚至数据顺序都会带来完全不一样的体验。

这篇文章就是想把整个 Qwen2 指令微调的入门过程拆开讲清楚:从“为什么要微调”到数据准备、LoRA 参数配置、推理验证,再到我实际踩过的坑。适合刚接触大模型微调、想让模型按特定格式回答问题的开发者,也适合已经跑过开源推理代码、但对训练侧还比较陌生的朋友。

1. 动手前先想清楚:指令微调到底在解决什么问题

很多同学上来就搜训练脚本,这没错,但我更建议先弄明白你要解决的是哪一层的问题。因为指令微调不是万能的,它只在特定范围内有效。

1.1 预训练模型和“会用”之间的差距

Qwen2 这类开源底座模型,预训练阶段做的事情本质上是“续写”。它的优化目标是根据前文预测下一个 token,所以它能生成通顺的文本、掌握大量知识,但不等同于会“回答问题”。

你直接拿底座模型做推理,输入“什么是注意力机制?”,它可能反问一句“你在说什么?”或者继续输出一段百科性质的阅读理解,而不是给你一个清爽的答案。这不是模型笨,是它的训练目标里根本没有“指令-回复”这种对齐结构。

指令微调的目的,就是在这套纯文本续写能力之上,通过一批“指令-回答”数据,引导模型学会一种新的交互模式。这个过程不改变模型的知识量,更多是改变输出的行为习惯。

提示:判断你需要的到底是知识注入还是行为对齐,是开始之前最重要的决策。如果你想要模型学会新领域知识,微调不是最优解;如果你只是想要它按固定格式输出、学会某种语气,指令微调刚好对症。

1.2 指令微调适合哪些场景,不适合哪些

我梳理了一下,指令微调最适合的场景有三类。

第一类是固定格式输出。比如你要做一个客服助手,所有回答都要求分“问题原因、处理步骤、注意事项”三段。底座模型做不到每次都严格按这个结构来,但几千条样例微调之后,格式稳定性会明显提升。

第二类是特定风格的对话。比如模型需要模仿你写周报的语气、带点幽默感,或者用简洁口语回答问题,这些都属于“输出分布”的迁移,微调非常管用。

第三类是私有知识库的问答入口。注意,微调并不能让模型记住大量新知识,但可以让模型在拿到检索片段之后,只依据片段内容作答、不乱编。这种“行为约束”恰恰靠指令微调建立。

不适合的场景也有。如果你想给模型注入新的技能,比如教它做复杂的数学推理,纯靠指令微调很难。推理能力更多依赖基座本身的水平,LoRA 能激发一部分潜力,但无法无中生有。另外,超长文本、多模态能力这类问题也不是指令微调能解决的,那是模型结构层面的事。

1.3 为什么选 Qwen2 起步

选 Qwen2 作为入门对象,理由其实很务实。

一是文档和社区生态成熟,模型权重的下载、推理、微调工具链都比较完善。遇到问题搜索一下基本都能找到同类情况。

二是尺寸选择灵活。你可以从 0.5B 这种小尺寸开始验证流程,跑通了再切换到 7B 甚至更大的版本。这点对入门非常友好。

三是它对中文的支持比较自然。如果你最终要做中文垂类场景,基座模型本身的“语感”会直接影响微调效果。Qwen2 在中英文混合指令上的表现稳定,不容易出现“中文问、英文答”的割裂感。

我个人的建议是:先拿 0.5B 或 1.5B 把全流程跑通,确认数据格式、训练参数和推理链路都没问题,再上 7B 跑正式实验。这样省时省钱,排查问题也快得多。

2. 环境搭建与数据准备:决定后续效果的第一道坎

这一步看着不起眼,但我在实际项目中见过太多“训练没问题、效果一团糟”的案例,根源都在数据。

2.1 依赖选型与硬件预估

先讲环境。微调 Qwen2 的主流方案是使用开源的 PEFT 库搭配 Transformers,训练框架可以用 PyTorch 原生的 Trainer,也可以用更上层的封装。对入门来说,直接基于 Transformers 的 Trainer 就够了,少一层封装,出了问题好定位。

基础的依赖大概是这样:

  • Python 3.10 左右
  • transformers 4.40 以上
  • peft
  • accelerate
  • bitsandbytes(量化相关)
  • torch 2.1 以上

硬件方面,如果你用 LoRA 微调 7B 模型,一张 24GB 显存的消费级显卡可以勉强跑,但序列长度得控制。用 1.5B 模型的话,16GB 显存也够用。

提示:显存不够优先考虑缩短 max_length,其次才是换更小的模型。很多人一上来就上 2048 长度,还没开始训练就 OOM,其实先跑通流程比追求长文本更重要。

2.2 数据格式与清洗

指令微调的数据格式,最常见的两种是 ChatML 风格和 Alpaca 风格。Qwen2 在预训练阶段使用 ChatML 格式的处理方式,微调时最好延续这种风格,这样模型不用花额外的能力去适应新的指令模板。

我自己习惯把数据结构化成一个包含conversations字段的列表,每条对话形如:

{ "conversations": [ {"role": "user", "content": "用一句话解释什么是进程和线程"}, {"role": "assistant", "content": "进程是操作系统分配资源的基本单位,线程是进程内部独立执行的子任务。"} ] }

有一点要特别注意:如果你用的是 Transformers 的 Trainer 加 DataCollator,需要把这种多轮对话结构转成模型可以接受的输入。最简单的方式是使用apply_chat_template,把对话转成包含特殊 token 的文本序列。

数据清洗我认为有四个必须检查的点。

第一,回答里不能出现“作为 AI 模型,我不能……”这种万能话术,除非业务场景真的需要。第二,所有指令和回答的格式要统一,标点、换行、语气都要检查一遍。第三,去除明显重复的样本,重复数据会让模型对某些模式过度记忆。第四,检查是否存在“指令与回答不匹配”的情况,这个比什么都致命。

我见过一个案例,某同学准备了两千条数据,训练结束后模型经常答非所问。排查了很久才发现,数据里大约 15% 的回答是从其他领域直接复制过来的,模型学着学着就把错误对应关系学进去了。

2.3 数据量级的选择

关于数据量,很多人喜欢追求“越多越好”,但实际上指令微调不是堆量就有用。

对入门项目来说,质量过关的前提下:

  • 单一任务、格式固定:300-500 条即可看到明显效果
  • 多任务、需要保持通用能力:2000-5000 条比较合理
  • 真正接近生产级的 Chat 模型:数万条是另一个故事

我建议第一次实验先准备 500 条左右的高质量数据,跑通流程并观察效果,再逐步扩充。这样就算数据有问题,也能快速定位,而不是等到训了一夜之后才发现数据格式错了一半。

如果手头数据不够,可以有限度地使用合成数据。比如用一个表现较好的模型对你的原始材料生成多个角度的问答对,再人工筛选。合成的数据需要注意多样性,不要只换表述方式而保留完全一样的信息结构,否则模型学到的只是表面句式。

3. LoRA 微调:用最小成本实现参数更新

环境没问题、数据也备好后,就进入训练环节了。这也是大多数人最关心的一步。

3.1 为什么优先 LoRA

刚开始接触微调时,很多人会纠结一个问题:到底做全参微调还是 LoRA?

我的答案是:入门阶段无脑选 LoRA。理由不是全参微调不好,而是 LoRA 用最小的代价让你跑通整个链路。

全参微调意味着更新模型的所有权重。7B 模型如果用 float16 做全参微调,优化器状态带来的显存开销非常大,个人电脑基本扛不住。LoRA 的思路是冻结原始权重,只在每层 Transformer 的 Attention 部分注入低秩矩阵,训练时只更新这些新增参数。

这样做的好处有两个。一是显存占用大幅下降。二是可插拔。训练完成后你可以把 LoRA 权重单独保存,推理时动态加载,不用保留训练状态。多个 LoRA 权重还可以针对不同任务切换,相当于一个底座模型配多个“小脑”。

3.2 关键参数配置与训练脚本

下面我给出一个基于 PEFT 的 LoRA 训练脚本主配置。这不是唯一答案,但至少是我验证过能稳定跑通的组合。

from peft import LoraConfig, get_peft_model from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments model_path = "Qwen/Qwen2-1.5B-Instruct" tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( model_path, torch_dtype="auto", device_map="auto" ) lora_config = LoraConfig( r=16, lora_alpha=32, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM" ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() training_args = TrainingArguments( output_dir="./qwen2-lora-output", per_device_train_batch_size=2, gradient_accumulation_steps=8, learning_rate=2e-4, num_train_epochs=3, logging_steps=10, save_steps=100, save_total_limit=2, fp16=True, remove_unused_columns=False )

几个参数我想展开说一下。

r代表低秩矩阵的秩,决定了新注入参数的规模。r=8适合简单任务,r=16是比较通用的起点。lora_alpha简单理解成缩放系数,一般设为r的两倍。

target_modules这段值得细看。不同模型的模块名不一样,Qwen2 系列里q_proj、k_proj、v_proj、o_proj是 attention 部分的四个投影层。如果只改 Attention 层效果不够,还可以考虑把 MLP 层也纳入。但我的经验是,先只动 Attention 层,效果稳定又快。

learning_rate和batch_size是互相影响的。LoRA 训练通常使用比全参微调更高的学习率,2e-4 附近是常用区间。学习率太高模型会学歪,太低则需要更多步数才能看到效果。

提示:remove_unused_columns=False这一项容易忽略。因为你的数据集里除了文本列之外可能还有一些元信息,如果默认让 Trainer 去除未使用列,可能把构造好的输入也一起删掉,导致训练报错。

3.3 训练过程中的损失观察

训练启动后,不要只盯着 loss 数值看。

我习惯记录 loss 曲线的走势,同时关注它在不同阶段的下降节奏。正常情况是前几百步 loss 快速下降,随后进入平台期。如果你发现 loss 下降极其缓慢,先检查学习率;如果 loss 出现突然跳升,考虑数据里是否存在异常样本。

更要紧的是:训练 loss 只能告诉你模型是否拟合了这批数据,不能告诉你它的泛化能力。一个常见误区是把训练 loss 压到 0.3 以下就觉得大功告成。实际上 LoRA 训练很少需要把训练 loss 压到极低,否则基本意味着过拟合。

训练过程中每隔一段时间就手动存一个 checkpoint,我至少会存三份:训练后期、训练中段、以及一个接近收敛的点。后面做效果对比时会非常有用。

4. 合并权重与推理验证:别急着庆祝

训练结束不是冲刺终点,真正的验证才刚刚开始。

4.1 模型的保存与合并

LoRA 训练结束后,你手里拿到的是训练器输出的 checkpoint,其中只包含适配器权重,不是完整模型。有两种使用方式。

一种是直接保存适配器权重,推理时加载原模型再加 LoRA:

from peft import PeftModel base_model = AutoModelForCausalLM.from_pretrained(model_path) model = PeftModel.from_pretrained(base_model, "./qwen2-lora-output/checkpoint-500")

另一种是先把适配器权重合并到原模型中,导出成一个完整的模型文件。合并方式很简单:

merged_model = model.merge_and_unload() merged_model.save_pretrained("./qwen2-lora-merged") tokenizer.save_pretrained("./qwen2-lora-merged")

合并导出之后,你就可以把模型当成一个普通底座模型来推理了,部署起来更方便。但要注意,合并后的模型体积会变大,因为原始参数全部被保存。

4.2 指令遵循能力验证

推理验证阶段,我强烈建议不要只准备两三道题测一下就觉得万事大吉。

我通常会把验证集分成几类,分别测试:

  • 直接复现:与训练数据格式很像的题目,看模型是否学会了格式
  • 简单泛化:同样领域、但没出现过的题目,看模型是否理解任务
  • 边界测试:刻意问一些训练阶段没有覆盖的角度,看模型会不会乱答

直接复现表现好是应该的,泛化表现好才是微调真正起效的证据。边界测试如果表现不理想也不用慌,这是正常现象,关键是判断模型的输出是“知识性错误”还是“行为模式错误”。前者可以接受,说明它只是没见过这个知识;后者需要警惕,说明它对指令的理解出现了偏差。

4.3 过拟合检查与简单评测

过拟合是 LoRA 微调最容易出现的问题,尤其数据量少的时候。判断方法很简单:把训练集里出现过的题目原封不动丢给模型,如果回答和训练数据一模一样、甚至一个标点都不差,这就是过拟合信号。

同时按 emotion 判断一下:如果模型在未见过的新问题上也能保持格式稳定,说明行为对齐学得不错;如果新问题上彻底胡言乱语,则要考虑数据量不足或学习率过高。

另外,如果你微调的时候给原始模型添加了特殊 token 或修改了模板,验证时千万要记得同样处理。我见过一个开发者在训练时自定义了一套对话前缀,推理时却用官方默认模板,结果模型输出没有灵魂,因为输入分布和训练分布完全对不上了。

5. 跑完一轮之后的踩坑记录与调优路线

最后这部分,我把自己实际踩过坑和总结出的调优路线放在一起说。

5.1 高频踩坑点

第一个高频坑是“模板不一致”。训练时用的 ChatML 模板、pad token 设置、以及 newline 处理,如果和推理时不统一,效果会千差万别。每次都把训练和推理放在同一个 tokenizer 下完成,能省很多排查时间。

第二个坑是“把 LoRA 参数焊死”。很多人直接照搬别人的配置,从来不看任务难度。简单分类任务用 r=16 三个 epoch 可能已经完全过拟合;长对话生成任务可能还需要更大秩和更长训练。参数是服务于数据和任务的,不是越统一越好。

第三个坑是“只看 loss 不看成例”。训练 loss 好看,但推理时模型总是回复“抱歉,我不太明白你的问题”,这是典型的“学过但对不上”。问题往往出在数据分布太窄,或者数据里缺少负例。可以适量加入一些“无法回答”的类型样本,让模型学会拒绝,而不是硬答。

第四,硬件相关。很多人训练到一半爆显存,第一反应是调小 batch size。但有时候梯度累积步数没同步调整,有效 batch size 突然变小,模型效果会漂。我在训练配置里习惯把per_device_train_batch_size和gradient_accumulation_steps绑定在一起调整,保证有效 batch size 不变。

5.2 后续的进阶方向

入门流程跑通一遍后,接下去的优化方向有几条比较好走。

数据层面,可以做负例筛选。把验证集里模型答错的样本重新清洗、修正,再放回训练集。这种方法往往比加更多新数据更有效,因为错误的样本直接影响模型行为。

训练层面,可以尝试在 LoRA 基础上结合不同层级的模块,比如把 MLP 层也纳入低秩更新。有人觉得效果提升有限,但在某些领域任务上,肉眼可见地降低幻觉频率。

推理侧也有优化空间。同一套 LoRA 权重,在不同采样参数下的表现可能天差地别。很多“微调无效”的结论,其实是temperature和top_p没调好,模型在随机采样模式下显得不稳定。

我个人实践下来最大的体会是:指令微调的项目里,数据整理时间和调参时间的比例,通常会超过 7:3。技术方案本身不复杂,真正的复杂度在于理解模型当前的问题、构造合适的数据、设计有效的验证方法,再一点一点逼近你想要的行为模式。

如果你已经能稳定地把 Qwen2 微调跑通,接下来真正拉开差距的,可能就不再是训练脚本里的参数,而是你对数据、任务和模型三者边界的理解了。

相关新闻

阶梯碳交易下含P2G-CCS耦合与燃气掺氢的虚拟电厂优化调度复现指南

阶梯碳交易下含P2G-CCS耦合与燃气掺氢的虚拟电厂优化调度复现指南

复现“基于阶梯碳交易的含P2G-CCS耦合和燃气掺氢的虚拟电厂优化调度”这类题目时,很多同学卡住的往往不是求解器调用,而是三个技术点叠加后产生的变量耦合关系。P2G和CCS明明是两个设备,为什么题目里叫“耦合”?燃气掺氢之后&…

2026/10/21 7:22:56 阅读更多 →
EasyAI vs Spring AI:轻量封装还是官方全家桶?

EasyAI vs Spring AI:轻量封装还是官方全家桶?

前阵子帮某团队做AI接入方案评审,架构师拿着一份技术选型的提案直接问我:既然Spring官方都出了Spring AI,为什么社区里还在推荐一个叫EasyAI的轻量封装库?我没急着回答,反手把两个依赖的jar包尺寸和历史提交记录调出来…

2026/10/21 7:22:56 阅读更多 →
XtraBackup还原实战:MySQL .xb备份文件完整恢复指南

XtraBackup还原实战:MySQL .xb备份文件完整恢复指南

备份文件拿到手不等于数据已经安全,真正验证备份是否可靠,还是要看能不能成功还原。之前接手一台MySQL 8.0实例的迁移,对方只给了两个文件:一个全量备份.xb,一个增量备份.xb,当时手边没有现成环境&#xff…

2026/10/21 7:21:56 阅读更多 →

最新新闻

Playwright Fixture机制:模拟数据库与API,根治测试数据污染

Playwright Fixture机制:模拟数据库与API,根治测试数据污染

做Web自动化测试这几年,我在实际项目里总结出三怕:一怕测试环境数据库连不上,二怕第三方接口突然限流,三怕测试数据互相污染。前两个问题会让你辛辛苦苦维护的用例在一夜之间全部标红,第三个问题则更隐蔽——明明昨天还…

2026/10/21 7:59:12 阅读更多 →
Qt 5.15.12 Windows 10下编译32位动态库完整指南

Qt 5.15.12 Windows 10下编译32位动态库完整指南

简介:面向Windows 10 32位应用开发的Qt 5.15.12动态库编译包,基于MSVC2019编译器构建,专为需要维护32位兼容性、或希望避开复杂编译配置的C开发者准备。压缩包内共2000个文件,以Qt头文件(.h)为主&#xff0…

2026/10/21 7:59:12 阅读更多 →
机械臂正运动学入门:从旋转矩阵到DH参数解析

机械臂正运动学入门:从旋转矩阵到DH参数解析

机械臂这个东西,入门时最容易迷惑的不是电机怎么转,而是“我要它去的点”和“它实际到的点”之间到底隔着什么。我刚开始折腾机械臂时,以为把每个舵机角度调到某个值就行,结果想让它末端走一条直线,它硬是在空中画了个…

2026/10/21 7:59:12 阅读更多 →
Java设计模式速通:场景+实战,快速掌握核心模式

Java设计模式速通:场景+实战,快速掌握核心模式

不是每一行代码都值得背下来,但设计模式是少数值得花时间想清楚的东西。尤其是Java生态里,框架源码、中间件、业务系统到处都嵌着这些模式,看不懂它们,读源码就像看天书;看懂了,重构和扩展的底气完全不一样…

2026/10/21 7:59:12 阅读更多 →
zzcms8.3建站系统实战:环境搭建、代码审计与二次开发改造指南

zzcms8.3建站系统实战:环境搭建、代码审计与二次开发改造指南

简介:ZZCMS 8.3 是一套开源内容管理系统源码,面向希望入门代码审计与 PHP 架构的开发者、安全学习者及建站人员。它既可用于搭建动态网站,也是一份可动手剖析的实战样本,帮助读者在真实项目中理解安全编码与设计模式。压缩包为 zi…

2026/10/21 7:59:12 阅读更多 →
EVE新手挖坟逃生指南:反跳、扰频、微曲与泡泡全解析

EVE新手挖坟逃生指南:反跳、扰频、微曲与泡泡全解析

简介:这是一份面向EVE新手玩家的挖坟逃生实操指南,作者结合多年实战经验,系统梳理了遗迹破译与逃生环节最容易翻车的细节。内容从破译箱子时的隐身快捷键设置、环绕距离把握讲起,逐步展开过星门遇泡泡的脱身路线规划、微曲与隐身技…

2026/10/21 7:58:11 阅读更多 →

日新闻

统一内存+量化:锐龙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 阅读更多 →