拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零构建AI工程:数据、部署、监控与成本控制全指南

从零构建AI工程:数据、部署、监控与成本控制全指南

1. 先想清楚什么是 AI 工程,别急着写代码

我见过太多人把"AI 工程"和"AI 编程"划等号。学了几个框架,跑通了一个开源模型,就觉得已经入了门。结果一到真实业务里,连第一个需求都扛不住——模型在笔记本上跑得好好的,上了服务器就崩;离线评测分数很高,上线后被业务方喷得体无完肤。问题出在哪?出在"工程"两个字上。

AI 工程真正要解决的,不是"模型怎么训练",而是"模型怎么稳定地在业务里产生价值"。它至少包含三块:模型开发、系统接入、持续运营。模型开发只是地基里的第一铲土,后面还有数据管线、服务部署、监控告警、成本控制、迭代机制——每一块都得从零开始搭。这个项目叫 "ai-engineering-from-scratch",我理解它的本意就是:不依赖任何现成的 AI 平台,不回避底层细节,一个人把整套体系从无到有建起来。

这一篇我就用自己折腾出来的完整路径来讲,中间有不少踩坑后的修正。适合谁看?两类人:一是刚入行的算法工程师,想搞明白"模型之外还有什么";二是后端开发者,想系统性补上 AI 工程能力。无论你是哪一类,看完能少走至少三个月的弯路。

1.1 AI 工程为什么容易被人低估

很多人低估 AI 工程,是因为他们看到的只是"模型推理"这个单点。但在真实项目里,模型推理只占整个系统的一小部分。我做过一个内容风控项目,最终上线的代码里,模型相关部分不到 20%,剩下 80% 是特征处理、规则融合、异步任务、缓存策略、人工复审流程、报表统计。

这个比例一点也不夸张。你想想,一个模型要真正服务业务,它得先有干净可靠的数据输入,得有稳定的服务框架托底,得有阈值策略应对不同置信度,得有兜底规则防止模型抽风,还得有完整的链路追踪让问题能被定位。每一环都是工程问题。

另一个原因是 AI 项目的不确定性比其他软件项目高得多。传统软件开发,需求明确后,工期估算基本靠谱。AI 项目不是这样——模型效果可能因为数据分布变化突然崩掉,第三方依赖可能因为版本升级带来行为变化,同一个 prompt 换个标点符号结果就不同。这种不确定性,只能用工程手段去对冲:灰度发布、A/B 测试、评测回归、持续监控。没有这些,项目就是在裸奔。

所以我的第一个建议是:从零开始做 AI 工程,先别急着跑模型,先把"工程"两个字吃透。你接下来做的每一个技术选型、每一段代码组织、每一次部署策略,都是在为不确定性上保险。

1.2 从零开始需要建立的三个思维

我总结了三个最重要的思维方式,它们比任何具体技术都关键。

第一个是"数据思维"。模型是数据的浓缩,数据质量决定了模型效果的上限。很多新人花大量时间调模型参数,却不愿意花时间去看原始数据长什么样。我见过最夸张的例子,有人用爬虫抓了 10 万条文本训练分类模型,后来发现其中 30% 都是重复的,还有 15% 的标签标错了。效果能好吗?肯定不能。

第二个是"评测思维"。永远先定义"什么叫做好",再动手做模型。没有评测标准,你就是在茫茫大海里游泳,永远不知道方向对不对。我自己的习惯是,任何项目开工第一天,先写评测脚本和第一批评估样本,哪怕用的是最简单的规则。

第三个是"成本思维"。GPU 很贵,人工审核很贵,数据标注很贵,甚至 prompt 里的 token 也是钱。方案设计阶段不考虑成本,上线后一定会被现实毒打。有一个项目最开始设计的是 streaming 方式流式返回,效果好但 GPU 占用极高,后来改成批量异步处理 + 缓存复用,成本直降 70%。

有了这三个思维打底,下面讲具体的技术路径才有意义。

2. 地基怎么打:环境、数据、代码三位一体

从零开始,第一个要解决的就是环境问题。Python 环境依赖、CUDA 版本、不同的框架之间的冲突,这些能消耗掉一个新手一整个星期,而且毫无成就感。我自己的方案经历了三次迭代,最后稳定在用 conda 管环境、用 Docker 管部署的双轨制。

2.1 环境管理:为什么是 conda + Docker 双轨制

先说开发环境。我强烈建议用 conda,不管是 Miniconda 还是 Anaconda。理由很简单:conda 不仅管 Python 包,还能管 CUDA、cuDNN 这些底层库。你用 pip 装 PyTorch GPU 版,它默认给你装一套 CUDA 运行时;但你系统里可能已经有一版 CUDA,两套互相之间经常出幺蛾子。conda 能把 CUDA toolkit 和 Python 包放在同一个虚拟环境里,互不干扰。

我每个项目的环境创建命令大概是这样的:

conda create -n ai-eng python=3.10 conda activate ai-eng conda install cudatoolkit=11.8 cudnn=8.6 -c conda-forge pip install torch==2.0.1

这里有个小技巧:conda install cudatoolkit和pip install torch分开装。如果你直接用 pip 装 torch,它会下拉 NVIDIA 的预编译 CUDA 库,虽然也能跑,但当你需要编译自定义算子时,经常和系统 CUDA 对不上版本。分开装的好处是,你能精确控制 CUDA 版本,编译不依赖运行时。这个坑我踩过两次,浪费了整整一天。

再说部署环境。开发环境是 conda,部署环境必须 Docker。为什么?因为你要保证"在我机器上能跑"变成"在任何机器上都能跑"。

我写过一份还不能公开的 Dockerfile,核心思路是:基础镜像用nvidia/cuda:11.8.0-cudnn8-devel-ubuntu20.04,然后创建一个非 root 用户运行服务。这个细节特别重要——很多人在 Docker 里用 root 跑模型服务,一旦容器被攻破,宿主机就裸奔了。

开发环境和部署环境之间的衔接也是一门学问。我的做法是:在 requirements.txt 里固定所有直接依赖的版本号,用 pip-tools 生成完整的锁定版本,然后在 Docker build 里按照锁定版本安装。这样开发环境即使用 conda,依赖却是完全一致的。

2.2 数据先行:数据版本管理和数据质量检查

代码有 Git 管版本,数据怎么办?很多人训练完模型后,数据文件散落在各个磁盘路径,别人问起"你这个模型的训练数据是哪个版本",没人说得清。这就是数据版本管理缺失的典型症状。

我在项目里试过 DVC(Data Version Control),它能像 Git 管理代码一样管理数据。第一次建模时,一个完整的档案在 DVC 里就是这样的流程:初始化仓库、把远程存储挂上、把数据添加进去、打 tag。到后面每次数据更新,就提交数据变更记录。

不过说实话,DVC 的体验一般,特别是对不熟悉命令行的新人来说门槛略高。后来我换成了一个更务实的方案:用文件命名和清单文件组合。数据目录结构长这样:

data/ ├── raw/ # 原始数据,只读,永不修改 │ └── 2024-06-01_crawled_posts.json ├── processed/ # 清洗后的数据 │ └── 2024-06-05_news_classification_all.jsonl ├── splits/ # 划分好的训练/验证/测试集 │ ├── 2024-06-05_train.jsonl │ ├── 2024-06-05_dev.jsonl │ └── 2024-06-05_test.jsonl └── manifest.csv # 数据文件清单

manifest.csv 里记录每个文件的生成时间、来源、行了、hash 值,谁生成了它、用了什么脚本。这样,任何时候回溯,都能找到一份数据的完整血缘。虽然不是最花哨的方案,但它简单可靠。

数据质量检查是更关键的一环。我在每个项目的第一周都会写一个data_quality.py脚本,跑一遍基础的统计检查:重复率、空值率、标签分布、数据来源分布、文本长度分布。这些指标在后续每次训练前都要跑一遍,防止数据集在某个环节被无声地污染。

2.3 代码结构:一个能成长的骨架

代码结构直接决定了这个项目能走多远。我见过很多 AI 项目的代码只有一个大 notebook,或者一个几百行的 main.py,里面什么都有:数据加载、模型定义、训练循环、评测、可视化。这种代码在第 1 个迭代还能用,到第 10 个迭代就是灾难。

我的标准结构是这样划分的:

ai-engineering-from-scratch/ ├── configs/ # 所有配置,YAML 格式 │ ├── data.yaml │ ├── model.yaml │ └── train.yaml ├── data/ # 数据目录(见上一节) ├── src/ # 核心代码 │ ├── data/ # 数据处理相关 │ │ ├── loader.py │ │ ├── preprocess.py │ │ └── quality_check.py │ ├── models/ # 模型架构定义 │ │ └── classifier.py │ ├── train/ # 训练相关 │ │ ├── trainer.py │ │ ├── loss.py │ │ └── optimizer.py │ ├── evaluate/ # 评测相关 │ │ └── metrics.py │ └── serving/ # 服务相关 │ ├── api.py │ └── inference.py ├── scripts/ # 入口脚本,尽量薄 │ ├── run_train.py │ ├── run_evaluate.py │ └── run_serve.py ├── tests/ # 单元测试,别偷懒 ├── notebooks/ # 探索性分析,不参与主流程 └── pyproject.toml

这个结构最核心的原则是:配置与代码分离、入口脚本保持薄、每个模块只做一件事。我见过太多人把超参写死在代码里,每次调参都要改代码重启,改着改着就忘了基准版本用的什么参数。配置文件 + 实验记录可以完美解决这个问题。

讲句不好听的,代码结构这件事,大多数教程都不会认真教你,但它在真实项目里比模型结构重要得多。我现在的习惯是:项目开工第一天就把骨架搭好,哪怕是一个空壳,先架起来再往里面填。

3. 第一个实战:从文案质检到多标签分类

地基打完了,该上真家伙了。我拿自己做过的"电商文案质量评估"项目作为示例,完整走一遍从零到一的流程。这个任务的背景是:电商平台上每天产生大量商品描述文案,运营团队需要从中找出低质量、可能误导用户的文案进行整改。以前靠人工抽查,一天只能看 200 条,漏检率极高。

这个任务天然的边界是:需求听起来很模糊——"什么是低质量文案"?不把这个定义清晰化,后面所有工作都没法展开。

3.1 把模糊需求拆成可量化的任务定义

我和运营团队开了一次需求对齐会,把他们平时审核文案时关注的点全部列出来,归纳出了五个维度:

  • 虚假宣传:使用了"顶级""最好""100%有效"等绝对化用语;
  • 信息缺失:缺少规格参数、材质、产地等关键属性;
  • 词不达意:文案与商品实际不符,比如明明是食品却写"帮助提升工作效率";
  • 语病错别字:存在明显语法错误或错别字;
  • 违规导流:包含微信、QQ 等联系方式或外链引导。

每个维度的判定标准写成了带正反例的文档,然后让运营团队一份一份地标注。这样,任务从"判断文案是否低质"变成了"对文案做五个维度的多标签分类"。多标签和单标签有个重要区别——一个文案可以同时命中多个问题,所以最终输出是五个维度的概率分布,而不是单一标签。

这一步是整个项目最重要的决策。如果你把模糊需求直接丢给模型"帮我判断文案好还是不好",模型的输出你没法解释,运营团队也不敢信。拆解成维度后,模型的每个输出都有明确含义,人机还可以协同——低置信度的案例进人工复审队列,高置信度的自动处理。

3.2 基线与评估指标先行

任务定义清楚后,我在动手写任何模型代码之前,先定义了两个基线:

第一个是规则基线。五个维度里,"违规导流"和"虚假宣传"实际上可以用正则表达式做得很准。导流特征无非是微信、QQ、手机号模式;绝对化用语有一份相当完整的词表。我写了一个规则引擎,跑了初版数据,效果已经不错。这个规则基线的意义在于——如果模型连规则都打不过,那它没有上线价值。

第二个是简单模型基线。用 TF-IDF + 逻辑回归做一轮快速实验,不追求最好效果,只求建立"模型路线"的起点。

评估指标上,五个维度都采用主要看 F1(精确率和召回率调和平均)的宏平均,其中"虚假宣传"维度单独看精确率,因为这个维度误伤运营的信任成本极高。同时,因为类别存在不平衡(语病错别字远少于其他维度),我还记录了每个维度的样本数和各自的 F1。

评估集是用一周的运营标注结果构建的,一共 1500 条,1000 条训练、250 条验证、250 条测试。在还没开始深度学习之前,我已经能回答"这个模型算不算好"这个问题。

3.3 快速构建和验证流程

规则基线效果非常好,"违规导流"F1 达到 0.94,"虚假宣传"达到 0.90,总耗时不到一个下午。TF-IDF + 逻辑回归的总 F1 是 0.78,明显不如规则。

这时候团队里有人提议:"那直接用规则算了,别搞模型了。"我没有同意。原因是规则的覆盖面有天花板:新话术、变种写法、复杂的语句结构,规则都没办法泛化。但我也没有说"规则没用"——最终方案是混排:规则层把确定性能处理掉的案例直接处理掉,剩下的模糊案例交给模型判断。

接着上深度学习模型。当时试了三个方案:TextCNN(轻量快速)、BERT base 微调、以及后来很流行的向量嵌入加分类头方案。预实验结果显示,BERT base 微调的总 F1 达到了 0.86,比逻辑回归高 8 个点,其中"词不达意"维度提升了 15 个百分点。

其实到这一步,我已经积累了一套可以复用的公式:先规则快速兜底,再用模型提升泛化,最后接人工复核流程。很多 AI 项目翻车,是因为要么只做规则太死板,要么只做模型不收约束。

这里再补充一个实际经验:微调 BERT 时,千万要固定随机种子,并且跑两到三次取平均。因为 BERT 的微调过程对随机性相当敏感,好几次实验结果差异只来自随机种子不同,却差点让我误判两个超参的性能优劣。

4. 把模型放进真实业务链路中的关键细节

模型在离线评测里拿到了 F1 0.86 的成绩,是不是就可以上线了?差得远。从"离线跑得好"到"线上用得稳",中间还要穿过一整片丛林。这一节讲我认为最容易被忽视的四个技术细节。

4.1 服务层选型:FastAPI 真够用,别过度设计

我用 FastAPI,不用 Flask,也不上 LangChain 之类的重型框架。理由很直接:FastAPI 自带 OpenAPI 文档、输入输出校验、异步支持,对模型服务这种 IO 密集型场景正好合适。Flask 的同步 WSGI 在模型推理时容易被阻塞,撑不住并发请求。

模型服务的核心代码比很多人想得要薄:

# serving/api.py(核心结构示意) from fastapi import FastAPI from pydantic import BaseModel, Field class EvalRequest(BaseModel): text: str = Field(..., min_length=1, max_length=2000) dimensions: list[str] = ["misleading", "missing_info", "irrelevant", "typo", "diversion"] class EvalResponse(BaseModel): results: dict[str, float] rule_hit: dict[str, bool] model_used: str app = FastAPI(title="TextQualityEval") @app.post("/v1/evaluate", response_model=EvalResponse) async def evaluate(req: EvalRequest): # 1. 先跑规则层 rule_result = rule_engine.check(req.text) # 2. 规则未覆盖的维度送进模型 model_dims = [d for d in req.dimensions if d not in rule_result or rule_result[d] is None] model_result = {} if model_dims: model_result = model_service.predict(req.text, model_dims) # 3. 合并结果,返回带解释的数据结构 return compose_response(rule_result, model_result)

这个"规则先行、模型兜底"的接口设计,带来的好处是响应时间大幅下降——约 35% 的请求根本不会走到模型层,规则在十几毫秒内就返回了。只有当规则不能确定时才进入模型推理,平均延迟从 800ms 降到了 500ms 左右。

响应结构里我刻意把rule_hit和model_used都传回去,这是为了方便排查问题——当你怀疑线上结果不对时,能直接看出到底是规则的功劳还是模型的判断,而不是两眼一抹黑。

4.2 上下文管理与结构化输出:稳定性的基本盘

这一小节看起来基础,但却是线上踩坑最多的一环。

先说上下文管理。如果你的应用是对话式、多轮式的场景,上下文长度是一个必须仔细管理的东西。很多真实故障都源于上下文无限累积:用户每轮都带完整历史进去,token 数很快飙升,然后触发长度限制或者直接报错。

我的做法是固定窗口大小,并且有一个"定点清理"机制:比如最多保留最近 8 轮对话,超过的自动截断;每条内容按时间衰减权重,权重低于阈值的先裁剪;必要时用摘要模型把旧文压缩成 100 字以内的提要再添回去。这一步起码能省 40% 的 token,还不影响关键信息。

再说结构化输出。模型直接输出自由文本,解析起来非常痛苦,prompt 里写了"用 JSON 返回",还是经常输出 Markdown 代码块或者多了一个逗号的 JSON。我的兜底方案是两步走:

第一步,prompt 里明确输出格式,并且附上一个极其简单的示例。不做复杂系统提示词,越简单越不容易出幺蛾子。

第二步,解析时用"先抽提取 JSON 段落、再校验、最后再修"的思路。直接用json.loads会是脆弱的高危操作,很容易被一个注释或尾随逗号搞崩。那些"看起来像 JSON 但又不严格是"的内容,我会写一个小的清洗函数。

如果业务上对输出的合法性和稳定性要求特别高,建议采用 constrained decoding 或者用函数调用机制来约束输出内容,而不是靠 prompt 碰运气。

4.3 监控、灰度与回滚:模型上线是一场手术

模型上线最忌讳的是"一把梭"。我的标准动作是:先拉一个影子环境,把线上真实流量同时发给旧模型和新模型,新模型的输出只在日志里记录、不真正生效;对比一段时间后,让新模型接管 5% 的流量,没有异常再逐步提升到 50%、100%。整个过程建议用配置中心控制,不改代码、不重新发版。

监控指标我通常分三层:

  • 系统层:响应时间、请求量、GPU 利用率、显存占用;
  • 模型层:预测置信度分布、规则命中率、模型拒绝率;
  • 业务层:用户反馈率、平均处理时长、复审率变化。

业务层指标最容易被忽略,但却是最重要的——它直接反映模型对业务目标有没有帮助。我见过太多 AI 项目,上线后系统层指标一切正常,业务指标却不降反升,就是因为没有监控业务层。

回滚机制方面,有一套设计的核心是"旧模型不能删"。我一般会保留最近两个版本的服务镜像,在配置中心切一个开关就能回到旧版本。如果新模型线上效果严重翻车,回滚应该在 5 分钟内完成,而不是花两小时重新部署。

5. 从能跑到好用的分水岭:评测、迭代与成本

模型上了线,只是万里长征走完了前半程。后面真正考验工程能力的,是持续迭代阶段。这一节讲三个分水岭问题。

5.1 评测集不能一劳永逸

我见过不少团队,一个测试集用了一年,模型迭代了好几版,所有版本都在同一个测试集上刷分。这带来的问题是灾难性的:模型过拟合测试集,分数虚高,真实场景效果却越来越差。测试集本身也会"过期"——业务的数据分布一直在变化,新的文案类型、新的违规手段,旧测试集根本覆盖不到。

我迭代评测集的方法是:月度重建测试集 + 滚动补充案例库。每次新模型上线后,从线上请求中随机抽取样本加入测试集,这个样本要保留真实的业务 label。人工审核流里发现的模型误判案例,也定期回流成测试样本。这样测试集数量持续增长,覆盖范围越来越接近真实分布。

这里还有一个细节:评测集必须和训练集严格互斥。如果有人误把训练数据混进测试集,评测结果就会严重失真。我在数据管线里加了 hash 级别的去重检查,从源头堵住这个隐患。

5.2 迭代:离线和上线之间的差距如何弥合

每次模型迭代,光看离线 F1 涨了多少没有意义。我坚持在离线评测之外,加一组合格性冒烟测试:把最近两周线上真实请求样本灌进新模型,看输出有没有异常值、会不会崩溃、延迟能不能接受。

然后重点来了:对比新旧模型在线上同一样本上的输出差异。我把这种对比行为称为"差异分析"——如果新模型和旧模型在 90% 的样本上输出完全一致,那说明这次迭代没有实质变化,不值得上线;如果差异集中在某几类样本上,那就得仔细看这些差异是好是坏。有时候新模型 F1 更高,但高出来的收益只集中在 20% 的样本上,另外 80% 的输出其实跟旧模型差不多。这种局部优势是否值得承担整体风险,需要业务方一起判断。

5.3 成本控制:AI 工程的隐性考核指标

最后聊成本,但它可能是整个项目存亡的关键因素。

还是以文案质量评估为例。BERT base 微调模型的单次推理成本大约是每 1000 条 0.15 元(按当时算力价格估算)。平台上每天产生约 10 万条新文案,光推理成本一天就是 15 元,一个月 450 元左右。确实不算多。但如果你把这个模型换成 GPT-4 API,每 1000 条可能要几十块钱,一个月下来可能几万块。同一个任务,成本可以差三个数量级。

我的成本优化三板斧是:规则前置过滤(便宜的先上)、模型蒸馏(大模型教小模型,小模型上线)、动态降级(高峰期走高质量模型,低峰期或非关键链路用轻量模型)。

还有 token 级优化:压缩 prompt 长度、用缓存复用历史计算的结果、批量推理减少重复加载。我算过,一个长文本任务,把 prompt 模板精简掉无用的背景说明后,token 量减少一半,成本直接跟着降一半,效果几乎不受影响。在 AI 工程里,抠 token 就是抠成本,而且是纯利。

6. 我踩过的几个大坑,以及最后的建议

写了这么多技术细节,最后想快速说说踩坑记录,都是一些看起来不起眼、但就能耗掉你一整天的问题。

第一个坑:Python 依赖冲突。有个周五我升级了一个数据预处理库的小版本,结果把 numpy 的版本也一起升级了,然后模型推理出现过一次难以解释的错误。查了半天才发现是 numpy 的兼容性问题。从那以后我养成了两个习惯:依赖全部锁定精确版本、升级任何库之前先看它的依赖树影响。这也回到了第二节讲的环境管理。

第二个坑:GPU 显存泄漏。上线初期服务跑一个周末,显存就被占满,服务直接 OOM 挂掉。排查半天,发现是推理代码里有个 Python list 在循环中不断追加张量,历史结果一直没释放。后来专门做了显存追踪,把每次推理前后的显存差值打日志,才定位到问题。这个教训告诉我:线上服务一定要有显存监控告警,不能等服务自己崩了才反应。

第三个坑:回滚机制没有前置验证。曾经出现过一次模型上线后效果不达标需要回滚,结果因为旧镜像的配置文件过旧,回滚后反而起不来服务。那次事故之后,我把"回滚演练"列入了上线 checklist——每次新版本发布前,先在测试环境演练一遍回滚流程,确保 5 分钟内能切回任意旧版本。

另外一个很容易被忽略的点:prompt 里的标点符号和措辞风格,对模型稳定性影响比想象中要大。在同样的任务上,一个用句号、一个用感叹号的 prompt,产出质量差异可能达到 5% 以上。这个差异在评测集上不容易看出来,但在线上真实流量里会被放大。所以 prompt 一旦定稿,就要做版本管理,不要随意改动措辞。

回看从零搭建 AI 工程的整个过程,最深的一个体会是:AI 项目的成功率,不取决于你用了多强的模型,而取决于你把多少精力花在模型之外的事情上。数据质量、任务定义、评测体系、服务稳定性、成本控制,每一件看起来都不那么"性感",但它们才是项目能落地、能持续产生价值的真正支撑。

我在实际项目里摸索出来的这套方法,不敢说放之四海皆准,但至少从一个零基础的状态走到现在,被证明是走得通的。希望这篇长文能帮你跳过那些我已经替你们踩过的坑。如果只能记住一句话,那就是:先定义清楚问题和评估标准,再动手写代码,最后才轮到模型。别反着来。

返回列表