ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI工程从零搭建:可复现、可追踪的完整实践

AI工程从零搭建:可复现、可追踪的完整实践 做 AI 工程我最怕听到一句话“我在 Notebook 里已经跑通了接下来只要搬上线就行。”这个说法坑过我不止一次也坑过我带过的不少新人。跑通一条训练脚本和交付一个 AI 工程中间隔着环境复现、数据版本、实验记录、服务封装、监控回溯一大堆事。这也是我为什么坚持把手上这个内部项目命名为ai-engineering-from-scratch——不靠现成全流程平台不靠 Copilot 自动生成的训练模版而是自己从零把整条链路搭一遍。这个项目的前身其实是个很常规的任务帮团队做一个销售预测模型。数据量不大特征也谈不上复杂任何一个熟悉 sklearn 的人花两周都能交差。但问题恰恰出在“交差”这两个字上。模型是跑通了可三个月后另一名同事接手先花两天装环境再花一天找数据版本又因为原来的脚本里写死了相对路径推理端调用时特征顺序对不上直接预测出一堆负数。于是我把项目推倒重来目标是一个人能从零开始把整条链路搭出来也能让任何陌生人在一台干净机器上快速复现结果。这篇文章就是我复盘之后的完整记录。适合正在从“调参跑脚本”往“正经做 AI 工程”方向过渡的同学也适合小团队里需要一个人扛起算法加开发的工程师。我不写大而全的架构只想把每一步为什么这样设计、踩过哪些坑、最终怎么落地讲清楚。1. 为什么我认认真真选择了“从零开始”的路线1.1 从零开始不等于造轮子它更像是把地基挖开看一遍很多人一听“从零开始”第一反应是拒绝框架不香吗现成平台不好用吗没必要重复造轮子吧。这个反应没问题但没有区分“从零实现算法”和“从零搭建工程链路”。前者我完全反对如今深度学习框架已经足够成熟自己手写反向传播除了练手没有工程意义。后者则是另一回事用现成平台虽然省事但你对内部机制的理解会停留在“按钮能点通”的程度。一旦平台升级、某个组件不再维护、或者你被要求部署到另一个受限环境里整个项目直接变成黑箱。我在这个项目里所谓的“从零开始”指的是不依赖任何模型训练平台、不依赖某个全家桶式解决方案而是自己组装开源组件把关键环节用一套轻量代码衔接起来。这样做初期确实慢但每一条依赖、每一个缓存、每一行配置我都心里有数。就像自己装修一套老房子拆墙重砌的时候确实费劲但水电管线在哪、哪堵墙是承重墙你会比请装修公司的人清楚得多。1.2 什么时候不该从零开始哪些模块可以直接借用如果我只强调“一切自研”那就成了另一种教条。事实上这个项目里我借用了大量成熟的底层库PyTorch 做模型训练FastAPI 做服务暴露uv 管环境pytest 做测试。这些都是市面上久经考验的组件我用它们不代表违背“从零开始”的初衷。真正的边界是凡是“通用能力”都尽量复用凡是“项目独有的流程逻辑”尽量自己写。通用能力包括依赖管理、网络通信、单元测试框架、深度学习算子项目独有逻辑包括数据切分、实验命名规范、模型配置加载、指标口径、服务输入验证。把这些区分开你才不会陷入“造轮子”和“踩平台黑箱”两个极端。再举个例子。特征工程部分我一开始也想直接套用某个特征平台后来发现我们的特征更新频率低、总量也不大引入一个独立平台纯属增加运维成本。于是我用一个简单的特征配置文件加一段缓存脚本自己维护反而一个月跑下来都稳定。此后我坚定了一个准则在这个量级别为不存在的规模买单。1.3 这个项目的“最小目标”一条可以被陌生人接手复现的流水线在正式写代码前我给自己定了一个验收标准不是“模型准确率多高”而是下面这几条新同事拿到仓库能照着 README 在半天内跑通训练脚本。训练产物模型文件、指标、日志能从磁盘完整找回来知道是哪份数据、哪次实验产生的。服务上线后能对在线请求做输入校验能区分“模型判断错了”和“输入根本不是预期格式”。这三条都达成我才认为这个项目配得上“AI 工程”四个字。模型精度是结果工程能力是保障。很多团队只盯着前者最后吃亏的往往是后者。2. 项目基础设施环境、依赖和“可复现性”2.1 环境管理我为什么从 conda 换到了 uv一开始我也随大流用 conda毕竟教程里到处都是。但几轮下来conda 最让我头疼的点有两个环境解析速度慢以及它对项目依赖的显式化支持不够干净。我经常遇到这种场景别人给我的 environment.yml 里明明写着 python3.9但我装完一跑就缺某个包原因是他当时在 base 环境里多装了一些包直接 pip install 进了当前环境却忘记写进配置文件。后来我换成了 uv。这个工具最大的价值在于它把依赖声明和锁文件分得清清楚楚pyproject.toml 声明你直接依赖什么uv.lock 则锁住所有传递依赖的具体版本。有了这层保证“环境一模一样”从口号变成了可验证的事实。uv venv .venv uv pip install -e .[dev] uv pip compile pyproject.toml -o requirements.lock我习惯在项目里同时维护三份文件pyproject.toml声明顶层依赖标记哪些是核心依赖、哪些是 dev 依赖。requirements.lock锁定完整依赖树直接传递给他人或 CI 使用。requirements.in可选如果团队更熟悉传统 pip 流程可以保留这个入口。如果你已经在用 conda也不一定非要立刻迁到 uv。我想说的核心其实是无论用什么工具依赖的“声明”和“锁定”必须分成两个动作。否则环境问题迟早会变成吞时间的无底洞。2.2 锁依赖requirements.lock 才是工程第一安全网我第一次被项目坑就是死在没有锁依赖上。同一个项目一周前还能正常训练一周后重新跑训练PyTorch 的一个小版本自动升级了损失函数数值出现细微抖动导致我一度以为是模型出了 bug排查了两天才意识到是底层算子精度变了。这些教训让我对“依赖锁定”近乎偏执。每次跑正式实验前我都会把依赖树完整导出一份和代码版本绑定。这在后来的问题排查中帮我省了大量时间。# 把当前环境依赖完整导出 uv pip freeze requirements.lock你可能觉得 lock 文件又长又乱看不出来有什么意义。但它最大的价值不是给人读的而是给环境构建时用的。别人拿到 lock 文件可以原样安装而不是从一个模糊的 home 环境里 pip install 一个版本不确定的包。再递进一层如果你的项目里用到 CUDA 相关的底层库光锁 Python 包还不够还要把 CUDA 运行时版本、驱动兼容性写进说明文档。环境问题最常见的事故往往不在于 Python 包缺失而是底层原生库不匹配。2.3 用 Makefile 把高频命令变成肌肉记忆刚开始我自己也是手动敲命令uv sync、python -m src.data.prepare、python -m src.train配置文件更新后又要再敲一遍。这些命令一多就会出现一个人敲错、另一个人照着他敲错的情况。所以我给项目加了一个 Makefile把高频流程收敛成几个固定目标。不需要解释看到目录就知道能跑什么。PYTHON : .venv/bin/python .PHONY: setup data train eval serve test lint setup: uv sync data: $(PYTHON) -m src.data.prepare --config configs/data.yaml train: $(PYTHON) -m src.train --config configs/experiment.yaml eval: $(PYTHON) -m src.evaluate --config configs/experiment.yaml serve: $(PYTHON) -m src.serve test: $(PYTHON) -m pytest tests/ -q lint: $(PYTHON) -m ruff check src/ tests/Makefile 的好处不只是给你一个统一入口它还把命令里的参数、路径这些隐式知识变成了显式约定。新人不用去猜“数据准备脚本在哪”直接 make data 就行。而且 Makefile 可以顺便校验一些前置条件例如在 train 之前检查 data 目录是否存在否则直接报错省去运行时才爆出“文件不存在”的尴尬。2.4 把 Docker 拉进来之前先让本地构建能三次成功容器化是环境复现的终极手段但它没法替代依赖管理。如果你本地连三遍安装都无法成功Docker 只能帮你把这个“不稳定”的问题复制到每一个环境里。我的实测建议是分两步走先在开发机上把 uv、锁文件、Makefile 这套跑顺然后再把同样的步骤搬进 Dockerfile。这样构建失败时你至少能把问题定位到“容器环境特有问题”和“项目配置问题”这两个不同的区间而不是一锅乱炖。这里还涉及一个细节不要在 Dockerfile 里使用通配符 COPY 整个项目目录。更好的是先只 COPY pyproject.toml 和 requirements.lock把依赖层构建好、缓存住再 COPY 代码。这样只要依赖没变镜像重建就不会每次都重新装包构建速度和缓存命中率都会大幅提升。3. 数据工程部分从“有一份 CSV”到“数据集版本”的跨越3.1 数据清单文件指纹、源、版本、修改时间很多工程师在“数据”上有个错觉我认为数据不就是 CSV 吗Excel 打开看一眼能读就完了。但一旦进入工程化数据文件至少需要四个属性来源、指纹、版本、修改时间。来源包含原始文件路径、下载链接、上游系统表名。指纹是数据的唯一标识通常是文件内容的哈希值。版本是业务意义上的划分比如 2024-06-v1。修改时间是操作时间轴上的一道标记也是排查数据异常时的重要线索。我当时踩过一个大坑分析师直接在本地 Excel 里改了一列数据没有更新版本号也没有修改描述。我们把那一版跑出来的模型上线后指标一直不达预期后来才发现是因为特征数据口径变了但大家都浑然不知。从那时起我就立了一条规矩任何数据变更必须伴随指纹变化任何模型实验必须记录所用数据的指纹。3.2 写一个轻量数据版本文件manifest流行的做法是引入 DVC 之类的数据版本管理库。如果你团队已经熟悉它完全可以用。但我在这个项目里决定自己写一个轻量 manifest原因是对数据大小、平台依赖和团队学习成本做了一次平衡。所谓 manifest其实就是一个 JSON 或 YAML 文件记录每个数据文件的详细信息。核心代码很短但效果很直接。import hashlib import json from pathlib import Path def file_md5(path: Path) - str: h hashlib.md5() with path.open(rb) as f: for chunk in iter(lambda: f.read(8192), b): h.update(chunk) return h.hexdigest() def build_manifest(root: Path, patterns: list[str]) - dict: records [] for p in root.rglob(*.csv): if p.name.endswith(patterns) or not patterns: records.append({ path: str(p.relative_to(root)), md5: file_md5(p), bytes: p.stat().st_size, modified: p.stat().st_mtime, }) return {generated_at: __import__(datetime).datetime.now().isoformat(), files: records}数据准备脚本每跑一次就重新生成一遍 manifest并且把它复制到名为 artifacts/metadata/ 的目录中。训练脚本在启动时先加载这个 manifest校验所有用到的文件指纹是否一致。不一致就拒绝训练。这样数据被误改时你永远不会在训练到一半时才发现问题。3.3 训练/验证/测试切分要做的三件事切分数据看起来简单但工程化切分和 Notebook 里的随机打乱完全不同。我在项目里坚持做到三件事。第一切分必须基于一个稳定键保证同一用户、同一订单不会同时出现在训练集和验证集。业务上这叫防数据泄漏。大部分模型在验证集上指标虚高往往就是因为切分时忽略了实体级别的重复。例如销售预测里同一天同一个 SKU 销售记录会被随机切到两边模型相当于直接背答案了。第二切分结果要持久化。不是每次准备数据时现场打乱而是先按规则生成一份切分映射文件后续训练、验证、服务都用同一份映射。这样复现实验时才不会因为随机种子差异导致结论不可比。第三切分必须产出三条独立数据路径并且对路径成文。我在项目里用一个 data/processed/ 目录存放最终产物里面固定有 train.parquet、val.parquet、test.parquet而原始数据放在 data/raw/ 里。这样任何人接手时不需要靠记忆去猜哪个是清洗后的数据。3.4 用 datasets 库还是自己写加载器我的取舍Hugging Face 的 datasets 库功能很强支持缓存、流式读取、转换。但用在业务型表格数据上反而有一点重。尤其是当你只需要按列读取、处理时间特征、做 groupby 操作时datasets 的抽象层级会让人觉得被框架带着走。我在这个项目里用 pandas 加 pydantic 做数据加载。pydantic 不直接加载数据而是用来定义字段约束和类型校验。例如一个销售记录日期列必须解析成 datetime金额列必须大于等于零类目列缺失时默认 unknown。这样在数据进入特征工程之前就已经被强制清洗过一轮。from pydantic import BaseModel, field_validator from datetime import datetime from typing import Optional class SalesRecord(BaseModel): shop_id: str sku_id: str date: datetime amount: float category: Optional[str] unknown field_validator(amount) classmethod def amount_positive(cls, v: float) - float: if v 0: raise ValueError(amount cannot be negative) return v数据加载器读入原始 CSV 后逐行转成这个模型校验失败的行直接进错误清单而不是悄悄丢弃。这个设计最初是为了防止脏数据累积但后来发现它还有一个附带好处能让数据分析师和算法工程师之间少吵很多架。双方看到同一份“哪些行不合法”的清单问题就成了标准问题而不是“你代码有 bug”。4. 训练 pipeline把模型脚本变成可调度任务4.1 配置驱动训练dataclass 与 yaml 的边界训练脚本如果全是硬编码参数比如在代码里写lr 1e-3、batch_size 32、hidden_size 64那每做一次实验都要改代码、提交版本、再跑。这会让实验变得不可追溯你跑了十次实验每次代码都不一样哪个版本的代码对应哪次结果压根说不清楚。我在项目里采用“YAML 配置文件 dataclass 配置对象”的组合方案。YAML 负责人类可读的参数修改dataclass 负责把 YAML 转成运行时强类型对象。配置文件的路径本身作为训练命令的参数传入。from dataclasses import dataclass, field from typing import Optional dataclass class TrainConfig: data_manifest: str artifacts/metadata/manifest.json split_file: str artifacts/metadata/split.json model_name: str lightgbm learning_rate: float 1e-2 max_depth: int 6 n_estimators: int 300 batch_size: int 256 epochs: int 10 seed: int 42 experiment_name: str baseline实际加载时我实现了一个简单的配置加载函数把 YAML 内容与 dataclass 字段做映射并检查有没有多出来的未知字段。这样能防止写错配置名比如把learning_rate误写成learn_rate程序会在启动阶段直接报错而不是默默使用默认值。4.2 训练生命周期钩子train、eval、checkpoint、metric真正的训练脚本不是 100 行代码全部塞进 main而应该按生命周期分块。每个阶段有明确的输入输出方便局部替换和单测。我把训练流程拆成五个阶段prepare: 加载配置和数据生成运行目录。train: 执行模型训练循环记录每一步关键指标。eval: 在验证集上计算离线指标输出对比基线。checkpoint: 保存模型权重、配置快照、数据指纹。report: 汇总运行信息写入实验记录。在代码层面我用一个简单的 PipelineRunner 类把阶段串联起来。难点不在于串联而在于每个阶段都能“独立失败、独立恢复”。比如训练到第 8 个 epoch 时数据文件被同事误替换了我的脚本应该立刻校验指纹失败并终止而不是继续产出不可信的结果。def run_stage(stage: str, config: TrainConfig) - None: if stage prepare: prepare_run(config) elif stage train: run_train(config) elif stage eval: run_eval(config) elif stage checkpoint: save_checkpoint(config) elif stage report: write_experiment_report(config)这样写还有个好处可以在 MAKEFILE 里单独调试某个阶段比如只跑 eval反复调整评估指标定义而不必每次从头训练一遍。4.3 记录实验每一条跑出来的记录都该能被回放实验记录是 AI 工程里最容易被忽视的一部分。很多人习惯只看 tensorboard 的曲线但那些曲线数据是临时的过了几天就没了。即使没丢你也没法从一条曲线反推出当时的完整环境。我在这个项目里没有引入重量级实验平台而是用一套轻量记录机制每次训练启动时在experiments/{experiment_name}/{run_id}/目录下写入三个文件config.yaml本次实验用到的完整配置。command.txt启动本次实验的完整命令和 commit id。metrics.json验证集指标、训练时间、日志路径。run_id 可以用时间戳加随机短码我一般用YYYYMMDD-HHMMSS-abc这种格式。训练完成后这套记录不会动任何人想回放某次实验只需用 command.txt 里的命令和 config.yaml 即可。如果业务上还需要更复杂的指标对比再把 metrics.json 汇总进一个 sqlite 表或 pandas DataFrame。切忌小团队一开始就搭 dashboard成本高收益低很容易变成没人看的装饰品。4.4 冒烟测试用 3 个样本跑通全流程避免深夜翻车很多模型脚本的第一次“崩溃”往往发生在正式训练启动后两小时原因可能是某个参数组合、某个数据处理分支没有被覆盖。为了避免这种深夜翻车我在项目里加入了冒烟测试。冒烟测试的思路很简单把完整流程的数据量缩小到几十条跑一遍 prepare、train、eval、checkpoint确认没有异常就通过。这不是性能测试只是“流程连通性测试”。# tests/test_smoke_training.py def test_training_pipeline_on_small_data(tmp_path): config TrainConfig( data_manifeststr(tmp_path / manifest.json), split_filestr(tmp_path / split.json), epochs1, n_estimators5, ) run_stage(prepare, config) run_stage(train, config) run_stage(eval, config) assert (tmp_path / experiment / model.pkl).exists()冒烟测试要放一个比较小的 batch size比如 8这样不仅验证流程还能暴露维度错误、类型错误这类基础问题。建议把它挂到 CI 里作为每次提交的默认检查项。这样做至少能把低级错误消灭在提交阶段而不是留到自动训练任务里烧算力。5. 模型服务与在线监控把位置从“训练机”挪到“生产”5.1 模型导出保存为 ONNX 还是继续用 PyTorch模型训练完只是工程的前半段后半段是能不能稳定地提供推理服务。导出格式的选择直接影响服务性能和部署自由度。我的建议是分场景如果模型最终要跑在资源受限的边缘端或者需要跨语言调用ONNX 是一个稳妥选择。它规范了计算图并且推理速度通常比原始框架导出方式更快。但 ONNX 对算子兼容性有要求某些自定义层可能不支持导出这时候就得回到公平比较继续用 PyTorch 原生推理或在服务里嵌入 TorchScript。我自己在表格类模型上更倾向 ONNX因为它可以脱离 Python 环境用一个 C 推理进程提供服务训练和推理彻底解耦。不过在导出前我会先写一个 golden test固定输入对比导出前后模型输出的差值是否在可接受范围内。这个测试能提前发现算子误差而不是上线后让用户替你发现。5.2 FastAPI 封装成服务输入校验比模型本身更重要在线推理和离线推理安全是两回事。离线测试数据是干净的在线请求则充满噪音。缺失字段、非法字符串、奇奇怪怪的时间格式都可能让模型推断崩溃或返回异常结果。我在服务层直接使用 FastAPI pydantic 做输入校验把请求体定义成一个严格的模型。字段缺失、类型错误、数值越界都会在进入模型前被拦截。from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field app FastAPI(titlesales-forecast-api, version0.1.0) class PredictRequest(BaseModel): shop_id: str Field(..., min_length1, description店铺ID不能为空) sku_id: str Field(..., min_length1) date: str Field(..., patternr^\d{4}-\d{2}-\d{2}$) amount: float Field(..., ge0) app.post(/predict) def predict(req: PredictRequest): try: features featurize(req) pred model.predict(features) return {pred: pred} except ValueError as exc: raise HTTPException(status_code422, detailstr(exc))输入校验容易给模型“额外安全感”但别误以为它是唯一的保护。真正上线前我还会用一个小规模的影子流量来验证服务路径。做法很简单把线上请求复制一份转发到新服务只记录预测结果不影响真实线上逻辑。等影子结果和旧服务结果对比稳定后再切换流量。5.3 在线监控三件套延迟、输入分布、预测漂移模型上线后真正需要盯的不是“准确率”而是三件事响应延迟、输入分布、预测漂移。响应延迟是直接的用户体感指标如果 p95 超过服务承诺的上限说明需要优化模型推理或增加并发处理。输入分布监控则是看请求字段的统计量是否发生突变。比如平时 date 字段都是近 30 天的日期突然出现大量 90 天前的日期说明上游业务逻辑变了模型预测很可能会失真。预测漂移相对复杂。一个简单做法是维护“近期预测均值”窗口用 EWMA 或简单滑动平均记录一旦偏离基线超过阈值就报警。不一定要用复杂的统计检验很多业务问题用均值、分位数加阈值就足够暴露。监控和日志要联合设计。每条预测记录除了输出结果还要附带模型版本号、特征哈希、请求接收时间。对故障排查来说模型版本号的缺失会让线上问题根本无法定位。这个坑我踩过线上模型更新了但日志里没有任何标记过了两周才发现有一半流量还在走旧模型。6. 常见问题与排查技巧实录6.1 复现失败为什么换一台机器结果就变了一个模样这是最经典的问题。很多人第一反应是代码问题但我建议按优先级排查四个地方依赖版本、资源限制、随机种子、数据读取顺序。依赖版本最容易锁定用 requirements.lock 对比即可。资源限制指 CPU 核数、内存、显存差异。这些参数会影响 PyTorch DataLoader 的并发行为可能改变批次顺序进而影响训练结果。随机种子的话需要在所有涉及随机数的环节显式设置 torch、numpy、random并且确保在分布式环境下每个 worker 的种子设置也有差异。数据读取顺序其实比想象中更隐蔽。如果数据文件是 dict 形式遍历的Python 版本不同可能导致字典排序不同从而影响训练集顺序。这个坑我遇到过最后用固定字段排序解决。6.2 训练卡住如何区分死锁、缺数据和寻优震荡训练卡住不一定是死锁症状不同排查路径完全不同。如果 loss 一直不动且 GPU 利用率很高可能是模型在某个数值区间反复震荡。此时观察学习率是否过大、梯度是否消失。如果 GPU 利用率很低CPU 时间却很长多半是数据加载瓶颈重点关注 DataLoader 的 num_workers 和 prefetch 设置。如果进程直接阻塞微信群里怎么叫都没反应那可能是死锁比如多个 dataloader 在共享内存或锁文件上互相等待。我自己的排查顺序很简单先看 CPU 和 IO 曲线再看 GPU 利用率最后才动模型代码。绝大多数“训练卡住”都不是模型本身的问题而是数据管线。6.3 内存爆掉dataloader 和缓存设计引起的连锁反应内存爆掉有时是模型过大但更多时候是数据预处理惹的祸。我在一个项目里把整个训练集一次性读进内存做特征工程跑一次是 8GB当时觉得没问题。后来数据集翻倍服务器直接 OOM。那之后我改成流式加载训练过程只保留当前 batch 的特征中间特征缓存按需落盘。引发内存爆炸的另一个原因是缓存设计不合理。比如在 dataloader 的__getitem__里做全量特征拼接虽然看起来优雅但每个 worker 都持有一份副本并发一多内存就爆。把通用的预处理抽到离线阶段训练阶段只做加载和 batch 组装更有利于规模扩展。6.4 服务上线即失败请求路径里最容易被忽略的隐式转换FastAPI 启动很快但服务上线时经常出现“离线测试好好的上线就报错”这种魔幻情况。排查后发现多数原因都集中在隐式类型转换和特征顺序上。举例模型训练时特征顺序是[shop_id编码, sku_id编码, date_ordinal, amount]但服务端构造特征时把 date_ordinal 放在了最后。模型表面看输入维度没变但实际上每一列含义错位预测结果自然不可用。这种问题最稳妥的解法是给训练和推理共用同一个特征编码脚本而不是各写一份。我工程上一直遵守单一事实来源原则特征编码逻辑只允许存在一份训练和服务都调用它。6.5 检查清单速查表检查对象关键问题推荐工具/做法环境依赖能否用锁文件在新机器复现uv requirements.lock数据版本数据文件是否被悄悄修改manifest 文件指纹训练脚本是否配置驱动且可回放YAML dataclass run_id模型导出导出前后输出是否一致golden test服务输入非法请求是否被提前拦截FastAPI pydantic线上监控是否能定位问题请求日志带版本号 输入/预测监控这张表基本就是我在每个 AI 工程开始前会贴在工位上自查的清单。不需要一次全做到但至少以它为目标越早补齐越省事。7. 最后想说的个人体会从零开始搭一套 AI 工程链路最明显的收获不是模型精度提升而是我终于可以放心地说“这个问题我能追踪到底”。新同事接手项目不用再靠“你跑一下试试”来摸索线上出了异常不用再靠玄学去猜是模型还是数据还是环境的问题。现在我反而觉得“从零开始”真正练的不是能力而是判断力。你知道什么时候该自己写、什么时候该用现成组件知道哪些环节值得花时间投资、哪些只配一个简单的脚本也知道一个工程最怕的不是功能少而是不可追踪、不可复现、不可解释。如果你正打算做自己的第一个 AI 工程项目我建议不要直接找一套全家桶平台把流程全部封装掉。先从一条最简单的数据管线开始自己把训练脚本写出可复现的样子把服务封装成能对外提供稳定接口的样子再把你踩过的每一次坑记下来。这个过程会慢但慢过之后你对 AI 工程的理解就不再是某一个框架的使用说明而是一套属于自己的方法论。最后分享一个小技巧给你的每次实验起一个“可读”的名字别用什么 exp_1、exp_final。我在项目里用的格式是{model_name}-{feature_set}-{timestamp}例如lightgbm-v2-20250112-1530。看起来只是命名习惯但它会让三个月后的你少掉很多头发。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进