写在前面:这不是一篇教你数学原理的文章
我见过太多人收藏了一堆“从入门到精通”的教程,报名了好几个训练营,最后却连一个能稳定跑起来的AI项目都拿不出来。原因不是他们笨,而是大多数教程都在讲模型内部怎么工作,很少有人讲“AI工程”本身该怎么落地。
我说的AI工程,不是调一次API、跑通一段notebook,而是从数据准备、模型选型、微调实验、评估上线到日常维护的完整链路。这个项目的名字很长——ai-engineering-from-scratch,翻译过来就是“从零开始搞AI工程”。它是我自己维护的一套实战笔记和代码骨架,目标很简单:让一个只有基础Python能力的人,也能按步骤搭出一套可运行、可迭代、可评估的AI系统,而不是停留在“跑通demo就结束”的阶段。
如果你是从传统软件开发转过来,或者已经在用大模型API、但想往前再走一步,这篇文章会非常适合你。下面我会把这条链路里的关键节点、我踩过的坑、以及我认为最值得投入的方向,按实战顺序完整讲一遍。
1. 为什么想写“ai-engineering-from-scratch”这个项目
1.1 网络上教程的断层:大家都在讲架构,没人讲搬砖
你随便搜一下“大模型入门”,出来的内容基本都是Transformer结构、注意力机制、损失函数推导。这些东西重不重要?重要。但对于一个想真正动手做项目的人来说,它们不是第一步。
真正的第一步是什么?是拿到一台干净的机器,把Python环境装好,把模型权重下载下来,把一份训练数据整理成模型能吃的格式,然后跑通一个最小的训练流程。这个过程看起来一点不“高级”,但它决定了你后面所有工作能不能顺利进行。
我见过不少朋友在Kaggle或GitHub上找到一个不错的项目,兴奋地clone下来,结果卡在环境依赖上两天,最后不了了之。教程断层就在这里——几乎没有人认真讲“如何让一个AI项目可复现、可维护”,而这恰恰是工程的核心。
1.2 AI工程和传统软件工程最本质的区别:不确定性管理
我在写这个项目之前有将近十年的后端开发经验。传统软件工程里,代码行为是确定的:输入A,走完逻辑,输出B。你可以写单元测试,可以精确控制流程。
AI系统完全不是这样。同一个Prompt,同一个模型,温度调到0它偶尔还会给你不同的答案。数据一更新,模型精度可能突然掉了几个点。上周测得好好的功能,这周用户反馈变了个说法,效果立刻变差。
所以AI工程的核心不是“实现功能”,而是“管理不确定性”。我们要做的所有工作——数据版本化、评估集设计、灰度发布、监控告警——本质上都是在把不可控的因素逐步关进笼子里。这是我写这个项目时最重要的一条主线,想明白了这一点,后面每一步你都知道自己为什么要做。
1.3 这个项目到底适合谁来学
先说结论:适合三类人。
第一类是从传统开发转过来的工程师。你已经具备代码能力,缺的是AI工程里数据怎么管、模型怎么选、部署形态怎么定这些“非模型知识”。第二类是在用API但想深入一步的产品或自学爱好者。你已经知道Prompt怎么写,但还想搞清楚微调是怎么一回事、什么时候该微调、什么时候不该微调。第三类是刚入行的学生,学校教了理论,但你需要一个可落地的最小闭环来建立体感。
不适合谁呢?如果你想从零推导数学公式,那出门左转去看论文。我这里只讲怎么把东西做出来、怎么让它稳定跑下去,不搞学术。
2. 环境与基建:先让项目达到“克隆即运行”
2.1 为什么我放弃conda和pip,选了uv做环境管理
先说一个现实问题:Python环境管理是AI项目里最烦人的事情之一。早期我习惯用conda,但conda装包慢、环境大,而且依赖解析经常撞车。后来项目多了,我彻底转到了uv这套工具上。
uv是一个用Rust写的Python包管理器,兼容pip和requirements.txt,但速度快得多。实测下来,一个常见的transformers环境,uv安装速度能比pip快五到十倍,锁文件清晰,还能自动管理Python版本。最关键的是它支持pyproject.toml——项目依赖写在声明文件里,任何人clone下来后只需要两条命令:
uv sync uv run python scripts/train_lora.py这基本实现了“克隆即运行”。我强烈建议所有新项目都从uv init开始,而不是继续手工维护那份容易过期的requirements.txt。
提示:如果是在国内网络环境下,安装
uv可以用cargo install uv或者直接下载发布包,速度都还不错。Python官方源如果太慢,记得把pip或者uv的index换到国内镜像,这点后面很多操作都会受益。
2.2 一个最小AI项目骨架长什么样
很多初学者拿到一个AI项目后第一反应是“代码在哪个文件”。但工程化之后,项目结构比代码本身更能决定你的效率。我现在的标准骨架大致是这样的:
ai-engineering-from-scratch/ ├── pyproject.toml ├── Makefile ├── src/ │ └── app/ │ ├── data/ │ │ ├── load.py │ │ ├── clean.py │ │ └── versions.py │ ├── model/ │ │ ├── infer.py │ │ └── train_lora.py │ └── eval/ │ └── run_eval.py ├── scripts/ │ ├── download_weights.sh │ └── run_web.sh ├── tests/ │ ├── test_data.py │ └── test_infer.py ├── data/ │ ├── raw/ │ ├── processed/ │ └── versions/ └── artifacts/ ├── checkpoints/ └── eval_results/几个关键模块我解释一下。
pyproject.toml是项目声明的核心,依赖、元数据、运行命令统统记在里面。Makefile则是给“记不住命令的人”准备的入口。比如我会把常用操作写成:
BIN := uv run train: $(BIN) python scripts/train_lora.py eval: $(BIN) python scripts/run_eval.py serve: $(BIN) python -m app.web.server这样你不需要记住去哪看命令行,只用make train、make eval就完事了。
data/raw和data/processed分开存放很重要——原始数据永不改动,任何清洗都生成新文件放到processed。这个习惯让我避免了很多“数据被覆盖后追不回来”的悲剧。
2.3 模型权重下载:Hugging Face CLI与镜像加速
环境搞定后,第一步就是下载底座模型。很多人卡在Hugging Face下载太慢,其实解法很简单:只用一行环境变量切到镜像站点。
export HF_ENDPOINT=https://hf-mirror.com然后命令行登录一次:
huggingface-cli login输入你的Access Token即可。有些模型(比如Llama系列)需要先接受使用许可,用CLI下载时如果遇到401或403,先去Hugging Face官网的模型页面点击同意协议,再回来重试。
之后下载权重就非常直接了:
huggingface-cli download Qwen/Qwen2.5-7B-Instruct --local-dir models/Qwen2.5-7B-Instruct提示:
--local-dir而不是--cache-dir的好处是,权重文件会以一个清晰明了的目录结构存在项目的models/下,不会被Hugging Face默认的缓存目录打散成一堆哈希文件夹。做工程,目录可读性比省几GB磁盘重要得多。
2.4 实验追踪:为什么用MLflow而不是TensorBoard
刚开始做微调实验的时候,我只是把loss打到终端日志里,结果第二天就忘了哪个参数组跑出了最好的结果。后来换成了MLflow做实验追踪。
选择MLflow而不是TensorBoard,最大理由是它的Tracking模块能同时记录参数、指标和产物,而且支持本地方案,不依赖云服务。我用一个最简单的初始化就能开跑:
import mlflow mlflow.set_experiment("qwen2.5-sft-v1") with mlflow.start_run(): mlflow.log_param("lora_r", 16) mlflow.log_param("lora_alpha", 32) mlflow.log_param("learning_rate", 2e-4) mlflow.log_metric("eval_f1", 0.83) mlflow.log_artifact("artifacts/checkpoints/best_model", artifact_path="checkpoints")记录完之后,浏览器打开http://localhost:5000就能看到每次实验的参数和指标对比。这一步看起来简单,但长期价值非常大——你以后做模型迭代时,能一眼看出上一次成功的配置是什么,不用再靠猜。
3. 数据工程:真正的护城河
3.1 为什么数据工程要放在模型前面
微调过模型的人都知道一个体会:模型效果不好,90%的情况不是模型不强,而是数据不行。脏数据、不均衡数据、错误标注,这些问题会直接反映到最终行为上。你想靠调参挽回数据问题,基本是徒劳。
所以我写这个项目时,把数据工程放到了所有模型操作的优先级前三位。这不是说模型不重要,而是模型已经是公开的商品化的能力,你对效果的控制权其实绝大部分来自数据。
3.2 数据清洗的具体规则
我用的清洗流程,大概分为五步,每一步都很朴素但不做不行。
去重:用text的哈希值做主键去掉完全重复的样本,再用模糊相似度(例如rapidfuzz的token set ratio)去掉语义重复的样本,阈值一般定在85分以上。
去污染:如果样本里包含改写过的Prompt痕迹、或者候选答案本身就是从某大模型生成的,这类数据对微调有潜在风险。我一般会做一个规则过滤,去掉含有“作为AI助手”“我不能帮助”这类标志性短语的样本。
去除低质量内容:按长度过滤,过短样本去掉,过长样本截断到目标上下文长度的1.5倍再保留。统一编码为UTF-8,去掉不可见字符和控制符号。
个人信息脱敏:手机号、邮箱、身份证号这类信息用正则替换成占位符,别指望模型为你保密,训练数据里不应出现真实隐私信息。
最后人工抽检:清洗完后随机抽100条人工过目,确认过滤规则没有误伤正常数据。这个抽检比例不高,但能拦住八成规则拍脑袋写错的情况。
3.3 标注策略:先用弱监督,再用人工校验
做监督微调(SFT)必须要标签,但AI项目里数据量大、标注成本高是永远的痛点。我的策略是“弱监督规则标注 + 人工抽样校验”。
比如做一个客服意图分类的数据集,我先写一批简单规则:
def weak_label(text: str) -> str: if any(kw in text for kw in ["退货", "退款", "换货"]): return "after_sales" if any(kw in text for kw in ["密码", "登录", "账号"]): return "account_issue" if "多少钱" in text or "价格" in text: return "price_query" return "other"先用这类规则给大量未标注数据打粗标签,然后人工抽300条做准确率评估。如果某个类别准确率低于80%,我就加规则或人工重标那一类。这样能花比较少的钱,得到一份质量可控的训练集。
注意:弱监督规则不能只标“对”的样本。很多新手会忽略“负样本”和“拒答样本”,比如用户表达感谢、闲聊时,应该引导模型给出恰当回应,而不是硬塞到业务分类里。训练数据里如果没有这类样本,模型上线后很容易把无关对话也强行归类。
3.4 数据版本化:别再用文件名记版本了
刚开始做项目时,我处理数据的习惯是train_v2.csv、train_final_final.csv,后来自己都分不清哪个是哪个。现在我用一套非常轻量的版本化方案。
核心思路是:每次生成新数据集时,同时生成一份dataset_card.json,记录来源、清洗规则、样本数、时间戳和hash值。
{ "name": "helpdesk_intent_v3", "source": "customer_logs_20250201", "clean_rules": ["dedup", "personally_identifiable_information_masking", "length_filter_8_1024"], "num_samples": 12580, "created_at": "2025-02-05T10:00:00Z", "sha256": "a3f2c8e1b9..." }真正训练的时候,直接在配置里写数据集版本的路径,模型训练完也把对应的dataset_card.json记录到实验追踪系统里。这样任何一个效果变差的问题都能反过来追溯:是数据变了,还是模型变了,还是代码变了。这一招在后续迭代里帮了我大忙。
4. 模型选型与微调:用决策树代替盲目试错
4.1 先算账再选模型
很多人选模型只看排行榜,谁分高用谁。但工程必须看约束条件。我的选型计算从三件事开始:显存、延迟、许可证。
显存估算有个非常实用的公式:加载FP16格式的模型大约需要参数量 × 2字节。一个7B模型大概需要14GB显存才能跑推理,13B需要26GB,34B需要68GB。如果做推理服务还需要加上KV Cache,实际占用量会比这个基础值再高一些。
我做了一张常用的估算表供参考:
| 模型规模 | 参数量 | FP16推理显存估算 | 微调建议 |
|---|---|---|---|
| 7B/8B | 约70~80亿 | 14~16GB | 单张24GB卡可跑QLoRA |
| 13B/14B | 约130~140亿 | 26~28GB | 单张48GB卡或双卡分布式 |
| 34B/72B | 340亿以上 | 68GB以上 | 多卡或托管API |
然后算延迟。同样一个请求,7B模型在单张A10上大约几十毫秒到几百毫秒,72B模型即使有A100,延迟也可能翻几倍。你的业务场景如果要求首字延迟低于200毫秒,那72B可能一开始就不该进候选名单。
最后看许可证。不同模型的商用条款差异很大,比如部分模型仅限研究使用,部分允许商用但要求履行额外披露义务。选型之前,把许可证条款和法务过一遍,别等上线了再发现不能用。
4.2 Base模型与Chat模型的差别
Hugging Face上同一个系列通常会出两种版本:Base版和Instruct/Chat版。新手经常搞混:是不是微调就应该从Base开始?
我的建议是:绝大多数业务场景,直接用Chat/Instruct版继续做LoRA,不要从Base重新训。
原因很简单。Chat版在发布前就已经做过大规模的监督微调和偏好对齐,它知道怎么回答问题、怎么拒绝不合适的请求。你的领域任务是在这个良好基础上做“风格和内容迁移”,而不是从零教它说话。从Base开始意味着你得自己准备大量通用对话数据,成本极高,而且很容易训出“能说领域话、但整体对话能力下降”的模型。
4.3 LoRA实操:一套能直接抄的配置
选完底座,微调本身我建议从LoRA入手,不要上来就全参数微调。LoRA的优势是显存占用低、训练快、一个底座可以同时挂多个任务适配器,切换部署非常方便。我更进一步的实践是直接用QLoRA,把底座量化到4bit再挂LoRA适配器,一张24GB的卡就能训7B模型。
参考配置:
model_name: Qwen/Qwen2.5-7B-Instruct lora_r: 16 lora_alpha: 32 target_modules: - q_proj - k_proj - v_proj - o_proj - gate_proj - up_proj - down_proj lr: 2e-4 batch_size: 4 gradient_accumulation_steps: 4 num_epochs: 1 warmup_ratio: 0.03 max_seq_len: 1024这些参数的相对关系比绝对值重要。lora_alpha一般设成lora_r的2倍,学习率在1e-4到5e-4之间比较稳。训练一个epoch就够了,SFT阶段真不需要多轮训练,轮数多了模型很容易在训练集上过拟合,导致通用能力下降。
训练时我还会固定住一个关键开关:use_chat_template必须开启。Chat模型的训练数据要按它的对话角色格式组织,如果丢给模型一段裸文本,它会非常困惑。数据格式大致是:
<|im_start|>user 如何查询我的订单状态?<|im_end|> <|im_start|>assistant 您好,请提供您的订单号,我帮您查看。<|im_end|>每一行都要严格走这个模板。用transformers自带的apply_chat_template方法生成,千万别手写拼接,手写必错。
5. 评估体系:别让“感觉变聪明了”骗了你
5.1 为什么训练loss下降不代表效果变好
这个问题我踩过很大的坑。有一次我微调完一个模型,训练loss从2.1降到了0.8,当时觉得稳了。结果一上测试集,意图识别的准确率反而掉了3个百分点。
原因后来分析清楚了:loss下降只能说明模型在训练数据上的拟合度提高,不代表它学会了“泛化”。尤其是生成任务里,模型经常会把答案背下来,而不是理解规律。所以评估必须用独立于训练集的、贴近真实分布的评测集,绝不能只看训练曲线。
5.2 离线评估:Golden Set怎么设计
我的做法是维护一个“黄金评测集”,目标是覆盖真实业务中最重要的场景和已知的模型弱点。这个评测集不大,通常几百到一千条,但每条都经过人工复核标注。
设计Golden Set时我坚持三条原则。
第一,贴近真实分布。用线上日志样本的抽样和标注结果做了100多条真实用户问题,其余再手工补充边界情况。不要用模型自己生成的数据评估自己,那会高估效果。
第二,包含已知弱点。比如我发现模型经常把“改地址”的意图误判成“物流查询”,就往评测集里专门加一批这类难样本,防止以后迭代时这类问题悄悄复发。
第三,定期更新。每次上线的新功能、每收到一批典型badcase,都要补充进评测集。Golden Set需要跟着业务长,不是做一次就放那吃灰。
对于生成任务,我倾向用“规则化指标 + 关键要素匹配”。例如客服回答质量,我预先定义3到5个必须覆盖的要素(订单号、解决方案、致歉),检查模型输出是否包含这些要素。这种方法比直接让大模型打分更稳定,也更容易定位问题。
5.3 在线评估和灰度发布
离线评估过完,线上依然要有护栏。不要让你的模型一上线就面对100%流量。
我第一次上线时直接全量切过去,结果半天后收到用户投诉“回答像换了一个人”。虽然离线指标都是好的,但线上用户有些固定表达方式,Golden Set没有覆盖到。
从此以后我坚持灰度流程:先切5%流量观察一小时,没有明显异常(报错率、用户差评率)再逐步扩大到30%、100%。同时把每一次线上应答日志回流到数据池,作为下一轮微调的数据来源。这个循环一旦跑起来,你的系统就像有了“自我进化”的能力,每个月的效果都会往上走。
6. 从实验到生产:部署形态、失效边界与成本权衡
6.1 三种部署形态,各有什么代价
模型训好了,部署方式的选择会直接影响体验和账单。我见过团队把一个7B模型用CPU服务,结果单次推理响应时间冲到几十秒,用户自然留不住。部署不能拿“能跑”当唯一标准,必须在延迟、成本、数据隐私这几个维度之间做权衡。
我整理了三种常见形态供对照:
| 部署方式 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 托管API(如OpenAI、国内大模型API) | 上手最快,效果强,无需GPU管理 | 单次调用费累积快,数据要出域,定制能力弱 | 快速验证、并发低、数据不敏感 |
| 自托管开源模型(vLLM/TGI) | 推理成本可控,数据不出域,可挂LoRA适配器 | 需要GPU机器,运维复杂度高 | 有稳定流量、需要定制模型 |
| CPU/边缘小模型 | 成本极低、无GPU依赖 | 延迟高,能力有限 | 内部工具、离线批处理、强环境约束场景 |
如果你的业务已经过了验证期,我通常建议走“自托管+适配器”的路线。GPU用按需租用,单张A10或L20起步,先把吞吐指标跑出来,再决定要不要长期包机。
6.2 失效边界设计:超时、重试、缓存、降级
模型服务和普通API不一样,它不是每次都能给出正确格式。你的工程一定要预设“如果模型罢工了,系统怎么兜底”。
超时:首字返回超过10秒直接断开,重新走降级逻辑。不要无限等待,那会拖垮整个调用链路。
重试:重试策略要有上限,而且要在指数退避基础上加随机抖动,避免重试风暴。对生成任务,相同输入重试结果可能不同,所以重试时可以考虑把温度调低,提高稳定输出的概率。
缓存:对高频且可复现的查询做结果缓存。比如“客服入口在哪”“退货政策是什么”,这类问题的答案基本固定,直接打缓存能省大量推理资源。我用Redis做LRU缓存,缓存命中率一度超过30%,等于白赚了三分之一的机器预算。
降级:当模型服务异常、或结果无法通过校验时,回退到模板话术或人工介入。你要的不是“永远不失败”,而是“失败了用户也不至于滞留”。
另外,解析模型输出时不要只祈盼JSON格式完美。真实场景里,大模型给出的JSON基本都会掺杂多余文本、换行或注释符。不要无脑重试,而是要写一个鲁棒解析函数:先用正则抽取最外层的{...},再做二次解析,失败后才进入重试流程。
6.3 成本账单:一次真实的估算
很多读者会问,自托管模型到底要花多少钱?
我按一个小场景算给你看。假设你有10个并发用户,每个用户平均每天20次请求,每次请求约300个输入token、200个输出token。
用7B模型自托管,单张A10 24GB显卡大概能把吞吐跑到10~20请求/秒,足够覆盖这种并发需求。按租用价每小时10元来算,全天跑满也就240元一天。托管API按每百万token大约5元的价格,同样请求量一天可能要花50到80元。看起来API更便宜?别忘了还有调用频率上去了、数据出域、以及无法使用自定义LoRA的隐性成本。
如果每天请求量再翻十倍,托管API的费用会线性增长,而自托管只要把显卡从1张加到3张,单位成本反而下降。这就是为什么流量稳定后我普遍建议迁移到自托管的原因:固定成本你付得起,边际成本你压得下。
6.4 监控和维护:模型上线只是开始
模型上线后,维护工作其实比训练更多。我每天会看四类监控指标:延迟分位数(P50/P95,关注长尾,而不是平均)、输入分布漂移(用户的话术模式突变要及时识别)、结果拒绝率(格式解析失败的占比,上升说明模型行为在漂移)、以及用户反馈信号(点赞、点踩、人工介入率)。
这些指标一旦触发阈值,我不会直接盲目重训模型,而是先回看对应的badcase,确认是不是数据分布变了。如果是,把最近的新样本补充进Golden Set,先线上微调规则或Prompt缓解,攒够一批数据后再做下一轮LoRA微调。这整套“监控→回流→重训→灰度”的循环,才是一个AI系统真正可持续的形态。
提示:内容安全过滤不要放在模型之后才做,最好在请求进来时、模型输出后都做一遍关键词和分类器检查。这是底线工程,宁可误伤,不可漏过。
最后再分享一个我个人的习惯
这个项目做到现在,我最深的体会是:AI工程最大的敌人不是“模型效果不够好”,而是“你搞不清楚哪儿出了问题”。数据版本化、实验追踪、Golden Set、灰度发布,这套流程看起来不酷,但它们才是让AI项目从“能用”走向“可信赖”的真正杠杆。
如果你刚准备起步,我建议你从一份真实的数据集开始,先把“数据清洗→微调→评估→部署”的最小闭环跑通,不要一开始就追求72B大模型和复杂分布式训练。先把小模型的全链路摸透,再慢慢往上加规模。这条路我已经替你们踩过一遍了,按这个顺序走,你能少绕很多弯路。