ARTICLE DETAIL

资讯详情

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

从零手搓AI工程:数据管道、模型推理与服务化实战

从零手搓AI工程:数据管道、模型推理与服务化实战 1. 从零手搓AI工程为什么我不建议你直接调包很多人一听到“AI工程”这四个字第一反应就是打开某个云平台调一个现成的大模型接口写几行胶水代码然后对外宣称自己做了个AI应用。我见过太多这样的项目跑个Demo没问题一旦要上生产、要控成本、要处理长尾请求立刻就露馅了。ai-engineering-from-scratch这个标题之所以值得认真对待恰恰是因为它戳中了一个被大多数人刻意回避的问题当你把AI系统的每一层都当成黑盒来用你就永远不知道它什么时候会崩、为什么崩、以及崩了之后该怎么修。我自己带过几个从零搭建的AI工程项目也接手过别人“调包调出来”的烂摊子。最深的体会是AI工程和传统软件工程最大的区别在于它的不确定性不是来自代码逻辑而是来自数据分布、模型行为和推理硬件的三角博弈。你写一个排序算法输入确定输出就确定但你搭一个检索增强生成系统同样的query今天返回这个答案明天可能因为索引更新、模型版本微调、甚至温度参数的微小差异就返回另一个答案。这种不确定性靠调包是理解不了的必须自己从最底层把每一块拼图拼一遍。这篇文章适合谁看如果你已经会用Python对机器学习有基本概念但一直停留在“调API”的层面想真正搞明白一个AI系统从数据到推理到服务的完整链路那这篇内容就是为你准备的。我会按照一个真实项目从零搭建的顺序把数据管道、模型推理、服务化、性能调优这几个核心环节拆开揉碎每一步都告诉你为什么这么做、不这么做会怎样、以及我踩过的那些坑。全文基于我在实际项目中的做法和教训不保证是唯一解但保证是能跑通、能上生产的解。2. 数据管道AI工程里最脏最累但最不能省的一步2.1 为什么数据清洗比模型选型更重要我见过太多人把80%的时间花在选模型上今天试试这个开源模型明天试试那个商业API结果数据管道一塌糊涂喂进去的文本里全是HTML标签、乱码、重复段落最后模型输出质量差还怪模型不行。说句不好听的在大多数实际业务场景里数据质量对最终效果的影响远比模型参数量从7B换到13B要大得多。从零搭建AI工程第一步必须是建立可复现的数据清洗管道。什么叫可复现就是你今天跑一遍清洗脚本明天再跑一遍只要输入数据没变输出必须一模一样。这意味着你不能在清洗过程中引入随机性不能用那种“随机采样看看效果”的临时脚本必须把每一步清洗规则都固化下来。我通常会把清洗管道分成四个阶段原始数据接入、格式规范化、内容过滤、分块与索引。原始数据接入阶段重点是保留原始数据的完整快照不要在这一步做任何修改。很多人喜欢边接入边清洗结果出了问题想回溯都回溯不了。我的做法是原始数据一律以只读方式存储清洗脚本从只读源读取输出到新的目录每一步都有日志记录处理了多少条、过滤了多少条、过滤原因是什么。格式规范化阶段核心任务是把各种奇形怪状的输入统一成纯文本。PDF要抽文字HTML要去标签Markdown要保留结构但去掉渲染符号JSON要展平嵌套字段。这里有个坑PDF抽文字时表格和公式往往会被抽成乱码如果你不做特殊处理这些乱码就会进入后续流程。我的经验是对于表格密集的PDF要么用专门的表格抽取工具单独处理要么在规范化阶段就把包含大量表格的页面标记出来后续人工抽检。2.2 内容过滤的规则设计与误杀处理内容过滤是数据管道里最需要拿捏分寸的一步。过滤太松垃圾数据进入模型输出质量下降过滤太严有用信息被误杀模型回答不完整。我一般会设置三层过滤硬规则过滤、统计特征过滤、语义质量过滤。硬规则过滤处理那些毫无疑问的垃圾比如空文本、纯符号、重复字符超过一定比例的文本、编码错误的乱码。这些规则简单直接误杀率极低。统计特征过滤处理那些“看起来像文本但质量不高”的内容比如平均句长过短、词汇丰富度过低、停用词比例异常。这一步需要根据你的业务场景调阈值没有万能参数。语义质量过滤最复杂通常用一个小型分类模型或者基于困惑度的打分来判断文本是否通顺、是否包含实质信息。这里重点说一下误杀处理。不管你规则设计得多好一定会有有用信息被误杀。我的做法是所有被过滤掉的内容都单独存一份并且记录过滤原因。每周抽检一次被过滤内容如果发现误杀率超过5%就调整规则。这个抽检机制看起来麻烦但能帮你避免“清洗完发现关键信息没了”的灾难。还有一个容易被忽略的点过滤规则本身也需要版本管理。你今天加了一条规则过滤掉某类内容明天业务需求变了这类内容又需要了如果没有版本记录你根本不知道当初为什么过滤它。我习惯在清洗脚本里用注释写明每条规则的添加日期、添加原因、以及预期影响范围这样半年后回头看也能快速理解。2.3 分块策略固定长度、语义分割还是递归分割数据清洗完下一步是分块。分块策略直接影响检索效果和模型输入质量。固定长度分块最简单按字符数或token数切但容易把一句话切成两半导致语义断裂。语义分割按段落或句子边界切能保持语义完整但块长度不均匀有的块特别长有的特别短。递归分割是折中方案先按大边界切如果块还是太长再按小边界切。我实测下来对于大多数中文文本递归分割配合重叠窗口效果最稳。具体参数上块大小设在256到512个token之间比较合适重叠窗口设在块大小的10%到20%。为什么要有重叠因为检索时如果关键信息刚好在块边界上没有重叠就可能被切掉导致检索不到。重叠窗口相当于给边界信息上了保险。但这里有个反直觉的点块不是越小越好。我见过有人把块大小设成64个token觉得这样检索精度高。实际上块太小会导致每个块包含的信息不完整模型拿到一个块也回答不了问题反而需要检索多个块拼凑增加了上下文长度和推理成本。块大小和你的业务问题粒度有关如果用户问题通常需要一段完整解释才能回答块就不能太小。分块之后还要做一件事给每个块打元数据标签。至少要有来源文档ID、块在文档中的位置、块长度、以及一个内容摘要。这些元数据在后续检索和排序时非常有用。比如你可以根据块位置做加权文档开头的块通常包含概述性信息权重可以高一点文档末尾的块可能是参考文献权重可以低一点。3. 模型推理从加载权重到生成第一个token3.1 推理框架选型为什么我最终选了原生PyTorch加少量优化模型推理这块市面上框架很多有专门做推理优化的有做服务化封装的还有做量化压缩的。我的建议是如果你是从零搭建先不要急着上那些重型框架用原生PyTorch把推理流程跑通理解每一步在干什么然后再根据性能瓶颈逐步引入优化工具。为什么因为重型框架虽然开箱即用但一旦出问题排查成本极高。你调一个API返回结果不对你根本不知道是模型权重加载错了、tokenizer配置不对、还是框架内部做了什么你不知道的优化。用原生PyTorch每一步都是你写的出了问题你能定位到具体哪一行。用原生PyTorch做推理核心步骤就四步加载模型权重、加载tokenizer、预处理输入、前向计算加解码。加载权重时要注意如果你用的是Hugging Face格式的模型from_pretrained会自动处理权重映射但如果你是自己转换的权重一定要检查每一层的shape是否匹配。我踩过一次坑权重加载时没有报错但推理结果全是乱码查了半天发现是某一层的转置矩阵没有正确加载因为shape刚好兼容所以没报错。tokenizer这块中文场景要特别注意。很多开源模型的tokenizer是在英文语料上训练的对中文的分词效果很差一个常见汉字可能被拆成多个byte级别的token导致同样长度的中文文本token数比英文多很多推理成本直接翻倍。我的做法是如果模型支持中文扩展词表一定要用扩展后的tokenizer如果不支持就在预处理阶段做一次文本规范化把全角转半角、统一标点符号能稍微缓解一下。3.2 解码策略贪心、束搜索还是采样解码策略决定了模型怎么从概率分布里选下一个token。贪心解码每次选概率最高的速度快但容易重复束搜索保留多个候选质量高但速度慢采样引入随机性多样性好但可控性差。我的经验是不同任务用不同策略。事实性问答用束搜索束宽设3到5能提高答案准确性创意写作用采样温度设0.7到0.9配合top-p设0.9能平衡多样性和连贯性代码生成用贪心或低温度采样因为代码对准确性要求极高随机性容易引入语法错误。这里有个容易被忽略的参数重复惩罚。模型在生成长文本时很容易陷入重复循环比如反复说同一句话。重复惩罚就是给已经出现过的token降低概率避免重复。但惩罚系数不能设太高太高会导致模型不敢用常见词输出变得生硬。我一般设1.1到1.2之间具体看任务。还有一个实战技巧最大生成长度不要设太大。很多人怕答案被截断把最大长度设成4096甚至更长。实际上大多数问答任务答案长度在200到500个token之间就够了。设太大不仅浪费计算资源还可能导致模型在答案已经完整后继续胡言乱语。我的做法是根据任务类型设一个合理的上限同时在生成过程中检测结束符一旦生成结束符就立即停止。3.3 批处理与流式输出吞吐量和延迟的权衡生产环境里推理服务不可能一次只处理一个请求。批处理能大幅提高吞吐量但会增加单个请求的延迟。流式输出能降低首token延迟但实现复杂度更高。批处理的实现方式有两种静态批处理和动态批处理。静态批处理是攒够一批请求一起送进模型简单但等待时间长。动态批处理是请求随时来随时加进当前批次等批次满了或者超时了再一起计算复杂度高但延迟低。我一般用动态批处理设置一个最大批次大小和一个最大等待时间比如批次大小32、等待时间50毫秒这样既能提高吞吐量又不会让单个请求等太久。流式输出这块核心是把生成过程拆成多个步骤每生成一个token就返回给客户端。实现上要注意流式输出和批处理会有冲突因为批处理要求所有请求同步计算而流式输出要求每个请求独立返回。我的做法是对流式请求单独走一条推理路径不参与批处理虽然吞吐量低一些但用户体验好很多。这里有个坑流式输出时如果客户端断开连接服务端要能及时检测到并停止生成否则会浪费计算资源。我见过一个线上事故客户端断开了但服务端还在傻傻生成结果大量计算资源被浪费其他请求排队等不到资源。后来我在生成循环里加了连接状态检查每生成一个token检查一次断开就立即终止。4. 服务化把推理脚本变成能扛住并发的API4.1 接口设计同步、异步还是流式把推理脚本变成API第一个要决定的是接口类型。同步接口最简单客户端发请求服务端算完返回结果适合短文本、低并发场景。异步接口是客户端发请求服务端返回一个任务ID客户端过一会儿拿任务ID来查结果适合长文本、高并发场景。流式接口是服务端边生成边返回适合对话类应用。我的建议是至少提供同步和流式两种接口。同步接口用于批处理任务和简单查询流式接口用于交互式对话。异步接口看业务需求如果单次推理时间超过10秒就应该考虑异步。接口设计上请求体要包含足够的控制参数输入文本、最大生成长度、温度、top-p、重复惩罚、是否流式。但不要暴露太多底层参数否则客户端会乱调。我一般只暴露温度、最大长度、是否流式这三个最常用的其他参数用服务端默认值。响应体要包含生成结果、token使用量、推理耗时、以及一个请求ID用于追踪。token使用量很重要如果你要按量计费或者做成本核算没有这个数据就没法算。推理耗时用于监控服务性能请求ID用于排查问题。4.2 并发控制线程池、协程还是进程池Python的并发模型是个老生常谈的问题。推理任务是计算密集型的受GIL限制多线程没法真正并行。多进程能并行但进程间通信开销大而且模型权重在每个进程里都要加载一份内存占用翻倍。协程适合IO密集型任务对计算密集型任务帮助不大。我的做法是推理服务用多进程加进程内批处理。启动时拉起多个worker进程每个进程加载一份模型权重请求通过一个调度器分发到各个worker。调度器负责攒批把多个请求合并成一个批次送给worker。worker算完后把结果返回给调度器调度器再分发给对应的客户端。这个架构的关键是调度器的攒批策略。攒批太少吞吐量上不去攒批太多延迟太高。我一般根据线上监控数据动态调整高峰期批次大小设大一点低峰期设小一点。具体实现上可以用一个队列加一个定时器队列里的请求数达到阈值或者定时器超时就触发一次批处理。还有一个细节模型权重加载。如果你有多个worker进程每个进程都加载一份权重内存占用是N倍。如果模型很大内存可能扛不住。解决方案是用共享内存或者内存映射文件让多个进程共享同一份权重。PyTorch支持torch.load时用map_location参数指定设备也支持通过share_memory共享张量。但共享内存有个坑如果某个进程修改了共享张量其他进程也会受影响。推理场景下我们只读不写所以是安全的。4.3 健康检查与优雅退出服务上线后健康检查和优雅退出是必须的。健康检查接口要能反映服务的真实状态不能只返回一个200就完事。我一般会检查三件事模型是否加载成功、推理队列是否积压、最近一次推理是否成功。如果队列积压超过阈值健康检查返回不健康让负载均衡器把流量切走。优雅退出是指服务收到停止信号后不立即杀死进程而是先停止接收新请求等正在处理的请求完成后再退出。这个机制在滚动更新时特别重要如果没有优雅退出更新过程中会有请求失败。实现上可以监听SIGTERM信号收到信号后把服务状态标记为“正在退出”负载均衡器检测到后不再转发新请求同时等待当前批次处理完成。我踩过一次坑优雅退出时没有设置超时结果某个请求卡住了整个进程一直不退出滚动更新卡了半个小时。后来加了超时机制最多等30秒超时后强制退出并记录日志方便排查。5. 性能调优从能跑到跑得快的几个关键手段5.1 量化精度损失换推理速度量化是把模型权重从高精度浮点数转换成低精度整数减少内存占用和计算量。常见的量化方案有INT8和INT4INT8精度损失小推理速度提升明显INT4压缩率更高但精度损失也更大。我的经验是对于大多数文本生成任务INT8量化几乎无损可以直接上。INT4量化要看模型大小大模型对量化更鲁棒小模型量化后质量下降明显。量化实现上PyTorch有内置的量化工具Hugging Face的optimum库也提供了方便的量化接口。但量化后的模型要重新评估效果不能直接上线。量化还有一个坑不是所有层都适合量化。注意力层的QKV投影和输出投影对量化比较敏感量化后容易导致注意力分布偏移。我的做法是先量化全模型评估效果如果下降明显就把敏感层保持高精度只量化其他层。这种混合精度量化实现起来复杂一些但效果更好。5.2 KV缓存自回归生成的加速利器自回归生成模型每次生成一个token都要重新计算所有历史token的注意力。KV缓存就是把历史token的Key和Value矩阵缓存下来生成新token时只计算新token的注意力然后和缓存的KV拼接。这个优化能大幅降低计算量尤其是生成长文本时。KV缓存的实现要注意内存管理。缓存大小和批次大小、序列长度、注意力头数、头维度都有关。如果批次大小32、序列长度2048、注意力头数32、头维度128缓存大小大概是32乘2048乘32乘128乘2乘4字节算下来好几个GB。所以KV缓存不能无限增长要设置最大长度超过就丢弃最早的缓存。还有一个优化技巧分页KV缓存。传统KV缓存是连续内存分配和释放效率低。分页KV缓存把缓存分成固定大小的页按需分配能减少内存碎片。这个技术在一些高性能推理框架里有实现自己实现的话复杂度较高但效果确实好。5.3 批处理调优找到吞吐量和延迟的平衡点批处理调优的核心是找到吞吐量和延迟的平衡点。批次越大吞吐量越高但单个请求的延迟也越高。这个平衡点取决于你的业务场景如果是离线批处理任务延迟不敏感批次可以设大如果是在线对话延迟敏感批次要设小。我一般会做一组压测测不同批次大小下的吞吐量和P99延迟然后画一条曲线找到吞吐量开始下降或者延迟开始飙升的拐点。这个拐点就是最优批次大小。压测时要注意请求长度要模拟真实分布不能全用短请求或全用长请求否则测出来的结果没有参考价值。还有一个动态批处理的技巧根据队列长度动态调整批次大小。队列长的时候批次设大一点提高吞吐量队列短的时候批次设小一点降低延迟。这个策略实现起来不复杂但效果很明显。6. 踩坑实录那些让我熬夜排查的典型问题6.1 内存泄漏推理服务跑着跑着就OOM了推理服务刚上线时跑得好好的跑几个小时就OOM重启后又能跑几个小时。这种问题最折磨人因为不是必现排查起来很费劲。我遇到过一次最后定位到是PyTorch的缓存分配器没有及时释放内存。PyTorch为了加速内存分配会缓存已经分配过的内存块不立即还给操作系统。如果推理过程中有大量不同大小的张量创建和销毁缓存会越来越大最终OOM。解决方案是设置环境变量PYTORCH_CUDA_ALLOC_CONF配置缓存分配器的行为或者定期调用torch.cuda.empty_cache()手动清理。还有一个内存泄漏来源是Python的循环引用。推理代码里如果创建了对象之间的循环引用垃圾回收器可能无法及时回收。我的做法是在推理循环里定期调用gc.collect()强制垃圾回收。虽然有点影响性能但能避免内存泄漏。6.2 结果不一致同样的输入两次输出不一样这个问题在调试阶段特别让人抓狂。同样的输入第一次跑输出A第二次跑输出B检查代码逻辑又没问题。后来发现是随机种子没有固定。PyTorch的随机种子、NumPy的随机种子、Python内置的随机种子都要固定。而且如果用了CUDA还要设置CUDA的随机种子。但固定随机种子只能保证单次运行内可复现不能保证跨运行可复现。因为CUDA的某些操作是非确定性的比如原子加操作不同运行顺序会导致微小差异累积起来就可能导致输出不同。要完全确定需要设置torch.use_deterministic_algorithms(True)但这会降低性能而且不是所有操作都支持确定性实现。我的建议是调试阶段固定所有随机种子保证问题可复现生产环境不追求完全确定但要监控输出质量如果发现输出质量突然下降及时排查。6.3 长文本截断模型只回答了前半部分问题用户问了一个很长的多部分问题模型只回答了第一部分后面的问题被忽略了。排查发现是输入长度超过了模型的最大上下文长度被截断了。这个问题看起来简单但实际处理起来要考虑很多。首先你要在预处理阶段检测输入长度如果超过最大长度要么截断要么分段处理。截断的话要决定截哪部分通常保留开头和结尾中间截断因为开头通常包含主要问题结尾可能包含补充说明。分段处理的话要把长文本切成多个段分别推理然后合并结果。合并结果时要注意不同段的答案可能有冲突需要设计一个合并策略。还有一个隐藏问题即使输入没超过最大长度如果输入加输出的总长度超过最大长度生成过程也会被截断。所以预处理时要预留输出长度比如最大长度4096输入最多设3000留1000给输出。7. 从零搭建的完整流程回顾与个人建议7.1 最小可行系统的搭建顺序如果你现在就要从零开始搭一个AI工程系统我建议按这个顺序来先搭数据管道把原始数据清洗成可用的文本块再搭推理脚本用原生PyTorch把模型跑通能生成结果然后把推理脚本封装成API能接收请求返回结果最后做性能优化量化、KV缓存、批处理逐个加上。这个顺序的好处是每一步都有可验证的产出。数据管道跑通后你可以人工检查清洗结果推理脚本跑通后你可以对比不同输入下的输出API跑通后你可以用curl测试性能优化后你可以压测对比。每一步都扎实了整个系统才稳。不要一上来就追求大而全先跑通最小闭环再逐步优化。我见过太多项目一开始就设计复杂的微服务架构结果数据管道还没跑通就在纠结用哪个消息队列最后项目延期甚至烂尾。7.2 监控与迭代上线只是开始系统上线后监控是必须的。至少要监控四个指标请求量、推理延迟、错误率、token使用量。请求量反映业务规模推理延迟反映服务性能错误率反映服务质量token使用量反映成本。除了这些基础指标还要监控输出质量。输出质量很难量化但可以用一些代理指标比如输出长度分布、重复率、以及人工抽检评分。如果发现输出长度突然变短、重复率突然升高可能是模型出了问题要及时排查。迭代方面我建议建立一个小型评估集包含几十到几百个典型问题每次模型更新或系统调整后跑一遍评估集对比效果变化。这个评估集不需要很大但要有代表性覆盖主要业务场景。有了评估集你才能客观判断每次改动是变好了还是变差了。7.3 一些个人体会从零搭建AI工程最大的收获不是学会了某个工具或框架而是建立了一套理解AI系统的思维模型。你知道数据怎么流动、模型怎么计算、服务怎么响应出了问题你能定位到具体环节而不是像调包时那样只能猜。这个过程确实累要处理很多琐碎的问题要读很多文档和源码要反复调试和压测。但当你看到整个系统从无到有跑起来能稳定处理请求能扛住一定并发那种成就感是调包给不了的。最后分享一个小技巧搭建过程中把每个决策和踩过的坑都记录下来写成文档或注释。过几个月回头看这些记录能帮你快速回忆当时的思路也能帮新加入的同事快速上手。AI工程这个领域变化很快但底层的工程思维和方法论是相对稳定的把基础打牢上层怎么变都不怕。
返回列表