
别人问我在做什么我说在做AI工程他们会先入为主地觉得我天天在训练模型、调参数。做了一整年之后我想说真正的AI工程大多数时间其实都在跟数据、接口、异常处理较劲模型反而是最好搞定的一环。这篇内容就是从我自己的经历出发聊聊一条“from scratch”的成长路径以及一个完全没有AI基础的人到底该怎么一步步把AI工程这件事跑通。这里的“from scratch”不是说非要自己从零手写神经网络的每一层而是指不依赖现成的“保姆级平台”——真正把数据、模型、训练、部署、调优全链路理解一遍亲手打通一套最小可用的系统。很多新人上来就想挑战大规模语言模型微调或者多模态系统结果被环境配置、数据清洗、显存溢出三座大山直接劝退。我的建议是换一条路走先把小模型的小任务做熟再一步步扩展到LLM应用。这篇文章适合几类人刚入门想做AI应用开发的学生工作中需要把算法落地成服务的工程师以及想转型做AI产品但不知道从哪下手的朋友。我会尽量把每一步的“为什么”也讲清楚而不只是给一段可以抄的代码。1. 从“调包侠”到AI工程一条反常识的学习路径1.1 为什么说“从零开始”不等于“从算法开始”多数人学习AI的第一反应是找一本深度学习教材从头啃反向传播、注意力机制啃完再去看PyTorch文档。这条路不是不对而是它对一个目标是“做工程”的人来说效率实在太低了。工程视角的“从零开始”应该是你想让一个系统完成什么任务把这个任务拆成数据、模型、服务、评估四块然后逐块打通。算法细节可以在用到的时候再补而不是在最开始就把自己淹没在数学公式里。我见过太多人卡在“我觉得我还没学好理论不敢动手写代码”这个心态上。实际上AI工程是典型的“先跑通再优化”的领域你第一次写的训练循环哪怕很丑、哪怕只在玩具数据集上能跑它给你的正反馈也比啃三章教材强得多。先建立完整的工程闭环感再回去补理论你会突然发现很多公式都有了落地的意义。1.2 以终为始先看清一条生产级流水线长什么样如果不清楚终点长什么样很容易在中间走弯路。一条哪怕是初级的AI工程流水线通常都包含下面这些环节数据获取与清洗原始数据永远脏到你难以想象格式不统一、缺字段、标签错误、编码乱这些才是日常。特征工程与预处理文本要分词、过滤停用词图片要缩放、归一化表格数据要处理缺失值。这块往往比模型本身更影响最终效果。模型选择与基线先有一个最简单的模型跑通流程拿到一个不漂亮的基线分数再去换更复杂的模型。训练与验证训练循环、验证集、早停、超参调整保证模型不仅能跑还能稳定收敛。部署与服务化把模型封装成接口处理并发、限流、日志、异常。监控与迭代上线之后观察真实数据分布漂移定期评估持续迭代。一个人做全流程可能听起来很吓人但关键是先把这个闭环跑起来哪怕每个环节都简陋一点。我自己的第一个项目只用了一千条数据、一个非常小的文本分类模型但完整经历了清洗、训练到部署的过程之后再看任何AI系统脑子里都会自动浮现出它处在流水线的哪一环。1.3 我现在推荐的学习顺序如果让我重新走一遍我会这样安排学习路径保证每个阶段都有看得见的产出Python基础与数据处理不用等到精通能熟练用pandas、numpy处理表格和文本就够了一周时间足够。机器学习最小闭环用scikit-learn跑一个分类/回归任务理解训练集、验证集、准确率这些核心概念。深度学习框架入门选PyTorch也可以选JAX但生态还是PyTorch最顺跑通一个图像或文本的小模型训练循环。部署最小实践用FastAPI封装模型在本地跑通接口调用理解前后端是怎么连起来的。再回头补理论这时候再读关于损失函数、反向传播、Transformer架构的资料你会觉得每一段都似曾相识且恍然大悟。最后再上LLM与RAG有前面完整的工程直觉再接触Prompt设计、向量检索、大模型API就不会觉得它们是玄学。这套顺序的最大特点是每两周都能有成果而不是学了三个月还在“打基础”。信心这个东西在工程入门阶段比智商重要多了。2. 最小可行项目用文本分类跑通第一条训练循环2.1 项目设定为什么选文本分类当第一个项目文本分类几乎是AI工程入门的最佳项目形态。它不需要处理图像那样复杂的数据增强也不需要像生成模型那样担心输出的多样性任务定义清晰评估指标直观而且数据集很容易通过公开渠道获取。我第一次完成的项目是“客服工单自动分类”把客服收到的用户反馈分成“账单问题”“网络故障”“设备维修”“其他”四类。数据集不过几百条Excel表格都是人工标注过的历史工单。这个项目看似简单但它强行让我把整条流水线的每个环节都亲手做了一遍从CSV里各种奇怪的换行符到模型上线后被人投诉分类不准每一步都是真实世界的毒打。为了读者复现方便我建议你找一个公开的数据集比如新闻分类、情感分析都可以。重要的是数据量不要太大几千条就够因为你的目标不是刷精度是打通流程。2.2 数据准备先别碰模型至少花一半时间在数据上多数新手最容易犯的错误是急着把数据丢给模型结果发现训练出来的东西根本不能用然后又不知道为什么。我可以负责任地说AI工程里70%的坑都出在数据处理阶段而不是模型阶段。以我当时的工单数据为例里面的真实情况包括同一类问题有七八种不同的描述方式、有的字段里混入了HTML标签、有些标签明显标错了、还有大量空值和重复记录。我想在真实工单数据集上训练出一个能用的模型第一步就是做数据探索。import pandas as pd df pd.read_csv(tickets.csv) print(df.head()) print(df.info()) print(df[category].value_counts()) # 清洗基本问题去空白、去重、统一文本大小写 df[text] df[text].astype(str).str.strip() df[text] df[text].replace(r\s, , regexTrue) df df.dropna(subset[category]) df df.drop_duplicates(subset[text])清洗完之后我还干了件很关键的事把类别不均衡的情况列出来看看。“网络故障”的样本可能是“设备维修”的五倍如果不处理模型学到的就是“永远猜网络故障”。最简单的处理方式是过采样少数类或者给损失函数加类别权重后者的实现成本更低。我在处理时用了class_weightbalanced这个参数让少数类样本在计算损失时获得更大的权重这个操作直接让“设备维修”这一类的F1分数提高了十几个百分点。建议你也在自己的项目里试一下你会很明显地看到分类报告里各类指标的变化。2.3 训练循环的骨架与其复制粘贴不如手打一遍在第一次跑通训练循环时我强烈建议你不要直接复制现成的完整代码而是自己动手敲一遍逻辑。原因很简单训练循环里的每一步都对应着概念手敲一遍之后你才能从“背代码”进步到“理解它在干什么”。一个标准的PyTorch训练循环骨架是这样的import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset class TicketDataset(Dataset): def __init__(self, texts, labels): self.texts texts self.labels labels def __len__(self): return len(self.texts) def __getitem__(self, idx): return self.texts[idx], self.labels[idx] # 模型不复杂Embedding LSTM 全连接 class SimpleClassifier(nn.Module): def __init__(self, vocab_size, emb_dim, num_classes): super().__init__() self.embedding nn.Embedding(vocab_size, emb_dim) self.lstm nn.LSTM(emb_dim, hidden_size64, batch_firstTrue) self.fc nn.Linear(64, num_classes) def forward(self, x): embedded self.embedding(x) output, (hidden, cell) self.lstm(embedded) last_hidden hidden[-1] return self.fc(last_hidden) # 训练 model SimpleClassifier(vocab_sizevocab_size, emb_dim64, num_classes4) optimizer torch.optim.Adam(model.parameters(), lr1e-3) loss_fn nn.CrossEntropyLoss() for epoch in range(10): total_loss 0 for batch_texts, batch_labels in DataLoader(train_dataset, batch_size32, shuffleTrue): optimizer.zero_grad() logits model(batch_texts) loss loss_fn(logits, batch_labels) loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch1}, loss: {total_loss})这个循环有四个关键动作缺一环都不行zero_grad()清掉上一轮的梯度forward前向传播拿到输出backward()计算梯度step()用梯度更新参数。新手最容易忘掉zero_grad()后果就是梯度在批次之间不断累加loss曲线飘忽不定。在这个项目里我用的词表是自己基于训练数据构建的。简单做法是给每个出现过的词编一个索引加一个UNK给没见过的词。值得注意的一点是验证集里如果出现词表外的词就映射到UNK不要跳过样本否则你的dataloader会在评估时报错。2.4 验证与过拟合准确率不是唯一的答案训练跑通之后我的第一个模型在训练集上准确率高达98%当时还挺兴奋结果放到验证集上一看只有72%。这就是典型的过拟合模型把训练样本背下来了而不是学到了模式。应对过拟合的思路按见效速度排增加训练数据哪怕做一个简单的数据增强。减小模型容量比如把LSTM的hidden_size从64降到32。加Dropout或L2正则化。用早停监控验证集loss连续若干轮不下降就停。我当时加了nn.Dropout(0.3)并采用早停验证集准确率稳定在了85%左右。这个数字并不惊艳但对一个小模型来说已经是能上线服务的水平了。还有一个新手容易忽略的点只盯着准确率会被坑。因为样本不均衡时模型全部预测多数类也能拿到很高的准确率。我当时就是看了分类报告才发现“设备维修”的召回率是0。正确的做法是看每个类别的精确率、召回率和F1分数对自己模型的效果有一个更精确的把握。3. RAG工作流从玩具到可用系统的关键转折3.1 什么时候该从“训练小模型”升级到“LLM应用”当你已经能在自己的小模型上完成训练、评估和部署的闭环再面对“我要做一个客服问答机器人”这类需求你很快会发现传统小模型的天花板——它能分类但没法生成内容、没法理解没见过的问法更没法回答需要外部知识的问题。这时候就该接触到LLM。很多人以为用LLM就是从OpenAI或开源模型调用API然后把Prompt一写就结束了。实际上把一个LLM从“聊天玩具”变成“可用的业务系统”关键要看两点私有知识怎么接入以及输出质量怎么保持稳定。这就很自然地引出了RAG检索增强生成。3.2 RAG的基本流程检索拼接生成RAG的思路一句话讲清楚不要指望大模型记住所有知识而是让它在回答之前先从知识库里检索到相关文本然后把这些文本连同用户问题一起喂给模型生成答案。它的好处是知识库可以随时更新不需要重新训练模型模型的回答有依据不容易随意编造而且你可以追溯它用的哪一篇资料便于处理“胡说八道”的争议。我在实践中搭RAG用的流程非常简单把文档比如产品的FAQ、操作手册切成片段一般按段落或固定长度切片。用Embedding模型把每个片段转成一个向量存进向量数据库。用户提问时把问题也转成向量在库里做相似度检索取Top 5相关片段。把这些片段和用户问题拼成一个Prompt交给LLM生成答案。用代码实现的话核心部分大概长这样from sentence_transformers import SentenceTransformer import numpy as np # 加载Embedding模型这里选的是小巧易用的开源模型 encoder SentenceTransformer(BAAI/bge-small-zh-v1.5) # 文档切片与向量化 chunks split_documents(documents, chunk_size300) chunk_vectors encoder.encode(chunks) # 检索把问题和所有片段向量的余弦相似度算一遍取最高分的几个 def retrieve(query, top_k5): query_vec encoder.encode([query])[0] scores cosine_similarity(query_vec, chunk_vectors) top_indices np.argsort(scores)[::-1][:top_k] return [chunks[i] for i in top_indices]如果是当作演示直接用numpy算相似度就够用。但真的要上到几十万条文档、还要做并发检索的级别就该换用专门的向量数据库在工程上更靠谱不然检索性能会成为瓶颈。3.3 检索质量比模型大小更重要一个常被忽视的真相在RAG系统里很多人把注意力全放在“用哪个生成模型”上结果忽略了一个更关键的问题检索结果到底准不准。如果检索回来的5个片段里有3个是无关内容再强的生成模型也只能被带到沟里去。我自己验证过一个很典型的案例同一个客服知识库换掉检索策略之后回答的正确率提升了将近30%。提升点主要来自三处切片策略把固定长度切片改成按语义段落切片避免一句话被切成两半。检索数量原来只取Top 3改为Top 5并让模型在Prompt里注明“如果下文没有相关信息请直接说不清楚”。查询改写把一次检索改成针对用户问题关键词的两次检索合并结果去重。这里的关键认知是RAG系统的质量天花板主要由检索环节决定而不是由生成模型决定。模型再聪明检索的内容不对它也只能巧妇难为无米之炊。在这个阶段你还需要建立“评估”的闭环。我当时的做法是维护一个固化的问答测试集每次改完检索策略都跑一遍这个测试集统计答案中“包含正确知识片段”的比例宁可慢一点也要保证每一次改动带来的都是正向效果。4. 部署与推理优化模型能用和好用是两码事4.1 模型包装成服务FastAPI是最顺手的解法模型训练好只是万里长征第一步在生产环境里别人只能通过接口来使用它。无论前面做得多么出色没有稳定的服务一切都等于零。所以要把“模型能用”变成“模型好用”部署服务这一环我强烈推荐用FastAPI异步、轻量又省事而且自带接口文档。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() model load_model() class Query(BaseModel): text: str class Response(BaseModel): label: str score: float app.post(/predict) def predict(query: Query): label, score model.predict(query.text) return Response(labellabel, scorescore)如果只是在本地跑一跑你可能体会不到服务化的意义。但一旦要对接前端、给别的同事调用、让脚本批量处理数据这个接口就是整个系统的门面。我当时第一次部署完用curl试了一下接口能返回正确结果那种感觉和“模型在Jupyter Notebook里跑出准确率”是完全不同的——前者意味着真的被使用了。部署环节有四个最容易出问题的点提前处理能省下大量时间模型预热服务启动后先跑一次推理避免第一个真实请求因为模型懒加载而超时。输入校验用Pydantic做好类型与字段校验别让脏数据跑到模型内部才报错。异常隔离把模型推理包在try/except里返回友好的错误信息而不是直接把堆栈抛给调用方。版本管理模型文件和数据预处理方式一起记录版本否则有一天改了数据预处理逻辑线上模型效果突然变了你都不知道为什么。4.2 量化、批处理与并发把成本打下来的实践调好接口之后你可能很快发现模型推理有点慢尤其是当模型体积稍大、请求量一上来一次推理几百毫秒就会成为瓶颈。优化推理性能的常见手段主要包括模型量化把浮点权重从FP32压到FP16甚至INT8速度能提升两三倍效果损失很多时候在可接受范围内。批处理把多个请求合并成一个批次推理能用满GPU并行能力。这在文本分类这类小模型上尤其明显批量推理吞吐量能提升一个量级。缓存对同样的输入直接返回缓存结果适合重复提问比例高的场景。我在文本分类那个项目里试用过最简单的PyTorch量化代码很短import torch model torch.quantization.quantize_dynamic( model, {torch.nn.Linear}, dtypetorch.qint8 )模型体积缩小近四倍推理延迟从13ms下降到5ms准确率几乎没有变化。这个优化对一套小服务来说收益非常明显。有一点要提醒量化不是、也不应该是无脑开启的。如果是复杂的生成模型量化后可能出现输出质量下降尤其是代码生成、数学推理这类任务非常明显。所以量化前后一定要保留评估集用数字对比说话而不是感觉“差不多就行”。4.3 上线后的监控与回归测试没有评估就没有迭代模型一旦上线事情并不会就此结束。真实用户的数据分布和你的训练数据很可能不一样用户提问的方式、术语、长度都可能漂移。所以从上线第一天起就要记录两样东西输入日志和预测日志。我当时在日志里发现了一个特别有意思的现象很多用户会发超长文本而模型在训练时基本没见过超过200字的样本。于是把所有超过最大长度的输入做了截断处理模型开始逐步输出合理结果。这种问题如果不在线上持续观察靠离线评估是根本发现不了的。另外一个工程上的好习惯是准备一套回归测试集。每次更新模型或者调整预处理逻辑都先跑一遍回归测试集确保老的正常功能没被自己改坏。我第一次更新模型时就没跑回归测试改完乱整了热词表结果之前好好的中性评论被分到负面类别还被产品同事发现了。这碗毒打尝过一次就够了之后我再也没跳过回归测试。5. 半年踩坑实录数据泄漏、显存溢出与不稳定的Prompt5.1 数据泄漏准确率虚高还会被当成荣耀数据泄漏是AI工程里最隐蔽也最致命的错误之一。它的本质是模型在训练时“偷看”了本该在测试时才能见到的信息导致训练时看起来很强真实场景里却一碰就碎。我在做文本分类时遇到过两个例子。第一个是把文本做TF-IDF向量化的时候对整个数据集先fit再拆分训练集和测试集实际上测试集的信息已经渗进了训练过程模型表现虚高得离谱。正确的做法是先拆分数据再单独对训练集fit、对测试集transform。第二个例子是数据去重时把同一用户的多条工单一起处理结果同属一个用户的样本同时出现在训练和测试集合里模型记住的是用户特征而不是内容特征。如果你发现模型在训练集上表现极好、验证集上也不错但一上线就崩建议第一时间排查数据预处理过程里有没有“全局统计量”被提前计算了。5.2 显存溢出从无脑换大卡到优雅地用尽每一字节早年在没有显存概念的时候我拿显卡跑模型动不动就“CUDA out of memory”。我的第一反应是提高显卡配置但后来发现问题没那么简单显存溢出其实是有很多优雅应对方式的。主要方案有几个减小batch size、序列截断、梯度累积、使用混合精度训练。很多人不知道“梯度累积”是怎么解决的思路其实原理是如果batch size32会爆显存那就先用batch size8每4步才更新一次参数效果在数学上和一次看32个样本基本等价。accumulation_steps 4 for i, batch in enumerate(DataLoader(ds, batch_size8)): loss model(batch) loss loss / accumulation_steps # 归一化 loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()混合精度训练则是用torch.cuda.amp把前向计算的一部分放到FP16显存占用能降低不少同时保留FP32的精度用于梯度计算。这套组合技对付显存不足的问题比直接换显卡更务实。5.3 LLM输出不稳定Prompt工程要有工程化的纪律大模型输出的随机性是个绕不开的问题。同样一个问题加热度参数调成0.8它可能给出三种不同风格的回答这让“稳定输出”成了工程上必须解决的事情。我的基本做法是把生成参数固定下来包括temperature、top_p、max_tokens等不让它们随业务状态随意改动。另一个更关键的问题是让模型输出结构化内容。需要模型返回JSON时我会在Prompt里要求它只输出JSON并在代码里加入解析失败时重试一次或降级返回默认值的逻辑。import json, re def safe_json_parse(raw_output): # 有些模型会输出Markdown代码块包裹的JSON需要剥掉 cleaned re.sub(rjson|, , raw_output).strip() try: return json.loads(cleaned) except json.JSONDecodeError: return {error: parse_failed, raw: raw_output}Prompt的调试也要有工程化的方式。我自己维护了一个测试Prompt的清单每次修改系统提示词都会用同一组问题跑一遍对比输出质量而不是凭感觉看着“这次回答好像更好”。只有把Prompt当成代码一样去管理、去测试大模型应用才算真正进入了“工程”的范畴。在整个学习路径里最容易让人放弃的时刻不是第一次见到复杂公式也不是环境配置反复报错而是同时面对数据脏、显存爆、模型效果差、接口没人用的多重打击。我的经验是每次只拆解一个小问题解决之后再往前推进一小步半年之后回头看你已经走完了最艰难的那一截。把第一个小项目完整跑通比收藏一百份学习资料都有用。