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

资讯详情

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

从零搭建AI工程体系:避开调包陷阱,构建稳定可迭代的AI系统

从零搭建AI工程体系:避开调包陷阱,构建稳定可迭代的AI系统

1. 从零搭建AI工程体系,为什么我劝你别急着调包

很多人一提到AI工程,第一反应就是pip install transformers,然后找个预训练模型跑个demo,觉得这就是AI工程了。我刚开始也这么想,直到真正接手一个需要上线的项目,才发现从“能跑通”到“能扛住”之间,隔着一整套工程体系。ai-engineering-from-scratch这个标题,说的就是从零开始搭建这套体系,而不是从零训练一个大模型。这两件事的难度差了好几个数量级,但后者才是绝大多数从业者真正每天要面对的东西。

先把这个项目的定位说清楚。它不是一个教你如何从随机初始化开始训练GPT的项目,那需要几百万美元和一支博士团队。它解决的核心问题是:当你手里有一个或者多个模型(可能是开源的,可能是API调用的),如何围绕它们构建一套稳定、可观测、可迭代的工程系统。这套系统包括数据管道、推理服务、评估体系、监控告警、成本控制这几个核心模块。适合谁来参考?如果你是一个后端工程师想转AI方向,或者是一个算法工程师发现自己的模型总是“实验室里很美,上线就拉胯”,再或者是一个技术负责人需要规划团队的AI基础设施,那这篇内容就是写给你的。

我见过太多团队在AI工程上踩的坑,本质上都是同一个原因:把AI系统当成普通后端系统来设计。普通后端系统的输入输出是确定的,一个HTTP请求进来,数据库查一下,返回JSON,逻辑清晰。但AI系统的输入是自然语言、图像、音频这些非结构化数据,输出是不确定的概率分布,中间还夹着一个黑盒模型。这就导致传统后端那套“写单元测试、看日志、加监控”的方法论,在AI场景下需要做大量适配。ai-engineering-from-scratch要解决的,就是这套适配问题。

这篇文章我会按照实际搭建的顺序来展开:先讲整体架构怎么设计,再拆解每个模块的具体实现,然后给出一套可以直接抄的实操流程,最后把我踩过的坑和排查技巧整理出来。全程不堆砌术语,每个技术选型我都会解释为什么这么选,以及不这么选会出什么问题。你不需要有很深的AI背景,但最好有一点后端开发经验,这样理解起来会更顺畅。

2. 整体架构设计与技术选型思路

2.1 为什么分层架构是唯一合理的选择

AI工程系统最忌讳的就是把模型调用逻辑散落在业务代码的各个角落。我见过一个项目,模型推理代码直接写在Flask的route函数里,结果后来想换模型,发现要改十几个文件,每个文件的调用方式还略有不同。这种代码结构在demo阶段没问题,但一旦要迭代,就是灾难。

合理的做法是分层。我推荐的分层是这样的:最底层是模型服务层,负责加载模型、执行推理、管理GPU资源;往上是编排层,负责处理请求路由、批处理、超时重试、降级策略;再往上是业务逻辑层,处理具体的业务规则;最上面是接口层,对外暴露RESTful API或者gRPC。每一层之间通过明确定义的接口通信,层与层之间可以独立替换。

这么分层的核心好处是关注点分离。模型服务层只关心推理性能,编排层只关心请求调度,业务层只关心业务规则。当你想把模型从BERT换成LLaMA的时候,只需要改模型服务层,上面的编排和业务逻辑完全不用动。这个解耦带来的维护成本降低,在项目中期会体现得非常明显。

还有一个容易被忽略的点:模型服务层和编排层之间应该用异步消息队列解耦,而不是直接函数调用。为什么?因为模型推理的延迟波动很大,同样的请求,第一次可能要加载模型花几秒,后面可能只要几十毫秒。如果直接同步调用,一个慢请求就会阻塞整个线程。用消息队列(比如Redis Stream或者RabbitMQ)做缓冲,编排层只管往队列里扔任务,模型服务层按自己的节奏消费,这样系统的吞吐量和稳定性都会好很多。

2.2 模型选型的三个核心维度

选模型不是看排行榜谁分高就用谁。实际工程中,我通常从三个维度来评估:延迟、成本、效果。这三个维度往往是互相矛盾的,你需要根据业务场景做取舍。

延迟方面,一个7B参数的模型在单张A10上推理,输出100个token大约需要1到2秒。如果换成70B的模型,同样的输出可能需要10秒以上。如果你的业务场景是实时对话,那70B基本不可用,除非你愿意堆多张卡做张量并行。成本方面,API调用的价格差异很大,从每百万token几块钱到几十块钱都有。效果方面,不是参数越大效果越好,很多7B的模型在特定任务上经过微调后,效果可以超过通用的70B模型。

我的建议是:先用API快速验证业务逻辑,等业务跑通了再考虑自部署。API的好处是零运维成本,按量付费,适合早期验证。但API的缺点是数据要出你的服务器,如果涉及敏感数据就不能用。自部署的好处是数据可控,长期成本低,但需要你懂GPU运维。这个决策点通常在项目启动后一到两个月出现,到时候根据实际调用量和数据敏感度来定。

具体到模型选择,我一般会准备一个模型矩阵:一个小模型(比如Qwen2.5-1.5B)做快速意图识别和路由,一个中等模型(比如Qwen2.5-7B)做主要生成任务,一个大模型(比如Qwen2.5-72B或者调用API)做复杂推理和兜底。这样大部分请求走小模型和中等模型,只有少数复杂请求才走大模型,整体成本和延迟都能控制住。

2.3 推理框架的取舍:vLLM还是TGI

自部署推理的时候,vLLM和TGI是两个主流选择。我两个都用过,说一下实际感受。vLLM的PagedAttention确实厉害,在高并发场景下吞吐量比TGI高不少,尤其是当请求的输入输出长度差异很大的时候。但vLLM的配置项比较多,有些参数(比如gpu_memory_utilization)设不好容易OOM。TGI的优势是部署简单,Docker一行命令就能跑起来,而且对HuggingFace的模型支持最全。

我的选择逻辑是:如果追求极致吞吐且团队有GPU调优经验,选vLLM;如果追求快速上线且模型是HuggingFace上的标准架构,选TGI。还有一个折中方案是用Ollama做本地开发,生产环境再用vLLM,这样开发体验和线上性能都能兼顾。Ollama的好处是安装极其简单,ollama run qwen2.5就能跑起来,适合在笔记本上做原型验证。

不管选哪个框架,有一个参数一定要调:最大批处理大小(max batch size)。这个值设小了GPU利用率上不去,设大了延迟会飙升。我的经验是从8开始试,逐步加到16、32,观察GPU利用率和P99延迟的变化,找到一个平衡点。通常对于7B模型,在A10上max batch size设16到24比较合适。

3. 核心模块拆解与实操要点

3.1 数据管道:别让脏数据毁了你的模型

数据管道是AI工程里最不起眼但最重要的模块。我见过太多项目,模型本身没问题,但输入的数据格式乱七八糟,导致推理结果完全不可用。数据管道的核心职责是:清洗、转换、验证。

清洗包括去除HTML标签、处理特殊字符、截断超长文本。这里有个坑:很多模型对输入长度有限制,比如4096个token。如果你直接把一篇一万字的文章扔进去,模型会截断,但截断的位置可能正好把关键信息切掉了。我的做法是在数据管道里做智能截断:优先保留开头和结尾,中间部分按句子边界截断,并加上省略号标记。这样虽然丢失了部分信息,但至少不会让模型看到半句话。

转换主要是把不同来源的数据统一成模型能接受的格式。比如你的系统可能同时接收网页表单、API调用、文件上传三种输入,它们的格式各不相同。数据管道要把它们都转成统一的JSON结构,包含text、metadata、source这几个字段。metadata里可以放用户ID、时间戳、来源渠道等信息,方便后续做分析和追踪。

验证是最容易被忽略的一步。我建议在数据管道里加一个输入校验层,检查文本长度是否在合理范围、是否包含非法字符、编码是否正确。如果校验不通过,直接返回错误,不要让脏数据进入模型。这个校验层的成本很低,但能避免很多莫名其妙的线上问题。我吃过一次亏,用户上传了一个包含大量emoji的文本,模型直接输出了乱码,排查了半天才发现是编码问题。从那以后,我在数据管道里强制做UTF-8编码检查和emoji过滤。

3.2 推理服务:从单机到集群的演进路径

推理服务的搭建有一个清晰的演进路径,我建议按这个顺序来,不要跳步。

第一阶段:单机脚本。写一个Python脚本,加载模型,读输入,输出结果。这个阶段的目标是验证模型效果,不用考虑并发和性能。代码大概长这样:

from transformers import AutoModelForCausalLM, AutoTokenizer model_name = "Qwen/Qwen2.5-7B-Instruct" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto") def infer(text): inputs = tokenizer(text, return_tensors="pt").to(model.device) outputs = model.generate(**inputs, max_new_tokens=512) return tokenizer.decode(outputs[0], skip_special_tokens=True)

这个脚本能跑通,但只能串行处理,一次一个请求。

第二阶段:加一层Web框架。用FastAPI或者Flask把推理脚本包起来,对外提供HTTP接口。这时候要处理并发问题。Python的GIL会导致多线程无法真正并行,所以要么用多进程(uvicorn --workers 4),要么用异步IO。但模型推理是CPU/GPU密集型操作,异步IO帮不上忙,所以多进程是更实际的选择。每个进程加载一份模型,显存占用会翻倍,所以要根据GPU显存来决定worker数量。

第三阶段:引入推理框架。当并发量上来之后,自己写的推理服务就扛不住了。这时候换成vLLM或者TGI,它们内置了连续批处理(continuous batching)和PagedAttention,吞吐量能提升5到10倍。切换的方式很简单,vLLM提供了一个OpenAI兼容的API server,你原来的FastAPI代码只需要把请求转发到vLLM的端口就行。

第四阶段:多机集群。当单机GPU不够用的时候,就需要多机部署。这时候要引入负载均衡(比如Nginx或者HAProxy),把请求分发到多个推理节点。每个节点跑一个vLLM实例,节点之间不需要通信,因为推理是无状态的。这个架构的扩展性很好,加机器就能加吞吐。但要注意模型版本的一致性,所有节点必须加载同一个模型,否则会出现同一个请求在不同节点上结果不一样的情况。

3.3 评估体系:没有评估就没有迭代

AI工程和传统软件工程最大的区别在于:传统软件的测试是二值的(通过/失败),AI系统的评估是连续的(好/一般/差)。这就需要一个专门的评估体系。

评估体系的核心是评估数据集和评估指标。评估数据集要从真实业务数据中采样,覆盖各种边界情况。我通常会把评估集分成三部分:常规集(占70%)、边界集(占20%)、对抗集(占10%)。常规集是典型输入,边界集是超长、超短、多语言混合等特殊情况,对抗集是故意构造的容易让模型出错的输入。

评估指标方面,生成任务和分类任务不一样。分类任务看准确率、召回率、F1值。生成任务看BLEU、ROUGE这些自动指标,但自动指标和人类判断的相关性往往不高。我的做法是自动指标做粗筛,人工评估做精筛。每次模型更新,先跑自动指标,如果指标下降超过阈值,直接打回。如果指标持平或上升,再抽样做人工评估。人工评估用A/B测试的方式,让评估者不知道哪个是旧模型哪个是新模型,避免主观偏见。

评估的频率也很重要。我建议每次模型更新必评,每周做一次全量评估,每天做一次抽样评估。全量评估跑整个评估集,抽样评估只跑100条左右,主要看有没有明显的退化。这个频率听起来很高,但可以用自动化脚本实现,实际人力成本并不大。

3.4 监控告警:看不见的问题最致命

AI系统的监控比传统系统复杂,因为除了CPU、内存、GPU这些硬件指标,还要监控模型层面的指标。我通常监控这几类:

性能指标:QPS、P50/P95/P99延迟、GPU利用率、显存占用。这些指标反映系统的健康度。P99延迟特别重要,因为AI推理的延迟分布往往是长尾的,平均值正常不代表没问题。

质量指标:输出长度分布、重复率、困惑度(perplexity)。输出长度突然变短可能意味着模型被截断了,重复率突然升高可能意味着模型陷入了循环,困惑度突然升高可能意味着输入分布发生了变化。这些指标不需要标注数据,可以实时计算,是发现问题的第一道防线。

业务指标:用户满意度、任务完成率、人工介入率。这些指标反映AI系统对业务的实际影响。如果用户满意度突然下降,但性能指标正常,那很可能是模型质量出了问题。

告警策略上,我建议分级告警。P99延迟超过阈值发P2告警,质量指标异常发P1告警,业务指标异常发P0告警。P0告警要直接打电话,P1告警发即时消息,P2告警发邮件。这样既能及时响应严重问题,又不会被大量低优先级告警淹没。

4. 完整实操流程:从零到一搭建一个问答系统

4.1 环境准备与依赖安装

我以搭建一个基于RAG的问答系统为例,走一遍完整流程。这个系统能回答用户关于特定文档的问题,适合企业内部知识库场景。

首先准备环境。我推荐用conda创建独立环境,避免依赖冲突:

conda create -n ai-eng python=3.11 conda activate ai-eng pip install vllm fastapi uvicorn redis sentence-transformers chromadb

这里解释一下每个依赖的作用。vllm是推理框架,fastapi和uvicorn提供Web服务,redis做消息队列和缓存,sentence-transformers做文本向量化,chromadb做向量数据库。版本方面,vllm建议用0.6以上,对Qwen系列支持比较好。

GPU环境需要CUDA 12.1以上,驱动版本535以上。可以用nvidia-smi检查。如果显存小于24G,建议用7B模型;24G到48G可以用14B;48G以上可以考虑72B的量化版本。

4.2 文档索引管道的搭建

RAG系统的第一步是把文档转成向量存起来。这个管道包括:文档加载、分块、向量化、存储。

文档加载要支持多种格式:PDF、Word、Markdown、纯文本。PDF用pymupdf解析,Word用python-docx,Markdown直接读文本。解析出来的文本要保留段落结构,因为分块的时候需要按段落来分。

分块是RAG系统最关键的一步。块太小,检索出来的信息不完整;块太大,检索精度下降。我的经验是块大小设在256到512个token之间,块之间保留50个token的重叠。重叠是为了避免关键信息正好被切在边界上。分块的时候优先按段落分,如果段落太长再按句子分,句子还太长才按字符分。

向量化用sentence-transformers的BAAI/bge-large-zh-v1.5模型,这个模型在中文语义相似度任务上表现很好。向量化的时候要注意归一化,因为余弦相似度对向量模长敏感,归一化之后可以用内积代替余弦相似度,计算更快。

存储用ChromaDB,它支持持久化,重启不会丢数据。每个文档块存三个字段:embedding(向量)、text(原文)、metadata(来源、页码等)。检索的时候用collection.query,返回最相似的top-k个块。

4.3 推理服务的部署与调优

推理服务用vLLM部署。启动命令如下:

python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --dtype auto \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --max-num-seqs 16 \ --port 8000

参数解释:max-model-len设成8192,因为RAG的输入可能比较长;gpu-memory-utilization设0.9,留10%给系统;max-num-seqs设16,控制并发批处理大小。这些参数需要根据实际GPU显存调整。如果启动时报OOM,先把max-model-len降到4096,再把gpu-memory-utilization降到0.85。

启动之后用curl测试一下:

curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "Qwen/Qwen2.5-7B-Instruct", "messages": [{"role": "user", "content": "你好"}], "max_tokens": 100 }'

如果能正常返回,说明推理服务跑通了。

4.4 编排层的实现细节

编排层是连接业务逻辑和推理服务的中间层。它负责:接收用户问题、检索相关文档、构造prompt、调用推理服务、返回结果。

检索部分,把用户问题用同一个向量模型编码,然后在ChromaDB里查top-5个相关块。这里有个技巧:检索的时候多查一些(比如top-10),然后用一个小的重排序模型(比如bge-reranker)精排,取top-3。这样能显著提升检索质量,代价是增加一点延迟。

Prompt构造要遵循模型的对话模板。Qwen2.5的模板是:

<|im_start|>system 你是一个知识库助手,根据以下文档回答问题。如果文档中没有相关信息,直接说不知道。 <|im_end|> <|im_start|>user 文档:{context} 问题:{question} <|im_end|> <|im_start|>assistant

注意system prompt里要明确告诉模型“不知道就说不知道”,否则模型会编造答案。这是RAG系统最常见的坑。

调用推理服务用httpx异步请求,设置超时时间为30秒。如果超时,返回降级结果(比如“系统繁忙,请稍后再试”)。同时记录请求日志,包括问题、检索到的文档、模型输出、耗时,方便后续分析。

4.5 缓存与成本控制

缓存是降低成本和延迟的利器。我在两个层面做缓存:向量缓存和结果缓存。

向量缓存是把用户问题的向量存到Redis里,key是问题的哈希值,value是向量。下次遇到相同或相似的问题,直接取缓存,不用重新编码。相似判断用向量余弦相似度,阈值设0.95。这个缓存能省掉向量编码的时间,大概几十毫秒。

结果缓存是把完整的问答结果存到Redis里,key是问题的哈希值,value是答案。这个缓存命中率取决于业务场景,如果是客服场景,重复问题很多,命中率能到30%以上。缓存过期时间设24小时,因为文档可能会更新。

成本控制方面,除了缓存,还要做请求限流。每个用户每分钟最多10次请求,超过就返回429。这个限流在编排层实现,用Redis的滑动窗口算法。限流不仅能控制成本,还能防止恶意请求打垮服务。

5. 常见问题与排查技巧实录

5.1 模型输出质量问题的排查路径

模型输出质量问题是AI工程中最常见也最难排查的。我总结了一个排查路径,按这个顺序走,能解决80%的问题。

第一步:检查输入。把模型的输入打印出来,看看是不是符合预期。常见问题包括:prompt模板拼错了、检索到的文档不相关、输入被截断了。我遇到过一次,检索模块返回的文档块是空的,但代码没报错,导致模型在没有任何上下文的情况下回答问题,输出完全跑偏。

第二步:检查模型加载。确认模型是否完整加载,有没有报warning。有时候模型下载不完整,加载的时候会静默失败,用随机权重推理,输出就是乱码。检查方法是看加载日志里有没有Loading checkpoint shards这样的信息,以及模型参数量是否和预期一致。

第三步:检查推理参数。temperature、top_p、max_tokens这些参数对输出影响很大。temperature设太高(比如1.0以上)输出会很随机,设太低(比如0.1)输出会很死板。我的经验是问答场景用0.3到0.7之间,创意场景用0.8到1.0。max_tokens设太小会导致输出被截断,设太大浪费计算资源。

第四步:检查模型本身。如果前面都没问题,那可能是模型能力不够。换一个更大的模型试试,或者用few-shot prompting给几个示例。如果换模型后效果明显提升,说明是模型能力问题;如果没提升,说明是数据或prompt问题。

5.2 性能问题的定位与优化

性能问题通常表现为延迟高或者吞吐低。定位方法是用火焰图或者py-spy做性能分析,看时间花在哪里。

常见原因一:GPU利用率低。用nvidia-smi看GPU利用率,如果低于50%,说明GPU在等数据。可能是数据预处理太慢,或者批处理大小设太小。优化方法是把数据预处理放到CPU上并行做,或者增大批处理大小。

常见原因二:显存碎片。长时间运行后,显存会出现碎片,导致无法分配连续的大块显存。vLLM的PagedAttention就是解决这个问题的,但如果用的是原生transformers,可以通过定期重启服务来缓解。

常见原因三:网络瓶颈。如果推理服务和业务服务不在同一台机器,网络延迟会成为瓶颈。优化方法是把推理服务部署在业务服务同一台机器上,或者用RDMA网络。实测下来,同机部署能降低30%以上的延迟。

常见原因四:Python GIL。如果推理服务用多线程,GIL会成为瓶颈。解决方案是用多进程,或者用vLLM这种底层用C++实现的框架。我试过用multiprocessing把推理服务包起来,吞吐量提升了3倍。

5.3 常见问题速查表

问题现象可能原因排查方法解决方案
输出乱码模型加载不完整检查加载日志重新下载模型
输出重复temperature太低检查推理参数调高temperature到0.7
输出截断max_tokens太小检查输出长度增大max_tokens
延迟突然升高GPU显存不足nvidia-smi查看减小batch size或重启服务
检索不相关向量模型不匹配检查编码模型统一用同一个向量模型
缓存不命中key设计不合理检查缓存key用问题哈希做key
服务OOM并发太高检查QPS加限流或加机器
结果不一致多节点模型版本不同检查各节点模型统一模型版本

5.4 几个我踩过的坑

坑一:忘了设置随机种子。模型推理默认是有随机性的,同样的输入两次输出可能不一样。这在调试的时候很麻烦,因为你不确定输出变化是代码改动导致的还是随机性导致的。解决方案是在推理前设置torch.manual_seed(42),这样输出就确定了。但注意,生产环境不要设固定种子,否则所有用户得到一样的回答,体验很差。

坑二:prompt里包含了特殊字符。有一次用户的输入里包含了<|im_end|>这个字符串,正好是Qwen的对话结束标记,导致模型提前结束了对话。解决方案是在拼接prompt之前,把用户输入里的特殊标记转义或者删除。这个坑很隐蔽,因为大部分时候没问题,只有特定输入才会触发。

坑三:Redis缓存没有设过期时间。一开始为了简单,缓存没设TTL,结果Redis内存越用越多,最后OOM了。解决方案是所有缓存都必须设TTL,而且要根据业务特点设不同的TTL。向量缓存可以设长一点(比如7天),结果缓存设短一点(比如1天)。

坑四:日志里记录了敏感信息。调试的时候为了方便,把完整的用户输入和模型输出都记到日志里了。后来发现日志里包含了用户的手机号和地址,有隐私风险。解决方案是日志脱敏,用正则把手机号、身份证号、邮箱这些敏感信息替换成占位符。

坑五:没有做优雅停机。服务更新的时候直接kill进程,导致正在处理的请求全部失败。解决方案是加一个/health接口,停机前先把这个接口返回设为unhealthy,等负载均衡把流量切走之后再等30秒,确保没有正在处理的请求,然后再停服务。

6. 迭代与扩展:系统上线之后做什么

系统上线不是终点,而是起点。上线之后要做的事情包括:收集反馈、分析bad case、持续优化。

收集反馈的方式有两种:显式反馈和隐式反馈。显式反馈是让用户点赞点踩,隐式反馈是分析用户行为,比如用户是否复制了答案、是否重新提问、是否离开了页面。隐式反馈的数据量更大,但噪声也更大,需要结合显式反馈一起分析。

Bad case分析是优化的主要依据。我每周会抽半天时间,把本周的bad case过一遍,分类整理。常见的bad case类型包括:检索不相关、模型幻觉、格式错误、拒绝回答。每种类型对应不同的优化方向。检索不相关就优化分块策略或换向量模型,模型幻觉就加强prompt约束或加事实核查,格式错误就加输出解析和重试,拒绝回答就调整system prompt。

扩展方向有几个:多模态(支持图片和音频输入)、多轮对话(维护对话历史)、个性化(根据用户画像调整回答风格)、多语言(支持中英文混合)。每个方向都需要对现有架构做调整,但核心的分层结构不用变。比如加多模态,只需要在数据管道里加一个图像编码器,在模型服务层换一个多模态模型,编排层和业务层基本不用动。这就是分层架构的好处。

最后分享一个我在实际项目中的体会:AI工程的难点不在AI,在工程。模型本身的能力是固定的,你能做的是围绕它构建一套可靠的系统。这套系统的价值在于:当模型出错的时候,系统能兜住;当模型升级的时候,系统能快速适配;当业务变化的时候,系统能灵活调整。ai-engineering-from-scratch这个项目要培养的,就是这种系统化思维。不要追求一步到位,先跑通最小闭环,再逐步迭代。我见过太多项目死在“想太多做太少”上,先让系统跑起来,比什么都重要。

返回列表