1. 这不是“搭积木”,而是亲手锻造AI系统的全流程实战
“AI Engineering from Scratch”——这个标题乍看像一句口号,实则藏着一套完整、严苛、不容取巧的工程实践体系。它不指代某个现成框架的快速上手,也不等于调用几个API拼出个demo;它意味着从零开始,亲手定义问题边界、设计数据通路、构建训练闭环、封装服务接口、建立监控反馈,并让整套系统在真实业务负载下持续稳定运转。我带团队做过7个从零启动的AI产品落地项目,最深的体会是:90%的失败不在模型精度,而在工程链路的断裂与失焦。有人把“from scratch”理解为从Python环境装起,有人以为是重写PyTorch底层,这都偏了——真正的“从scratch”是回归工程本质:用可复现、可审计、可运维、可演进的方式,把AI能力变成组织内可调度的基础设施。它面向三类人:想摆脱黑盒依赖的算法工程师、需要交付可靠AI能力的后端/DevOps工程师、以及真正要靠AI驱动业务增长的产品与技术负责人。你不需要是博士,但必须懂数据版本如何影响线上效果,必须清楚GPU显存碎片化怎么拖慢推理吞吐,必须能看懂Prometheus指标曲线里那条异常抖动背后的真实瓶颈。这不是教程,是我在产线踩坑三年后,把所有散落的螺丝钉、拧断的扳手、烧糊的电源线,重新归类、编号、标注扭矩值后整理出来的实操手册。
2. 为什么必须“从零锻造”?——避开三大工程幻觉陷阱
2.1 幻觉一:“模型跑通=系统可用”——忽略数据流的熵增定律
很多团队在Jupyter里跑通ResNet50准确率92%,就宣布AI项目成功。结果上线后第一周,线上A/B测试显示效果下跌17%。查日志发现:训练用的是清洗后的COCO子集,而线上图片来自用户手机直传,包含大量模糊、旋转、强反光、非标准裁切。这不是数据漂移,是数据通路设计缺失。真正的“from scratch”第一步,不是写model.py,而是画出端到端的数据血缘图:原始数据源(数据库binlog?IoT设备MQTT?用户上传S3?)→ 清洗规则引擎(是否支持动态阈值?异常样本自动隔离?)→ 特征生成流水线(离线batch vs 实时streaming?特征缓存一致性如何保证?)→ 训练数据快照(带哈希校验的immutable dataset version?)→ 模型输入适配层(resize策略是否与线上预处理完全一致?)。我见过最痛的教训:某电商搜索排序模型,训练时用PIL.Image.open(),线上用OpenCV cv2.imread(),仅因RGB/BGR通道顺序差异,导致TOP10结果全错。这种错误无法靠单元测试覆盖,只能靠数据契约(Data Contract)强制约束——每个环节输出必须附带schema.json和sample_checksum,下游消费前强制校验。所谓“从零”,就是从第一行数据进入系统那一刻,就建立不可绕过的契约锚点。
2.2 幻觉二:“MLOps平台=自动化一切”——低估人工干预的刚性需求
Kubeflow、MLflow、Weights & Biases这些工具确实强大,但它们解决的是“如何记录”,而非“如何决策”。我们曾接入某头部MLOps平台,自动触发训练任务,却在模型上线前卡了三天:平台无法判断新模型是否真的优于旧版——因为业务指标(如GMV提升率)与离线指标(如AUC)存在长期滞后且非线性关系。最终靠人工拉取7天用户行为日志,用双重差分法(DID)做因果推断才敢发布。真正的AI工程化,核心不是自动化程度,而是人工干预路径的显性化与可追溯性。“from scratch”意味着你要亲手设计:谁有权审批模型上线?审批时必须查看哪些证据?(至少包含:离线指标对比表、线上小流量AB结果、关键bad case分析报告、回滚预案验证截图);当监控告警触发时,自动执行的只是“暂停流量+发钉钉”,真正决策者必须在15分钟内完成根因判断——这要求告警信息里直接嵌入特征重要性热力图、典型失败样本、最近三次训练的数据分布对比直方图。工具只是载体,工程思维才是骨架。那些宣称“一键部署”的平台,往往把最复杂的决策逻辑藏在黑盒里,等你出事时才发现连日志都找不到源头。
2.3 幻觉三:“微服务架构=天然适合AI”——忽视计算范式的根本冲突
把模型打包成Docker镜像,扔进K8s集群,看似标准。但很快会撞上硬伤:一个BERT-base模型加载需2.3GB显存,而K8s默认Pod调度器只认CPU/MEM,对GPU显存碎片毫无感知。结果出现:集群总显存剩余40GB,却因单卡剩余不足2.5GB,导致新Pod始终Pending。更致命的是冷启动延迟——每次请求都要加载模型权重、初始化CUDA上下文,平均耗时800ms,远超业务要求的200ms SLA。我们试过预热机制,但流量波峰时仍频繁超时。最终方案是彻底放弃“请求-响应”范式,改用长连接流式推理服务:GPU节点常驻进程,维护模型实例池,HTTP请求转为gRPC流式调用,复用CUDA context,冷启动延迟压至12ms。这要求你亲手写服务发现模块(基于Consul健康检查)、设计请求队列(优先级+超时熔断)、实现显存隔离(NVIDIA MIG或cgroups v2限制)。所谓“from scratch”,就是敢于打破微服务教条,根据AI计算的本质特征(高显存占用、低频高吞吐、状态强依赖)重构服务形态。不是所有云原生最佳实践都适用于AI,工程的第一课,是学会对抽象概念说“不”。
3. 核心四阶锻造法:从代码到生产环境的完整链路拆解
3.1 阶段一:定义“可工程化”的问题边界(Week 1-2)
这不是写PRD,而是用工程语言重述业务需求。例如业务方说:“提升推荐点击率”。这不行。必须拆解为:
- 输入契约:用户实时行为流(Kafka topic: user_action_v3),字段包括user_id(string)、item_id(string)、action_type(enum: click/view/cart)、timestamp(unix_ms);
- 输出契约:推荐列表(JSON array),每个item含id(string)、score(float, 0-1)、reason(string, 用于前端展示“为什么推荐”);
- SLA硬约束:P99延迟 ≤ 300ms(含网络传输),日均处理峰值QPS ≥ 12,000;
- 可观测性基线:每分钟采集指标:request_count、error_rate(>5%触发告警)、avg_latency_ms、gpu_util_percent;
- 回滚机制:当新模型导致CTR下降>0.5%持续15分钟,自动切回v1.2.3版本,并保留该时段全量请求日志供复盘。
我们用一份《AI工程需求规格书》(AERS)固化这些条款,签字方包括产品、算法、后端、SRE。它比任何技术文档都重要——因为所有后续开发,都是对这份契约的履约。曾有个项目因未明确定义“reason”字段的生成方式(是规则引擎还是模型副产物),导致算法团队用Attention权重生成,而前端按字符串模板渲染,上线后出现大量乱码。AERS里明确写:“reason字段必须为UTF-8纯文本,长度≤32字符,由post-processing module生成,禁止含HTML/JS”。这就是“from scratch”的起点:用契约消灭模糊地带。
3.2 阶段二:构建可审计的数据工厂(Week 3-6)
抛弃“数据清洗脚本”,建设声明式数据流水线。核心组件:
- Source Connector:用Debezium监听MySQL binlog,生成Avro格式变更事件,Schema Registry强制校验;
- Transformation Engine:采用dbt(data build tool),所有清洗逻辑写成SQL模型,支持依赖图谱自动生成、测试用例嵌入(如
test not_null on column user_id); - Feature Store:自建轻量级服务(非商业版Feast),关键设计:
- 离线特征:Hive表分区按
ds=YYYYMMDD,每日全量重算,保证幂等; - 实时特征:Redis Hash存储,key为
user:{id}:features,TTL=24h,更新由Flink作业触发; - 一致性保障:离线/实时特征key命名统一(如
user_last_7d_click_cnt),通过feature_version字段标识来源;
- 离线特征:Hive表分区按
- Dataset Versioning:每次训练前,执行
dataset create --name rec_train_v20240501 --source "select * from dbt_prod.fct_user_behavior",生成唯一SHA256哈希,存入MinIO并写入元数据库。训练脚本必须指定--dataset-hash xxxxx,否则拒绝运行。
实操心得:我们曾因Flink作业重启导致Redis特征短暂丢失,线上服务降级。解决方案是在Redis之上加一层本地内存缓存(Caffeine),设置refreshAfterWrite=10m,即使Redis不可用,也能用旧特征兜底。这并非妥协,而是工程韧性设计——可接受特征轻微过期,但绝不允许服务中断。数据工厂的终极目标,是让任何人在任何时间,都能用一行命令复现“2024年5月1日训练所用的全部数据”。
3.3 阶段三:打造可演进的模型服务(Week 7-10)
拒绝“一个模型一个服务”的反模式。采用统一推理网关+插件化模型容器架构:
- Gateway层(Go编写):
- 负责路由(根据请求header
X-Model-Version: v2.1)、鉴权、限流(令牌桶)、日志(结构化JSON)、指标上报(Prometheus); - 关键设计:支持灰度发布,可按user_id哈希分流(
hash(user_id) % 100 < 5→ v2.1);
- 负责路由(根据请求header
- Model Container(Python + Triton Inference Server):
- 每个模型打包为独立Docker镜像,基础镜像固定为
nvcr.io/nvidia/tritonserver:24.03-py3; - 模型配置
config.pbtxt明确定义:input/output tensor shape、data type、dynamic batching参数; - 启动脚本
entrypoint.sh强制执行:nvidia-smi -q -d MEMORY | grep "Used" | awk '{print $3}' > /tmp/gpu_used.log,便于监控;
- 每个模型打包为独立Docker镜像,基础镜像固定为
- Orchestration:K8s Helm Chart中,为每个模型定义独立Deployment,但共享同一个Ingress;资源请求精确到GPU显存:
resources.requests.nvidia.com/gpu: 1,resources.limits.memory: 8Gi。
避坑经验:Triton默认启用dynamic batching,但某些模型(如RNN序列生成)在batch size变化时输出不稳定。我们的解法是在config.pbtxt中关闭:dynamic_batching [ ],改用Gateway层做固定batch(如每次聚合16个请求)。这牺牲了部分吞吐,但换来结果确定性——对金融风控类场景,这是不可妥协的底线。
3.4 阶段四:建立闭环反馈与持续演进机制(Week 11+)
AI系统不是“上线即结束”,而是以周为单位的PDCA循环:
- Plan:每周一,SRE导出上周核心指标报表(延迟P99、错误率、GPU利用率、特征新鲜度);算法团队基于此,提出3个待验证假设(如“增加用户停留时长特征,可提升CTR 0.3%”);
- Do:数据工程师在dbt中新建feature模型,算法工程师提交训练脚本PR,CI流水线自动执行:
- 数据质量检查(空值率<0.1%、分布偏移KL<0.05);
- 离线指标测试(AUC提升≥0.005);
- 小流量AB测试(5%流量,运行48小时);
- Check:AB结果自动写入Tableau看板,触发Slack通知;若CTR提升显著(p-value<0.01),进入发布流程;
- Act:发布后,自动触发“影子模式”(Shadow Mode):新模型与旧模型并行预测,对比输出差异,生成
diff_report.html(含top100差异样本及原因分析),存入S3供算法复盘。
最关键的创新是人工审核门禁:任何模型升级,必须由至少两名资深算法工程师在内部系统中签署电子意见,意见字段强制填写:“已确认diff_report中无高风险case(如价格预测偏差>10%)”。这看似增加流程,却避免了某次因特征缩放系数错误导致的批量资损事故。所谓“from scratch”,就是把人的经验、判断、责任,编码进自动化流程的每一个关键节点。
4. 工具链选型深度解析:为什么不用“最火”的,而选“最稳”的
4.1 编程语言:Python仍是主力,但必须划定边界
Python的生态优势无可替代,但其GIL和内存管理缺陷在AI工程中会被放大。我们的规范:
- 数据处理层(ETL、特征计算):强制使用
modin(pandas兼容)或polars(Rust backend),避免原生pandas在大表上的OOM; - 模型训练层:PyTorch为主,但禁止在
__init__中加载大型预训练权重——改用torch.hub.load()按需下载,配合~/.cache/torch/hub目录挂载PV,防止Pod启动时并发下载压垮镜像仓库; - 服务层:Python仅用于模型推理逻辑,HTTP服务、负载均衡、健康检查全部交给Go(Gin框架)或Rust(Axum),实测QPS提升3.2倍,内存占用降低67%;
- 胶水层(调度、监控、告警):Bash + Python混合,但所有关键脚本必须有
set -euxo pipefail开头,错误立即退出,避免静默失败。
提示:曾因某团队在训练脚本中用
os.system("rm -rf /tmp/*")清理临时文件,误删了同节点上其他服务的PID文件,导致集群雪崩。现在所有文件操作必须用tempfile.mkdtemp()生成唯一路径,且脚本末尾强制rm -rf "$TMPDIR"。
4.2 基础设施:K8s不是银弹,要为AI定制
标准K8s发行版(如EKS、AKS)对AI负载支持有限。我们基于Rancher RKE2二次开发:
- GPU调度增强:集成
nvidia-device-plugin,并自研gpu-scheduler扩展,支持按显存碎片大小智能分配(非简单整卡分配); - 存储优化:训练数据存于CephFS(POSIX兼容),但模型权重、日志、指标存于对象存储(MinIO),通过
rclone mount挂载为本地路径,规避NFS性能瓶颈; - 网络加速:禁用Calico,改用Cilium,开启eBPF加速,实测GPU间NCCL通信延迟降低40%;
- 成本控制:Spot Instance + Karpenter自动扩缩容,但关键训练任务标记
priorityClassName: high-priority,确保不被驱逐。
注意:Karpenter的
ttlSecondsAfterEmpty参数设为300秒(5分钟),而非默认30秒。因为模型加载需时间,过短会导致Pod刚启动就被销毁,造成“启动风暴”。
4.3 监控体系:不止看GPU利用率,要看“有效计算”
Prometheus + Grafana是标配,但指标设计必须AI-aware:
- 基础层:
node_gpu_utilization(nvidia-smi)、container_memory_usage_bytes; - AI层:
triton_inference_request_success_total{model="rec_v2"}triton_inference_queue_duration_us{model="rec_v2"}(排队等待时间)model_feature_staleness_seconds{feature="user_last_7d_click_cnt"}(特征新鲜度)ab_test_ctr_lift_percent{experiment="v2.1_vs_v1.2.3"}(业务指标)
- 告警规则:
avg_over_time(triton_inference_queue_duration_us[5m]) > 10000000(排队超10秒)→ 立即扩容max_over_time(model_feature_staleness_seconds[1h]) > 3600(特征超1小时未更新)→ 触发数据管道告警abs(avg_over_time(ab_test_ctr_lift_percent[24h]) - 0) < 0.001(CTR提升趋近于0)→ 提示算法团队检查模型退化
我们拒绝“大盘式监控”,每个指标必须绑定明确的Action Plan。比如queue_duration告警,自动执行:kubectl scale deployment rec-v2-gateway --replicas=5,并在Slack发送执行日志。监控不是看板,是自动化的运维指挥中心。
5. 实操过程全记录:从零搭建电商个性化推荐系统(真实案例)
5.1 Day 1-3:环境奠基与契约签署
在阿里云ACK集群上初始化:
# 创建专用命名空间 kubectl create ns ai-engineering # 部署NVIDIA Device Plugin kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.5/nvidia-device-plugin.yml # 部署自研GPU Scheduler(已编译为helm chart) helm install gpu-scheduler ./charts/gpu-scheduler --namespace ai-engineering同步启动AERS评审会议。争议焦点在于“reason字段”:业务方坚持要动态生成(如“您常买牛奶,所以推荐酸奶”),算法团队认为模型无法解释。最终妥协方案:reason由规则引擎生成(基于用户历史购买品类Top3),模型只输出score,两者在Gateway层合并。AERS文档第3.2条明确:“reason字段不参与模型训练,其生成逻辑独立于AI pipeline,由rule-engine-service提供”。当天完成三方电子签。
5.2 Day 4-14:数据工厂流水线搭建
dbt项目结构:
models/ ├── staging/ # 原始数据映射 │ ├── src_mysql_users.sql │ └── src_kafka_actions.sql ├── intermediate/ # 清洗后宽表 │ └── int_user_features.sql └── marts/ # 业务主题 └── rec_training_dataset.sql # 推荐训练数据集关键SQL片段(int_user_features.sql):
{{ config(materialized='table', tags=['daily']) }} with base as ( select user_id, -- 使用窗口函数计算滚动统计,避免map-reduce倾斜 avg(click_cnt) over (partition by user_id order by ds rows between 6 preceding and current row) as last_7d_click_avg, max(timestamp) over (partition by user_id) as last_active_ts from {{ ref('stg_kafka_actions') }} where action_type = 'click' and ds >= dateadd('day', -7, current_date()) ) select user_id, last_7d_click_avg, last_active_ts, -- 添加数据质量标记 case when last_7d_click_avg is null then 1 else 0 end as has_missing_feature from base每日凌晨2点,Airflow执行:
# dag_rec_data_pipeline.py def run_dbt_task(): # 执行dbt run -s marts.rec_training_dataset # 生成dataset hash hash_cmd = "sha256sum /data/dbt/target/compiled/marts/rec_training_dataset.sql | cut -d' ' -f1" dataset_hash = subprocess.check_output(hash_cmd, shell=True).decode().strip() # 上传到MinIO subprocess.run(f"aws s3 cp /data/dbt/target/compiled/marts/rec_training_dataset.sql s3://ai-datasets/rec_train_{dataset_hash}.sql", shell=True) # 写入元数据库 insert_sql = f"INSERT INTO dataset_registry VALUES ('rec_train_v{ds}', '{dataset_hash}', '{ds}')" # ... executeDay 14交付物:rec_train_v20240501数据集,SHA256=a1b2c3...,已通过数据质量检查(空值率0.02%,分布KL=0.003)。
5.3 Day 15-28:模型服务容器化与网关开发
Triton模型配置config.pbtxt:
name: "rec_v2" platform: "pytorch_libtorch" max_batch_size: 16 input [ { name: "user_features" data_type: TYPE_FP32 dims: [128] } ] output [ { name: "scores" data_type: TYPE_FP32 dims: [100] } ] instance_group [ [ { count: 2 kind: KIND_GPU gpus: [0] } ] ] dynamic_batching [ ]Go网关核心路由逻辑(main.go):
func handlePredict(w http.ResponseWriter, r *http.Request) { // 解析header获取模型版本 modelVer := r.Header.Get("X-Model-Version") if modelVer == "" { modelVer = "v1.2.3" // 默认版本 } // 构建Triton gRPC请求 conn, _ := grpc.Dial(fmt.Sprintf("triton-%s:8001", modelVer), grpc.WithInsecure()) client := pb.NewGRPCInferenceServiceClient(conn) // 发送请求(省略序列化细节) resp, err := client.ModelInfer(ctx, request) if err != nil { // 记录错误详情到结构化日志 log.Error("triton_infer_failed", zap.String("model", modelVer), zap.Error(err)) http.Error(w, "Inference failed", http.StatusInternalServerError) return } // 合并reason字段 reason := getReasonFromRuleEngine(resp.UserFeatures) result := map[string]interface{}{ "scores": resp.Scores, "reason": reason, } json.NewEncoder(w).Encode(result) }Day 28完成:rec-v2-gatewayDeployment上线,支持X-Model-Version路由,P99延迟稳定在210ms。
5.4 Day 29-42:闭环反馈机制上线与首次迭代
AB测试配置(ab_config.yaml):
experiment_name: "rec_v2_launch" traffic_split: v1.2.3: 95 v2.0.0: 5 metrics: - name: "ctr" sql: "SELECT COUNT(*) FILTER (WHERE action='click') * 100.0 / COUNT(*) FROM events WHERE ds = '{{ds}}'" baseline: "v1.2.3" target: "v2.0.0"自动化流水线触发逻辑:
- Airflow检测到
ab_config.yaml更新,自动创建K8s ConfigMap; - Gateway读取ConfigMap,动态调整分流比例;
- 每2小时,Python脚本执行SQL计算CTR,写入
ab_results表; - 当
v2.0.0的CTR连续3次高于v1.2.3且p-value<0.05,自动执行:kubectl set env deployment/rec-v2-gateway DEPLOY_VERSION=v2.0.0
Day 42成果:v2.0.0模型上线,首周CTR提升0.82%,GMV提升1.2%。同时,diff_report.html发现12个高风险case(如新模型对老年用户推荐过度集中于保健品),推动算法团队优化特征交叉策略。
6. 常见问题与排查技巧实录:产线踩坑的21个真实瞬间
6.1 数据层问题:当“清洗干净”反而毁掉模型
现象:训练集清洗后AUC达0.85,但线上效果暴跌。
排查路径:
- 对比线上/线下样本分布:用
scipy.stats.ks_2samp计算各特征KS值,发现user_age特征在线上分布右偏(老年人占比高37%); - 检查清洗脚本:发现
staging/src_mysql_users.sql中有一行WHERE age BETWEEN 18 AND 65,过滤掉了所有老年用户; - 根因:业务方提供的“用户画像表”中,65岁以上用户age字段为NULL,清洗时被误判为无效数据剔除。
解决方案:
- 在AERS中补充数据契约:“age字段NULL值代表未知年龄,不得过滤,需替换为中位数”;
- dbt测试新增:
test not_null_on_age_or_mark_unknown; - 线上服务增加兜底逻辑:当
user_age为NULL,自动填充全局中位数。
实操心得:数据清洗不是追求“干净”,而是追求“业务真实”。所有过滤条件必须经业务方书面确认,且在测试用例中留痕。
6.2 模型层问题:GPU显存“神秘消失”
现象:Triton服务启动后,nvidia-smi显示显存占用95%,但torch.cuda.memory_allocated()仅报告1.2GB。
排查路径:
nvidia-smi -l 1持续观察,发现显存占用缓慢爬升;lsof -p $(pgrep triton)发现大量/dev/nvidiactl文件句柄未释放;- 查阅Triton源码,确认是CUDA Context泄漏;
- 升级Triton至24.03版本(修复了该bug)。
解决方案:
- 制定GPU组件版本矩阵表,明确Triton、CUDA Driver、PyTorch版本兼容性;
- CI流水线增加
nvidia-smi显存泄漏测试:启动服务→空载运行10分钟→对比显存变化,>5%则失败。
注意:不要迷信最新版,AI工程追求的是经过产线验证的稳定组合。我们当前矩阵锁定为:CUDA 12.1 + Triton 24.03 + PyTorch 2.2.0。
6.3 服务层问题:HTTP 503不是服务宕机,而是熔断生效
现象:Gateway返回大量503,但Pod状态正常,CPU/GPU利用率均低于阈值。
排查路径:
- 查看Gateway日志,发现
circuit breaker open字样; - 检查熔断配置:
failure_threshold=5, timeout=10s, half_open_after=60s; - 追踪上游Triton日志,发现
TRITONSERVER_ERROR_INTERNAL错误,根源是模型输入tensor shape不匹配(线上请求batch_size=1,但模型配置max_batch_size=16且未启用dynamic batching);
解决方案:
- Gateway层增加shape校验中间件,在请求进入Triton前,用
jsonschema验证输入; - Triton配置强制开启
dynamic_batching,并设置preferred_batch_size: [1,2,4,8,16]; - 熔断器配置改为
failure_threshold=20,避免单点抖动触发全局熔断。
提示:503是保护机制,不是故障。真正的故障是503后没有清晰的日志指向根因。所有中间件必须输出可追溯的trace_id。
6.4 监控层问题:指标“好看”但业务在恶化
现象:Dashboard显示GPU利用率90%,P99延迟200ms,但业务方投诉推荐质量下降。
排查路径:
- 拉取原始日志,发现大量
{"user_id":"U123","item_id":"I456","score":0.0012},score普遍偏低; - 检查模型输出:发现sigmoid层后数值被截断(float16精度损失);
- 根因:Triton配置
data_type: TYPE_FP16,但模型导出时未做proper量化。
解决方案:
- 所有模型导出脚本强制添加精度校验:
torch.onnx.export(..., opset_version=17, export_params=True, do_constant_folding=True); - 监控新增指标:
model_output_score_range_min、model_output_score_range_max,设置告警:min < 0.001 or max > 0.999; - AERS中明确:“模型输出必须为[0,1]区间float32,精度损失>0.001视为严重缺陷”。
实操心得:监控指标必须与业务语义对齐。GPU利用率再高,如果输出全是0.001,也是无效计算。
6.5 工程协作问题:当算法工程师提交了“不可部署”的代码
现象:算法PR通过CI,但部署时失败,报错ModuleNotFoundError: No module named 'transformers'。
排查路径:
- 检查Dockerfile:
FROM python:3.9-slim,未安装transformers; - 查看PR描述:算法工程师本地用
conda install transformers,但未更新requirements.txt; - 根因:缺乏“环境即代码”规范。
解决方案:
- 强制所有Python依赖写入
requirements.in,CI中执行pip-compile requirements.in生成requirements.txt; - Dockerfile必须
COPY requirements.txt .后RUN pip install -r requirements.txt; - PR模板增加检查项:“✅ 已更新requirements.in ✅ 已验证requirements.txt可安装 ✅ 已测试Docker镜像构建”。
注意:工程协作的基石是“可重复”。任何依赖、配置、环境变量,都必须代码化、版本化、自动化验证。
7. 我的体会:所谓“from scratch”,是把敬畏刻进每一行代码
做完这个项目,最大的改变不是技术栈的更新,而是心态的重塑。以前觉得“搞定模型就赢了”,现在明白:模型只是整个工程链条中最短的一环,而最长的、最易断裂的,是人与人之间、系统与系统之间的契约。那个被我们反复争论的AERS文档,最后成了团队最常打开的文件——不是因为它多完美,而是因为每一次线上事故的复盘,都能在里面找到当初没写清楚的那句话。AI Engineering from Scratch,本质上是一场持续的“契约建设运动”:和业务方契约化需求,和数据团队契约化供给,和运维团队契约化SLA,甚至和未来的自己契约化可维护性。它不追求炫技,只求在每一个交接点,都留下清晰、可验证、不可绕过的印记。当你亲手为第一个特征写完schema校验,为第一个模型配置好显存隔离,为第一个告警设定明确的Action Plan时,你就已经站在了AI工程化的起点。这条路没有捷径,但每一步凿下的痕迹,都会成为系统韧性的基石。