最近总有朋友问我:想系统入门 AI 工程,到底该不该从框架和现成库开始?说实话,我自己最早就是这么学的,先装了 PyTorch、HuggingFace,跑通了几个 Demo,觉得自己已经“入门了”。可真到要独立做一个项目时,数据一换就崩、参数一调就乱、效果一差就不知道是模型问题还是数据问题。那段时间最大的挫败感不是“不会用工具”,而是“脑子里没有完整的工程地图”。
后来我把整个项目刻意归零,不依赖任何现成的 pipeline,从数据组织、训练循环、推理服务到评估反馈,一层一层自己搭,才真正理解了所谓 ai engineering from scratch。这个“from scratch”并不是让你重新造轮子,而是让你亲手把每个环节的依赖关系摸清楚,把“黑盒”拆成“白盒”。这篇内容就是我从那次归零重构里沉淀下来的完整思路,适合想真正落地 AI 应用的工程师、准备深入研究大模型原理的研究者,以及那些不想停留在“调包侠”阶段的人。你会看到一套可执行的工程骨架、小规模的 reasoning model 实践路径,以及我在真实部署过程中踩过的最隐蔽的坑。
1. 先纠正一个误区:从零开始不是重新发明轮子,而是重建心智模型
我发现很多学习者的路径是反的:先学 FastAPI、先学 LangChain、先学 Transformers,遇到问题就搜索“如何用 X 实现 Y”,结果项目做完一问“为什么这个 tokenizer 要加 special token”,答不上来。这不是你的错,而是大部分教程把“使用”和“理解”混为一谈了。
真正的“从零开始”应该解决三件事:第一,让你知道每个组件为什么存在;第二,让你明白组件之间的数据流向;第三,让你在系统出现故障时不至于只能重新跑一遍。
1.1 框架给你的是一条捷径,但捷径会遮蔽依赖关系
用现成的库,常常只需要三行代码就能加载一个预训练模型:
from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("gpt2") model = AutoModelForCausalLM.from_pretrained("gpt2")这三行代码背后,至少隐藏了六个环节:词表切分、特殊 token 管理、嵌入矩阵初始化、位置编码、多层自注意力计算、语言模型头的输出映射。如果这些环节没有在你的头脑里建立基本认知,当你想换成自己的 tokenizer、想改模型层数、想给注意力加一个 mask 时,就会寸步难行。
所以我在重构项目时,给自己定的规矩是:每次引入一个现成库之前,先想一想“如果不用它,我要自己怎么写”。哪怕不真的从零手写,只是写出伪代码,也有效。
1.2 从零重建心智模型的三个层次
- 操作系统层:理解数据如何从原始文本变成 tensor,如何构造 attention mask,如何做 padding 和 truncation。这一层决定你能不能处理真实场景里格式脏乱的数据。
- 算法层:理解 transformer 的前向传播、反向传播的梯度流向、学习率策略的意义。这一层决定你能不能稳定地训练一个模型。
- 系统层:理解 GPU 显存管理、推理服务并发、请求批处理、监控告警。这一层决定你能不能把模型真正交到用户手里。
有一个我常用的类比:用框架做 AI 项目就像在毛坯房里做精装修,但如果你不知道承重墙在哪里、水管怎么走,一旦遇到问题就得砸墙重来。从零开始搭过一遍毛坯管线的人,后来做精装修的速度反而更快。
1.3 三种学习者,三种路径建议
- 如果你只是想快速验证一个想法,可以直接用现成库,但至少要过一遍“最小重建清单”:自己写一个二分类模型的训练循环,不用 Trainer。
- 如果你想进入大模型应用研发,我建议从 prompt 和一个极小的模型开始,逐步构建评估、反馈、迭代的闭环。
- 如果你想研究模型原理,不要急着上大规模,先把一个 1 亿参数不到的模型从零跑到收敛,再去看大模型的论文,会顺畅很多。
从零开始的意义,不是否定轮子的价值,而是让你成为那个能修轮子的人。
2. 工程地图:数据闭环、模型生命周期、推理服务、评估反馈
在我重构项目的过程中,最受益的一步是绘制了一张“AI 工程地图”。这张地图把整个 AI 工程拆成四层,我再也不会因为局部问题而全局慌乱。
2.1 四层架构与它们的连接关系
- 数据层:包括采集、清洗、标注、增强、版本管理。它的产出物是“可被信任的数据集”。
- 模型层:包括选择预训练基座、微调、从零训练、checkpoint 管理。它的产出物是“可被复现的模型实例”。
- 推理层:包括接口封装、服务化、批处理、性能优化。它的产出物是“稳定低延迟的服务”。
- 评估层:包括离线指标、在线评测、用户反馈收集、回归测试。它的产出物是“可量化的质量度量系统”。
它们不是一个单向流水线,而是一个闭环。数据决定模型上限,模型影响推理策略,推理产生用户反馈,反馈反哺数据与模型迭代。我见过很多团队把四个环节拆给了四个小组,结果模型效果好但服务超时,或者评估指标高但用户根本不满意,就是因为缺少闭环视角。
2.2 数据层:缺失的版本管理比模型性能更致命
很多从零开始做 AI 工程的人,把 80% 时间花在调模型上,但真正影响效果的往往是数据。我建议从一开始就建立三个习惯:
- 给数据集加版本号,哪怕只是用 Git 管理数据生成脚本,并把数据文件哈希记录在日志里。
- 数据采样前先做 EDA,统计长度分布、标签分布、脏数据率,而不是直接开训。
- 划分数据时保持时间一致性,比如按时间顺序划分,避免未来数据泄露到训练集。
一个具体做法是,使用带校验的 JSONL 格式存储训练样本,每一行包含id、input、output、metadata。metadata 里记录来源、采样时间、标注版本。这样即使模型跑偏了,也能追查是哪一批数据引入的问题。
2.3 模型层:从基座到微调之间的决策树
模型层最常见的问题不是“怎么训练”,而是“该不该训练”。我给自己整理了一个决策顺序:
- 现有开源模型能否在 zero-shot 或 few-shot 下解决 80% 的问题?能,就别训练。
- 不能,但通过 prompt 调整能接近目标?先不要微调,先优化 prompt 和数据组织。
- prompt 已经榨干,仍不够好?做领域微调或指令微调。
- 需要极其特殊的推理能力且微调无效?才考虑从头训练,且优先从较小参数量开始。
这个顺序能帮你节省大量时间和 GPU 成本。尤其在资源有限的情况下,从零训练并不是浪漫主义,而是非常具体的工程决策。
2.4 推理层:不只是把模型包成 HTTP 接口
我重构后的推理层做了几件不复杂但很关键的事:
- 动态批处理:并发请求到达后,在窗口时间内聚合为同一 batch,提高 GPU 利用率。
- 超时分级:简单请求给短超时,复杂请求给长超时,并通过队列做优先级调度。
- 优雅降级:模型服务过载时,返回缓存结果或降级文案,而不是直接报错。
这些看起来是通用后端工程,但结合模型推理的计算特征(显存占用、时长不确定)后,变得非常考验细节。比如 batch 内序列长度差异过大会造成大量 padding 浪费,所以服务端要按长度分桶组 batch。
2.5 评估层:把评估当成一套持续集成测试
我见过太多项目只在最后跑一次测试集,得出一个准确率就宣布完成。但 AI 系统最怕回归——今天效果不错的功能,可能因为某次数据更新而悄悄变差。因此我把评估做成两个层次:
- 离线回归测试:每次更新模型或 prompt 后,在固定测试集上跑一遍指标,包括准确率、召回、token 级别的鲁棒性检查。
- 在线质量监控:对线上请求抽样,用模型裁判(LLM-as-judge)或规则检查评估回答质量,并建立人工复核通道。
离线指标解决“这次改动有没有变好”,在线监控解决“用户实际感受有没有变差”。两者缺一不可。我后来很多次发现问题,都是在线监控先报警,然后再回查离线测试集——因为测试集没有覆盖长尾场景。
3. 从零搭建工程骨架:目录、配置、数据版本与实验追踪
等地图成型,我做的第一件事不是写模型代码,而是搭一套哪怕换一个人也能接着跑的工程骨架。这比想象中重要得多:AI 项目迭代速度快,如果没有统一骨架,两周后你自己都看不懂自己的代码。
3.1 一个经过实战检验的目录结构
ai-engineering-from-scratch/ ├── configs/ # 所有实验配置,YAML 或 JSON │ ├── base.yaml │ ├── train_small.yaml │ └── serve.yaml ├── data/ # 数据目录(通常不入库) │ ├── raw/ # 原始数据 │ ├── processed/ # 清洗后数据 │ └── versions/ # 带版本号的数据集快照 ├── src/ │ ├── data/ # 数据加载、清洗、增强 │ ├── models/ # 模型定义、训练器 │ ├── inference/ # 推理服务、批处理 │ ├── evaluation/ # 评估指标、评测脚本 │ └── common/ # 通用工具 ├── experiments/ # 每个实验的产物 │ ├── exp_001/ │ │ ├── config.yaml │ │ ├── metrics.json │ │ └── checkpoints/ ├── scripts/ # 从数据准备到部署的脚本 └── pyproject.toml 或 requirements.txt这套结构的核心思路是:配置和代码分离、数据和模型分离、实验产物可以追溯。你不需要一开始就上一个重型的机器学习平台,一个 Git 仓库加一个本地文件规范就够了。
3.2 配置管理的核心不仅是方便,而是让实验可复现
我用 YAML 文件管理所有超参数,同时在代码里通过配置类强制校验字段。比如:
from dataclasses import dataclass @dataclass class TrainConfig: model_name: str = "gpt2" seq_len: int = 512 batch_size: int = 8 lr: float = 5e-5 epochs: int = 3然后在运行时把配置内容、Git commit、数据版本一起写入experiments/exp_XXX/config.yaml和metrics.json。以下是我的记录模板:
{ "experiment_id": "exp_001", "git_commit": "a1b2c3d", "data_version": "2025-06-01-v3", "config": {}, "metrics": {}, "notes": "第一次从零训练小模型" }你可能觉得这很啰嗦,但某个深夜当你发现两天前最好的结果不知道用的哪份数据时,就会庆幸自己做了这些记录。
3.3 数据版本与实验追踪最简单的落地方案
先别急着上复杂的云端平台。对一个从零开始的项目,我用了一套几乎零成本但足够可靠的方案:
- 数据版本:用
sha256计算数据文件哈希,记录在data/versions/manifest.json里。 - 实验追踪:用 Git 管理代码,用 JSON 文件记录每次实验的配置与指标。
- 长期保存:每周把重要 checkpoint 上传到对象存储,并在本地保留最近 3 个版本。
这套方案足够支撑几十次到上百次实验。如果实验数量再多,再引入 MLflow 或 W&B 也来得及,而且你已经对数据流有了清晰认知,工具迁移会很轻松。
4. prompt engineering 与 harness engineering 的边界:可组合的提示系统
当模型选型结束,最容易被高估的一环就是 prompt。很多人以为只要在系统提示里堆叠规则,模型就会乖乖听话。但真实工程里的 prompt,只负责“表达意图”,真正稳定的系统需要 harness 来组织一切。
4.1 从两个概念的区别厘清设计边界
- Prompt engineering:面向模型的文本输入设计,包括指令、示例、格式要求、上下文组织。它解决的是“模型在单次推理中如何更好理解任务”。
- Harness engineering:面向系统的外部框架设计,包括工具注册、调用循环、记忆管理、错误恢复、安全过滤器。它解决的是“模型如何在多轮、多工具、多请求的复杂环境中稳定工作”。
我见过一个团队花了大量时间调 prompt,想让大模型稳定调用外部 API。但问题根本不在 prompt,而是他们的 harness 缺少工具调用的反馈回路:模型生成了一个 API 调用,结果 API 超时了,harness 直接把这个错误结果传给了模型,然后就乱了。
4.2 最小 harness 的六大模块
如果你要从零实现一个 AI Agent 或复杂推理系统,我建议先实现一个最小 harness,包含以下模块:
- 上下文组装器:把系统提示、历史记录、工具说明、当前输入拼装成模型可理解的上下文。
- 工具注册表:定义每个工具的
name、description、input_schema,供模型选择。 - 解析器:把模型输出解析为结构化指令,比如 JSON 格式的
tool_call。 - 执行器:调用真实工具,并处理超时、异常。
- 结果回填器:把工具结果按固定格式追加到上下文,形成“调用-观察-思考-行动”的循环。
- 策略控制器:控制最大轮数、终止条件、安全兜底。
以下是一个极简伪代码流程:
while not done and step < max_steps: prompt = context_assembler(system_prompt, memory, tool_schemas) response = model.generate(prompt) action = parser.parse(response) if action.type == "finish": done = True else: result = executor.execute(action) context_assembler.append_observation(result)这个循环听起来简单,但它把所有坑都暴露出来了:模型输出不合法 JSON 怎么办?工具结果太长导致上下文爆炸怎么办?连续调用同一个工具太多次怎么办?这些都需要在 harness 里不断加防御性设计。
4.3 上下文管理的工程级技巧
大模型上下文窗口是很大,但实际可用容量会被质量和成本压缩。我常用的三个技巧:
- Token 预算分层:给系统提示、历史、工具说明、当前输入分别设上限,超出后优先压缩或裁剪历史。
- 关键信息重排:重要信息放在开头和结尾(primacy 和 recency 效应),中间放次要内容。
- 动态摘要:当历史超过阈值时,调用一个小模型把旧历史压缩为摘要,保留关键实体和决策。
这里有一个现实工程经验:不要为了保留每一句对话而让上下文无限增长。根据我的实测,许多模型在长上下文上的表现不比“摘要+近期原文”好多少,但 token 成本却高得多。
5. 小模型起步:从零构建一个可运行的推理模型的路径
现在来到大家最感兴趣的部分。标题里有“from scratch”,而当前社区里很火的短语是 “build a reasoning model from scratch”。如果你有耐心和有限的资源,完全可以从一个几亿参数的小模型开始,走完预训练、指令微调、强化学习对齐的完整流程。接下来我分享的是一条低成本可执行的路径。
5.1 为什么先用小模型,而不是直接对标大模型
大语言模型的成功,依赖数据规模、算力和分布式训练的工程能力。个人或小团队想从零复现 GPT-4 级别的模型不现实。但“小模型”的价值在于:它让你在几小时或几天内看到完整的训练-评估-迭代闭环。我建议初始参数量控制在 1 亿以下,用大众显卡也能跑。比如,一个 4 层、隐藏维度 512、6 头注意力的小 GPT,参数量大约 6 千万,足够做简单的数学推理任务。
这样的小模型不能跟 70B 模型比知识量,但它可以验证“模型结构、数据组织、训练策略、推理方法”是否走得通。这就像学飞行先上螺旋桨小飞机,而不是直接开喷气式客机。
5.2 数据合成:先解决模型“看到什么”的问题
推理模型需要的是带思考过程的训练数据。一个常见做法是“从粗到精合成推理轨迹”:
- 准备基础问题集,比如小学数学应用题、逻辑谜题。
- 用规则或较强的大模型生成逐步推理过程,但你需要检查每一步。
- 把所有数据洗牌、去重、过滤格式错误,存储为统一 JSONL。
样例如下:
{ "question": "一个盒子有 12 个苹果,拿走 3 个,又放进去 5 个,现在有多少个?", "reasoning": "先拿走 3 个:12-3=9。再放进去 5 个:9+5=14。", "answer": "14", "source": "synthetic_v1" }注意,合成数据质量比数量更重要。十个带完整正确推理步骤的样本,好过一千个只有答案没有推理过程的样本。在后续训练里,模型会从“模仿推理格式”开始,逐步学会“推理的因果结构”。
5.3 从零定义一个极简 Transformer 骨架
如果你真的想“from scratch”,你可以不直接使用nn.Transformer,而是手写一个极其简小的模块化实现。核心只需要三件套:
- Token 嵌入与位置编码
- 因果自注意力
- 前馈网络
下面是核心骨架的简化代码片段(用于演示思路,不是完整实现):
import torch import torch.nn as nn class TinyAttention(nn.Module): def __init__(self, d_model, n_heads, causal=True): super().__init__() self.qkv = nn.Linear(d_model, 3 * d_model) self.out = nn.Linear(d_model, d_model) self.n_heads = n_heads self.causal = causal def forward(self, x): B, T, C = x.shape qkv = self.qkv(x).reshape(B, T, 2, self.n_heads, C // self.n_heads) q, k, v = qkv[:, :, 0], qkv[:, :, 1], qkv[:, :, 2] att = (q @ k.transpose(-2, -1)) / (C // self.n_heads) ** 0.5 if self.causal: mask = torch.triu(torch.ones(T, T), diagonal=1).bool().to(x.device) att = att.masked_fill(mask, float("-inf")) att = att.softmax(-1) out = att @ v out = out.transpose(1, 2).reshape(B, T, C) return self.out(out)这段代码重点是展示注意力 mask 和维度变换逻辑。真的实现时,还要考虑 KV Cache、量化等。但从这里起步,你会非常自然理解为什么大模型里需要 mask,为什么 attention score 要除以根号 d。
5.4 训练一个“能说步骤”的小模型
准备工作完成后,训练分为两个阶段:
- 阶段一:语言建模预训练。用普通文本训练模型学习基础语言规律,损失函数是交叉熵。这个阶段可以让模型先学会“生成连贯文本”。
- 阶段二:推理指令微调。在合成推理数据上继续训练,输入是
question,输出是reasoning和answer。这一阶段的关键是避免灾难性遗忘,可以混合一部分通用文本数据。
对于 6 千万参数的小模型,我用单张消费级显卡也能在几小时内跑完阶段一(视数据规模)。阶段二更快,几百条到几千条样本就能看到明显的“行为改变”。
5.5 推理:贪心还是采样?
训练完成后,推理阶段也有一些小但重要的选择。贪心解码适合需要稳定答案的任务,但对多步骤问题容易陷入重复;带温度的采样能增加多样性,但可能产出错误步骤。我建议在推理任务上,先用带一定温度的采样生成多条候选,再用一个简单的验证器(比如计算最终答案是否正确)做原则性筛选。
这种“生成多个候选 + 验证器筛选”的思路,实际上是很多 reasoning model 的雏形。它的核心理念是:让模型探索更多路径,然后让系统选择可信路径,而不是让模型一次性输出完美答案。
6. 从可跑通到可部署:性能、成本与监控的实战权衡
模型在实验环境跑通只是起点。真正把系统交给用户使用,会遇到比训练更琐碎的问题。我在此分享一些可复制的部署策略。
6.1 性能优化决策:按场景选择武器
部署一个 AI 服务,不是把所有请求都塞给最大的模型。我的配置思路是“分级路由”:
| 场景 | 模型方案 | 延迟目标 | 成本 |
|---|---|---|---|
| 实时对话 | 小模型或量化模型 | 1s 内 | 低 |
| 复杂推理 | 大模型 + 多步骤 harness | 5s-30s | 高 |
| 异步任务 | 离线批处理,不要求实时 | 分钟级 | 可调度 |
一个常用技巧是:先用小模型过滤简单问题,只有遇到低置信度或复杂关键词时,才把请求升级到大模型。这样能节省相当可观的成本。
6.2 模型优化:量化、蒸馏与缓存
- 量化:将模型权重从 FP16 降到 INT8 或 INT4,显存占用大幅下降,速度提升,但可能在边缘 case 上损失质量。建议先在评测集上验证退化程度。
- 蒸馏:用大模型的输出作为小模型的训练目标,让小模型学习“大模型的判断边界”。如果你的场景响应延迟敏感,这比直接调用大模型更经济。
- 缓存:对重复性高的 Prompt 和结果做语义缓存。比如很多用户的问题只有少量变体,可以用 embedding 相似度检索,命中缓存就不调模型。
6.3 可观测性:不要等用户投诉才发现问题
我把 AI 系统的监控分成三个等级:
- 基础设施级:GPU 利用率、显存、延迟、请求 QPS。
- 模型质量级:回答长度、拒绝率、缓存命中率、平均置信度。
- 业务效果级:用户点击、停留时长、工单解决率。
很多时候,基础设施一切正常,但模型质量悄悄崩了。所以我强烈建议在推理服务里加入“影子评估”:随机抽取 1% 的线上请求,记录 prompt 和 response,第二天用评测模型做一次质量打分,并按天汇总趋势。如果你的评分曲线突然下降,往往说明有数据分布漂移或 prompt 被误改。
6.4 一次真实的线上问题复盘
有一次,我们的模型回答质量突然下降。看起来像模型退化,但检查 GPU 利用率正常。后来通过影子评估发现,评分从 4.2 降到了 3.6,再追踪 prompt 记录,原来是某个上游系统把用户输入的编码从 UTF-8 变成了带 BOM 的格式,导致第一段文本多了一个不可见字符。模型其实没坏,坏的是数据链路。
这个经历让我非常坚定:AI 工程里,90% 的“模型变笨了”其实是数据链路变化,而不是模型本身出了问题。没有完善的日志与追踪,你只能盲猜。
7. 避坑指南:三个我在从零实践里踩过的隐蔽陷阱
最后这一部分,我不讲宏大架构,只想分享几个让很多人头大的真实教训。这些坑都有一个共同点:看起来很平常,但破坏力很大。
7.1 评估集污染:你的指标在说谎
我刚开始做实验时,习惯把随机划分的数据作为评估集。后来发现训练样本和评估样本有大量重复语义,比如同一个问题只是换了数字,模型记住了模式,评估分数虚高。但这个“高分模型”一上线就暴露问题。
现在我的做法是:
- 用时间或来源划分数据,保证评估集的分布与线上尽量一致。
- 每次新增数据时,先计算与已有评估集的相似度,去重后再进入训练集。
- 保留一个“金标准集”,永远不参与训练,只用于最终评估。
7.2 上下文窗口的假象:看得见不等于用得好
很多模型支持 128K 上下文,但实际效果取决于注意力可以触及多远。我做过对比实验:把关键信息放在中间位置,模型经常忽略;放在首尾,准确率显著提升。还有一个现象是,上下文越长,模型在后续生成时越容易重复或走神。
因此我在工程设计里从不假设“模型能使用全部上下文”。关键信息必须前置,或者通过强制格式(比如 XML、JSON)让结构更清晰。必要时,把长文档拆分成多个子任务,分别处理后汇总结果。
7.3 实验管理混乱:昨天最好的结果,为什么复现不了
这个问题几乎人人都遇到过。早期我为了快速迭代,直接改同一个脚本,不断覆盖原来的配置。结果有一天发现之前的指标很好看,但代码已经改得面目全非,完全不知道当时的参数组合。这个坑不是靠聪明能解决的,就是靠规范。
我现在的强制流程是:
- 每次实验前,从
main分支创建新分支或实验目录。 - 实验结束,立即填写实验记录表,包含配置、数据版本、指标、结论。
- 每个新 idea 都是新实验,不修改历史实验产物。
虽然听起来像“写文档”,但它真的能让你避免大量返工。尤其是部署上线前,你要回滚到“之前的最好版本”,一套完整的记录系统就是你的保命绳。
这里我还想加一个隐藏的教训:不要急着一个文件里同时解决数据和模型逻辑。AI 项目代码的演进速度极快,模块化设计可以让你在新增一个数据来源或换一个模型时,只动对应的模块,而不是把整个项目推倒重来。从零开始,不等于永远从小开始,而是让你拥有随时“重组”的能力。
如果你也正在做 ai engineering from scratch,我最后的小建议是:把目标拆成两个递进的小闭环。第一个闭环是“数据-训练-评估”,哪怕用最小的模型,也要走完整;第二个闭环是“请求-推理-监控”,哪怕没有复杂业务,也要把线上链路打通。这两条闭环会成为你后续所有 AI 工程能力提升的骨架。