ARTICLE DETAIL

资讯详情

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

从零开始AI工程:手写Transformer的完整实践复盘

从零开始AI工程:手写Transformer的完整实践复盘 把标题定为“ai-engineering-from-scratch”的人大约都经历过同一种微妙的不满足感用现成API跑通一个Demo很快但总觉得自己只是个“调度员”把别人封装好的能力拼来拼去至于里面发生了什么、为什么是这个结果、出问题时该往哪查全都是一团黑。我最初就是这个状态。调接口、改提示词、看返回结果事情能推进但一旦模型表现不对劲我连从哪个环节下手都判断不了。后来我把手头所有“能跑的方案”暂时放下从环境搭建、数据构造、训练脚本到部署链路把一条AI应用的完整工程路径亲手走了一遍。这个过程很慢但它把我从“会用工具的人”变成了“能判断工具该怎么做的人”。这篇文章就是我这段“从零开始”之路的完整复盘。它不适合想在一小时内出结果的人更适合愿意花几天时间、把AI工程的底层逻辑摸清楚的人。我会把路线、选型理由、实操步骤、翻车记录统统写出来包括那些文档里不会告诉你的细节。1. 为什么我坚持“从零开始”做AI工程而不是直接API套壳先说实话从零开始不是最高效的交付路径但它是最能建立判断力的路径。我可以用一个很直观的例子说明这件事。大多数人接触AI工程第一步是注册某个大模型平台的账号拿一个Key然后调用接口。这没有错这也是真实项目里最常见的做法。但这里有个隐性问题当你的业务完全构建在别人接口之上时你的技术边界就被锁死了。输入要做成什么样才能稳定触发某个能力模型回答为什么在长文本场景下突然变啰嗦量化之后效果退化到底退在哪一层这些问题在套壳模式下根本没有答案因为你接触不到中间层。我决定“从零开始”时定的原则很简单不调用任何大模型托管API不使用一键训练平台所有环节尽量用开源工具和裸命令手工构建。目标是亲手搭出一条包含数据清洗、分词、建模、训练、评估、部署的完整流水线。有人会问现在这么做是不是有点反效率我的判断是不反效率。从零开始的核心收益不是“自己训一个能打的模型”而是把AI工程拆分成若干个可控的积木块然后亲手拼一次。有了这次经历后面再用任何高级工具、托管服务你都能准确判断它在整个流程里替代了哪一块、引入了什么约束、哪些参数还掌握在自己手里。这个项目的产物也不是一个多么惊艳的模型而是一套我能完全讲清楚每个环节的工程链路。它可能跑得更慢、效果更一般但它是透明的。透明意味着你可维护、可调试这是工程能力的根基。如果你是一个后端工程师、数据工程师、或者是想转行做AI应用但一直觉得隔层纱的人我觉得这条路值得走一遍。1.1 我给自己定下的四个边界为了让“从零开始”不变成无底洞我在项目开局就给自己划了四条边界算是立项约束只使用CPU做小规模训练验证不碰GPU云资源先把流程跑通再说模型结构用手写实现不直接用from transformers import AutoModel这种全封装方式训练数据自己从公开语料中采集和清洗不用现成的csv或者huggingface数据集一键加载评估指标自己算不依赖平台自带的可视化报告。这四条约束不是为了自虐而是为了把“从零”的分寸感拿捏住。CPU训练意味着模型必须小模型小就意味着你必须在数据质量上花更多功夫而数据恰恰是AI工程里最容易被忽略的核心资产。公开语料自己清洗会让你对“脏数据长什么样”产生肌肉记忆这是看别人清洗一百遍也换不来的。2. 从零开始前必须补齐的四大基础能力动手敲代码之前我先把基础能力盘了一遍。很多人一上来就写训练脚本结果连loss为什么下降得这么慢都解释不了这就是基础功缺位的表现。我并不主张把所有数学啃完才动手但下面这四块内容必须达到“能解释、能推导、能调参”的程度。2.1 机器学习核心概念不只是调用fit我会用最简单的方式说假设你的任务是根据前面的文字预测下一个词机器学习在这里做的事情其实就是“找一个函数”。这个函数的输入是一段已经出现的文本输出是下一个词的概率分布。而在训练之前这个函数只是一堆随机参数。loss就是函数输出和真实答案之间的差距。你每次根据loss的梯度把参数往正确的方向挪一点点这个行为叫做梯度下降。挪太快会震荡挪太慢会龟速所以有了学习率这个概念。这些都不是高深的理论但它们是你在训练过程中每天都要打交道的具体对象。我的建议是在第一周不要去碰任何深度学习框架先用numpy手动实现一个最简线性回归和一个最简softmax分类器把loss的计算、梯度的推导、参数的更新这三件事完整走通。我当时把这一步做完之后再看任何框架的训练日志都像看仪表盘一样亲切因为我知道每一块数字都在描述什么。2.2 语言数据的表示方法从文本到Token再到向量文本不能直接喂给计算单元必须先变成数字。但怎么变是有讲究的。最朴素的方法是字级切分把每个汉字当作一个独立的单元建立一个“字-编号”的映射表。这个方法简单到朴实适合做小规模实验但缺点也很明显就是模型看到的是一个个孤立的字它对“中国”和“中国人民”这种常见组合本身没有先验感知。更常见的是子词切分。它介于字和词之间会把常见词保留为整体把罕见词拆成更小的片段。HuggingFace的tokenizers库就是做这个的但我在这个项目里选择自己实现一个简化版。原理不复杂统计语料中的字符组合频率迭代地合并频次最高的相邻字符对。没错就是BPE算法中文里常说的字节对编码。这里我想强调一个容易踩的坑分词方案和模型结构是绑定的。你训练时用的是什么字典部署时就必须用同一个字典中间多一个空格、少一个字符都会导致预测结果错乱。我一开始没意识到换了字典之后模型的输出变得面目全非查了很久才发现是训练和推理的tokenizer版本不一致。这个教训后面会详细讲。2.3 深度学习框架的操作习惯别当黑盒用选PyTorch的人很多但大部分人的使用水平停留在“调用nn.Linear、nn.Embedding”这个层级。从零开始做项目我建议至少会手写一个简单的两层的反向传播哪怕只是用来练手。PyTorch的autograd机制确实方便但你要理解它背后做的事情每一层在前向传播时记录计算图反向传播时沿着图自动求梯度。理解这件事之后你才能看懂为什么某些操作比如原地修改tensor会破坏计算图为什么会报RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.这类错误也才能在网络结构出错时快速定位问题。我不是说你必须能默写Backprop的公式但要达到这个程度当网络训练出现NaN loss时你能按顺序排查是学习率过大、数据中有脏值、还是梯度爆炸。不具备这个能力的话你大概率只能在网上搜索“loss NaN”的帖子然后碰运气。2.4 工程基础设施版本控制、日志和实验管理这是最容易轻视的一块。很多初学者的项目文件夹长这样final_v2_test.py、final_v3_真的别再改了.py。我可以负责任地说这种习惯在一个超过三天的AI项目中会直接摧毁你的心智。我在项目开始时就用git管理代码每跑完一次实验记录下对应的数据版本、模型结构、超参数和结果指标。不一定要上MLflow或者wandb这种平台一张结构化的表格就够了。但你必须能随时回答这个问题当前这个模型效果最好它是用什么数据、什么参数、什么代码跑出来的这些基础能力看起来都不起眼但它们决定了你后面能不能独立走完全程。对于准备真正把AI工程作为技能来建设的人我希望你能重视它们。3. 环境与工具链选型把坑留在第一天踩完很多人以为环境配置是跑通第一个训练脚本的那一刻就结束了实际上是从那一刻才开始。我这一阶段的主要任务是搭建一条稳定的工具链并确认每个环节都能独立打通。3.1 硬件选型和系统配置我用的是一台不带独显的笔记本CPU是8核16线程内存32GB。这个配置在纯CPU训练的场景下属于“能跑但别指望快”的水平但我一开始就做好了心理建设这次的目标是理解流程不是生产大模型。操作系统我选了Ubuntu 22.04 LTS。如果你用的是Windows我的建议是直接上WSL2不要勉强在原生Windows里折腾CUDA和编译器因为很多底层包在Linux生态里支持最好遇到问题时你的排查路径会顺畅得多。Python版本锁定在3.10。这个选择的理由很简单PyTorch、tokenizers等核心库对它的支持最稳定。Python 3.9也能用但部分新版本的包已经开始降低对它的适配优先级Python 3.11以上则可能在部分老版本依赖库上出现二进制不兼容。不建议在这个阶段追求最新版本稳定压倒一切。3.2 虚拟环境与核心依赖虚拟环境是万万不能省的。AI项目的依赖极其敏感numpy的版本都可能造成训练结果差异更不要说PyTorch的版本变化会直接改变某些API行为。我用conda创建了一个独立的虚拟环境核心依赖版本如下包名版本用途python3.10解释器pytorch2.1.2CPU版深度学习框架numpy1.26.x数值计算基础pandas2.0.x数据处理scikit-learn1.3.x传统机器学习算法与指标计算jieba0.42.x中文分词参考tqdm4.66.x进度条为什么指认版本因为在AI工程里可复现性是生命线。你今天的代码明天换个环境跑结果不一样就无法判断是代码出了变化还是环境出了偏差。把这些版本固定在environment.yml文件里是工程化的第一步。3.3 我踩过的一个最气人的环境坑装依赖的时候遇到过一个很隐蔽的问题pip安装的numpy和conda安装的pytorch之间出现二进制不匹配训练前向传播没有任何报错但loss一直不降。查了好久才发现是numpy版本过高导致某些底层操作在数据对齐时产生了静默错误。这次经历让我养成了一个坏到极致的好习惯每次pip install之前先检查当前环境里核心库的兼容矩阵。PyTorch官方发布页面会写清楚每个版本对应的numpy推荐版本先对齐再安装。这个习惯帮我省了后面无数次调试时间。4. 数据工程从公开语料到干净训练集如果你认为AI工程是从“写模型结构”开始的那就大错特错了。真正开工时我被数据卡了整整两天。4.1 数据源选择与采集我选的数据源是中文维基百科的公开dump文件这算是一个相对干净、体量适中、且可合法使用的语料来源。下载下来是一个巨大的XML文件里面夹杂着大量的模板语法、内部链接和页面元数据需要做一道复杂的清洗工序。清洗流程我整理成了下面这几步用wikiextractor工具把XML中的正文提取出来去除HTML标签、XML转义符以及其他非正文内容按段落切分丢弃掉长度过短小于20字和明显是列表/表格的片段统一标点符号格式把全角数字转半角根据自定义黑名单过滤掉包含特定敏感词或非中文比例过高的段落。你可能想问为什么不直接用别人已经清洗好的公开数据集我很理解这种想法但我要说自己完成一遍清洗你才会真正理解“数据质量直接影响模型效果”这句话。清洗之后你手上握着的是一个你能量化、能解释的数据集而不仅仅是来自某个平台的一个下载链接。4.2 数据分布的检查方法数据清洗完后别急着训练。先做一次分布检查。我会统计以下指标语料总字符数、总段落数平均段落长度、中位数长度高频字符的Top50分布标点分布是否异常比如某个标点占比过高可能意味着清洗不彻底随机抽取50个段落人工阅读确认没有残留乱码或模板痕迹。这套检查我建议做成一个自动化脚本放在项目里每次清洗完数据跑一遍五分钟内出报告。它能帮你拦截绝大多数低级数据问题比如编码错误、三分之一的段落是重复内容等。我当时就发现数据里混入了大量“参考文献”段落和编辑注释这些内容在模型眼中就是“出错的文本”和“无意义的短句”对学习语言规律毫无帮助。清洗它们之后相同模型配置的训练loss下降速度有了明显提升。这就是数据质量的立竿见影之处。4.3 构建训练集如何切分和编码数据清洗完毕之后还需要把文本转换成模型真正的输入输出。这一步我用字级切分来降低复杂度因为我的目标是把流程跑通字级切分在中文场景下一样能学到有意义的模式。具体做法是建立字符表所有出现过的字和标点都进表再加上pad和unk两个特殊标记把每段文本切成等长的样本长度设为64个字符步长也设为64避免重叠输入序列是前64个字符标签序列是后移一位的64个字符每个序列转换成对应的整数索引列表。比如原始文本是“人工智能正在改变世界”切分后输入就是“人工智能正在改变世”标签就是“工智能正在改变世界”。模型要做的事情就是给定前者预测后者。这个设计逻辑听起来很简单但是它是所有基于语言模型的AI应用的基础。搞懂它后面阅读理解、摘要生成这些任务你就都不会觉得神秘了。5. 模型结构手写一个极简Transformer和它的训练循环这一章是整个项目的核心。我会带你完整过一遍我如何从零手写了一个极简Transformer并用它跑通了训练和生成。不依赖transformers库里的AutoModel整个模型结构清晰透明出一行代码你就知道它在干什么。5.1 为什么选Transformer而不是更简单的RNN既然是从零开始为什么不选结构更简单的RNN呢这是我当时认真考虑过的问题。RNN的实现确实简单几十行就能搞定文字生成也能跑起来。但我最终还是选了Transformer理由有两个第一RNN的顺序计算特性让它很难并行化在CPU上训练长序列非常痛苦而Transformer的自注意力机制可以一次性处理整个序列计算效率高得多。第二Transformer是当前几乎所有主流语言模型的基础结构一步到位去理解它对你后续阅读任何模型论文都是巨大的帮助。5.2 极简Transformer的核心模块拆解我这套极简Transformer的结构如下Token编码和位置编码一个多头自注意力模块我简化为单头方便调试一个前馈网络模块一个输出层把隐藏状态映射到词表大小的概率分布。先说嵌入层。输入的每个字先通过一个可学习的查表操作变成向量这一层的参数本质上就是字向量矩阵形状是vocab_size * hidden_dim。位置编码方面我用的是经典的正余弦公式。当时有个细节印象很深如果没有位置编码Transformer就变成了“词袋模型”句子顺序完全失效因为它本身没有天然的顺序感知能力。加位置编码这个步骤是必备的不是选项。自注意力机制是核心中的核心。通俗地讲它做的事情就是让每个位置的Token去“看”序列中其他位置的Token并计算它们之间的相关性权重。对语言模型而言这里有个关键设计用掩码遮住未来位置。训练时给模型看前64个字符预测第65个字符如果在第10个位置就已经“偷看”了第20个位置的字符这个学习过程就毫无意义了。前馈网络相对简单就是两个全连接层加一个激活函数。它只对每个位置独立操作不跨位置交互作用是对注意力层提取的信息做进一步的变换。5.3 训练脚本的结构设计我的训练脚本分为以下几个部分加载数据把训练集和验证集分开构造数据迭代器每次喂给模型一个batch形状是batch_size * sequence_length前向传播计算logits和标签做交叉熵损失反向传播计算梯度每隔固定步数打印一次loss并在验证集上计算困惑度。关键的超参数我用了这些小规模的配置hidden_dim128num_heads1num_layers2sequence_length64batch_size32learning_rate1e-3epochs5。训练时的两个细节我想分享。第一梯度裁剪非常重要。纯CPU环境下模型偶尔会因为某几步出现超大梯度一不留意loss就变成NaN了。加上max_grad_norm1.0之后这个风险降低了非常多。第二学习率调度需要配合步数做热身和衰减。我先线性warmup到目标学习率再用余弦方式衰减到接近零。这能明显提升loss收敛的稳定性。5.4 第一个里程碑loss从8.0降到4.5我不追求结果惊艳但要确认流程闭环。第一次完整跑完一个epoch之后训练集loss从初始的8.0降到了5.3左右验证集困惑度从几千降到了不到200。到了第五个epoch结束训练loss稳定在4.5附近。这个数字意味着什么如果模型完全没有学到任何规律预测下一个字的难度就是词表大小约5000取对数大约是8.5左右。降到4.5说明模型已经能捕捉一部分语言规律了虽然还远远谈不上流畅但它不再是在乱猜。我用同样的模型做了一次文本生成输入“人工智能正在”作为开头生成的输出中包含了一些语法正确但内容不连贯的片段。比如它会冒出“人工智能正在成为社会发展的重要方向”这种看上去很通顺但上下文之间没有严格逻辑关联的句子。这说明模型已经开始学到高频搭配和短语结构但还没有学到长距离的语义一致性。对于一个手写小模型来说这是一个合理的阶段性成果。5.5 训练中最容易忽略的一个环节定期验证很多人训练时只看训练集loss下降就信心满满但模型非常容易过拟合尤其是数据量不大、模型规模不小的时候。验证集的困惑度能告诉你模型的泛化能力是否同步提升。我在每个epoch结束时会在验证集上随机采样一些段落用模型对下一字进行预测并计算准确率。记录下来的曲线能直观看出第几个epoch之后训练集还在继续下降而验证集开始回升——这就是过拟合信号。对于小项目来说在过拟合信号出现时提前停止训练即可保存验证集表现最好的那一个checkpoint不要一味训练到最后。6. 工程化落地从训练笔记本到可复现的交付物模型训练结束不代表项目结束。“从零开始”的最后一步是把所有散落的脚本变成一个能被旁人复现的工程。这一步的目标是把项目压缩成一个git clone加两个命令就能跑通的东西。6.1 项目目录是怎么设计的我的项目目录结构大概长这样ai-engineering-from-scratch/ ├── configs/ │ └── tiny_transformer.yaml ├── data/ │ ├── raw/ # 原始语料 │ ├── processed/ # 清洗后的训练数据 │ └── vocab.txt # 词表 ├── models/ │ └── checkpoints/ # 模型权重 ├── scripts/ │ ├── 01_download_data.py │ ├── 02_clean_data.py │ ├── 03_build_vocab.py │ ├── 04_train.py │ └── 05_generate.py ├── utils/ │ ├── tokenizer.py │ ├── dataset.py │ └── metrics.py └── README.md每个脚本都有明确的输入输出边界脚本与脚本之间通过中间文件衔接。运行完一个脚本看一眼输出目录就能确认这一步是否成功。这种流水线设计的好处是出错时定位极快。训练效果不好你知道问题大概率出在数据或模型配置上而不是脚本本身的逻辑里。6.2 配置管理超参数不要写在代码里这是我非常坚持的一点。学习率、batch大小、序列长度这些超参数我全部放进一个YAML配置文件。改参数只改配置文件代码一行不动。实验追踪也以此为基础每个实验对应一份配置备份结果指标记录在实验表里绝不把配置散落在脚本的魔术数字里。这一习惯帮我避免过一次严重事故有一次我为了调试临时改了一行学习率结果忘记改回来导致后续跑出的所有实验结果都无效。有了配置化之后这种问题基本根除了——因为每次实验的配置都是可追溯的。6.3 部署前的最后一道整合写一个可复现的README很多个人项目死在README这一步。如果你自己都过一个月都跑不起来自己的项目那这个项目就是一次性的垃圾。我花了整整一个下午把README写好包含环境创建与安装命令数据下载与处理命令训练命令生成命令一个最小可跑通的示例输出示例。这类文档的价值不在于展示给别人看而在于强迫你从零到一把流程再走一遍。我在写README的过程中发现有两个命令的参数顺序不对导致无法运行。这些问题如果没在“文档化”这一步暴露将来复现时会浪费更多时间。6.4 训练成果的合理预期管理我要非常明确地说这个项目的成果不是一个生产级模型。它是一套你可以完全理解、完全掌控的AI应用工程链路。数据怎么处理、模型怎么设计、怎么训练、怎么评估、怎么部署这个闭环本身就是成果。如果你后续要接入真实业务路径也很清晰把数据换成业务数据、把模型结构换成更好的开源底座、把训练参数换成更大规模。但因为你已经在“从零开始”阶段亲手走过一遍你做的每一步调整都会是基于理解的主动选择而不是盲目试错。7. 从零开始训练中翻过的五个大车避坑总结这一节我想把整个项目中最痛苦的几段经历整理成清单。它们未必能让你完全避开同样的坑但至少能让真遇到的时候有个方向感。7.1 第一个大坑Tokenization不一致前面简单提过我在数据预处理阶段和训练阶段使用了不同的词表版本导致生成结果错乱。具体表现是训练时loss正常下降但生成的文本经常出现unk标记。排查过程花了我半天时间。我的排查顺序是先看生成脚本的数据预处理再看模型加载逻辑最后对比了训练和推理时的词表文件发现两个文件的hash不一致。那一刻真相大白训练用的词表是早期版本推理加载的是另一个版本的词表。这看起来是个低级错误但它警醒我所有涉及字典的环节都要做完整性和一致性校验。我在数据预处理阶段加了一个vocab_hash校验工具加载模型时先校验模型训练记录里的vocab_hash与当前词表是否一致不一致就拒绝加载给出明确报错信息。7.2 第二个大坑学习率与数据量的错配一开始我把学习率设为1e-2结果loss从8.0降到7.6之后直接停滞。原因我分析下来大概是学习率过大导致参数在最优解附近震荡模型难以稳定收敛。解决办法是降低学习率到1e-3再加上前500步的warmup。改完之后loss在同样的epoch数内多降了约1.0。这件事让我深刻理解了一个道理学习率不是一个随便填的数字它和你数据量、模型规模、优化器类型都有关系。最佳实践是写一个小的超参数搜索脚本取1e-4、5e-4、1e-3、3e-3几个值各跑几百步看收敛速度再定最终值。7.3 第三个大坑梯度爆炸导致loss突然变成NaN训练到第三个epoch的时候loss突然从4.8跳到了NaN一切看起来完好没有报错但loss从此一蹶不振。我排查时第一反应是学习率过大降到1e-4之后能训练但训练速度骤降。后来检查梯度模长发现某些步的梯度模长能到几百甚至上千。这就是典型的梯度爆炸前兆。加上max_grad_norm1.0的梯度裁剪后loss波动明显平稳了。对于Transformer这种深度网络在训练初期和损失陡峭的区域梯度爆炸是高频事件绝不能省掉这一层防护。7.4 第四个坑把评估指标当作模型好坏的全部依据我的验证困惑度降到了170左右但实际生成的文本依然很烂。这背后的原因其实很直白困惑度过低说明模型对训练数据分布拟合得很好但它学到更多的是“高频模式”而不是“语义理解”。一个尺寸极小的模型很难有足够容量去学习深层语义。这让我意识到评估指标和业务效果之间永远是两回事。在做AI工程时你要同时维护两套评估体系一套是科学指标loss、perplexity、accuracy另一套是人对生成样本的主观多维评价流畅度、相关性、多样性、事实性。只有这两套指标并行你才不会因为指标好看就过早自满也不会因为指标一般就否定整个链路的价值。7.5 第五个坑在CPU上“及时刹车”不要当长跑选手最后这个坑几乎和算法无关而是时间管理。纯CPU训练一次完整流程要几个小时如果你每次都重新跑全量数据迭代速度会极慢。我的解决方案是在整个语料中先切出1%作为调试集所有超参数和代码调试全部在调试集上完成确认无误后再在完整数据集上跑最终训练。代价是最终训练时间长了一些但换来的是整个调试周期从“两天一次”缩短到“十分钟一次”效率差距巨大。8. 下一步演进从“跑通”到“跑好”你还可以做什么最后这段不是总结而是我给自己列的“未来几周接下来要去做的事”。如果你也走完了从零开始的闭环下面这几个方向可以按需选取。8.1 方向一把字级模型换成子词模型字级模型学习效率低、序列长、语言单位过碎这是它的硬伤。下一步最自然的演进是用BPE或Unigram算法构建子词词表把常见词保留为完整Token减少序列长度同时在一定程度上提升模型对词义的感知。这一步对语言模型生成质量的提升非常明显。8.2 方向二加深网络结构从一层、两层到四层、六层的Transformer参数量会成倍增加。但这个方向的前提是你必须换GPU或者接受极慢的速度。我认为比较务实的做法是升级到一块24GB显存的GPU租用也行把模型参数量提升到千万级那时你能明显观察到生成质量的质变。参数规模的扩大不是线性提升效果而是在某个量级之后会发生质变。8.3 方向三做一次指令微调完成预训练之后可以在少量人工标注的“指令-回答”数据上继续训练让模型学会针对指令做出回应。这就是所谓的SFT阶段。中文公开的指令数据集不少规模也不大一张普通显卡就能跑。这个方向会让你的模型从一个“接续下文”的生成器变成一个能完成特定任务的对话助手。这一步是把“技术Demo”变成“产品原型”的关键一步。8.4 方向四引入RLHF如果你想往上再走一层可以让模型在人类偏好上更进一步。原理是训练一个奖励模型来模拟人类对回答质量的评分再用强化学习的方式优化生成策略。这个方向实现复杂度偏高但它是当前主流高质量对话AI产品背后的核心方法论之一了解它的原理对你理解整个行业非常有帮助。我个人实际体会是当你完整地经历过一次“从零开始”的链路再去看高级工具和框架的文档再也不会觉得它们在“黑盒发功”了。你看到的是它们如何替换你亲手做过的那一个个环节每一层抽象之下是什么逻辑。这种掌控感是教程和框架本身不会教给你的。
返回列表