1. 先搞明白:AI工程和“调模型”到底差在哪
1.1 什么是真正的 ai-engineering,而不是跑通一个 demo
先说个我自己的经历。前两年我第一次接触一个所谓的“AI项目”,实际上就是拿到一个公开的模型权重,写了个30行的Python脚本,调用了一下,输出了几个结果。
那叫“用AI”,不叫“AI工程”。
真正让我意识到两者差距的,是后来接了一个内部需求:公司想让员工在一个集中式入口里,用自然语言提问,然后系统从几十万份技术文档里找到答案,还要带着来源引用返回。
一开始我觉得这有什么难的——模型加载起来,做个embedding,接个向量库,完事了。
结果真正做下去才发现,模型加载跑通只是这个项目里最不起眼的5%。剩下95%的时间,都在处理数据质量、环境一致性、训练稳定性、线上延迟、评测标准、回归回归回归——这才是ai-engineering from scratch的核心。
所以这篇文章想分享的,不是某一个模型的调用代码,而是当你决定“从零开始把一条AI产品线真正落地”时,你需要面对的那些绕不开的问题,以及我踩过之后留下的方案。适合刚入门的算法工程师、想把模型产品化的后端开发,以及正在复现论文或开源项目、却被环境、数据、部署折磨到怀疑人生的学生。
标题里的“from scratch”有两层含义:
- 技术栈上的从零:不是用别人封装好的Paas平台拖几个组件,而是自己掌握链路里的每一个关键环节。
- 认知上的从零:打破“模型就是一切”的幻觉,建立工程化思维。
1.2 为什么选择自建,而不是直接套现成平台
做AI工程,第一件事通常是选型:用现成的大模型API,还是自己微调部署?用托管的向量数据库,还是自己起一个?这里没有标准答案,但有决策框架。
我当时的判断标准是四看:
- 看数据敏感性:企业内部文档涉及业务细节,不能随便出网。
- 看定制深度:问答效果依赖对私有术语的理解,通用模型明显不够。
- 看长期成本:调用量上来之后,按token付费的费用远高于自建推理。
- 看团队成长:希望团队借这个项目真正建立AI工程能力,而不是变成一个API转发器。
四条下来,“自建 + 开源模型微调 + 私有部署”是唯一合理的路径。
但自建有一个被严重低估的代价:链路变长了。以前用一个API,只需要关注输入输出;自建之后,你要关注数据管道、GPU资源、训练稳定性、推理吞吐、监控告警。每多一个环节,就多了一批潜在的故障点。这是所有从零开始的人要先做好的心理建设。
1.3 一个贯穿全文的实例:内部文档问答机器人
为了让下面的内容不至于飘在理论上,我以“企业内部文档问答机器人”作为贯穿案例。
项目目标很明确:用户输入一个问题,系统返回一段答案,并附带来源文档和页码。业务上要求:准确率不低于85%,首字节延迟低于2秒,支持30个并发。训练数据来自公司内部的技术文档、产品手册、历史工单,大约50万条。
这个例子覆盖了AI工程的主要模块:数据管道、模型微调、推理部署、评测反馈。后面每个章节都会用它来说明“为什么这么做”“参数怎么定”“踩了什么坑”。
2. 环境与基础设施:搭地基的姿势决定上层建筑的稳定程度
2.1 GPU选型与驱动,别一上来就被显存卡死
做AI工程,第一道坎通常是硬件环境。
我当时拿到的是一台双卡机器,配置是2块24G显存的GPU。很多人会问“显存选多大”,其实这个问题不应该脱离模型规模去谈。如果目标是微调一个7B参数量的模型,采用LoRA方式,24G显存跑fp16是够的;如果要全参数微调,24G会很吃力。训练和推理的显存需求差异也很大,推理阶段可以用量化大幅压缩。
选型的时候我列过一个粗略的评估表:
| 模型规模 | 推理最低显存(FP16) | 微调推荐显存(LoRA) | 全参微调推荐显存 |
|---|---|---|---|
| 1B以下 | 4G | 8G | 16G |
| 7B | 16G | 24G | 80G |
| 13B | 32G | 48G | 160G+ |
注意这只是经验值,实际要看序列长度和batch size。序列越长、batch越大,显存需求越高。
驱动和CUDA版本这块,我吃过一次亏:机器上预装了CUDA 11.8,但PyTorch版本要求CUDA 12.1,结果程序没有报CUDA错误,而是直接报“illegal memory access”,排查了一天最后才发现是版本不匹配。现在我的原则是:先查PyTorch官方对应的CUDA版本,再回过头配置驱动。跑一个简单的验证命令确认环境正常:
python -c "import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0))"如果输出显示的GPU名称正确,CUDA可用,再开始装模型,能省掉一大半玄学问题。
2.2 用Docker把环境固化下来,不要相信“在我电脑上是好的”
AI工程的复现性有一个大敌:环境漂移。
“在我电脑上跑得好好的”这句话,在AI项目里出现的频率远超其他软件项目。原因很简单,训练脚本依赖的Python包有几百个,每个包又有不同版本,某次升级一个小版本,可能就带来数值变化。为了把环境固化,我强烈建议从第一步就引入Docker。
我用的基础镜像形如:
FROM pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime RUN apt-get update && apt-get install -y git curl RUN pip install --no-cache-dir transformers datasets accelerate peft trl COPY ./requirements.txt /app/ RUN pip install --no-cache-dir -r /app/requirements.txt这样做的好处有三个:
- 环境与宿主隔离,不会再因为系统Python升级导致依赖崩掉。
- 训练、推理、测试用同一个镜像,结果可以对比。
- 部署的时候直接把镜像推到内部仓库,生产机和开发机环境完全一致。
我用的是conda还是Docker?答案是都要。conda负责创建Python虚拟环境,Docker负责把整个系统层传下去。在Docker里面再用conda管理Python环境,可以兼顾灵活与可复现。
2.3 实验跟踪:没有记录的训练都是白训
从零开始做AI工程,最容易忽略的一个环节是实验记录。
我早期就是这样:改个参数,跑一晚上,第二天醒来发现效果好了,但忘了昨天改了什么。那种“明明前一天还在变好,现在怎么退步了”的困惑,基本都源于没有做好跟踪。
后来我引入了MLflow,配合一套简单的命名规范。每个实验记录三样东西:
- 超参数:学习率、batch size、epoch数、LoRA rank
- 指标:训练loss、验证loss、准确率、F1
- 产物:模型检查点路径、评测报告、日志文件
命名规范是“日期-模型-版本-说明”,比如“20240601-bert-base-zh-v01-lr2e5”。不要小看命名,一个清晰的名字能让你三个月后翻记录时省下大量时间。
如果你不想引入额外的服务,也可以用最简单的CSV记录表。核心不是工具,而是“每次改动都有迹可循”这个习惯。
2.4 项目目录结构:让数据、代码、模型、实验各归其位
从零搭建AI工程,第一个该建立的不是模型,而是目录结构。混乱的目录是后期维护的地狱。我最后固定的结构是这样的:
ai-engineering/ ├── configs/ # 所有训练、推理、评测的配置 ├── data/ # 数据(原始、清洗后、切分后) ├── src/ # 代码 │ ├── data/ # 数据处理逻辑 │ ├── models/ # 模型定义 │ ├── train/ # 训练脚本 │ ├── infer/ # 推理逻辑 │ └── eval/ # 评测逻辑 ├── experiments/ # 实验记录与产物 ├── models/ # 最终模型权重 └── scripts/ # 运行入口脚本这个结构的原则是:数据和模型不跟代码混在一起,配置和实验结果又跟代码分开。实际用下来的好处是,很多时候只需要拷贝某个子目录就能完成交接,不用把“那一堆乱七八糟的文件”一起拷走。
3. 数据是真正的上线难点:清洗、版本化、切分
3.1 从原始文档到训练数据:清洗规则怎么定
很多从零开始做AI工程的人,会低估数据处理的比重。真实情况是:一个生产中可用的模型,其数据工程耗时往往占总工时的60%以上。尤其像文档问答这种场景,原始数据是PDF、Word、HTML混在一起,排版各异,直接拿去训练根本不可能。
我的清洗分四步走:
第一步,做格式转换。PDF用解析工具抽出文本,Word转成纯文本,HTML剥掉标签。这里要注意,PDF双栏排版很容易抽取错乱,需要按栏切分再拼接。
第二步,做段落切分。按标题层级和段落边界把文档切成“语块”,而不是简单按字符数硬切。我当时用了一套基于标题编号的规则,同时保留每块的来源元数据(文档名、章节路径、页码)。
第三步,做规则过滤。批量去掉页眉页脚、导航重复段落、空行、乱码字符。规则要写成脚本反复迭代,不要手动删。
第四步,做人工抽检。每清洗完一批数据,随机抽100条检查质量。如果错误率超过2%,说明清洗规则还需要补。
一点心得:清洗规则宁多勿少,因为脏数据给模型带来的伤害是隐性的——它不会报错,只会让效果差一点、再差一点,最终你根本不知道是模型的问题还是数据的问题。
3.2 数据版本管理:改过什么,永远要能查
谈到数据,AI工程师圈子里流传着一句话:“模型是数据的影子。”你改了一版数据,模型效果完全不同。这时如果你没记录数据版本,那么复现就是一个笑话。
我做数据版本管理的办法不复杂:用类似DVC的方式,但核心是把两样东西固定下来——数据文件的哈希值,和一份数据清单。
每当数据变更,我会生成一个新的数据版本号,格式是“v日期-序号”,并记录:
- 原始数据来源
- 清洗脚本版本
- 清洗参数
- 清洗后样本数量
- 人工抽检结果
这份记录放在数据目录的“CHANGELOG.md”里。
为什么这么在意?因为当我发现模型效果下降时,第一件事就是检查数据版本是否被动过。有一次团队同事顺手“优化”了清洗脚本,去掉了一个去重步骤,结果数据量从50万变成55万,模型准确率掉了3个点。查了两天,最后定位到就是数据版本变了。如果一开始就查CHANGELOG,十分钟就能解决。
3.3 切分与分布检查:训练集验证集的划分,要防止“作弊”
数据切分看起来是个简单问题——随机分一下就行?其实不是。
文档问答场景有一个典型风险:同一份文档的不同段落,可能既出现在训练集又出现在验证集。模型见过相关内容,验证集分数虚高,上线后面对新文档效果骤降。这就是数据泄露。
我的切分原则是:
- 按文档级别切分,而不是按段落切分。同一篇文档的段落放进同一个集合。
- 验证集和测试集不使用同一批文档。
- 切分之后人工检查类别分布。比如工单类的数据占了60%,验证集也要保持类似的占比,否则指标会有偏。
切分完一定要跑一个简单的分布检查脚本,输出训练/验证/测试三个集合的样本量、来源分布、平均长度。看到三组数字大致匹配,再进入训练环节。
这里特别提醒一点:不要反复在测试集上调参。测试集的唯一使命是最终评估一次。想调参,用验证集。这个原则违反了,你的指标就只是“对测试集的记忆”,不是真实能力。
4. 模型训练与微调的实操:从加载到收敛
4.1 训练循环骨架:每一行都有存在的理由
从零开始写训练循环,不是把模型的forward和backward拼在一起就完事。一个生产可用的训练脚本,要考虑的东西比教科书示例多得多:梯度累积、混合精度、梯度裁剪、checkpoint保存、日志频率。
我用的训练循环骨架大致长这样:
for epoch in range(num_epochs): model.train() for step, batch in enumerate(train_loader): batch = {k: v.to(device) for k, v in batch.items()} outputs = model(**batch) loss = outputs.loss / grad_accum_steps loss.backward() if (step + 1) % grad_accum_steps == 0: torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() scheduler.step() optimizer.zero_grad() if step % log_steps == 0: logger.info(f"epoch {epoch}, step {step}, loss {loss.item():.4f}") # 每N步做一次验证 evaluate(model, val_loader)看起来平平无奇,但有几个点值得展开。
梯度累积是为了在单卡显存受限时用多个小batch模拟大batch的效果。grad_accum_steps = 理想batch size / 实际batch size。比如理想batch是32,显存只放得下8,那accum_steps就是4。
混合精度(AMP)可以让训练加速且省显存。PyTorch里用torch.cuda.amp即可,一般不改业务代码。但要注意,部分模型在FP16下数值不稳定,loss会突变成nan。如果你遇到这种问题,可以退回FP32,或者用BF16(在许多新卡上更稳)。
4.2 超参数的起点,以及调参的顺序逻辑
超参数是玄学吗?某种意义上是的,但调参顺序是有逻辑的。
我的经验是从学习率开始调。找一个大致的范围,比如1e-5到5e-5,先用小数据集快速跑几个epoch,观察loss走势。loss不降,说明学习率太小或者模型结构有问题;loss震荡,说明学习率太大。选定学习率之后,再调batch size。batch size主要影响训练稳定性和收敛速度,太大会显存爆炸,太小会导致梯度噪声大、收敛慢。
再然后是epoch数。epoch数其实不是一个需要“调”的参数,而是和早停(early stopping)配合的概念。我一般设置一个上限,比如10个epoch,如果验证指标连续3个epoch没有提升,就提前结束训练,并保存验证集上表现最好的那个检查点。
warmup比例也值得一说。很多模型微调时会设置一段“预热期”,学习率从零线性上升到目标值,再按余弦曲线下降。warmup的比例我习惯设为总步数的3%到5%。它的作用是防止训练初期由于学习率过大导致模型参数剧烈震荡。
4.3 什么时候上分布式:单卡能解决的事,别浪费时间在多卡上
很多从零开始的人有个误区:一上来就研究分布式训练框架。其实大多数项目,单卡就能搞定。
我的判断标准是:如果单卡训练一轮的时间在可接受范围内(比如一个晚上能跑完),就绝对不要上分布式。分布式训练带来的增益不是没有,但它同时引入通信开销、调试难度和版本兼容问题。性价比极低。
只有当训练时间超过资源容忍度,比如单卡要跑一周以上,才是考虑DDP(DistributedDataParallel)的时机。
如果可以,优先用LoRA这种参数高效微调技术,而不是全参微调。它在保持效果接近的情况下,显存占用和训练时间都能大幅降低。微调一个7B模型的LoRA,24G单卡完全够用。这个决策让我的团队省下了一整台A100预算。
4.4 模型产物管理:检查点怎么存,选哪个版本上线
一个训练任务跑完,会留下很多检查点:每个epoch保存一次,加上早停保存的最佳检查点,以及最后一轮检查点。不可能全部上线,需要一套选择逻辑。
我现在的规则是:
- 候选检查点都跑一遍评测集,记录核心指标(准确率、F1、延迟)。
- 上线选择不只看单一指标。比如准确率最高的检查点,可能在某类问题上的稳定性很差,我会看分项指标再决定。
- 所有检查点统一命名格式:模型名-训练数据版本-epoch号-指标值。这个格式保证三个月后还能定位到“为什么选它”。
保存检查点的时候,不只是存权重,还要把训练配置和tokenizer一起打包。有过一次教训:三个月后想恢复一个模型继续训练,结果只找到权重文件,tokenizer配置丢了,那个模型直接报废。权重可以不要,tokenizer和config一定要留在模型目录里。
5. 推理部署与性能调优:模型跑起来只是开始
5.1 在线服务还是离线批量:先想清楚业务形态
部署的第一步不是写API,而是判断业务到底需要什么形态。
文档问答这个场景,用户需要实时交互,所以必须是在线服务,接口响应要快。而像“每周自动给全量工单打标签”这种就不需要API,跑一个批量脚本就行。
在线服务又分同步和异步。同步返回适合单次问答场景,简单直接。异步适合处理慢操作,比如生成很长的答案,先返回任务ID,用户再通过轮询拿结果。我当时的业务首字节延迟要求是2秒,同步就够用,没有引入额外的消息队列。
这里提醒一点:不要为了“看起来高级”而加消息队列。每多一个组件,就多一层运维负担。先把同步接口做好,发现瓶颈再演进。
5.2 推理加速:量化、批处理、缓存三板斧
模型部署之后,第一件事就是压测。我的经验是:不要凭感觉判断性能,直接用压测工具跑指标。
压测看三个数字:QPS(每秒能处理多少请求)、P99延迟(最慢的1%请求耗时)、显存占用。我的压测结果是单卡初始QPS只有5,P99延迟4秒,远远不够。之后我做了三板斧优化:
第一板斧是量化。把模型从FP16量化到INT8或INT4,显存占用直接下降50%以上,推理速度提升明显。当时用的是类似GPTQ的思路,通过校准集选择量化参数。INT4的量化会让效果略有下降,但在这个业务场景下可接受。
第二板斧是批处理。在线推理服务默认是一个请求一个请求地过模型,GPU利用率很低。改成动态批处理:服务在极短的时间窗口(比如50ms)内收集多个请求,合并成一个batch同时推理。这个改动把GPU利用率从20%拉到80%以上,QPS直接翻了几倍。
第三板斧是结果缓存。高频问题往往占线上请求的大头。对完全相同或高度相似的query,直接用Redis缓存之前的结果。这个改动把30%的请求变成“零推理”,效果立竿见影。
优化后,我的最终指标是QPS 25,P99延迟1.2秒,满足业务需求。别小看这些优化,它们是“从能跑到能扛流量”的关键。
5.3 API服务怎么做才像话
模型推理服务本质上是一个高并发的接口服务,工程上不能裸奔。
我用FastAPI搭的服务,骨架大概长这样:
from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI() class Query(BaseModel): question: str top_k: int = 3 @app.post("/v1/qa") async def qa(query: Query): try: result = pipeline.run(query.question, top_k=query.top_k) return {"answer": result["answer"], "sources": result["sources"]} except Exception: raise HTTPException(status_code=500, detail="internal error")但光有这个还不行,生产环境至少还要有:
- 超时控制:每个推理请求设置超时上限,比如5秒,避免慢请求拖垮线程池。
- 重试策略:对偶发的GPU显存不足、临时CUDA错误做有限重试。
- 限流:对用户或客户端IP做每分钟请求数限制,防止恶意调用。
- 队列上限:当并发过大时,服务的策略是“快速失败”还是“排队等待”,要提前定好。我当时选的是快速失败,返回503,让调用方自己退避。
5.4 上线后的监控:没有监控的服务是一颗定时炸弹
服务上线后,我开始搭建最基本的监控指标。不是要什么花哨的可观测性平台,而是先把最关键的指标看住。
GPU监控:利用工具每10秒采集一次显存使用率、GPU利用率和温度,写入时序数据库,画到面板上。显存持续上涨是泄漏的典型信号,基本可以断定服务代码或推理框架有内存管理问题。
请求监控:记录QPS、P99/平均延迟、超时率和5xx错误率。延迟突增和错误率飙升是两大核心告警。
数据漂移监控:上线后的模型会面对未知的线上数据分布。我的做法是定期从线上日志里取最新的query样本,和训练时的query做embedding距离分布对比。如果分布偏移过大,就需要考虑重新训练。这个问题在文档问答场景很常见——业务人员提问习惯会随时间变化。
不做监控,模型就像蒙着眼开车。你永远不知道它在生产环境里已经被用户的真实请求折磨成什么样了。
6. 评测体系与回归测试:不评测,等于白做
6.1 评测集是AI工程的“法律文件”
模型好用不好用,不是训练指标说了算,而是评测集说了算。
很多人只盯着训练集上的loss降没降,这是最大的误区。Loss降了可能是过拟合,可能是数据泄漏,可能只是“背题”。真正衡量模型能力的,是一份和训练集完全独立、由人工标注、覆盖真实业务场景的评测集。
我当时花了两周时间做评测集。从50万条数据里挑出2000条作为评测样本,覆盖几十种问题类型,每条由三个人独立标注,取多数作为最终答案。同时保证评测集里的文档不会出现在训练集里。
评测集要包含两类样本:
- 常规样本:绝大多数线上请求长什么样,评测集就长什么样。
- 边界样本:故意放一些模糊问题、带错别字的query、语义相近但答案不同的陷阱题。
没有边界样本,你的评测分数会很漂亮,但上线后被真实用户一问就原形毕露。
6.2 自动化回归测试:改一个参数,不要把全线上搞挂
有了评测集,下一步是把评测自动化。
我设计了一套回归测试流程,每次模型更新或者数据更新,都会跑一遍。流程分三段:
第一段是冒烟测试。用10条左右的高置信度样本,验证模型能不能正常加载、推理会不会报错。耗时两分钟,作用是把明显的代码问题挡在门口。
第二段是核心评测。用完整的2000条评测集跑一遍,生成详细报告。包括总体准确率、分类型准确率、和上一版模型的对比。如果新版模型总体指标下降超过1%,除非有明确理由,否则默认不通过。
第三段是错误case留档。每次评测失败的样本,自动记录下来,标记“待人工分析”。这些case是数据迭代最宝贵的来源。
我从这个流程里吃到最大的甜头是:再也不用担心团队里任何人“偷偷把参数改了”之后影响线上效果看不到——自动化测试会用指标说话。
6.3 基于bad case的迭代闭环:AI工程的“永动机”
效果提升不是靠突然的灵感,而是靠对bad case的持续分析。
我的操作流程是:
- 收集错误样本:从评测失败和线上日志里汇总bad case。
- 聚类分析:把bad case归类。比如“模型分不清‘退款’和‘退货’的区别”“模型对缩写理解错误”“检索结果缺失导致生成失败”。每类给一个标签。
- 定位根因:有些bad case是数据问题,有些是检索问题,有些是模型能力边界。根因不同,解决方案完全不同。
- 数据补强:针对根因,补充对应的训练数据或调整清洗规则。
- 重新训练和评测:回到训练阶段,跑完自动化测试,看是否解决。
这个循环跑上个10轮,模型效果就会逐步逼近业务预期。数据版本管理、实验记录、自动化评测的意义在这个闭环里全部体现出来。
7. 常见问题与排查技巧实录
7.1 环境与训练阶段的高频问题
| 现象 | 常见原因 | 排查思路 |
|---|---|---|
| 启动就报“illegal memory access” | CUDA版本与PyTorch不匹配 | 先核对PyTorch官方要求的CUDA版本,再检查驱动与CUDA |
| 训练中途显存OOM | batch size过大或显存碎片化 | 先减小batch size,或开启梯度累积;再检查是否有变量未释放 |
| loss是NaN | 学习率过大、FP16数值不稳定、数据里有脏标签 | 降低学习率,可尝试BF16,检查数据清洗 |
| loss一直不降 | 学习率太小或训练数据分布混乱 | 快速跑5个epoch看loss斜率,排除数据标签大量错误 |
| 训练完指标高,线上效果差 | 数据泄漏或评测集覆盖不足 | 检查切分是否按文档级别,补充边界样本 |
OOM是我遇到最多的。一开始我以为是GPU显存不够,排查老半天发现是某个代码片段把中间结果列表全部积在内存里,batch的loss都正常,但内存先爆了。用free -g看一眼系统内存,能快速区分是显存还是内存问题。
7.2 部署与服务阶段的高频问题
| 现象 | 常见原因 | 排查思路 |
|---|---|---|
| P99延迟持续走高 | 并发排队过多或批处理窗口设置不合理 | 看队列长度;调整动态批处理最大等待时间 |
| GPU显存使用率只增不减 | 推理框架或服务代码内存泄漏 | 用监控工具观察显存曲线;最小化复现定位是框架还是业务代码 |
| 请求大量返回503 | 队列上限触发快速失败 | 放宽队列上限或增加实例;检查限流配置是否误伤正常用户 |
| 修改代码后推理结果变了 | 未固定随机种子或模型未加载同一检查点 | 固定PYTHONHASHSEED和随机种子;核对模型路径与config |
| 服务重启后首次请求特别慢 | 模型冷加载 | 上线时写预热脚本:启动后发送几条请求让模型预热再对外服务 |
部署阶段有个细节容易被忽视:模型文件的加载路径。我犯过一次错,部署时从网盘下载模型,下载完成度没有校验,结果服务用的是一半的权重文件,准确率低到不可思议,还没有报错。后来我在所有模型加载环节都加了一步校验:加载后跑一遍冒烟测试,检查输出是否符合预期。这个习惯救了我好几次。
7.3 数据层面的问题排查
数据的问题最隐蔽,因为不报错。我发现过一个现象:模型对英文缩写理解特别差,一开始以为是模型能力问题,后来分析是清洗脚本把所有句点都去掉了,导致“U.S.A”这种词被粘连成“USA”,含义还算对,但“app.v1.2.3”这种版本号被破坏,训练样本全乱了。这类问题只能靠“清洗后人工抽检”和“数据分布统计”这类预防机制,出了再查往往为时已晚。
另一个高发问题是重复数据。很多文档的内容是重复粘贴的,如果不做去重,某些句子在训练集中出现几十次,模型就会严重偏向这些说法,对其他同义表达不敏感。至少做一次MinHash或simhash去重,比任何调参都管用。
8. 最后分享两个亲测有效的习惯
写到最后,说点不太能写进项目汇报里的体会。
第一个习惯是:把每一步的关键决策和踩坑记录下来。不是写API文档那种,而是记录“为什么当时那么选”“后来发现有什么问题”“下次怎么做更好”。这种草根笔记看着不起眼,但在项目中期回头查问题时,它的价值超过任何一本教科书。
第二个习惯是:永远从最小闭环开始。不要一开始就规划一个完美的大系统,而是先做一个丑但通全链路的版本:10条数据、1个模型、1个接口、10条评测样本。跑通之后,再一步步把工程化能力加进去。所有大的AI工程问题,都是在闭环里逐个暴露、逐个解决的。
从零到一搭建AI工程确实不容易,但这个过程建立的工程判断力和排错能力,才是真正的资产。项目会结束,模型会迭代,但这些能力会留在这个团队的血液里,后面的路自然会越走越稳。