拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零构建AI工程:基于PyTorch手写Transformer到推理模型全流程

从零构建AI工程:基于PyTorch手写Transformer到推理模型全流程

“ai-engineering-from-scratch”这个标题,我盯着看了很久。它既不是一个具体项目的名字,也不是某篇论文的标题,更像是一整条学习路线的代号:从零开始,自己动手构建一套AI工程能力——不做调包侠,不靠黑盒API,把数据、模型、训练、评估、部署这条路亲手走一遍。这几年大模型火起来之后,很多人直接站在“应用层”往上看,会用别人的接口就算会AI了,但真正遇到问题的时候无从下手。从一个从业者的视角看,从零构建这件事,不只是补基础,更是建立“我能掌控它”的底气。

这篇文章适合两类人:一类是有一定编程基础、想从应用开发转向模型训练和AI工程方向的人;另一类是已经在用大模型API做产品,但总觉得底层是个黑盒、想拆开看一看到底发生了什么的人。我会按自己实际走过一遍的路线来拆解,把从零训练模型、微调、构建推理模型这条路上绕不开的环节、踩过的坑、值得借鉴的思路都讲清楚。文章篇幅会比较长,建议收藏后分段阅读。

1. 从零构建AI工程:先看懂整张路线图

1.1 这里的“从零”到底指什么

很多人一听到“from scratch”,第一反应是“我要自己写一个Transformer”,或者“我要复现GPT-4”。如果抱着这个目标,大概率一个月后就放弃了。从我自己的经验来看,“从零”正确的理解方式应该是:不依赖现成的预训练模型和AutoML平台,亲手完成数据准备、模型搭建、训练、调优、评估这一整套流程,每一步都知道它在做什么、为什么这么做。

这就好比学做饭。有人说“从零开始学做菜”,你不是要从种水稻开始,而是要学会自己买菜、洗菜、切菜、掌握火候,最终能做出一桌像样的菜。你能分清生抽和老抽的区别,知道什么时候该爆锅,什么时候该小火慢炖——这才是“从零”真正的意义。至于能不能复现米其林餐厅的招牌菜,那是后话。

所以我在规划这条学习路线的时候,给自己定了一个很务实的目标:不用任何第三方封装好的大模型库,用PyTorch从张量运算层面写出一个可训练的语言模型,在合理的硬件条件下让它跑通训练流程,产出能用的结果。然后再往推理方向延伸,让模型不仅会“续写”,还会“思考”。

1.2 技术栈选型:Python、PyTorch、CUDA与硬件

技术栈的选择直接决定你在这条路上能走多远。我推荐的组合很固定:Python + PyTorch + HuggingFace生态工具 + 一张至少8GB显存的NVIDIA显卡。为什么是PyTorch而不是TensorFlow或JAX?因为PyTorch的调试体验是目前几个主流框架里最好的,动态计算图意味着你可以在训练循环里随意打印张量、打断流程做检查,这对新手期特别重要。

Python这边没什么好说的,AI工程的主战场语言。但我要强调一点:不要在“学Python语法”上花太多时间,直接在项目里学。你只需要掌握张量操作、类、循环、条件判断、文件读写这些基础,然后马上下水。我见过太多人把《Python从入门到精通》翻了一遍又一遍,结果连一个Dataloader都没写过,这属于本末倒置。

硬件方面,如果你打算从零训练一个小型语言模型(比如千万到亿级参数),8GB显存勉强够用,但会非常痛苦。16GB以上的显存会让你舒服很多,24GB(比如RTX 3090/4090)基本可以覆盖大多数实验场景。没有N卡的话,可以考虑云GPU实例,按小时租用,前期实验完全够用。需要注意,苹果的M系列芯片虽然能跑PyTorch的MPS后端,但很多CUDA生态的优化特性用不上,踩坑成本会高不少。

1.3 两条典型的“从零”路线:经典机器学习与大语言模型

规划路线的时候,你会发现自己面前有两条路。一条是经典机器学习路线:线回归、逻辑回归、决策树、SVM、梯度提升树,然后延伸到基础神经网络。另一条是大语言模型路线:直接上手Transformer、注意力机制、预训练、微调、对齐。

我的建议是:两条路都要走,但顺序有讲究。先从经典机器学习入手,不需要学得很深,重点理解梯度下降、过拟合、交叉验证这几个核心概念,因为它们在不同模型间是通用的。然后直接跳到语言模型路线,把Transformer的每一个细节吃透。很多人想跳过第一步直接学大模型,结果连“学习率”为什么要调整都搞不明白,更别提理解训练曲线了。

这两条路线我用一张脑图来整理过,简单说就是:经典路线负责建立“直觉”,LLM路线负责建立“体系”。直觉让你面对一个陌生问题时知道往哪个方向找答案;体系让你知道答案的每一层是怎么拼起来的。只有直觉没体系,你是民间高手;只有体系没直觉,你是理论复读机。从零构建的核心,就是两条腿走路,缺一不可。

2. 核心原理拆解:训练一个模型绕不开的四个环节

2.1 数据工程的起点:文本清洗与分词器

很多人一想到训练语言模型,脑子里浮现的全是“注意力机制”“Transformer”这些高深名词。但真正动手做的时候你会发现,第一个拦住你的不是模型架构,而是数据。模型吃进去的是数字,不是文字。把文本变成数字的过程,就是分词器(Tokenizer)的工作。

分词器的作用是把一段文本切分成子词单元,再映射成ID序列。“我爱AI”这样一句话,可能会被切成“我”、“爱”、“AI”三个token,也可能被切成“我爱”、“AI”两个token,取决于分词器的词表设计。常用的BPE(Byte Pair Encoding)算法会从字符级开始,反复合并出现频率最高的相邻字符对,最终得到一份词表。这个方案的好处是,它既不会像整词分词那样遇到生词就抓瞎,也不会像纯字符分词那样让序列长到无法训练。

从零训练分词器的时候,需要重点考虑词表大小。词表太小,句子会被切得很碎,序列变长、计算量变大;词表太大,Embedding矩阵参数量暴涨,小数据集根本学不好。我个人的经验是,中英文混合场景下,词表大小设在16000到32000之间比较合适。另外,分词器的训练语料要和训练语料保持一致,否则会出现大量token被切成碎片的情况。

数据清洗也值得多说两句。网上爬来的原始文本里,HTML标签、URL、乱码符号、重复字符、敏感信息,全都要处理。我踩过最大的坑是语料里去重没做干净,模型在训练时会“背诵”大段重复文本,生成的内容就像复读机。清洗流程至少要包含:统一编码、去HTML、去URL、去重、按质量过滤、长度过滤这几步。每一步都会直接影响最终效果,别偷懒。

2.2 模型架构:从零手写Transformer核心组件

如果只学一个模型架构,那就是Transformer。从零构建AI工程,核心中的核心就是亲手实现一个Transformer编码器或解码器。不要先用现成的nn.Transformer,先自己写一遍多头注意力层、前馈网络、残差连接和层归一化,再回头看现成实现会轻松很多。

多头注意力机制是Transformer的灵魂。它做的事情用一句话概括:让序列中的每个位置都能“看到”其他位置,并根据相关性加权整合信息。拆开来看,输入向量通过三个不同的权重矩阵映射成Q(查询)、K(键)、V(值),然后计算Q与K的点积获得注意力分数,经过Softmax归一化后加权求和V。多头的意思是做多次这种映射,每次学习不同的关系子空间,最后拼在一起再过一层线性层。

从工程角度看,需要注意的点包括:注意力分数通常要除以sqrt(d_k)做缩放,否则点积结果过大会把Softmax推到梯度饱和区;因果语言模型(Causal LM)还需要做mask,让每个位置只能看到当前位置之前的信息,预测下一个token。这些细节不亲手写一遍代码,很容易在使用现成库的时候忽视掉。

训练一个语言模型的时候,输入和输出之间的关系很微妙。输入是一串token ID,比如“我 爱 学 习”,输出是这串token往右平移一位的“爱 学 习 ”。模型要做的事情是:给定“我爱学”,预测下一个位置最可能的token是“习”。这个设计就是自回归语言模型的基本范式,ChatGPT系列的大模型都是这样训练的。

2.3 训练循环:前向传播、反向传播与梯度更新

核心训练循环其实并不复杂,多年来基本没变过。循环里包含四步:取一个batch的数据、计算损失、反向传播得到梯度、更新参数。但是真要从零实现,里面的细节多到你头皮发麻。

前向传播阶段,数据经过Embedding层变成向量,然后层层通过Transformer Block,最后一层线性层把隐状态映射到词表大小的logits。接下来计算损失函数,常用的是交叉熵损失,但需要做一点调整:因为每个序列都有seq_len个位置,每个位置都对应一次预测,所以要把[batch, seq_len, vocab_size]的logits和[batch, seq_len]的labels对齐,只对真实存在的token位置计算损失,填充位(padding)要mask掉,不参与梯度计算。

反向传播阶段,PyTorch的自动求导机制会替你完成大部分脏活累活,但你需要知道哪些张量需要梯度、哪些不需要。比如Embedding层的参数需要梯度,而padding mask生成的布尔矩阵就只是普通张量。loss.backward()执行完,梯度就累积在每个requires_grad=True的叶子张量上了。

参数更新阶段,优化器的选择会对训练产生直接影响。AdamW是目前训练大模型的主流选择,它把权重衰减从L2正则化中解耦出来,对Transformer这类架构更友好。学习率通常配合warmup策略一起使用:前几千步从0线性增长到峰值,然后按余弦曲线衰减。至于峰值的取值,小模型可以用1e-3到3e-4之间,大模型要降到1e-4甚至更低,具体要靠实验观察loss曲线来定。

2.4 损失函数与优化器:交叉熵、AdamW与学习率策略

交叉熵损失在语言模型里通用得几乎不需要思考,但我还是建议你亲手实现一次并对比一下PyTorch内置版本的结果。实现的细节在于logits要先经过LogSoftmax,再和真实标签计算负对数似然。你可能会想:“这有什么好对比的?”实际做一次之后,你会彻底理解为什么PyTorch的CrossEntropyLoss底层把LogSoftmax和NLLLoss合并在一起了——为了数值稳定性。

优化器方面,AdamW和Adam的区别值得认真理解。Adam自带L2正则化的方式会让带有大梯度的参数受到更强的正则化,这在某些情况下会让泛化变差。权重衰减的初衷是对所有参数施加相同的衰减比例,让它只与参数本身的数值相关,而AdamW就是修正这个问题的版本。实践中用AdamW替换Adam通常能带来更稳定的训练。

学习率策略对新手来说是最难调的部分之一。我见过不少朋友刚开始训练,loss居高不下,第一反应是“模型有问题”,结果只是学习率太高导致震荡,或者没有warmup导致早期梯度爆炸。从零构建的早期阶段,建议固定住一个标准配置:AdamW,学习率3e-4,warmup步数占总步数的5%到10%,然后观察loss曲线,一次只改一个变量。这个习惯能帮你省掉大量无效实验。

3. 实操记录:从零训练一个ChatGPT风格小模型

3.1 环境搭建与项目结构规划

动手之前先把环境理清楚。我用的是Python 3.10 + PyTorch 2.x + CUDA 11.8的组合,项目根目录下划分成data/、tokenizer/、model/、train.py、config.py这五块。很多初学者喜欢把所有代码塞进一个文件里,实验一多就完全失控。从零训练模型本身已经够难了,别让工程结构拖后腿。

配置管理的优先级容易被低估。训练一个模型涉及到的参数非常多:词表大小、嵌入维度、层数、头数、学习率、batch大小、梯度累积步数、最大序列长度、训练步数、warmup步数、随机种子……如果这些参数散落在代码各处,复现实验会变成一场灾难。我用一个config.py把全部参数集中起来,每个实验复制一份配置并命名好版本号。别嫌麻烦,你一定会为一个“昨天还正常今天就不收敛”的模型找半天原因,这时候配置归档能救你的命。

环境层面,如果你用的是云GPU,记得提前测一下CUDA版本和PyTorch的编译版本是否匹配,torch.cuda.is_available()返回False是新手最容易撞上的墙。本地RTX系列的话,直接用官方推荐的pip安装命令即可。另外,固定随机种子(torch.manual_seed)很重要,否则你每次训练结果都不一样,无法判断改动到底是好是坏。

3.2 数据准备:从原始文本到Tensor数据集

数据准备这一步,我以训练一个中文对话小模型为例。原始数据来源可以是开源的中文语料,比如维基百科的中文dump、社区问答数据等。从零构建的阶段,数据量不需要太大,几千万token就够了,重点是流程要完整。拿到原始文本后,先做清洗和过滤,然后按段落切分成训练样本,再用自己训练好的分词器把每段文本编码成token序列。

编码完成之后,我需要把样本组装成固定长度的序列。我采用的方式是:把全部token拼成一个超长的一维数组,然后按max_seq_len + 1切块,每块的前max_seq_len个token作为输入,后移一位的max_seq_len个token作为标签。这样做的好处是避免逐条padding带来的算力浪费,也让模型在训练时看到更连贯的长文本上下文。

数据装载方面,直接使用PyTorch的Dataset和DataLoader,设置好batch_size和shuffle。有一个容易忽略的细节:DataLoader的num_workers可以根据CPU核数调大,默认值0意味着主进程处理数据,会成为训练瓶颈。GPU训练的时候,数据加载往往才是那个隐性瓶颈。

# 数据切块示例:把长文本切成固定长度的训练样本 import torch from torch.utils.data import Dataset class TextDataset(Dataset): def __init__(self, tokens, seq_len): # tokens: 完整的编码后token ID列表 self.seq_len = seq_len total = (len(tokens) - 1) // seq_len self.samples = [] for i in range(total): start = i * seq_len x = tokens[start:start + seq_len] y = tokens[start + 1:start + seq_len + 1] self.samples.append((torch.tensor(x), torch.tensor(y))) def __len__(self): return len(self.samples) def __getitem__(self, idx): return self.samples[idx]

3.3 训练脚本:一个迷你但完整的PyTorch训练循环

有了数据之后,就到了最核心的部分:训练循环。我直接给出一个精简版本,这个结构你以后做任何模型训练都能复用。需要说明的是,为了可读性我简化了部分细节,实际工程中你还需要加入梯度累积、EMA、早停、checkpoint保存等逻辑。

模型定义方面,我建议直接用PyTorch的nn.TransformerEncoder或者从HuggingFace借用GPT2LMHeadModel的小配置先跑通流程,然后再自己从零写一遍核心层。为什么这么建议?因为如果一开始就完全手写,出bug的概率太大了,你不知道到底是“数据错了”还是“模型写错了”还是“训练逻辑错了”。先跑通,再重构,这是工程思维,不是走捷径。

import torch import torch.nn as nn from torch.utils.data import DataLoader from torch.optim import AdamW def train_step(model, batch_x, batch_y, optimizer, criterion): model.train() optimizer.zero_grad() logits = model(batch_x) # [batch, seq_len, vocab_size] loss = criterion(logits.view(-1, logits.size(-1)), batch_y.view(-1)) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() return loss.item() model = GPTConfig.build_model() optimizer = AdamW(model.parameters(), lr=3e-4) criterion = nn.CrossEntropyLoss(ignore_index=-100) for step, (batch_x, batch_y) in enumerate(train_loader): batch_x, batch_y = batch_x.to(device), batch_y.to(device) loss = train_step(model, batch_x, batch_y, optimizer, criterion) if step % 500 == 0: print(f"step {step}, loss {loss:.4f}")

梯度裁剪这行代码经常被忽略,但对语言模型训练来说,它是稳定训练的关键。Transformer的训练早期,梯度范数可能突然飙升到几百甚至上千,不做裁剪的话一个step就能把参数打飞到loss变成NaN。我把max_norm设为1.0,这是一个很保守但稳妥的起点。另外,CrossEntropyLoss里我用ignore_index=-100来屏蔽padding位的损失,这是处理变长序列的通用技巧。

3.4 评估与生成:训练完不等于能用

训练循环跑完之后,模型loss降到了一个相对平稳的值,但这只能说明“模型记住了训练数据里的统计规律”。要判断模型好不好,最直接的办法是让它生成文本,亲眼看一看。

生成文本的流程很简单:给模型一个起始token序列,让它预测下一个token,然后把这个token拼到输入末尾,继续预测下一个,循环往复。这个自回归生成过程就是ChatGPT回复你的基本机制。需要注意的是,每步直接选概率最大的token(贪心搜索)会让生成结果陷入重复,你可以试一下在采样时引入随机性,比如temperature和top-p。

def generate(model, tokenizer, prompt, max_new_tokens=100, temperature=0.8, top_p=0.9): model.eval() ids = tokenizer.encode(prompt) for _ in range(max_new_tokens): input_ids = torch.tensor(ids).unsqueeze(0).to(device) with torch.no_grad(): logits = model(input_ids)[0, -1, :] / temperature probs = torch.softmax(logits, dim=-1) sorted_probs, sorted_idx = torch.sort(probs, descending=True) cumsum = torch.cumsum(sorted_probs, dim=-1) cutoff = (cumsum > top_p).nonzero()[0].item() sorted_probs[cutoff:] = 0 sorted_probs /= sorted_probs.sum() next_id = sorted_idx[torch.multinomial(sorted_probs, 1)].item() ids.append(next_id) if next_id == tokenizer.eos_id(): break return tokenizer.decode(ids)

temperature的作用是调节概率分布的“锐利程度”:值越低,越倾向于选高概率token,输出更保守;值越高,随机性越强,输出更多样但可能胡言乱语。top-p则是只在累积概率达到p的候选集中采样,避免从概率极低的垃圾token中抽样。这是工程上最常用的两个采样参数,值得反复体会。

4. 从语言模型到推理模型:让模型学会“思考”

4.1 为什么预训练后的模型还不会“思考”

当你把一个小型语言模型训练到loss不错、能生成通顺文本之后,很快会发现一个尴尬的事实:它能造句,但不会算数、不会推理、不会按照指令要求回答问题。比如你问它“小明有3个苹果,吃了1个,还剩几个”,它可能会一本正经地回答“还剩3个”。原因在于预训练的目标只是预测下一个token,它学到的是文本中的统计相关性,而不是因果逻辑。

所谓推理能力(reasoning),是怎么来的?如果你读过大模型技术发展的脉络,会发现思路已经从“让模型记住更多知识”转向了“让模型在推理时多想几步”。OpenAI的o1系列模型展示了一个事实:在回答复杂问题之前,让模型生成一长串内部思维过程,可以显著提升正确率。这就像解数学题,直接写答案容易错,但如果先在草稿纸上一步步推演,正确率就会高很多。

从零构建一个推理模型,核心思路是:先用普通预训练模型做基础,再通过微调和强化学习让模型学会“长思考”。这不再只是“续写文本”的问题,而是“如何在答案前生成有效的思考过程”的问题。

4.2 从零构建推理模型的几种主流路线

目前从零构建一个reasoning model,工程上比较成熟的方案有几种。最直接的路线是做监督微调(SFT):准备一批包含“思维链”(Chain of Thought)的训练数据,让模型学会模仿这种思考格式。训练数据和普通SFT没有本质差别,只是label部分变成了“思考过程+最终答案”。

再往前走一步,就是用强化学习来优化推理过程。这条路线因为DeepSeek-R1的公开而被广泛讨论,核心是GRPO(Group Relative Policy Optimization),一种比PPO更节省显存的强化学习算法。做法是:针对同一道数学题或逻辑题,让当前模型生成多个回答,每个回答算出一个奖励分数,然后以“这组回答内的相对优劣”作为信号,更新模型参数,让平均分更高的回答被强化。

4.3 一个简化的RL训练流程示例

这里我必须先说清楚,完整的GRPO实现涉及策略模型、参考模型、奖励模型、KL散度约束等多个组件,不是一个小工程。但为了让你理解核心逻辑,我给你一个精简的思路:把一次训练迭代拆成三步。

第一步,采样阶段:给定一组prompt,让策略模型(当前正在训练的模型)生成N个回答。第二步,评估阶段:用一个规则型奖励函数给每个回答打分。推理任务的最大好处是可以用规则的确定性来评估,比如数学题可以比对最终答案是否正确、格式是否符合要求。第三步,更新阶段:计算这组回答中每个回答的奖励相对值,用策略梯度方法更新模型,注意需要同时约束模型参数不要偏离原始模型太远,防止它为了拿奖励而输出胡乱内容。

# GRPO中计算相对优势值的简化示意 def compute_advantages(rewards, group_size): import torch rewards = torch.tensor(rewards, dtype=torch.float) mean = rewards.mean() std = rewards.std() + 1e-4 advantages = (rewards - mean) / std return advantages

你可能已经注意到,这整条路线和“预测下一个token”的语言模型训练很不一样。它不再只是模仿数据,而是一种“在环境里试错、根据奖励调整行为”的学习方式。从零构建的后期,如果能把这条路线跑通,你基本上就掌握了当前大模型领域最前沿的一整套训练方法。

5. 常见问题与避坑技巧实录

5.1 训练不收敛:先把loss曲线拆开看

训练不收敛是最常见的问题,但它可能是好几种原因叠加造成的。我的排查顺序向来很固定:先看loss是不是完全静止,再看是不是在震荡,然后看是不是NaN。完全静止通常是学习率太小或者模型本身有问题;震荡通常是学习率太大或者batch太小;NaN则复杂得多,可能是数据里有NaN、学习率过高、梯度爆炸、或者层归一化数值不稳定。

一个特别容易被忽略的坑是数据问题。有些文本清洗不干净,里面夹杂着异常字符,经分词器处理后会产生巨大数值。我自己遇到过一种情况:文本编码后长度超过预设max_seq_len,截断后出现了没有任何有效信息的长段padding,导致模型学习效率极低。排查方法是先单独写脚本做数据检查,统计token分布、样本长度分布、空样本数量,确认数据本身没问题再怀疑模型。

5.2 显存不够:梯度累积、混合精度与序列长度取舍

显存不够几乎是每个人都会撞上的墙。最直接的缓解方案是减小batch_size,但batch太小会导致训练不稳定。替代方案是梯度累积:每过几个小batch才做一次参数更新,等效于增大了batch_size,同时控制住显存峰值。我经常用gradient_accumulation_steps=4,让“每4个batch更新一次参数”。

混合精度训练(AMP)是另一个立竿见影的优化手段。PyTorch的torch.cuda.amp可以自动把部分计算变成FP16精度,显存占用直接砍半,同时通过梯度scaler保证训练精度不受影响。在RTX 30系及以上显卡上,AMP几乎是必开的选项。另外,序列长度也不是越长越好,如果显存紧张,可以先从256的序列长度开始训练,验证思路后再逐步拉长。

5.3 数据质量陷阱:模型的“上限”是数据决定的

训练模型的时间长了,你会越来越深刻地体会到一句话:垃圾进,垃圾出。模型的参数只是从数据里提取规律的工具,数据里没有的信息,模型不可能无中生有。我在数据集上栽过最大的跟头是“不知道样本重复率有多高”。网上爬的语料经过简单去重后,我以为干净了,结果训练出来的模型总是复读某些固定段落。后来用MinHash做了一遍去重,才发现数据里存在大量同源改写,重复度远超预期。

数据质量还体现在多样性和分布上。如果你的语料全是技术文档,那模型生成任何内容都会带着技术腔;如果语料里网络用语扎堆,生成内容就会轻浮且缺乏事实性。从零构建实验时,建议先人工浏览几万个样本,建立对数据分布的直觉。这个工作虽然枯燥,但对后续所有训练实验都有指导意义。

5.4 书籍与学习资源:如何系统补充知识

文章最后补一个学习资源板块。很多人会问:“从零开始到底该读什么书?”我的建议是,如果只读一本,选《Build a Large Language Model (From Scratch)》——注意要去官网或正规书店购买正版电子书,不要贪图网盘里的盗版资源。这本书几乎就是冲着“ai-engineering-from-scratch”这个标题写的,从数据准备、注意力机制、预训练、微调一直讲到部署,代码全部配套可跑,适合放在手边边看边敲。

再推荐几篇值得反复读的论文:《Attention Is All You Need》是Transformer的原始论文,必读;DeepSeek-R1的technical report是理解推理模型强化学习路线的最佳材料之一;PPO和GRPO的原始论文用于理解RL训练细节。论文阅读有门槛,但配合实操读一遍,收获会非常大。

我个人在实际操作中的体会是:从零构建AI工程,真正的分水岭不在你是否能跑通代码,而在你是否能回答出每一个“为什么”。代码跑通只是“知其然”,能把训练曲线的每一个波动都解释清楚、能在出问题时快速定位到数据、模型或训练逻辑中的具体环节,这才是“知其所以然”。这种能力没有任何捷径,只能靠一次一次的实验、一次一次的踩坑、一次一次的复盘堆出来。希望这篇内容能帮你少走一些弯路,也欢迎你在评论区聊聊自己从零构建过程中踩过的坑,我们互相学习。

返回列表