☰
从零开始搭建AI工程:推荐系统全链路实战详解
2026/11/3 14:49:30 网站建设 项目流程

写这篇东西的起因,是最近一年被问得最多的一个问题:想做 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、GAUC(Group AUC,按用户分组计算)、以及 Ranking 类指标如 Recall@K。其中 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 工程的价值不在于模型有多新,而在于系统能不能持续稳定地产生业务价值。从零拉起一个项目,最大的成本不是算法难度,而是决策成本——什么时候该简化、什么时候该正规化、什么指标才是真的关键。希望这篇分享能让你在从零起步的路上少走几个弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询