news 2026/10/1 1:07:34

AI工程从零开始:数据、模型与部署的完整落地指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI工程从零开始:数据、模型与部署的完整落地指南

1. 项目概述与学习起点

1.1 为什么“从零开始”往往是最难的操作

如果你搜过“ai-engineering”,大概率会看到两类内容:一类是铺天盖地的课程广告,从“七天入门AI”到“三个月拿下大厂Offer”;另一类是各种知识星球、付费社群里的大神贴,动辄甩出几十个链接,让你自己看论文、刷LeetCode、啃源码。

我在这个行业里待了十一年,做过算法岗也做过平台架构,带过应届生也带过社招转行的新人。说实话,最让我担心的人才缺口,不是那些“算法题能刷到周赛前排”的人,而是能在真实业务环境里,把一个AI系统从零开始搭起来、跑起来、并且稳定运行下去的人。

如果你是我,你会怎么教一个新人?这个问题我认真想过。后来我总结出来的答案是:不要从“深度学习”开始,不要从“Transformer”开始,甚至不要从“调库调包”开始。真正有效的ai-engineering起点,是先想明白一个朴素但致命的问题:在真实世界里,一条AI落地路径的前后左右,到底站着哪些角色、哪些技术、哪些流程、哪些坑。

这也是我写这个项目的初衷。“ai-engineering-from-scratch”不是一个炫耀术语的标题,而是我这一年多来沉淀下来的一套完整学习路径。它面向的不是天才,而是那些愿意一步一步把地基打牢、愿意在调试和排错中真正理解系统全局的普通人。不管你是刚入门的技术新人,还是已经被业务催着上模型的老后端,我想这篇文章都可能帮你在“AI工程到底是什么”这件事上,建立一条更清晰的轴距。

1.2 这个项目解决什么问题

如果你身边有做过AI项目的朋友,你可以问问他:项目里最耗时间的是哪个环节?答案大概率不是“写模型代码”,而是“去搞清楚为什么代码在训练时和上线后行为不一致”,或者是“数据管道又在半夜挂掉了”。

这条经验背后的原因很有意思。传统软件工程面对的是确定的输入输出,你可以用单元测试把逻辑钉死;但AI系统面对的是统计意义上的“不确定”,数据分布一偏移,模型精度就会无声下跌,而系统本身并不会报错。这就是AI工程和普通后端的本质区别:你要维护的不是一段代码,而是一整套“数据 + 模型 + 运行环境”的耦合体。

从零开始的AI工程,核心目标就是让你具备驾驭这套耦合体的能力。具体拆开来看,需要覆盖四块核心内容:

  • 数据工程的底座能力:知道拿到的原始数据如何清洗、如何标注、如何做版本管理,明白训练数据和上线后的数据为什么不能有太大差异
  • 模型训练与评估的工程化:不再用Notebook里那套“跑通了就行”的野路子,而是搭出可复现、可追踪、可回滚的训练流程
  • 部署与交付链路:搞懂在线推理、离线批处理、A/B实验,以及模型上线后到底怎么监控它的健康状态
  • 持续迭代与维护的手段:模型会老化、数据会漂移,没有反馈闭环的AI系统撑不过三个月

我要特别说明的是,这套体系不只属于大厂。一个人数不到三十的小团队、一个做垂直行业SaaS的公司,同样能用这套方法论把自己的AI能力建立起来。这也是我在“ai-engineering-from-scratch”这个项目里反复强调的一个立场:小团队更需要工程化,因为小团队没有人力去反复救火。

2. 核心技术与工具选型解析

2.1 AI工程的基本技术栈分层

很多刚入行的朋友喜欢一上来就看榜单,哪个模型分高就玩哪个,哪个框架热就学哪个。这种学习方式不能说完全错,但效率确实很低。因为它忽略了工程落地里最关键的“分层结构”思维。

我习惯把AI工程的技术栈切成四层:

第一层是基础组件层,包括Python、SQL、Linux操作、Docker基本功。这一层相当于盖楼用的砖和水泥,不牢固的话,后面每一层都会抖。有个细节可能很多人没意识到:熟练的SQL能力在AI项目里比“会写PyTorch”更重要,因为绝大多数AI项目的时间其实花在数据取用和预处理上。

第二层是数据与特征层,包括数据管道(ETL)、数据质量检测、特征工程、数据版本管理。这一层在“学习路线图”里通常被跳过去,但我在项目实践中发现,它对最终模型效果的影响能占到六成以上。数据里藏着脏数据、重复样本、标签噪声,每一个都会让模型表现“看起来还行,实际线上崩”。

第三层是模型训练与调优层,包括模型架构选择、超参数调优、训练追踪、实验管理。注意我这里把“训练追踪”和“实验管理”放得和模型算法本身同等重要,因为在团队协作的场景里,连“哪个实验对应哪份数据和哪份参数”都说不清楚的项目,复盘时就是一笔糊涂账。

第四层是生产交付层,包括模型部署、在线推理服务、性能监控、模型回退、CI/CD流水线。这一层是“AI工程”区别于“AI研究”的分水岭。很多学算法出身的人卡在这一层,不是因为难,而是因为压根没有这个概念框架。

理解了这个分层,你再回来看市面上各种课程和框架,就不会晕头转向了。选工具之前先定位自己的需求落在哪一层,再去比较同一层里的具体方案,思路会清晰很多。

2.2 工具选择的三个关键视角

工具选型是个常聊常新的话题。今天前端框架都换了好几代了,AI领域的工具迭代更是快。但万变不离其宗,不论工具怎么出新的,你在选型时只要盯住三个视角就不会跑偏。

第一个视角是数据流的可追溯性。你的数据从原始文件变成训练样本,中间经历了哪些SQL运算、哪些清洗脚本、哪些特征编码?如果这些逻辑散落在各种临时脚本里,没有统一的编排和记录,后期排查问题会让你生不如死。所以我建议工具选型的第一优先级先看“谁在管理数据血缘”,而不是“谁训练模型更快”。

第二个视角是实验的可复现性。你周一跑出来的那个85%准确率的模型,到了周五还能原样复现吗?版本、种子、数据切片、依赖库版本,只要漏了任何一个,复现就会失败。这个维度上,MLflow这类实验追踪工具几乎成了标配,后面我会专门讲。

第三个视角是运行环境的可移植性。本地能跑通不代表服务器能跑通,CPU跑得通的模型不一定能在GPU上复现相同逻辑。Docker和Kubernetes这类容器化方案,就是把环境差异的锅从根上扔掉。哪怕是个人项目,我也建议至少用Docker把环境锁死,避免“我机器上明明能跑啊”这种经典对话反复发生。

工具选型本质上不是技术洁癖,而是风险控制。这三个视角对应了工程里最常翻车的三个风险点:数据不可追溯、实验不可复现、环境不可迁移。把它们控制住了,你的AI系统就好比在安全屋里干活。

2.3 推荐的技术栈组合(个人亲测)

我知道光说原则还不够,直接给一套我用了很久、在多个项目里验证过的组合会更实在。这套组合覆盖上面说的四层,而且尽量选了社区活跃、资料多、上手成本低的方案。

层级推荐工具选型理由
容器与运行环境Docker + Docker Compose环境一致性、本地/云端迁移无痛
数据管道Airflow(轻量用Prefect)可视化调度、重试机制、任务依赖直观
数据版本管理DVC对Git友好、学习曲线平缓、支持大文件存储
实验追踪MLflow追踪实验参数/指标/产物,自带模型注册中心
特征工程(可选)Feast统一线上/线下特征口径,避免训练/推理不一致
模型训练PyTorch / Scikit-learnPyTorch灵活,适合研究到生产;Sklearn适合快速基线
部署与服务FastAPI + ONNX Runtime / TritonFastAPI轻量,ONNX加速跨平台推理
监控与告警Prometheus + Grafana云原生可观测性的事实标准,配合自定义指标

这套组合的实用之处在于:既有“重”组件(Airflow、MLflow),也有“轻”方案(FastAPI、DVC),能根据团队体量和项目阶段灵活裁剪。对个人学习和极小型项目,你完全可以砍掉Airflow,用Prefect甚至纯Cron加Shell脚本先跑起来。工具是辅助,思路才是主菜。

3. 从零到一:可复制的落地实操流程

3.1 第一步:搭好开发环境和项目骨架

好,接下来进入硬核实操环节。我以“从零开始搭建一个可落地的AI工程项目”为例,演示完整流程。这个项目我选择的是一个经典场景:商品评论情感分类。原因很简单:任务本身不复杂,数据获取容易(公开数据集一大堆),但又能完整覆盖数据处理、模型训练、推理部署、监控反馈的所有环节,非常适合拿来讲透AI工程链条。

项目目录我建议这样组织:

. ├── configs/ # 配置文件,含模型参数、路径、训练超参 ├── data/ │ ├── raw/ # 原始数据(不可变) │ ├── processed/ # 清洗后数据 │ └── versions/ # DVC数据版本目录 ├── src/ │ ├── data_processing/ # 清洗、转换、特征工程 │ ├── training/ # 模型训练、评估、实验记录 │ ├── deployment/ # 推理服务、API接口 │ └── monitoring/ # 指标采集、告警逻辑 ├── models/ # 模型产物存储 ├── tests/ # 单元测试与数据校验测试 ├── docker-compose.yml ├── .env.example └── requirements.txt

这个目录结构不算复杂,但它严格区分了“数据”“代码”“模型”“配置”四个核心对象,这是AI工程与普通Web工程最容易忽视的差异。你写Web接口时,配置和数据通常没那么要紧;但AI模型产出的本质是“参数 + 结构 + 代码环境”的绑定体,缺一个都跑不起来。

创建虚拟环境和基础依赖:

python -m venv .venv source .venv/bin/activate pip install mlflow dvc pandas numpy scikit-learn fastapi uvicorn pydantic

这里我先把核心依赖装上。DVC用于数据版本控制,MLflow做实验追踪,FastAPI提供推理服务接口。这些工具在后续流程中各有明确用途,装完不是摆设。

3.2 第二步:数据获取与版本化

我先准备一份公开的IMDb影评数据集(或者任何你自己标注过的分好类的文本数据),按照data/raw/目录放好。这一步看似简单,但有个重要的工程决策:从原始数据进入项目的那一刻,就要做版本管理。

运行DVC初始化:

dvc init dvc add data/raw/imdb_reviews.csv git add data/raw/imdb_reviews.csv.dvc .dvc/config git commit -m "add raw imdb reviews dataset"

这里有个容易被新手忽略的细节:dvc add不会把真实数据文件提交到Git,它只是生成一个指针文件。数据本身可能存到本地磁盘、S3或者MinIO等远程存储里,Git仓库记录的是“这份数据文件的元信息和哈希指纹”。这种设计的好处是,Git仓库永远保持轻量,且数据内容严格不可变、可溯源——哪天你想回头复现“上周一用90%训练数据跑出的模型”,一条dvc checkout命令就能把当时那份数据完整拉回来。

数据清洗阶段,我用Pandas做基础处理,包括去重、去空值、标签统一、文本长度过滤。这一步除了写代码,我还建议增加一条“数据校验”流程,比如断言“不存在空标签”“样本数不少于X”“正负样本比例不超出阈值”。这也是我从线上事故里学到的教训:数据管道静默地跑,常常会把脏数据无声带进训练集,等你发现时模型已经学歪了。

# src/data_processing/validate.py import pandas as pd def validate_data(df: pd.DataFrame) -> bool: assert len(df) > 10000, "样本量太少,不具备训练意义" assert df["label"].isin([0, 1]).all(), "标签取值异常" assert df["text"].map(len).min() > 10, "存在过短无效文本" return True df = pd.read_csv("data/processed/train.csv") if not validate_data(df): raise ValueError("数据校验未通过,阻止进入训练")

这段校验代码虽然简单,但能挡掉不少低级错误。我见过太多团队在拼命调模型参数,结果发现训练数据里有大段空字符串、标签错位的问题——浪费几天时间,还不自知。

3.3 第三步:训练代码结构设计与实验追踪

接下来是最核心的模型训练工程化环节。为了演示,我选择用逻辑回归加TF-IDF特征作为基线模型,之后可以无缝切换到BERT这类深度学习模型。用逻辑回归做开局不是因为技术上老旧,而是因为工程链路可以完整跑通,且速度极快,方便验证“管道是否畅通”。先把烂流程跑通,再谈好模型,这是我一贯的原则。

训练脚本的入口设计:

# src/training/train.py import mlflow import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.metrics import accuracy_score, f1_score mlflow.set_tracking_uri("http://localhost:5000") mlflow.set_experiment("sentiment_analysis") with mlflow.start_run(run_name="lr_tfidf_baseline"): train_df = pd.read_csv("data/processed/train.csv") X_train = train_df["text"] y_train = train_df["label"] vec = TfidfVectorizer(max_features=5000, ngram_range=(1, 2)) clf = LogisticRegression(C=1.0, max_iter=1000) pipeline = Pipeline([("vec", vec), ("clf", clf)]) pipeline.fit(X_train, y_train) val_df = pd.read_csv("data/processed/val.csv") X_val = val_df["text"] y_val = val_df["label"] y_pred = pipeline.predict(X_val) acc = accuracy_score(y_val, y_pred) f1 = f1_score(y_val, y_pred) mlflow.log_params({"max_features": 5000, "ngram_range": "1,2", "C": 1.0}) mlflow.log_metrics({"accuracy": acc, "f1_score": f1}) mlflow.sklearn.log_model(pipeline, artifact_path="model", registered_model_name="SentimentLR")

你会发现这个训练脚本里面几乎没有任何“特殊”的算法操作,无非是常规的数据读取、模型拟合和指标计算。但正因为多了MLflow的追踪封装,每一次运行都会留下:参数是什么、指标是什么、模型产物在哪个位置、用了哪份数据。当实验次数积累到几十上百次之后,这个记录就是你唯一的对比依据。没有追踪的训练,都是没有记忆的漂流瓶。

超参数优化阶段,我建议用Sklearn的GridSearchCV结合MLflow的嵌套记录,把每次组合对应的结果都存下来,而不是只记最优结果。原因在于,最优值可能受数据影响而偶然偏高,你周围的环境一变(数据增加、业务口径调整),未必还是最优。把所有实验过程记录下来,相当于给自己留了一条可回溯的决策路径,下次调整时有据可依。

3.4 第四步:模型部署与推理服务

模型训练完成只是第一步。接下来的部署环节,才是我认为最能区分“会AI”和“会AI工程”的分水岭。

我用FastAPI实现了一个轻量级在线推理服务。它接收一段文本,返回情感分类标签和置信度。注意在服务内部,文本特征化和模型预测必须走同一套预处理逻辑,这个一致性是部署环节最大的隐形陷阱。为了确保线上和训练时的特征处理口径一致,最稳妥的办法是把特征器放进同一个Pipeline里整体保存,不要分开保存再在服务端重建。

# src/deployment/app.py from fastapi import FastAPI from pydantic import BaseModel import mlflow import numpy as np app = FastAPI() model = mlflow.sklearn.load_model("models:/SentimentLR/latest") class TextInput(BaseModel): text: str class Prediction(BaseModel): label: int confidence: float @app.post("/predict", response_model=Prediction) def predict(input: TextInput): prob = model.predict_proba([input.text])[0] label = int(np.argmax(prob)) confidence = float(np.max(prob)) return Prediction(label=label, confidence=confidence)

写完代码后,用Docker把服务封装:

FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY src/ ./src/ CMD ["uvicorn", "src.deployment.app:app", "--host", "0.0.0.0", "--port", "8000"]

然后用Docker Compose把服务、MLflow追踪服务、PostgreSQL(存MLflow元数据)一起编排起来:

version: "3.8" services: mlflow: image: ghcr.io/mlflow/mlflow:v2.3.0 ports: - "5000:5000" command: mlflow server --backend-store-uri postgresql://mlflow:mlflow@db/mlflow --default-artifact-root ./mlruns db: image: postgres:14 environment: POSTGRES_USER: mlflow POSTGRES_PASSWORD: mlflow POSTGRES_DB: mlflow api: build: . ports: - "8000:8000" depends_on: - mlflow

容器化部署的核心价值在于:把模型的运行环境固化成镜像,换机器、加副本、弹性伸缩时都不会因为环境差异而行为失常。这一步做得好,后续的监控、A/B测试、蓝绿发布都有了可靠的底座。

3.5 第五步:监控、告警与持续迭代闭环

模型上线后,还有一件很多人没有意识到的事:模型上线的那一天,才是工作真正开始的那一天。传统软件只要逻辑不变就不会“变旧”,但模型的性能会随时间推移而衰减,因为数据分布会漂移。这就是AI工程特有的“模型老化”问题。

我建议至少监控三类指标:

  • 业务指标:比如准确率、F1、用户满意度,这是最终要看的
  • 输入分布指标:比如文本长度均值、词表覆盖、类别概率分布,这类指标能提前暴露“数据漂移”
  • 基础设施指标:如推理延迟、P95响应时间、CPU/内存占用、错误率
# src/monitoring/metrics.py from prometheus_client import Counter, Histogram, Gauge PREDICTION_COUNTER = Counter("predictions_total", "Total prediction requests") PREDICTION_LATENCY = Histogram("prediction_latency_seconds", "Prediction latency") INPUT_TEXT_LENGTH = Gauge("input_text_length", "Length of input text") # 修改app代码,在predict函数中记录指标

Prometheus配上Grafana,就是一个非常成熟的可观测性组合。更进一步的,你还可以接入“模型性能看板”,把数据漂移检测指标(如PSI,群体稳定性指数)画成趋势线,当PSI超过告警阈值时自动触发重新训练提醒。这样你就不用来回问“模型现在还行不行了”,系统会自动告诉你“训练分布开始偏移了,该考虑补充数据了”。

4. 常见问题与避坑指南

4.1 新手最常见的五个工程问题

我总结这些年在带人过程中最常见的问题,做个速查表。很多问题看起来截然不同,根子上都是同一个毛病:没有把AI项目当系统工程来做。

问题现象根因解决方案
模型在验证集精度很高,上线后效果断崖下跌训练/推理数据分布不一致;特征处理口径分裂统一Pipeline保存;上线前做数据分布对比
“换个随机种子结果就不一样”训练过程未锁定随机性,数据shuffle顺序不一致固定随机种子;对数据顺序做确定性控制
服务器上复现不了本地结果依赖库版本不一致;硬件差异导致浮点数行为不同使用Docker锁定环境;尽量统一GPU型号
实验跑了一堆,复盘时不知道哪个对应哪个缺乏实验追踪,参数和结果散落各处立刻接入MLflow或同类工具,形成规范习惯
数据管道半夜挂了,第二天才发现缺少自动化告警和数据质量监控用Airflow/Prefect做任务编排,配置失败重试与告警

这里有两条建议可以说价值千金。第一条是:从第一个实验开始,就不要跑“裸代码”,哪怕只是手动把参数和指标存到一个文本文件也好,先养成记录习惯。第二条是:不要在Notebook里完成“最后一步的训练”,Notebook适合分析和探索,不适合作为训练产物的正式源头,因为它的执行顺序不可控,很容易产生“上一个细胞跑过才有变量、新环境跑不了”这种问题。

4.2 团队协作中的AI工程规范

如果你不是孤军奋战,而是有三五个人一起做项目,那AI工程规范会直接决定协作效率。

我强烈建议用Git分支管理实验代码,DVC管理数据和模型版本,二者组合正好形成一个完整闭环。常规做法是:

  1. 每个实验或者每个功能开一个分支;
  2. 分支上改代码或调整参数,用Git提交代码快照;
  3. 用DVC记录本次实验对应的数据快照和模型产物快照;
  4. 合并代码前,必须确保训练脚本能在干净环境里跑通,并且产出的模型能加载进推理服务。

这套组合拳打通后,就等于给团队了一套完整的“时空穿梭机”:任何一个实验,都可以精确回溯到当时的代码、当时的参数、当时的数据和当时的模型。我带过的团队里,凡是坚持这么做的,基本没有因为“搞不清上次怎么跑出这个数”而争吵过。

另外一个细节是环境依赖管理。不要用pip freeze > requirements.txt这种偷懒方式,它会把我们这台机器的所有包全部导出,连无关的包都会进去。建议用pip-tools或者poetry这类工具,生成精确但有层次的依赖文件,至少区分requirements/base.txt和requirements/dev.txt,避免测试环境装出一堆用不上的包。这个习惯在长期维护的项目里,能省下无数个“为什么我环境装不上”的下午。

4.3 我的复盘与一些小建议

踩了这么多年的坑,我越来越觉得,做AI工程最需要的不是聪明,而是耐心和系统性。

有一件事我反复和团队强调:先把“脏活累活”(数据管道、实验追踪、监控、部署链路)建设好,再谈模型技巧。很多新手一进来就想搞最潮的模型架构,这是方向性的错误。在工程链路不健全的项目里,哪怕你把模型精度从80%调到82%,模型上线后的表现也只会是玄学——恶化的可能比变好还大。反之,链路健全之后,哪怕你的模型只是最简单的逻辑回归,它可以通过稳定迭代一点一点优化,最终也能达到可观的效果。

我还特别喜欢用“盖房子”来做类比。模型结构是装修风格,工程体系是钢筋水泥。你会因为装修风格好看就忽略承重墙吗?在AI项目里,很多人恰恰就是这么干的。他们全身心扑在“用哪个SOTA模型”,却忽视了数据漂移检测、实验可追溯、A/B测试这些真正的承重墙。等到业务波动、数据漂移、模型崩溃的时候,再回头补工程,代价至少翻三倍。

从我个人的实操体会来说,坚持把工程基础做扎实,带来的收益是越滚越大的。最开始你会觉得麻烦,为什么要记录实验参数?为什么要做数据校验?为什么要写Dockerfile?但当你半年后需要重构一个旧模型、回答老板“为什么线上越来越不准”、或者把一个同事用过的东西交接给另一个同事时,你会发现,所有当初嫌麻烦的步骤,其实都是在为未来的自己省钱。这条路我已经反复验证过很多次,也建议你们在下一个AI项目里,真的试试。

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

ESP32-P4NRW32X核心板实战:无无线RISC-V主控的算力与内存优势

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

作者头像 李华
网站建设 2026/10/1 1:06:59

Vue + AntV G6 + Element Plus 构建字段血缘关系图实战

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

作者头像 李华
网站建设 2026/10/1 1:06:05

电机控制必知:IIR数字滤波器在FOC与高频注入中的应用

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

作者头像 李华
网站建设 2026/10/1 1:05:47

行人重识别数据集全解析:选型、使用与自制方法

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

作者头像 李华
网站建设 2026/10/1 1:04:49

SSA-BP+NSGAII:麻雀搜索优化BP超参数与Pareto前沿

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

作者头像 李华
网站建设 2026/10/1 1:04:49

UE5游戏崩溃排查全攻略:日志定位与CVar优化实战

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

作者头像 李华