
写这篇东西的起因是最近一年被问得最多的一个问题想做 AI 工程但没有相关背景应该从哪里入手我自己的路径比较野前后端都写过中间折腾过运维和数据处理真正开始系统做 AI 工程是两年前从零搭一个内容理解的推荐系统。那会儿没有现成的团队带路全靠啃源码、跑实验、看生产环境报警一步步把整个链路摸了出来。这篇就把我从立项到上线再到迭代的完整思路和实操记录下来包括当时踩过的坑、换过的方案、以及最后沉淀下来的工程判断尽量做到你照着这个逻辑也能把项目从零拉起来。1. 项目定义与方案选型背后的取舍1.1 这个项目到底要解决什么问题“ai-engineering-from-scratch”这句话听起来很大但落到具体项目里必须收敛成一个可度量、可上线的东西。我当时给自己定的目标很简单做一个基于用户行为的实时内容推荐服务。输入是用户点击、停留、收藏等行为日志输出是每个用户每次请求时的 Top 20 候选内容列表端到端延迟压在 200 毫秒以内。为什么拿推荐系统当切入点因为它几乎覆盖了 AI 工程的所有核心环节数据采集与清洗、特征工程、模型训练、离线评估、在线推理、A/B 实验、监控反馈闭环。你把这些跑通了再去看 NLP、CV 或者大模型应用工程骨架都是一样的只是数据和模型形态不同。1.2 从零起步先定两个原则第一个原则能做简单方案就不上复杂架构。刚开始没有海量流量压力Kafka Flink TensorFlow Serving Redis 这种工业级全家桶看起来很美但事实是你连日志规范都没定清楚先把实时计算引擎搭起来纯属给自己加负。我第一版直接用了 MySQL 存日志、Python 脚本离线算特征、Flask 起推理服务整套东西一周就能跑起来后面才逐步替换组件。第二个原则每一层都要能单独验证。数据层、特征层、模型层、服务层每一层都要有清晰的输入输出和质检手段否则出了问题根本不知道是哪一环挂了。我会在每一层加一个最小可用的校验脚本哪怕它只是统计行数和空值率。2. 技术栈怎么选从第一版到第三版的演进2.1 第一版极简技术栈第一版说白了就是能跑通不追求性能数据存储MySQL一张 user_behavior 表字段包括 user_id、item_id、behavior_type、timestamp。特征计算Python 脚本每天凌晨跑批算出用户近 7 天、近 30 天的类目偏好向量存成 JSON 落到表里。召回基于类目偏好和热门内容用 SQL 直接查候选集。排序一个简单的逻辑回归模型特征用用户偏好、内容热度、内容新鲜度训练用 sklearn。服务Flask 提供/recommend接口内部先查用户特征再查候选集最后灌模型打分排序返回。这套东西非常简陋但它让我在两周之内把完整的推荐流程跑通了。当时最大的收获不是模型效果而是理解了延迟链路里的瓶颈到底在哪儿大部分时间耗在数据库查询上模型计算反而是最不起眼的部分。2.2 第二版把数据链路正规化第一版上线两周后发现两个问题日志表膨胀很快而且线上特征和离线训练特征不一致。前者好解决加个定时归档就行后者很致命因为模型在离线训练时看到的特征分布和生产环境实时算出来的特征分布不一样会导致预测偏差。所以第二版做了三件事引入特征平台的概念把特征计算从训练和推理的代码里独立出来统一时间窗口逻辑离线用同一套代码算历史特征在线用同一套代码算实时特征样本表单独存放每一条样本都带上特征快照方便复盘和重训。技术栈调整为PostgreSQL 替代 MySQL 做主力存储特征计算抽成独立 Python 包模型训练和推理共用同一个特征函数。2.3 第三版上了真正的在线学习第三版开始追求“实时”了。目标是把用户刚点的内容立刻反映到下一次请求的推荐结果里。这就绕不开在线学习。我用了很轻量的方案排序模型还是逻辑回归但改成小批量更新。每次服务收到用户反馈后把特征和标签写进 Redis 里的暂存队列攒够 100 条或者每隔 30 秒触发一次增量训练用新数据更新模型权重然后热加载到服务内存。这套方案比用 Flink 做实时特征流简单了两个数量级但对当时的业务体量完全够用。核心思路是实时性不一定非要靠重量级的流处理框架而是看业务对“延迟”的容忍度。我们允许反馈到生效的延迟在 30 秒到 1 分钟之间这就让小批量更新成为非常务实的方案。3. 数据工程最容易翻车但又最容易被忽视的部分3.1 埋点规范和数据质量检查我接手之前系统里的前端埋点是乱的同样一个“点击”事件有的端上报的是click有的是view还有的是show。做 AI 工程第一件事不是建模而是把数据字典定清楚。我们最终定义了统一事件模型事件 ID全局唯一由前端生成。用户 ID匿名 ID 或登录 ID。内容 ID内容侧的唯一标识。事件类型曝光、点击、停留、收藏、分享。事件时间客户端时间和服务端接收时间都记录方便对齐时钟偏差。扩展属性JSON 格式承载页面位置、实验分组等。数据质量检查也是从那时建立的。每天跑一组规则脚本重点看空 ID 率是否超过阈值事件时间的延迟是否异常核心事件的量级波动是否超过正常范围。这套检查救了我好几次有一次上线推荐逻辑的代码时没有做兼容处理导致部分老版本客户端上报的事件格式异常如果不是空值率报警及时拉停影响面会大得多。3.2 离线数据 pipeline 的构建细节离线 pipeline 当时用 Airflow 做调度DAG 分三层原始数据层每天从 MySQL 导出当天的行为日志落成 parquet 文件。特征层跑统一的特征计算任务生成用户特征宽表和内容特征宽表。样本层把点击行为转成正样本未点击的曝光转成负样本并拼接当时的特征快照。这里有个容易被忽视的细节正负样本的构造逻辑。刚开始我把曝光但没点击的样本直接当负样本结果模型预测值整体偏低线上指标却很差。后来排查发现是问题出在“曝光”事件本身有偏——有些内容根本没有曝光机会它们被选为负样本只是因为不在热门池里。修正方案是只使用真正展示给用户的曝光记录来构造负样本同时加入随机负采样来补充。4. 特征工程从想法到落地的关键细节4.1 特征怎么设计才有效特征设计完全取决于你要预测什么。在推荐场景里我们预测的是“用户点击内容的概率”所以特征围绕三方面展开用户侧近 7 天活跃度、近 7 天各类目点击分布、近 30 天收藏内容数、历史平均停留时长。内容侧内容类目、发布时间、作者历史表现、内容的平均点击率。交互侧用户与内容类目的匹配度、用户对该作者的历史点击情况、当前时间段的活跃权重。最有效的往往不是花哨的深度特征而是几个简单特征之间的交叉。比如“用户近 7 天对某一类目的点击占比”和“当前内容的类目”交叉比单独的数值特征提升明显这个交叉特征我在第二版之前一直没有加加上之后 AUC 提升了 0.8 个百分点属于性价比极高的改进。4.2 特征一致性一个反复强调的话题特征一致性是离线训练和在线推理之间最大的坑。我当时吃过一个亏离线训练样本里的“内容发布时间”是直接用数据库里的原始时间戳但在线推理时为了省时间用了一个预先算好的“内容年龄”字段。问题在于这个字段只在每天凌晨更新白天发布的新内容在在线推理时拿到的年龄不是 0而是负数或是一个默认值。模型一下就乱了。之后的处理方式是所有的特征计算统一收敛到同一个函数库训练和推理必须走同一套代码。为了让这个约束真正落地我把特征函数做成了 Python 包训练和在线服务都依赖同一个版本每次改动必须同时更新两边并设置了一个自动化测试来校验两边产出是否一致。4.3 归一化和缺失值处理逻辑回归这类线性模型对特征尺度敏感我当时对所有数值特征做了 z-score 归一化。这里要注意归一化的均值和方差必须从训练集统计然后固化下来线上推理时直接套用同一组参数绝对不能在线实时计算否则会因为样本分布波动导致特征值和离线差异很大。缺失值的处理也要提前想清楚。数值特征我统一填空值用训练集的均值类别特征填一个UNKNOWN字符串然后在特征工程层就完成填充模型侧不感知缺失。这样做的好处是线上逻辑简单不会因为特征缺失导致回归失败。5. 模型选择与训练细节5.1 为什么从逻辑回归起步我知道现在很多人一上来就想上 DeepFM、DIN 或者 Transformer 之类的模型但我强烈建议第一个版本先用逻辑回归。原因有三个模型简单意味着问题定位简单。模型不收敛、特征有 Leakage、样本构造错了这些在简单模型上能很快暴露。线上推理开销小不需要 GPU一台 4 核 8G 的云主机就能扛住流量。可解释性好特征权重直接能看出每个特征的影响方向业务方也容易理解。我的经验是先用简单模型跑通基准再逐步加复杂度。如果简单模型的效果已经很差深度模型往往会放大这个差的结果而不是拯救它。5.2 训练样本的时间窗口训练样本的时间窗口选择直接决定模型的时效性。我当时用近 14 天的行为日志作为训练集每 24 小时重训一次。为什么不是 7 天或者 30 天这也是试出来的7 天窗口样本量不够模型波动大AUC 在验证集上忽高忽低。30 天窗口样本量充足但内容热度变化快老样本对当前时刻的分布代表差。14 天窗口在当时的业务体量下样本量和时效性达到了相对平衡。所以训练窗口不是一个拍脑袋定的数而是基于样本量和业务变化速度的判断。5.3 增量训练与全量训练的配合第三版上了在线学习之后我采用了双轨策略每天凌晨跑一个全量重训把近 14 天的样本全量训练一遍得到基准模型白天的增量更新在这个基准模型上做小步长更新用于捕获实时变化。这里要特别小心增量训练导致模型偏移的问题。模型持续跟着实时反馈走容易被少数极端反馈带偏比如某个时间段一小撮用户的异常点击行为。我的处理方式是给增量训练设置一个权重上限且增量更新的步长设置为全量训练步长的十分之一保证模型主体稳定、细节实时。6. 推理系统的设计与线上表现6.1 服务架构千万别过度设计推理服务我最终拆成了三个模块特征服务、候选生成器、排序服务。特征服务负责从 Redis 和 MySQL 读取特征候选生成器负责产出用户可能感兴趣的候选内容排序服务负责给候选打分排序。早期只有一张表和一个逻辑回归时我试过把所有逻辑塞进一个 Flask 接口里。后来发现每次改动都要重新发版而且性能问题不好定位才拆分开。但注意拆分到三个模块就够了不要着急上微服务、服务网格、Kubernetes 那套。我当时在流量还没有起来的时候就是一台 8 核 16G 的云主机跑这三个进程再加上 Nginx 做入口转发稳定运行了一年多。6.2 延迟优化的三个维度推荐服务最核心的指标就是端到端延迟。我优化延迟主要从三个角度入手缓存用户特征和内容特征的读取用 Redis 做一层缓存并设置合理的过期时间。用户特征缓存 60 秒内容特征缓存 5 分钟因为内容侧的属性变化得比用户侧慢。并行化候选生成和特征读取没有强依赖我用 Python 的线程池并行执行延迟从 150 毫秒降到了 90 毫秒左右。注意 Python 的多线程受 GIL 限制但这里的主要耗时是数据库和网络 IO线程池在 IO 密集型场景下依然有效。压缩特征量初始版本的特征数量有 80 多个后来通过特征重要性分析砍到了 30 个以内排序延迟明显下降AUC 反而小幅提升因为很多相关性弱的特征对模型来说是噪声。6.3 服务稳定性与降级方案线上服务稳定性是 AI 工程的一个重要议题。模型不是永远能给出合理的预测结果特征服务也不是永远可用。我提前做了三档降级方案正常态走完整链路特征 召回 排序。降级态特征服务挂了直接用热门内容作为候选按内容热度排序。熔断态整个推理服务异常直接在 Nginx 层返回兜底缓存的内容列表。这三级降级条数都是提前压测过的结果不是拍脑袋。每一级降级都要有对应的监控报警确保系统不会从正常态直接掉到熔断态而没有感知。7. 评估体系离线指标和线上指标要分开看7.1 离线评估的正确姿势离线评估我主要看三个指标AUC、GAUCGroup AUC按用户分组计算、以及 Ranking 类指标如 RecallK。其中 GAUC 比全局 AUC 更能反映推荐系统的真实效果因为它按用户聚合避免了头部用户主导整体分数的问题。离线评估最需要注意的坑是 Leakage。我之前犯过一个经典错误把用户对内容的点击行为同时用于构造特征和标签导致离线 AUC 虚高到 0.97上线上实际效果却很一般。原因是特征里已经包含了标签的信息。排查之后把特征和标签在时间上做了严格隔离特征只使用标签时间之前的行为数据标签使用之后的行为数据。7.2 线上 A/B 测试的方案线上评估我在简单场景里用到了 A/B 测试。设计 A/B 测试时要注意几个要点分流层级按用户 ID 哈希分为实验组和对照组两组用户的流量各占 50%保证同层互斥避免同一个用户同时命中多个实验。观察指标核心看点击率CTR、次均停留时长、人均推荐消费内容数辅助看人均举报数和负反馈率。测试时长至少跑满一个自然周覆盖工作日和周末的行为差异。只跑两三天就下结论没有意义。线上实验结果有时和离线评估不一致这很正常。离线评估看的是历史数据上的表现线上存在延迟反馈、探索利用问题两者天然有差异。我见过很多团队因为离线指标好就直接全量上线结果线上指标反而下滑这种事情在推荐系统里非常常见。8. 常见问题与排查实录8.1 模型上线后点击率反而下降了现象离线 AUC 比旧模型提升 2 个百分点全量上线后 CTR 反而下降 5%。排查过程先看数据分布检查线上请求的特征分布和训练样本的特征分布发现部分新内容的“年龄”特征在线上出现了值域溢出训练集里最大的年龄是 1440 分钟线上却出现了 2880 分钟。再看特征链路特征服务对未知值的处理方式是返回默认值这个默认值在模型里对应的权重正好是负权重导致新内容被系统严重打压。最后定位根因特征服务对“未见过”的内容没有做默认初始化处理。解决方案在特征服务启动时加载一份“冷启动特征模板”对出现在召回结果里但不在特征库中的内容用同类目平均特征初始化并且把这个逻辑做成可配置项方便后续调整。8.2 增量训练把模型带偏了现象上线在线学习后某一类极端小众内容的点击率异常飙升但整体点击率没有变化。排查过程第一步看样本发现这类小众内容在实时反馈里被大量点击原因是某个外部渠道导入了大量对该类内容感兴趣的新用户这些用户的行为模式跟老用户差异很大。第二步看权重增量训练的步长对异常样本没有做保护导致模型快速朝这部分用户偏移。解决方案给增量训练加了重要度采样对来源异常的用户群进行降权同时增加了样本分布漂移监控当实时样本的类目分布和训练集差异超过阈值时暂停增量更新并报警。8.3 候选集生成时 Redis 缓存穿透现象线上出现短暂的毛刺延迟平均延迟从 85 毫秒飙升到 2 秒。排查过程发现是某个热点内容被大量并发请求命中所有请求同时去查数据库回源缓存一直没有命中形成了缓存穿透。解决方案布隆过滤器前置拦截对不存在的内容 ID 直接返回空结果不触发数据库查询同时缓存空结果并设置较短的过期时间后续二次请求直接从缓存拿结果。8.4 特征延迟导致新用户推荐质量差现象新注册用户没有任何历史行为推荐结果和热门榜单完全一样体验很差。解决方案做了一个基于内容属性的召回分支——新用户没有行为特征时用注册时选填的兴趣标签作为冷启动特征同时使用一个独立的冷启动逻辑回归模型专门处理行为稀疏用户。效果是次日留存提升了 3 个百分点。9. 一些想分享给你的工程心得9.1 工程化不是炫技是把边界想清楚AI 工程和算法工程师最大的区别在于算法关注的是模型效果的提升边界而工程关注的是整个系统从数据到服务的可靠性边界。我见过太多项目死在不稳定上离线效果很好线上抽风单机测试没问题一上并发就崩。把这些边界想清楚比调参重要得多。9.2 每一步都要留观测点从数据摄入开始每一步都要埋观测点原始日志量、清洗后保留率、特征覆盖率、样本数、模型得分分布、线上延迟分位数、缓存命中率。我后来做的监控面板上大约挂了 40 多个指标。看起来繁琐但正是这些指标帮我在问题发生的最初几分钟内定位根因而不是等业务方发现后再去翻日志。9.3 算法和工程永远是一起迭代的一开始我以为模型效果提升要靠改模型结构后来发现 70% 以上的提升来自数据质量和特征链路的优化。同一个位置数据质量变好后模型 AUC 提升了近 10 个百分点这比换任何模型结构都管用。所以我的一个长期习惯是每次做优化之前先花时间检查数据再检查特征最后才轮到模型结构。这个顺序沿用到了现在。9.4 给想从零开始做 AI 工程的人一个实操路径如果你现在什么都没有只有一台普通电脑和一点编程基础我建议按这个顺序推进第一步用 Python 写一个简单的新闻分类器学习数据清洗、分词、向量化、模型训练、指标评估的完整套路。第二步把训练好的模型封装成 Flask 接口用 curl 调用一次体验模型和服务的衔接。第三步把接口部署到一台云服务器上学习用 systemd 管理进程、用 Nginx 做反向代理理解线上服务的基本素养。第四步引入消息队列做异步数据回流尝试在服务里做小批量的模型更新。第五步至此你已经具备 AI 工程的最小闭环能力。接下来再深入了解某个垂直方向推荐、检测、文本处理都不再困难。我个人在实际操作中的体会是AI 工程的价值不在于模型有多新而在于系统能不能持续稳定地产生业务价值。从零拉起一个项目最大的成本不是算法难度而是决策成本——什么时候该简化、什么时候该正规化、什么指标才是真的关键。希望这篇分享能让你在从零起步的路上少走几个弯路。