ARTICLE DETAIL

资讯详情

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

从零搭建AI工程能力:手写数据管道、推理引擎与向量检索的完整路线图

从零搭建AI工程能力:手写数据管道、推理引擎与向量检索的完整路线图 1. 从零搭建AI工程能力为什么我劝你别一上来就调包这两年AI应用开发的门槛肉眼可见地降低了随便拉个框架、调个API就能跑出一个能对话的Demo。但我带过不少新人也面试过不少号称“做过AI项目”的候选人发现一个很普遍的问题模型能跑起来但一问到“为什么这么设计”“数据怎么流转”“线上挂了怎么排查”基本就卡壳了。这就是典型的“会调包但不懂工程”。ai-engineering-from-scratch这个标题我理解它想表达的核心诉求是不依赖现成的高级封装从最基础的环节开始把AI工程这条链路完整地走一遍、搭一遍。它适合那些已经会用Python、了解一点机器学习概念但没真正独立负责过一个AI系统从数据到上线的开发者。也适合那些一直在做传统后端想转AI工程方向但不知道从哪下手的同学。我自己走过这条路也踩过不少坑。最开始我也是直接拿开源框架跑通就完事结果线上流量一上来推理延迟飙升、内存泄漏、数据管道断流各种问题全冒出来那时候才发现自己对底层的理解几乎是空白。后来我强迫自己把每个环节都手写一遍哪怕写得丑、性能差但至少知道每一层在干什么。这篇文章就是把我这套“从零搭建”的思路和实操细节完整地分享出来包括整体架构怎么设计、核心模块怎么实现、参数怎么算、坑怎么避。你不需要完全照搬但至少能拿到一套可参考的路线图。2. 整体架构设计与技术选型思路2.1 为什么选择“从零手写”而不是直接上框架很多人会问现在有那么多成熟的AI工程框架为什么还要从零写这不是重复造轮子吗我的回答是造轮子不是为了替代轮子而是为了理解轮子。你只有自己实现过一遍数据加载、模型推理、请求调度这些环节才能真正理解框架帮你做了什么、在什么情况下会出问题。具体来说从零搭建有这几个实际好处。第一排查问题的能力会质变。当线上推理服务出现延迟抖动时如果你知道底层的数据预处理、批处理、内存分配是怎么运作的你就能快速定位是哪个环节拖慢了整体链路而不是只能重启服务碰运气。第二技术选型会更有判断力。你亲手写过简单的向量检索就知道不同索引结构在召回率和速度上的取舍选型时就不会被宣传材料牵着走。第三定制化能力更强。实际业务场景往往有特殊需求比如特定的数据清洗逻辑、非标准的模型输入格式如果你只会调框架的API遇到框架不支持的情况就只能干等社区更新。当然从零搭建不意味着所有东西都自己写。我的原则是核心链路自己实现边缘工具可以用现成的。比如HTTP服务可以用FastAPI数值计算用NumPy但数据管道、推理调度、缓存策略这些核心环节我会自己写一遍。2.2 分层架构把AI系统拆成四层我习惯把AI工程系统拆成四层从下到上分别是数据层、模型层、服务层、应用层。这个分层方式不是教科书上的标准答案而是我在实际项目中总结出来的好处是每一层的职责边界清晰出问题时容易定位。数据层负责原始数据的采集、清洗、格式转换和存储。这一层最容易被忽视但实际上大部分AI项目的失败都跟数据质量有关。模型层负责模型的加载、推理、版本管理。服务层负责把模型能力包装成可调用的接口包括请求调度、批处理、缓存、限流等。应用层则是面向具体业务场景的逻辑比如对话管理、推荐排序等。每一层之间通过明确的接口通信数据层输出标准化的数据格式模型层输出统一的推理结果服务层负责编排。这样设计的好处是任何一层需要替换或升级时只要接口不变其他层基本不用动。2.3 技术栈选择与理由技术栈的选择上我遵循“够用、可控、可替换”三个原则。编程语言用Python因为AI生态最完善虽然性能不如C但开发效率高而且关键性能瓶颈可以用C扩展或异步IO来缓解。Web框架用FastAPI它的异步支持和自动文档生成在实际开发中非常省事而且性能比Flask好不少。数值计算用NumPy这是基础中的基础几乎所有AI相关的Python库都依赖它。模型推理方面如果是从零搭建我会先用NumPy实现一个简单的推理引擎理解矩阵运算和前向传播的过程然后再接入ONNX Runtime或PyTorch的推理接口做对比。数据存储用SQLite做本地开发生产环境换成PostgreSQL向量存储先用NumPy数组实现暴力检索数据量大了再引入FAISS或Milvus。这里要特别说一下为什么先用暴力检索。很多人一上来就上向量数据库结果发现数据量才几千条暴力检索的延迟完全可接受反而引入了额外的运维复杂度。我的经验是数据量在十万条以下时NumPy的矩阵运算做余弦相似度检索配合一些简单的索引优化性能完全够用。等到数据量真正上来了再迁移到专用向量数据库也不迟。3. 核心模块拆解与实操要点3.1 数据管道从原始文本到模型输入数据管道是整个系统的入口也是最容易出问题的地方。我见过太多项目在Demo阶段用干净的数据跑得很好一上真实数据就各种报错。所以这一节我会讲得细一些。数据管道的第一步是数据采集。实际项目中数据来源可能包括数据库、日志文件、第三方API等。我的做法是写一个统一的采集接口每种数据源实现自己的采集逻辑但输出格式统一。比如定义一个DataRecord类包含id、content、metadata三个字段所有采集器最终都返回这个格式。第二步是数据清洗。这一步的坑最多。常见的清洗操作包括去除HTML标签、处理编码问题、过滤空值和异常值、统一文本格式等。我踩过的一个坑是不同来源的文本编码不一致有的用UTF-8有的用GBK直接合并会导致乱码。解决办法是在采集阶段就统一转成UTF-8并且对无法解码的字符做替换处理而不是直接丢弃。第三步是数据分块。对于长文本需要切分成适合模型处理的片段。分块策略直接影响后续的检索和推理效果。我的经验是分块大小控制在256到512个token之间比较合适太小会丢失上下文太大则会影响检索精度。分块时要注意在句子边界处切分避免把一句话截断。具体实现可以用正则表达式按标点符号切分然后合并短句直到达到目标长度。第四步是向量化。把文本转成向量表示这一步可以用预训练的嵌入模型。如果是从零搭建我会先用TF-IDF做一个基线理解向量化的基本原理然后再换成神经嵌入模型。TF-IDF的实现很简单用sklearn几行代码就能搞定但它的局限也很明显无法捕捉语义相似性。神经嵌入模型效果好得多但需要注意模型的输入长度限制和推理速度。实操心得数据管道一定要加日志和校验。我习惯在每个环节结束后统计记录数量、检查字段完整性、抽样人工验证。有一次线上检索效果突然变差排查了半天才发现是上游数据源改了字段名导致部分数据清洗时被静默丢弃。如果当时有数量校验这个问题五分钟就能发现。3.2 推理引擎从矩阵运算到批处理调度推理引擎是AI工程的核心。从零搭建的话我建议先用NumPy实现一个最简单的全连接网络推理理解前向传播的计算过程。具体来说就是实现矩阵乘法、激活函数、softmax这些基础操作。虽然实际项目中不会用自己写的推理引擎但这个过程能帮你理解模型推理的计算瓶颈在哪里。理解了基础之后再接入实际的推理框架。我常用ONNX Runtime因为它跨框架、跨平台性能也不错。接入时需要注意几个关键点。输入输出的形状和类型必须严格匹配ONNX模型对输入的形状很敏感动态形状需要显式指定。批处理大小需要根据实际场景调整批处理能提高吞吐量但会增加单次延迟需要权衡。线程数也要配置ONNX Runtime默认会用所有可用核心在高并发场景下反而可能导致上下文切换开销过大。批处理调度是推理引擎里最值得花时间优化的部分。我的做法是维护一个请求队列当队列长度达到阈值或等待时间超过上限时触发一次批处理推理。阈值和超时时间需要根据实际流量特征来调。比如在低流量场景下超时时间可以设短一些保证响应速度在高流量场景下可以适当增大批处理大小提高吞吐量。# 简化的批处理调度逻辑示意 import time import numpy as np class BatchScheduler: def __init__(self, max_batch_size32, max_wait_ms50): self.max_batch_size max_batch_size self.max_wait_ms max_wait_ms self.queue [] self.last_flush time.time() def add_request(self, input_data): self.queue.append(input_data) if len(self.queue) self.max_batch_size: return self.flush() if (time.time() - self.last_flush) * 1000 self.max_wait_ms: return self.flush() return None def flush(self): if not self.queue: return None batch np.stack(self.queue) self.queue [] self.last_flush time.time() return batch这段代码只是示意实际使用还需要考虑异步、超时处理、错误隔离等问题。但核心思路就是攒一批请求一起推理用批处理换吞吐量。3.3 服务层接口设计与性能优化服务层是把模型能力暴露给外部调用的环节。我用FastAPI搭建HTTP接口核心接口通常包括推理接口、健康检查接口、指标接口。推理接口的设计要注意几点请求体用JSON格式字段命名清晰响应体包含结果和元信息比如推理耗时、模型版本错误处理要统一不同错误类型返回不同的状态码和错误信息。性能优化方面异步处理是关键。FastAPI原生支持async/await推理请求可以用异步方式提交到批处理调度器避免阻塞主线程。缓存也很重要对于相同的输入可以直接返回缓存结果省去推理开销。缓存可以用内存字典实现生产环境换成Redis。限流是保护服务的手段可以用令牌桶算法实现简单的限流器。# 简化的限流器实现 import time class TokenBucket: def __init__(self, rate10, capacity20): self.rate rate self.capacity capacity self.tokens capacity self.last_refill time.time() def allow(self): now time.time() elapsed now - self.last_refill self.tokens min(self.capacity, self.tokens elapsed * self.rate) self.last_refill now if self.tokens 1: self.tokens - 1 return True return False这个限流器逻辑很简单令牌以固定速率补充每个请求消耗一个令牌令牌不足时拒绝请求。实际使用中还需要考虑分布式场景下的限流那就需要借助Redis等外部存储来做全局计数。3.4 向量检索从暴力搜索到索引优化向量检索是很多AI应用的核心功能比如语义搜索、推荐召回。从零搭建的话我建议先用NumPy实现暴力检索理解余弦相似度的计算过程然后再逐步引入索引优化。暴力检索的实现很直接把所有向量存成一个矩阵查询时计算查询向量与所有向量的相似度取Top-K。用NumPy的矩阵运算几千条数据的检索延迟在毫秒级别完全可用。但数据量到十万条以上时暴力检索的延迟就会明显上升这时候需要引入近似最近邻搜索。近似最近邻搜索的常见方法包括基于树的索引、基于哈希的索引、基于图的索引等。我常用的是基于图的HNSW算法它在召回率和速度之间取得了不错的平衡。如果不想引入额外的库也可以自己实现一个简单的聚类索引先用K-Means把向量聚成若干簇查询时只搜索最近的几个簇。这种方法实现简单召回率略低但速度提升明显。注意事项向量检索的召回率评估很重要。我习惯用一组标注好的查询-文档对来测试计算Top-K召回率。如果召回率不达标优先检查嵌入模型是否适合当前领域而不是急着换索引结构。很多时候问题出在嵌入质量上而不是检索算法上。4. 完整实操流程从环境搭建到服务上线4.1 环境准备与依赖管理环境搭建这一步看似简单但实际项目中经常因为依赖版本冲突浪费大量时间。我的做法是用虚拟环境隔离项目依赖并且用requirements.txt锁定版本号。Python版本建议用3.9或3.10这两个版本在AI生态中兼容性最好。核心依赖包括NumPy用于数值计算FastAPI和Uvicorn用于Web服务ONNX Runtime用于模型推理scikit-learn用于基础机器学习工具Redis用于缓存和限流。如果要做向量检索可以加上FAISS或hnswlib。开发阶段还可以加上pytest用于测试black和isort用于代码格式化。# 创建虚拟环境 python -m venv venv source venv/bin/activate # Linux/Mac # venv\Scripts\activate # Windows # 安装核心依赖 pip install numpy fastapi uvicorn onnxruntime scikit-learn redis依赖安装完成后我习惯先跑一个简单的冒烟测试确认NumPy和ONNX Runtime能正常工作再开始写业务代码。这一步能避免后面调试时把环境问题误判为代码问题。4.2 数据准备与预处理实操数据准备阶段我会先准备一个小规模的真实数据集比如几百条文本记录用来快速验证整个链路。数据集不需要很大但一定要真实因为真实数据里的噪声和边界情况是合成数据模拟不出来的。预处理的具体步骤包括读取原始数据、清洗文本、分块、向量化、存储。每一步我都会写独立的函数方便单独测试和替换。比如清洗函数只负责文本清洗输入是原始字符串输出是清洗后的字符串不涉及其他逻辑。这样当清洗规则需要调整时不会影响其他环节。向量化这一步如果是从零搭建我会先用TF-IDF跑通流程确认数据管道没有问题再换成神经嵌入模型。TF-IDF的实现用sklearn的TfidfVectorizer几行代码就能搞定。神经嵌入模型可以用sentence-transformers库它封装了常见的嵌入模型使用很方便。from sklearn.feature_extraction.text import TfidfVectorizer import numpy as np # TF-IDF向量化示例 corpus [这是第一条文本, 这是第二条文本, 这是第三条文本] vectorizer TfidfVectorizer() tfidf_matrix vectorizer.fit_transform(corpus) print(tfidf_matrix.shape) # 输出 (3, N) # 计算相似度 from sklearn.metrics.pairwise import cosine_similarity sim cosine_similarity(tfidf_matrix[0:1], tfidf_matrix) print(sim)这段代码展示了TF-IDF的基本用法。实际使用中需要注意TF-IDF的词汇表是基于训练语料构建的如果后续有新词汇出现需要重新拟合或者使用HashingVectorizer来避免词汇表膨胀。4.3 推理服务搭建与接口测试推理服务的搭建分为几个步骤加载模型、定义请求响应格式、实现推理逻辑、启动服务。模型加载我习惯在服务启动时完成避免每次请求都加载模型。请求响应格式用Pydantic定义这样FastAPI会自动做参数校验和文档生成。from fastapi import FastAPI from pydantic import BaseModel import numpy as np app FastAPI() class InferenceRequest(BaseModel): text: str top_k: int 5 class InferenceResponse(BaseModel): results: list latency_ms: float # 模拟的推理逻辑 app.post(/infer, response_modelInferenceResponse) async def infer(request: InferenceRequest): import time start time.time() # 这里替换为实际的推理逻辑 results [{id: i, score: float(np.random.rand())} for i in range(request.top_k)] latency (time.time() - start) * 1000 return InferenceResponse(resultsresults, latency_mslatency)服务启动后用curl或Postman做接口测试确认请求能正常处理、响应格式正确、错误情况有合理提示。我习惯写一组自动化测试用例覆盖正常请求、空输入、超长输入、非法参数等场景每次修改代码后跑一遍确保没有引入回归问题。4.4 性能压测与调优记录服务上线前一定要做性能压测。我用locust或wrk做压测模拟不同并发数下的请求观察吞吐量、延迟、错误率的变化。压测的目的是找到系统的瓶颈和容量上限为线上部署提供参考。压测时我会重点关注几个指标P50延迟中位数延迟反映典型用户体验、P99延迟尾部延迟反映最差情况、QPS每秒查询数反映吞吐能力、错误率。如果P99延迟远高于P50说明系统存在长尾问题可能是批处理调度不合理或资源竞争导致的。调优的方向通常包括增大批处理大小提高吞吐量、调整线程数减少上下文切换、增加缓存减少重复计算、优化数据管道减少预处理耗时。每次调优只改一个变量观察指标变化避免多个改动混在一起无法归因。5. 常见问题与排查技巧实录5.1 推理延迟突然飙升怎么排查推理延迟飙升是最常见的问题之一。我的排查思路是从外到内逐层定位。先看服务层的指标确认是所有请求都慢还是部分请求慢。如果所有请求都慢可能是模型推理本身变慢了检查是否有其他进程占用CPU或内存。如果部分请求慢可能是特定输入触发了性能问题比如超长文本导致预处理耗时增加。再往下查看批处理调度是否正常。如果队列积压严重说明推理速度跟不上请求速度需要扩容或优化推理逻辑。如果队列正常但延迟高可能是单次推理耗时增加检查模型是否被意外重新加载、输入数据是否有异常。还有一个容易被忽视的点是垃圾回收。Python的GC在内存压力大时会触发导致短暂的停顿。如果延迟抖动呈现周期性可以尝试调整GC阈值或使用gc.freeze()减少GC频率。5.2 向量检索结果不准确怎么优化检索结果不准确首先要区分是召回问题还是排序问题。召回问题是指相关文档根本没被检索出来排序问题是指相关文档被检索出来了但排名靠后。区分方法是人工检查Top-K结果中是否包含相关文档。如果是召回问题优先检查嵌入模型是否适合当前领域。通用嵌入模型在专业领域如医疗、法律的表现可能不佳需要换用领域适配的模型或做微调。其次检查分块策略分块过大或过小都会影响召回效果。如果是排序问题可以引入重排序模型对召回结果做二次排序。实操心得我习惯维护一个“坏案例”集合把检索效果差的查询记录下来每次优化后重新测试这些案例确保优化确实有效而不是碰巧改善了其他查询。5.3 服务内存持续增长怎么处理内存持续增长通常意味着存在内存泄漏。Python中常见的内存泄漏原因包括全局变量不断累积、缓存没有淘汰策略、循环引用导致GC无法回收。排查方法是定期打印内存使用情况观察增长趋势然后用tracemalloc或objgraph定位泄漏对象。缓存没有淘汰策略是最常见的原因。如果缓存用字典实现且没有大小限制随着请求增多缓存会无限增长。解决办法是引入LRU淘汰策略或者用Redis并设置过期时间。循环引用问题可以用gc.collect()手动触发回收但更好的做法是避免在对象之间建立循环引用。5.4 常见问题速查表问题现象可能原因排查方法解决方向推理延迟高批处理过大、线程竞争、GC停顿查看P50/P99延迟、CPU使用率调整批处理大小、线程数、GC阈值检索不准确嵌入模型不匹配、分块不合理人工检查Top-K结果换嵌入模型、调整分块策略、加重排序内存持续增长缓存无淘汰、循环引用tracemalloc定位、观察增长趋势引入LRU、避免循环引用服务启动失败依赖版本冲突、端口占用查看启动日志、检查端口锁定依赖版本、更换端口请求超时推理耗时过长、队列积压查看队列长度、单次推理耗时扩容、优化推理、增加超时时间这张表是我在实际项目中总结的覆盖了大部分常见问题。遇到新问题时我会先对照这张表快速排查如果不在表里再深入分析。6. 我在这条路上踩过的几个坑第一个坑是过早优化。刚开始搭建时我花了很多时间在向量索引的优化上结果数据量才几千条暴力检索完全够用那些优化根本没派上用场。后来我学乖了先跑通链路等性能真正成为瓶颈时再优化。第二个坑是忽视数据质量。有次检索效果一直不好我以为是模型问题换了好几个嵌入模型都没改善。最后发现是数据清洗时把一些关键信息过滤掉了导致嵌入质量差。从那以后我在数据管道里加了严格的校验和抽样检查。第三个坑是没有做优雅降级。有次模型推理服务挂了整个应用直接不可用。后来我加了降级逻辑推理服务不可用时返回缓存结果或默认结果至少保证应用不崩溃。这个经验让我意识到AI工程不只是把模型跑起来还要考虑各种异常情况。第四个坑是忽略监控。早期我觉得项目小不需要监控结果线上出问题时两眼一抹黑。后来我加了基础监控请求量、延迟、错误率、内存使用问题排查效率提升了很多。监控不需要很复杂几个关键指标就够了。这些坑说到底都是工程经验的问题不是算法问题。从零搭建AI工程能力最重要的不是把每个模块写得多完美而是完整地走一遍链路理解每个环节的作用和相互关系。等你真正上手做过一遍再去看那些框架的文档会有完全不同的理解。后续如果想继续深入可以尝试把某个模块替换成更专业的实现比如用FAISS替换暴力检索、用Triton替换自写推理服务在对比中加深理解。
返回列表