多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从零搭建AI工程能力:三次踩坑经验与完整落地指南

从零搭建AI工程能力:三次踩坑经验与完整落地指南 从零搭建AI工程能力这件事我前前后后折腾过三回。第一回是跟着网上的教程跑通了几个Demo觉得自己行了第二回是接手一个真实项目发现Demo和工程之间隔着一条鸿沟第三回才算真正摸到了门道——不是模型调得多好而是整条链路的每个环节都能兜住底。这篇内容就是把这三次踩坑的经验揉在一起讲清楚一个AI工程项目从零到能跑起来到底需要哪些东西、为什么需要、以及怎么落地。适合刚入行想建立完整认知的工程师也适合做了几年传统开发想往AI方向转的朋友。我不会只给你一堆概念而是把每个环节的选型逻辑、实操细节和容易翻车的地方都摊开讲。1. 先搞清楚“从零搭建”到底从哪个零开始很多人看到“AI工程”四个字第一反应是去学Transformer架构、去啃注意力机制的公式。这个方向不能说错但如果你目标是搭建一个能用的AI工程系统从数学公式开始大概率会让你在半路放弃。我见过太多人卡在反向传播的推导上最后连一个完整的推理服务都没跑起来。1.1 三种“零”的起点对应三条完全不同的路径“从零”这个词其实很模糊。根据我的观察不同背景的人说的“零”根本不是同一个东西。第一种零是编程零基础。这类朋友需要先解决的是编程语言和基本工具链的问题AI工程对他们来说还太远。我的建议是先花两三个月把Python和命令行用熟能独立写出一个读写文件、调用接口的小脚本再来看AI工程的内容。第二种零是有编程基础但没接触过AI。这是最主流的群体也是这篇内容主要服务的对象。你懂变量、循环、函数可能还写过Web服务或者数据处理脚本但没训练过模型、没部署过推理服务。你的“零”在于不知道AI系统的各个组件怎么串起来。第三种零是做过AI实验但没做过AI工程。你在Notebook里跑通过模型调过参甚至微调过开源模型但这些东西只存在于你的本地环境里。你的“零”在于不知道怎么把它变成一个别人能访问、能稳定运行、能持续迭代的服务。我下面讲的内容主要面向第二种和第三种起点。如果你属于第一种先把编程基础打牢这篇可以先收藏。1.2 为什么我不建议一上来就啃模型原理这里说一个可能有点反直觉的观点搭建AI工程能力优先级最高的不是模型知识而是系统思维。原因很简单。在实际项目里模型往往是最容易被替换的组件。今天用这个开源模型明天可能换成另一个今天用API调用明天可能换成自部署。但数据管道、服务架构、监控体系这些东西换起来成本极高而且它们决定了你的系统能不能稳定运行。我举个例子。假设你要做一个文档问答系统。模型层面你可以调用现成的接口也可以用开源模型自己部署。但真正花时间的部分是文档怎么切分、向量怎么存储和检索、检索结果怎么和模型输出拼接、用户提问怎么做预处理、返回结果怎么做后处理、整个链路的延迟怎么控制。这些才是AI工程的核心。我的建议是先用现成的模型接口把整条链路跑通理解每个环节的作用然后再回头深入模型层面做优化。这个顺序反过来很容易陷入“模型调得很好但系统跑不起来”的困境。1.3 一个最小可用的AI工程系统包含哪些模块在动手之前先在心里画一张地图。一个能跑的AI工程系统不管多简单通常包含这几个部分输入处理层接收用户请求做格式校验、预处理、路由分发推理层调用模型完成核心计算可能是API调用也可能是本地推理后处理层对模型输出做解析、过滤、格式化数据层存储对话历史、向量索引、配置信息等监控层记录请求日志、延迟、错误率、资源占用这五层里推理层反而是最“薄”的一层。很多新手把90%的精力花在推理层结果系统上线后发现瓶颈全在输入处理和数据层。2. 环境搭建那些教程里不会告诉你的细节环境搭建看起来是最没技术含量的部分但根据我的经验新手在这一步卡住的时间占总时间的30%以上。不是因为你笨而是因为教程往往假设你的环境是干净的而现实中的环境从来不是干净的。2.1 Python环境管理的正确姿势先说一个我踩过的坑。早期我直接用系统自带的Pythonpip install各种包结果不同项目之间依赖冲突最后把系统环境搞崩了。后来改用虚拟环境但一开始用的是venv每个项目手动创建时间长了也乱。现在我固定用conda来管理环境理由有三个第一conda不仅能管理Python包还能管理非Python的依赖比如某些需要编译的库第二conda的环境隔离更彻底第三conda可以导出完整的环境配置文件换机器时一键复现。具体操作上我习惯给每个AI工程项目建一个独立环境conda create -n ai-eng python3.10 conda activate ai-engPython版本我选3.10不是最新的但兼容性最好。很多AI相关的库对3.11和3.12的支持还不完善用3.10能省去很多麻烦。环境建好后第一件事是装pip和setuptools的最新版pip install --upgrade pip setuptools wheel这一步很多人会跳过但老版本的pip在解析依赖时经常出问题升级一下能避免很多莫名其妙的报错。2.2 依赖安装的顺序有讲究装包不是一条pip install就完事的。AI工程涉及的包大致分几类基础科学计算库numpy、scipy、深度学习框架PyTorch、TensorFlow、推理加速库onnxruntime、tensorrt、服务框架FastAPI、Flask、数据处理库pandas、polars。我的安装顺序是先装numpy再装框架最后装服务框架。为什么因为numpy是很多库的底层依赖先装好它后面的库在编译时能找到正确的链接。如果顺序反了有时候会出现numpy版本被覆盖或者链接错误的问题。另外PyTorch的安装一定要去官网查对应的命令。不同操作系统、不同CUDA版本对应的安装命令完全不同。我见过有人直接pip install torch装上了CPU版本然后纳闷为什么推理这么慢。# 以CUDA 11.8为例具体命令以官网为准 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu1182.3 硬件资源的评估与选择AI工程对硬件的要求取决于你的场景。如果只是调用外部API那普通开发机就够了。如果要本地推理就要认真评估。我一般按这个标准来估算7B参数的模型FP16精度下需要约14GB显存4bit量化后约4GB。13B模型翻倍。这只是模型本身的占用实际运行时还要加上KV Cache、中间激活值等开销通常要再留30%到50%的余量。如果你没有GPU也不是不能做AI工程。可以用CPU推理小模型或者用外部API。但要做好心理准备CPU推理的速度可能只有GPU的十分之一甚至更低。一个实用的建议刚开始学习时优先用外部API或者云端GPU按需付费不要一上来就买硬件。等你确定了方向再根据实际需求配置设备。2.4 项目目录结构的约定环境搭好后先别急着写代码。花十分钟把目录结构定下来后面会省很多事。我常用的结构是这样的project/ ├── configs/ # 配置文件 ├── data/ # 数据文件 │ ├── raw/ # 原始数据 │ └── processed/ # 处理后的数据 ├── src/ # 源代码 │ ├── data/ # 数据处理模块 │ ├── models/ # 模型相关 │ ├── services/ # 服务层 │ └── utils/ # 工具函数 ├── tests/ # 测试代码 ├── notebooks/ # 实验性代码 ├── requirements.txt # 依赖清单 └── README.md这个结构的好处是职责清晰。notebooks放实验代码src放正式代码data放数据configs放配置。新手最容易犯的错是把所有代码堆在一个目录里等到项目变大就理不清了。3. 数据管道AI工程里最容易被低估的部分如果说模型是AI系统的大脑那数据管道就是血管。血管堵了大脑再聪明也没用。我在实际项目里见过太多这样的情况模型效果不好大家第一反应是换模型、调参数折腾一圈后发现根本问题是数据管道有问题——要么数据没清洗干净要么检索逻辑有缺陷。3.1 数据清洗不是可选项而是必选项原始数据几乎不可能直接拿来用。以文本数据为例常见的脏数据包括HTML标签残留、特殊字符、重复内容、编码错误、格式不统一。我处理文本数据一般走这几步第一步是编码统一。把所有文本统一转成UTF-8遇到无法解码的字符用替换策略处理。这一步看起来简单但很多乱码问题都出在这里。第二步是去重。完全重复的内容直接删掉近似重复的用相似度算法识别。我常用的是MinHash加LSH速度快效果也不错。第三步是格式规范化。把全角转半角、统一标点符号、去除多余空白字符。这些操作看似琐碎但对后续处理影响很大。第四步是质量过滤。根据业务需求设定规则比如过滤掉过短的文本、过滤掉包含特定关键词的内容、过滤掉语言不匹配的内容。import re import unicodedata def clean_text(text): # 统一编码 text unicodedata.normalize(NFKC, text) # 去除HTML标签 text re.sub(r[^], , text) # 去除多余空白 text re.sub(r\s, , text) # 去除控制字符 text .join(ch for ch in text if unicodedata.category(ch)[0] ! C) return text.strip()这段代码不复杂但能解决大部分常见的文本脏数据问题。3.2 分块策略决定了检索质量做检索增强生成RAG类应用时文档分块是最关键的决策之一。分块太大检索精度下降分块太小上下文信息丢失。我的经验是分块大小要根据文档类型和查询类型来定没有万能参数。对于技术文档我通常按段落分块每块300到500个token块之间保留50到100个token的重叠。重叠的目的是防止关键信息被切断。对于对话记录我按对话轮次分块一个完整的问答对作为一块。对于长篇文章我先按章节分章节内再按段落分形成层级结构。分块时还要考虑一个因素你的嵌入模型的最大输入长度。如果嵌入模型最多处理512个token那你的分块就不能超过这个数否则会被截断。3.3 向量化与索引构建的实操细节文本分好块后下一步是转成向量并建索引。嵌入模型的选择上英文场景我常用all-MiniLM-L6-v2体积小、速度快、效果够用。中文场景可以用BGE系列或者M3E系列。如果预算充足用外部API的嵌入服务效果更好但要注意数据隐私和调用成本。索引构建我推荐用FAISSFacebook开源的向量检索库支持多种索引类型。小规模数据百万级以下用Flat索引就够了精度最高。数据量再大可以用IVF或者HNSW索引牺牲一点精度换速度。import faiss import numpy as np # 假设embeddings是N x D的numpy数组 dimension embeddings.shape[1] index faiss.IndexFlatIP(dimension) # 内积索引 # 归一化后内积等价于余弦相似度 faiss.normalize_L2(embeddings) index.add(embeddings) # 检索 query np.array([query_embedding], dtypefloat32) faiss.normalize_L2(query) distances, indices index.search(query, k5)这里有个细节用内积索引前一定要做L2归一化否则内积和余弦相似度不等价。这个坑我踩过当时检索结果乱七八糟排查了半天才发现是没归一化。3.4 数据版本管理别等到出问题才后悔数据是会变的。今天用的数据集明天可能被更新、被修正、被替换。如果没有版本管理出了问题你都不知道是哪个版本的数据导致的。我的做法是给每次数据处理的结果打上版本号记录处理时间、处理脚本的commit hash、输入数据的来源。可以用DVC这类工具也可以简单地用文件命名约定加一个记录表。一个血泪教训曾经有一次模型效果突然下降排查了两天才发现是上游数据源更新了而我们的管道没有做版本记录无法回滚。从那以后我养成了给数据打版本的习惯。4. 模型选型与推理服务在效果和成本之间找平衡模型选型是AI工程里最纠结的环节之一。开源模型那么多闭源API也在不断更新到底选哪个我的答案是没有最好的模型只有最适合你场景的模型。4.1 选型时我实际关注的几个维度网上很多模型对比只讲效果分数但实际工程中效果只是其中一个维度。我通常从这几个方面评估维度说明权重任务效果在你的具体任务上的表现高推理延迟单次请求的响应时间高吞吐量单位时间能处理的请求数中部署成本硬件需求或API调用费用高上下文长度能处理的最大输入长度中可控性能否微调、能否本地部署视场景稳定性服务可用性、输出一致性高注意这里的权重是“视场景”的。比如做实时对话延迟权重就很高做离线批处理吞吐量更重要做涉及敏感数据的场景可控性就是硬性要求。4.2 API调用与本地部署的决策逻辑这是新手最常问的问题到底用API还是自己部署我的决策逻辑是这样的如果数据敏感度低、请求量不大、团队没有GPU运维能力优先用API。省心省力按量付费不用操心硬件和扩缩容。如果数据不能出本地、请求量大且稳定、团队有运维能力考虑本地部署。长期来看成本更低而且完全可控。如果请求量波动大、想快速验证想法先用API跑通等模式验证了再考虑迁移到本地。还有一个中间方案用API做主力本地部署一个小模型做兜底。当API不可用时降级到本地模型保证服务不中断。4.3 推理服务的性能优化手段如果你选择了本地部署性能优化就是绕不开的话题。我常用的手段有这几个量化是最直接的手段。把FP16的模型量化成INT8或INT4显存占用能降到原来的四分之一甚至八分之一速度也有提升。代价是精度会有所下降但很多场景下这个损失可以接受。批处理能显著提升吞吐量。把多个请求攒成一批一起推理GPU利用率更高。但要注意批处理会增加单个请求的延迟需要根据场景权衡。KV Cache优化对大模型推理很关键。通过PagedAttention等技术可以更高效地管理KV Cache提升并发能力。模型蒸馏是用小模型学习大模型的行为在保持大部分效果的同时大幅降低推理成本。但这个需要训练门槛较高。# 使用量化模型加载的示例以transformers为例 from transformers import AutoModelForCausalLM, AutoTokenizer model AutoModelForCausalLM.from_pretrained( model_name, load_in_4bitTrue, # 4bit量化 device_mapauto, # 自动分配设备 torch_dtypeauto )4.4 服务框架的选择与接口设计推理服务需要一个Web框架来暴露接口。我首选FastAPI理由是异步支持好、自动生成文档、类型校验强、性能足够。接口设计上我遵循几个原则请求和响应都用JSON格式字段命名清晰支持流式输出提升用户体验请求中带上超时参数避免长时间等待响应中带上耗时信息方便排查性能问题from fastapi import FastAPI from pydantic import BaseModel import time app FastAPI() class GenerateRequest(BaseModel): prompt: str max_tokens: int 512 temperature: float 0.7 class GenerateResponse(BaseModel): text: str latency_ms: float app.post(/generate, response_modelGenerateResponse) async def generate(req: GenerateRequest): start time.time() # 调用模型推理 result await model_inference(req.prompt, req.max_tokens, req.temperature) latency (time.time() - start) * 1000 return GenerateResponse(textresult, latency_mslatency)这个接口看起来简单但包含了几个关键设计请求参数有默认值、响应包含延迟信息、使用异步处理。这些细节在实际运行中很重要。5. 监控与迭代让系统在真实环境中活下来系统上线只是开始真正的挑战在上线之后。没有监控的AI系统就像没有仪表盘的汽车你不知道它什么时候会出问题。5.1 必须监控的核心指标我通常把监控指标分成四类性能指标请求延迟P50、P95、P99、吞吐量、错误率。这些是最基础的能反映系统是否健康。资源指标GPU利用率、显存占用、CPU使用率、内存占用。这些能帮你判断是否需要扩容。质量指标模型输出的质量评分、用户反馈、异常输出比例。这些指标比较难量化但对AI系统特别重要。业务指标请求量、活跃用户数、任务完成率。这些和业务直接挂钩。监控工具上Prometheus加Grafana是经典组合开源免费功能强大。日志收集可以用ELK或者Loki。如果不想自己搭也可以用云服务商提供的监控方案。5.2 日志记录的正确方式日志不是越多越好也不是越少越好。关键是记录对排查问题有用的信息。我的日志里通常包含请求ID用于追踪、时间戳、请求内容摘要、响应内容摘要、耗时、模型版本、错误信息如果有。注意不要记录敏感信息比如用户的隐私数据。如果必须记录要做脱敏处理。import logging import uuid logger logging.getLogger(__name__) def log_request(prompt, response, latency, model_version): request_id str(uuid.uuid4()) logger.info({ request_id: request_id, prompt_length: len(prompt), response_length: len(response), latency_ms: latency, model_version: model_version }) return request_id5.3 效果下降时怎么排查模型效果下降是迟早会遇到的问题。排查时我按这个顺序来先看数据。输入数据的分布是不是变了有没有出现之前没见过的类型数据管道有没有出问题再看模型。模型版本有没有变推理参数有没有被改动有没有出现异常输出然后看服务。延迟是不是变高了有没有超时有没有降级最后看外部依赖。如果用了外部API对方服务是不是有变化网络是不是稳定这个排查顺序的逻辑是从最可能变化的部分开始查。数据是最容易变的模型和服务相对稳定外部依赖不可控但影响明显。5.4 迭代节奏的把控AI系统的迭代不能太慢也不能太快。太慢跟不上需求变化太快容易引入不稳定因素。我的经验是小步快跑但每次变更都要可回滚。具体做法是每次只改一个变量改完观察一段时间确认没问题再改下一个。同时保留上一个版本的完整配置出问题能快速切回去。变更类型上配置变更可以频繁一些模型变更要谨慎架构变更要非常谨慎。因为影响范围不同风险也不同。6. 从能跑到好用几个提升工程质量的实操习惯前面讲的都是“怎么让系统跑起来”这一节讲“怎么让系统跑得好”。这些习惯不会让你的系统功能变多但会让它更可靠、更好维护。6.1 配置与代码分离新手常犯的错是把配置写死在代码里。模型名称、API地址、超时时间、批处理大小这些都应该放在配置文件里。我用YAML格式的配置文件结构清晰支持注释。不同环境用不同的配置文件通过环境变量指定加载哪个。# configs/production.yaml model: name: model_name max_tokens: 512 temperature: 0.7 service: host: 0.0.0.0 port: 8000 timeout: 30 database: host: localhost port: 5432 name: ai_service这样做的好处是改配置不用改代码不同环境用不同配置配置可以纳入版本管理。6.2 错误处理要区分类型AI系统的错误类型比传统系统多。除了网络错误、超时错误还有模型输出格式错误、内容过滤触发、资源不足等。我的做法是定义一套错误码不同类型的错误走不同的处理逻辑。比如输入格式错误返回400提示用户修正模型超时返回504可以重试内容过滤返回200但标记过滤原因资源不足返回503触发告警class AIError(Exception): def __init__(self, code, message, retryableFalse): self.code code self.message message self.retryable retryable class ModelTimeoutError(AIError): def __init__(self): super().__init__(504, Model inference timeout, retryableTrue) class ContentFilteredError(AIError): def __init__(self, reason): super().__init__(200, fContent filtered: {reason}, retryableFalse)区分错误类型的好处是调用方知道什么情况该重试什么情况该放弃监控系统能按错误类型统计快速定位问题。6.3 测试不能只测正常路径AI系统的测试比传统系统难因为输出不是确定的。但有几类测试是必须做的单元测试覆盖数据处理、格式转换、错误处理这些确定性逻辑。集成测试验证整条链路能跑通从请求到响应。回归测试用一组固定的输入对比新旧版本的输出差异。这个不能保证输出完全一致但能发现大的变化。边界测试用极端输入比如超长文本、空输入、特殊字符看系统会不会崩。def test_empty_input(): 测试空输入的处理 result process_input() assert result.status error assert empty in result.message.lower() def test_max_length_input(): 测试超长输入的处理 long_text a * 100000 result process_input(long_text) assert result.status in [success, truncated]6.4 文档和注释的取舍AI工程项目的文档我重点写三部分架构说明、接口文档、运维手册。架构说明讲清楚系统由哪些模块组成、模块之间怎么交互、数据怎么流动。接口文档讲清楚每个接口的输入输出、错误码、调用示例。运维手册讲清楚怎么部署、怎么扩容、怎么排查常见问题。代码注释我遵循一个原则注释解释为什么代码说明是什么。不要写“这行代码给变量加一”这种废话注释要写“这里加一是因为索引从零开始而业务逻辑从一开始”。6.5 持续学习的方向AI工程这个领域变化很快新的模型、新的工具、新的方法层出不穷。但底层的东西变化没那么快数据管道、服务架构、监控体系这些核心能力是通用的。我的学习策略是花70%的时间巩固核心工程能力20%的时间跟进新模型和新工具10%的时间做实验性探索。核心工程能力包括系统设计、性能优化、可靠性工程、数据处理。这些能力不会因为模型换代而贬值。新模型和新工具要保持关注但不要盲目追新。等一个工具成熟了、社区验证过了再上手能省很多时间。最后分享一个我自己的习惯每做一个项目我都会写一份复盘文档记录做了什么、遇到了什么问题、怎么解决的、下次怎么改进。这份文档不对外但对我自己的成长帮助很大。AI工程是个实践性很强的领域光看资料不动手永远学不会。
返回列表