news 2026/9/29 4:50:04

嵌入式+LLM的落地姿势:从硬件约束到闭环构建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式+LLM的落地姿势:从硬件约束到闭环构建

1. 先说清楚:嵌入式 + LLM 到底是个什么组合

最近总有人问我:嵌入式工程师要不要学大模型?我的答案是——学,但千万别学成“把服务端那套搬过来”。真正的难点不在于会不会调库、能不能写两行Prompt,而在于你能不能把大模型这条新链路,塞进一块跑着实时任务、只有几百兆内存、还要伺候各种外设中断的板子里。这才是标题里“嵌入式 + LLM”的真正含义:不是把 ChatGPT 塞进单片机,而是在真实的硬件约束下,让 LLM 变成嵌入式系统的一个功能模块。

顺着标题拆开看,三个关键词刚好对应三条主线:约束(Constraint)是前提,构建(Build)是方法,硬件闭环(Hardware Closed-loop)是最终目标。我理解“正确姿势”的本质,就是学会在这三者之间找平衡——模型再聪明,也得先把 IO 时序、资源预算、通信协议这些硬规矩立住;知识库再全,也得通过构建链路把数据和模型真正串起来;最后还要让模型的分析结果能回到执行器上,形成从“感知—推理—执行”的完整闭环。

这篇文章的目标读者是两类人:一类是像我一样从嵌入式转过来的工程师,想搞明白 LLM 项目落地到底卡在哪里;另一类是搞算法但没碰过硬件的同学,想知道现场部署时那些恼人的“破事”从哪来。花十分钟看完,你会对整条链路有个清晰的认知框架,也能直接拿去指导自己的小项目。

2. 约束:先学会给系统立规矩

2.1 硬件资源约束:从算力预算到运行规划

嵌入式系统最缺的不是智商,是资源。我做边缘设备上的 LLM 部署时,第一件事永远是做资源预算表,而不是先选模型。这里的预算不是随便拍脑袋,我一般按四项算:CPU/GPU 算力、内存带宽与容量、Flash/存储空间、功耗上限。比如一块常见的中高端 ARM 开发板,双核 A72 主频 1.8GHz,配 4GB LPDDR4,理论上有 10-20 TOPS 的 NPU 算力(如果板子带 NPU),但实际能跑什么规模的模型,还得看内存带宽。

为什么内存带宽这么关键?因为 LLM 推理是典型的访存密集型任务。7B 参数的模型,如果只做 4bit 量化,参数量大约是 3.5GB。按一个 token 需要读取全部参数计算一次来看,假设我们要达到每秒 10 个 token 的速度,每秒至少要搬运 35GB 数据。这个数字很多工程师没概念——普通 DDR 内存的带宽也就是 10-20GB/s 量级,还要跑操作系统、应用、图形界面。一算下来你就知道,不调优、不量化、不裁剪,连个像样的对话系统都跑不动。

所以我在实际项目里会先画一张“算力-内存-功耗”三角预算表。基本判断标准是这样的:如果你的板子内存小于 1GB,就别碰超过 1B 参数的模型;如果功耗预算小于 5 瓦,就老老实实考虑 NPU 加速而不是靠 CPU 硬扛;如果有实时中断任务在跑,那 LLM 的推理线程一定要绑核或者降优先级,否则一次长推理可能直接导致看门狗复位。这些约束看起来都很朴素,但九成项目翻车都是因为前期没做这层规划。

2.2 接口时序约束:IO约束、XDC约束与时钟MUX约束

标题下的热词里出现了 io约束、xdc约束、时钟mux约束,这几个词放在一起,懂行的朋友应该立刻想到了 FPGA 和高速接口设计。其实这类硬件约束思维放在嵌入式系统里同样适用——它们都是在告诉系统:哪条信号路径必须在什么时间内完成、哪个时钟域的边界不能越界、哪个信号和哪个信号之间必须满足时序关系。

在 FPGA 项目里,IO 约束定义了引脚电平标准、驱动强度、上下拉属性,XDC 约束(Xilinx Design Constraints)则进一步规定了时钟周期、输入输出延迟、伪路径等时序规则。我记得有个项目,DDR 控制器读写老是不稳定,查了半天发现是时钟 MUX 切换时没加约束,导致时钟切换瞬间产生毛刺。这种问题在纯软件层面根本看不见,但放到搭了 LLM 推理加速器的系统里,一旦内存控制器出问题,模型推理的随机错误会让你误以为是算法写错了——其实底层时序早就没守住。

对于做嵌入式 Linux 的工程师来说,可能不用碰 FPGA,但等价的概念无处不在。设备树里的时钟频率配置、I2C 总线的时序参数、PWM 的占空比更新策略,本质都是“约束”的具体形态。我给一个建议:开始移植或调试任何外设驱动时,先把接口的时序约束文档打出来贴在工位上。很多同学跑起 LLM 之后喜欢反复改代码调 Prompt,但整个系统不稳定的时候,先回头查约束——时钟、电源、通信时序这些底层规矩立住了,才有资格谈上层模型表现。

2.3 编码规范与接口契约约束:团队协作不翻车

做嵌入式的时候,编码规范约束往往是培养团队战斗力的第一课,但嵌入式和 LLM 结合之后,编码规范的价值被进一步放大了。现在嵌入式代码和模型推理代码往往出自不同人之手:C 语言的采集线程、Python 的推理脚本、还有负责打通数据链路的胶水代码。如果没有统一的接口契约,分分钟冒出一堆“智能”项目死于时空错乱。

我在团队里推的是一套非常朴素的接口约束:所有跨模块数据必须用结构体或类定义传输,禁止裸传 JSON 字符串指针;所有数据处理模块必须声明好输入输出的数据范围与时间戳;所有模型调用必须带超时和失败回退路径。这些看似和“算法”无关的规则,恰恰是让 LLM 真正可控的关键。你想,一只机械臂正在执行关键动作,结果 LLM 模型推理超时了,返回了一个空结果,如果代码里没有约束兜底,下一拍就可能直接让执行器飞出安全边界。这不是代码风格问题,是安全问题。

还有一点是从“编码添加编码规范约束”这个热词里得到的启发——现在的 AI 辅助编程工具确实很强大,但如果不给它们定规范,生成的代码质量完全不可控。我给团队的做法是:把项目规范写成一个 markdown 文件,作为上下文喂给编码助手,要求它遵守命名风格、错误处理模式、内存管理约定。实测下来,生成的代码符合预期比例明显提高,那种“能跑但根本不敢在硬件上部署”的野代码少了很多。

2.4 Token约束:给LLM也写上约束文件

热词里有一句话特别戳我——“llm的token三个点 key我是谁、query我在找什么、value我能提供什么”。这句话放在学术语境里解释的是注意力机制中 QKV 的含义:Key 是内容索引、Query 是检索意图、Value 是实际承载信息的向量。但放到嵌入式 + LLM 的场景里,它给了我们一个更实用的提示:Token 是 LLM 世界里最小的“资源单元”,你必须像管理 IO 资源一样管理 Token 预算。

嵌入式系统里跑 LLM,Token 约束体现在几个层面。第一是上下文长度限制,端侧模型的 context window 通常不会太大,尤其被量化后,长上下文表现很容易退化;第二是输出长度限制,我给模型设的 max tokens 一般不超过 256,因为嵌入式场景要的是结构化短回答,不是长篇大论;第三是总 Token 预算,如果你在跑 RAG(检索增强生成)流程,每次查询要把用户问题、检索到的知识片段、系统提示词一起拼进上下文,Token 不够了就得忍痛裁剪。

所以我在具体项目里会做一件很多人觉得“很不 AI”的事:把可用的 Token 预算写进配置头文件里,像定义缓冲区长度一样定义它。系统提示词固定占用多少 Token、知识检索最多占多少、留给回答的 Token 是多少,全部写死。这样模型永远在一个“已知的盒子”里工作。这个思路和写 XDC 约束极其相似——既然算力、内存、时序都要有边界,为什么对话的长度和格式可以没有边界?

3. 构建:把知识、模型、链路搭起来

3.1 构建LLM知识库:从零散资料到可检索的wiki

“构建”这个词在嵌入式和 LLM 两个领域都有明确指向:嵌入式里讲构建,说的是交叉编译工具链、依赖库、镜像生成;在 LLM 这边,构建更多指知识库、检索链路、提示模板的建设。热词里反复出现 llm wiki、llm wiki知识库,其实背后的核心需求非常实际:通用的模型不知道你的私有硬件细节、你的项目代码逻辑、你的产品故障特征,要把这些东西教给它,最靠谱的办法不是继续预训练,而是建一个可检索的知识库。

我的习惯是先用 Markdown 维护一个“项目知识 wiki”:内容包括硬件规格、外设映射表、通信协议帧格式、已知问题清单、代码模块接口说明。然后把这个 wiki 作为 RAG 检索的语料来源。但建库不是把文档往数据库里一扔那么简单,我踩过的坑是——很多人把几十 MB 的 PDF 一股脑切块灌进去,结果检索质量惨不忍睹。真正的做法是:先做清洗、转换格式、按主题切块,再人工标注一批高质量问答对。有了问答对,你就能自动生成每个知识块的向量索引,检索时会准很多。

另外我习惯在 wiki 里专门开一节叫“设备的脾气”。记录的是那种看手册根本查不到、全靠现场调试积累的隐性知识。比如某个传感器在低温下 I2C 读取偶尔会 NACK、某个电机的电流尖峰会触发误报警这一类。这些内容喂给 LLM 之后,现场值班人员再遇到类似问题,就能通过自然语言查到一个相当靠谱的排查建议,而不是求着老工程师半夜起来救火。

3.2 构建嵌入式侧的运行环境:交叉编译、依赖与部署方式

嵌入式系统跑 LLM 推理,环境构建和纯服务端完全不同。服务端那边通常是 Python 虚拟环境 + pip install 一把梭,但嵌入式侧往往连 Python 都不一定装得全,更别说 PyTorch 这样的重型依赖。我现在的标准做法是:固件侧用 C/C++ 做实时采集和控制,推理侧用专门的推理引擎(比如 llama.cpp、ONNX Runtime、或厂商 SDK),通过进程间通信或共享内存把两侧连起来。

构建过程里有三个关键点。第一,交叉编译工具链版本必须和板子厂商 SDK 保持一致,否则会在链接阶段遇到各种奇奇怪怪的 ABI 问题;第二,所有推理依赖库尽量静态编译,这背后的原因很简单——嵌入式目标板的根文件系统里可能缺一堆动态库,等到现场才发现缺 libstdc++.so.6,那种体验我帮大家试过了,非常酸爽;第三,推理引擎的构建参数要针对目标 CPU 微架构打开指令集优化,比如 ARMv8.2 以上的板子可以打开 fp16 向量指令,性能差距能在 30% 以上。

很多从应用层开发转过来的朋友习惯把“构建”理解成跑一个 maven 或 npm 命令。但嵌入式构建的本质是一个完整的产物生命周期管理:交叉编译出二进制、打根文件系统、烧录镜像、上电自检、灰度发布。热词里出现“应用层开发是不是嵌入式”的搜索,说明很多人还在纠结这个转型问题。我的看法是:如果你没有接受过“构建产物最终要变成一块硬件上的固件”这种思维训练,那么做嵌入式大模型应用,第一关不是算法,而是把工具链和构建体系跑通。

3.3 构建RAG检索链路:让模型学会查你的手册

RAG 是实现“私有大模型问答”最常见的构建方式。前面讲的 wiki 知识库是 RAG 的“语料层”,而真正决定体验的是“检索链路”怎么搭。我的做法分四步:

  • 第一步,把知识库文本切块。切块策略我经过大量对比,按 300-500 字的语义段落切效果最稳,太短会丢失上下文,太长又导致检索命中精度下降。
  • 第二步,为每个文本块生成向量,嵌入模型同样要在端侧运行或通过离线任务生成,不要在设备上临时联网调用。
  • 第三步,用户提问时,先做意图分类,再生成检索 query,有些问题还要拆成多个子查询,分头检索后合并结果。
  • 第四步,把检索到的知识片段和对话历史按模板拼装成提示词,送到生成模型。

有一个被很多人忽略但又极其重要的细节:知识库检索的时间戳问题。你项目的硬件版本可能已经迭代了好几轮,但知识库里还残留着老版本的接口描述。如果不给每个知识块打上版本标签,并在检索时做版本过滤,模型就会非常自信地告诉你一个已经废弃的寄存器地址,让你在硬件上折腾半天。这种事情我碰上过不止一次,后来干脆在知识块元数据里强制加上适用硬件版本,检索链路再过滤一轮。

这种“检索—过滤—拼装—生成”的模式,说白了就是从“让模型背知识”变成“让模型查资料”。这和嵌入式工程师平时做事的思路完全一致:你不要求 CPU 记住所有外设寄存器的地址,你给它的驱动代码里写好了查表逻辑。RAG 也是查表,只是这个表变成了语义索引。

3.4 约束求解器在资源配置上的应用:当“穷”成为主题

热词里出现 cp-sat 约束、约束求解器stp安装、混合约束自动机,这让我想起一个和 LLM 资源分配紧密相关的话题:当设备和算力处处受限时,怎么安排才能跑得最顺?

如果你要在嵌入式板上同时跑实时控制任务、视频采集任务、LLM 推理任务,单靠测试时“调优先级”是不科学的,因为你面对的是一个组合优化问题。我试过把任务执行顺序和资源分配建模成约束满足问题,用 OR-Tools 的 CP-SAT 求解器来算。比如:帧率必须不低于 15fps、LLM 单次推理延迟不能超过 2 秒、实时控制周期必须稳定在 1ms,把这些问题建模成约束,求解器能帮你找到一个满足所有硬约束的调度方案。这比人工试探要靠谱得多,因为里面变量太多、互相牵扯。

不过我也要说句实在话:约束求解不是万能的。求解器算完的调度方案还要面临工程现场的噪声——比如某个任务实际执行时间比预估长了 20%,那整个“最优方案”就失效了。所以我的建议是:用 CP-SAT 求解器做上层的可行性分析和初步排程,但在底层再留一档退化策略。比如 LLM 推理如果超时,就直接走规则引擎兜底,返回预设结论,不阻塞控制链路。这样你既享受了约束优化的收益,又不至于被硬约束反噬。

4. 硬件闭环:让模型真正上手干活

4.1 边缘侧的闭环架构设计:感知—推理—执行怎么串起来

聊完约束和构建,终于到了最关键的“硬件闭环”。这里的“闭环”不是指 PID 那种毫秒级的反馈回路,而是一个更大的智能环路:硬件传感器采集环境数据,经过预处理后送给 LLM 模块进行语义理解和决策,决策结果再变成指令发给执行机构,执行的效果又会被传感器重新采集——形成一个不断迭代的智能控制系统。

这个闭环里最容易犯的错误,是把它做成“开环”:传感器采完数据,模型给了一段好看的分析报告,然后呢?没有然后了。报告放在工程师手里还能看看,但如果是无人值守的现场设备,模型输出没有直接落到执行器上,这个系统就没有产生实际价值。所以我在设计架构时,一定会画清楚一条指令链路:模型分析结果 → 结构化决策(JSON 格式)→ 规则引擎翻译成设备指令 → 写入执行器控制寄存器。

举一个我实际做过的小项目:用一个 ARM 开发板连接温度、振动、电流传感器,跑着一个 0.5B 级别的量化模型,它的任务是诊断电机异常。正常状态下,模型每 5 分钟做一次推理,输出“正常”即保持运行;一旦推理结果出现“过热”或“异常振动”,系统会结合历史巡检数据生成一条包含置信度的告警信息,同时自动把电机转速降到一个安全值。这个过程全自动,不依赖云服务器,断网也照样工作。这就是硬件闭环的意义:模型不是陪聊的,而是参与控制的。

4.2 五种通信协议在闭环里的分工:UART、I2C、SPI、CAN、Ethernet

热词里“嵌入式 5种通信协议”出现了不只一次,这个点特别值得展开,因为一个硬件闭环里,几乎没有哪种协议能包打天下——每种协议都有它最适合的位置,而你要做的就是把他们放进一个合理的体系里。

以我那个电机诊断项目为例,整条链路上就同时用到了多种协议。传感器节点内部,MEMS 加速度计挂在 SPI 总线上,因为它需要高吞吐、低延迟读取原始数据;温湿度传感器用 I2C,因为这类器件本身数据量小、速率要求不高、接线还省;而分布在车间不同位置的几个采集节点之间,用 CAN 总线做数据汇总,CAN 的差分信号和仲裁机制天然适合工业现场的强干扰环境;节点汇总数据到主控板,走的是工业以太网(或者简单点用 UART 转 RS485),保证远距离传输的稳定性。

有些场景还要用无线协议(比如 BLE、LoRa、WiFi)和 5G/4G 模块做远程回传,不过核心要点是一样的:你选择哪种协议,取决于传输速率、距离、节点数、抗干扰能力、功耗这五维度的综合权衡。我在选型时会先列一张表,把每个通信链路的速率要求和时延要求填进去,然后看哪个协议恰好匹配。千万别干那种“所有传感器都挂 SPI”的偷懒事——那会把 CPU 的大量时间耗在片选切换上,导致 LLM 推理拿不到足够的 CPU 资源。

4.3 端侧模型部署:量化、推理引擎调用与性能拉伸

到了真正把模型跑起来这一步,部署是最见真章的时刻。我最初入坑时走了不少弯路,现在总结下来,端侧模型部署的核心流程基本是:下载或训练模型 → 转换为适合目标平台的格式 → 量化压缩 → 用推理引擎加载 → 性能测试与优化。

模型转换和量化部分我特别想多说几句。嵌入式平台上的 LLM 部署,量化几乎是必选项。4bit 量化是最常用的方案,原因是它在精度损失和体积压缩之间取到了一个相对性价比最高的点。7B 模型从 FP16 压到 4bit,体积能减少约 70%,而绝大多数任务场景的精度损失在可接受范围内。如果你的板子内存特别紧张,还能考虑 2bit 量化,但说实话效果会差不少,大多数场景我不推荐。

推理引擎的选择,我一般按这个逻辑:优先用板子厂商提供的 SDK,因为算子优化、内存分配、NPU 调度都替你解决了;厂商 SDK 不好用或不受支持,再考虑社区通用的 llama.cpp 或 ONNX Runtime。前者对 ARM CPU 的优化很扎实,配合 mmap 加载大模型能省掉不少内存拷贝;后者模型生态更丰富,方便跨平台切换。还有一个小技巧:修改推理引擎的线程数配置,不是越多越好——我在一个 4 核板子上实测,线程数从 4 调到 8,推理速度反而下降了,因为 CPU 大部分时间都耗在线程上下文切换上。正确做法是看推理引擎的实际 CPU 利用率曲线,留下一点余量给系统的其他实时任务。

4.4 一个具体的闭环案例:设备异常诊断系统的完整流程

用一个可以照着搭的流程来收束整个话题吧。假设你现在手头有一块 ARM Linux 开发板、一路 RS485 连接的温度变送器、一个继电器输出模块,目标是用 LLM 做一个“循环水温度异常自动保护系统”。

系统框图其实很清晰:RS485 采集温度 → Linux 用户态 C 程序解析为结构化数据 → 写入共享内存 → Python 推理进程读取,结合最近 10 条温度记录生成格式化的“运行摘要” → 作为 Prompt 前缀送给量化模型 → 模型返回 JSON 结构(状态字段:normal/warning/critical;建议字段:具体操作建议)→ 规则引擎执行动作。若状态是 critical,继电器动作,切断加热器电源;warning 状态则记录日志并提示用户关注。

这套流程难在哪?难在把“模型输出”变成“可信决策”的过程。模型的输出永远有概率性,但硬件执行必须确定性。我给这套系统设了一条铁律:只有模型的输出格式完全符合 JSON schema、状态枚举值合法、且置信度达到阈值,才允许执行器动作;任何解析失败、缺字段、超时,一律走默认安全态——保持现状并告警。这条规则看起来保守,但恰恰是它让系统在长达数月的现场运行中保持零误动作。做嵌入式 + LLM 的项目,宁可让系统“笨”一点,不能让它“疯”起来。

5. 常见问题与排查技巧实录

做这套东西的这一年多,我踩过的坑挺多的,挑几个最有代表性的整理成速查表,给后来人省点时间:

现象排查方向处理思路
板子跑 LLM 推理时,实时控制任务周期性卡顿CPU 资源争抢推理线程绑核并降低优先级;给实时任务分配独立核心
模型推理输出偶尔乱码内存校验错误或供电不稳检查 DDR 时序参数、电源纹波;加入 ECC 校验(如果硬件支持)
RAG 检索找不到关键文档内容文本切块不合理或语料清洗不彻底调整切块策略;在知识库中增加人工问答对
模型总是生成过度冗余的回答系统提示词缺乏格式约束在 Prompt 里明确输出格式和最大长度;必要时使用结构化采样
交叉编译的推理引擎在目标板上有段错误工具链 ABI 不匹配用厂商 SDK 重新编译;关闭高级指令集选项逐一对比
设备重启后模型加载时间超长模型文件存放在慢速存储将模型镜像放到更快分区或用 mmap 方式加载

几个关键心得再单独强调一遍:第一,在嵌入式系统里集成 LLM,先写好系统的“退出路径”——模型失败时系统做什么,一定要在一开始就定清楚。第二,所有模型输入输出都做严格的 schema 校验,拿模型当不可靠的外部设备看,而不是当自家函数用。第三,保持推理链路的数据结构化,模型输出最好永远是 JSON 或有限枚举,不要输出自由文本让下游去猜。

我个人在实际操作中的体会是,嵌入式 + LLM 的路线和传统嵌入式开发有很多相通之处——核心从来不在于某个模型多聪明、某个框架多流行,而在于你能不能给它划好边界、立好规矩,再把它嵌进一条真实存在的物理链路里。如果这篇文章能帮你少走几步弯路,那就值得了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 4:48:31

AD24工程化避坑:库导入、差分走线、DRC与Gerber输出

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

作者头像 李华
网站建设 2026/9/29 4:46:33

LED点阵模块驱动原理与STM32实战指南

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

作者头像 李华
网站建设 2026/9/29 4:46:23

DCDC控制方式选择:从原理到物理实现的硬约束决策

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

作者头像 李华
网站建设 2026/9/29 4:44:41

AXI Memory Mapped to PCIe IP核:FPGA端点设计调优

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

作者头像 李华
网站建设 2026/9/29 4:44:35

AI编程 | 2分钟看懂Vibe Coding:用TaoToken统一Key跑通AI编程工作流

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

作者头像 李华
网站建设 2026/9/29 4:42:49

基于IPFS和以太坊的去中心化文件存储DApp实战

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

作者头像 李华