1. 从零搭建AI工程能力:为什么我劝你别一上来就调包
这两年AI应用开发的门槛肉眼可见地降低了,随便拉个框架、调个API就能跑出一个能对话的Demo。但我自己带过几支团队、也帮朋友救过好几个“Demo很惊艳、上线就崩盘”的项目之后,越来越确信一件事:真正拉开差距的不是你会不会调包,而是你懂不懂这套东西从底层到工程化是怎么串起来的。“ai-engineering-from-scratch”这个方向之所以值得认真做一遍,核心就在于它逼着你把那些平时被框架封装掉的环节亲手走一遍——数据怎么进、模型怎么算、推理怎么加速、服务怎么扛住并发、成本怎么控住。
我先把话说在前面:这篇不是教你从零训练一个GPT,那是另一个量级的事情。这里说的“from scratch”,指的是从工程视角把AI应用的完整链路自己搭一遍,包括数据处理、特征与向量化、模型调用与本地推理、服务封装、性能压测、监控与迭代。走完这一遍,你再回头看那些框架,会发现它们帮你省掉的是什么、又在哪些地方悄悄埋了坑。
适合谁看?如果你已经会用Python写点脚本、调过一两次大模型接口,但一到“要上线、要稳定、要省钱”就心里没底,那这篇就是写给你的。如果你是完全零基础,也没关系,我会把每个环节的“为什么”讲清楚,你照着做也能跑通。整篇内容偏实操,参数和步骤我都会给到能直接抄的程度,中间穿插我自己踩过的坑。
2. 整体架构设计:先想清楚链路,再动手写代码
2.1 为什么我坚持先画链路图再写代码
很多人做AI项目的第一反应是打开编辑器写import openai,然后一路往下堆。我早期也这样,结果就是代码写到三千行的时候发现:数据格式在三个地方不一致、缓存层没地方插、想换个模型要改二十个文件。后来我强制自己养成一个习惯——任何AI项目,先花半小时把数据流画出来,再动手。
一个完整的AI工程链路,我一般拆成六段:数据接入层、预处理与向量化层、推理层、服务层、可观测层、迭代层。这六段不是每个项目都要全上,但你在设计阶段必须知道哪几段是当前需要的、哪几段是未来要预留的。比如一个内部知识库问答,数据接入和向量化是重点,推理层可能直接调云端API;而一个要做实时语音转写的产品,推理层的本地部署和延迟优化就是生死线。
画链路图还有个隐性好处:它逼你把“输入是什么、输出是什么、中间态存在哪”想明白。我见过太多项目挂在“中间态”上——embedding存哪、缓存key怎么设计、会话上下文怎么截断,这些在Demo阶段无所谓,一上量全是问题。
2.2 技术选型的三个决策维度
选型这件事没有标准答案,但有几个维度你必须过一遍,不然就是拍脑袋。
第一个维度是延迟预算。用户能接受的响应时间是多少?如果是聊天类,首字延迟超过1.5秒体感就明显变差;如果是批处理任务,几秒到几分钟都无所谓。延迟预算直接决定你是走云端API还是本地推理,是走小模型还是大模型。
第二个维度是成本结构。云端API按token计费,本地推理按GPU小时计费,两者的成本曲线完全不同。调用量小的时候云端便宜且省心,调用量一大本地部署的边际成本优势就出来了。我一般会算一个盈亏平衡点:假设本地一张卡每小时成本X元,云端每百万token Y元,那么当你的吞吐超过某个阈值时,本地就更划算。这个计算后面我会给具体例子。
第三个维度是数据敏感度与可控性。有些数据不能出内网,那就只能本地;有些场景需要频繁微调,那本地部署的迭代速度也更快。这三个维度交叉一下,选型基本就定了。
2.3 一个我常用的最小可行架构
对于大多数中小规模的AI应用,我推荐的最小可行架构是这样的:FastAPI做服务层 + Redis做缓存和会话 + 向量库(Qdrant或pgvector)做检索 + 云端API或本地vLLM做推理。这套组合的好处是每一层都可以独立替换和扩展,不会牵一发动全身。
为什么是FastAPI?因为它异步支持好、生态成熟、上手快,而且和Pydantic结合做请求校验非常舒服。为什么向量库推荐Qdrant或pgvector?Qdrant是专门的向量库,性能和过滤能力强;pgvector的好处是你如果已经有Postgres,不用额外维护一个组件。这两个我都用过,小规模pgvector够用,上了千万级向量还是Qdrant更稳。
推理层我一般先用云端API把业务跑通,等调用量和成本数据出来了,再决定要不要迁到本地。这个顺序很重要,不要一上来就折腾本地部署,那会让你在业务还没验证的时候就陷进运维泥潭。
3. 核心环节拆解:数据、向量化与推理的实操要点
3.1 数据预处理:脏数据是AI项目最大的隐形杀手
我做过一个统计,在我接手过的出问题的AI项目里,超过一半的根因在数据,而不是模型。数据预处理这一步看起来枯燥,但它决定了你后面所有环节的天花板。
文本数据预处理我一般分四步走。第一步是清洗,去掉HTML标签、多余空白、乱码字符。这一步用正则就能搞定,但要注意别把有意义的格式也洗掉了,比如代码块里的缩进。第二步是分块(chunking),这是RAG场景的重头戏。分块策略直接决定检索质量,我试过固定长度、按段落、按语义三种方式,实测下来按语义分块 + 适当重叠效果最好。
具体参数上,我一般把chunk size设在300到500个token,overlap设在50到100个token。为什么是这个范围?太小了语义不完整,检索出来答非所问;太大了噪声多,而且会挤占上下文窗口。overlap的作用是防止关键信息正好被切在边界上,这个细节很多人忽略,但实测能明显提升召回。
第三步是元数据抽取,给每个chunk打上来源、时间、类别等标签。这些标签在检索时可以做过滤,比如“只搜最近三个月的数据”,没有元数据你就只能全量搜。第四步是去重,重复数据会让检索结果同质化,浪费上下文窗口。
注意:分块参数没有万能值,一定要拿你自己的数据做小规模测试。我一般会准备20个典型问题,用不同参数跑一遍,看召回率和答案质量,再定最终参数。
3.2 向量化:模型选择与批量处理的性能陷阱
向量化就是把文本变成一串数字(embedding),让语义相近的文本在向量空间里距离也相近。这一步的模型选择很关键,我一般看三个指标:维度、语言支持、推理速度。
维度方面,常见的有384、768、1024、1536。维度越高表达能力越强,但存储和检索成本也越高。我一般用768或1024,性价比比较平衡。语言支持上,如果你的数据有中文,一定要选多语言模型,纯英文模型在中文上效果会断崖式下跌,这个坑我踩过。
推理速度是很多人忽略的点。向量化通常是一次性离线做的,但如果你的数据在持续增长,增量向量化的速度就很重要了。我一般会用ONNX Runtime或者直接上GPU来加速,批量大小(batch size)设在32到128之间,具体看显存。
这里有个性能陷阱要特别说:不要一条一条地调embedding接口。我见过有人写个for循环逐条请求,一万条数据跑了一下午。正确做法是批量请求,一次传几十上百条。如果是本地模型,用DataLoader做批处理;如果是云端API,看它的批量接口限制,一般一次能传几十条。
# 批量向量化的典型写法 from sentence_transformers import SentenceTransformer model = SentenceTransformer('BAAI/bge-base-zh-v1.5') texts = [...] # 你的文本列表 # batch_size根据显存调整,GPU上一般64或128 embeddings = model.encode(texts, batch_size=64, show_progress_bar=True, normalize_embeddings=True)注意最后那个normalize_embeddings=True,做归一化之后余弦相似度计算会更快,而且很多向量库默认用内积,归一化后内积等价于余弦相似度,这个细节能省不少事。
3.3 推理层:云端API与本地部署的取舍实操
推理层是整个链路的心脏。我一般先用云端API把业务逻辑跑通,因为这个阶段你要验证的是产品而不是基础设施。云端API的好处是零运维、模型强、上手快,缺点是成本随量线性增长、数据要出网、延迟受网络影响。
当你决定要迁到本地时,vLLM是目前我最推荐的推理框架。它的PagedAttention机制对显存利用率的提升非常明显,吞吐能比朴素实现高好几倍。部署一个本地推理服务大概是这样:
# 启动vLLM服务,以Qwen2.5-7B为例 python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --port 8000几个参数解释一下。tensor-parallel-size是多卡并行数,单卡就设1。gpu-memory-utilization是显存占用比例,0.9意味着留10%给其他进程,设太高容易OOM。max-model-len是最大上下文长度,设得越大占的显存越多,按实际需求来。
迁到本地之前,我强烈建议先算一笔账。假设一张消费级显卡整机成本约1.5万元,按三年折旧、每天跑10小时算,每小时成本约1.4元。云端API假设每百万token收费10元,一个7B模型每秒大概能出50个token,一小时就是1.8亿token,云端要1800元,本地只要1.4元。这个差距是数量级的,但前提是你的利用率要够高,如果一天只跑几分钟,那还是云端划算。
4. 服务化与性能优化:让AI应用真正扛得住
4.1 服务层设计:异步、限流与优雅降级
把模型跑起来只是第一步,让它作为一个服务稳定对外提供能力,是另一回事。我用FastAPI做服务层的时候,有几个设计是必做的。
首先是全异步。AI推理是IO密集和计算密集混合的场景,同步阻塞的写法会让并发能力惨不忍睹。FastAPI的async def配合httpx的异步客户端,能把并发能力提升一个数量级。但要注意,如果你在async函数里调用了同步的阻塞代码(比如某些本地模型的同步推理),整个事件循环会被卡住,这时候要用run_in_executor把它丢到线程池里。
其次是限流。AI服务很容易被打爆,一个用户疯狂发请求就能拖垮整个服务。我一般用令牌桶算法做限流,按用户维度或IP维度限制QPS。Redis是实现限流的好帮手,用INCR加过期时间就能做一个简单的计数器限流。
第三是优雅降级。当后端推理服务超时或过载时,不能直接给用户报错,要有降级策略。常见的降级有:返回缓存结果、切换到更小的模型、返回预设的兜底话术。我一般会设一个超时阈值,比如3秒,超过就触发降级。
# 带超时和降级的异步调用示例 import asyncio import httpx async def call_llm(prompt: str, timeout: float = 3.0): try: async with httpx.AsyncClient() as client: resp = await client.post( "http://localhost:8000/v1/chat/completions", json={"model": "qwen", "messages": [{"role": "user", "content": prompt}]}, timeout=timeout ) return resp.json() except (httpx.TimeoutException, httpx.ConnectError): # 降级:返回缓存或兜底 return {"fallback": True, "content": "当前请求较多,请稍后再试"}4.2 缓存策略:省下的都是真金白银
缓存是AI工程里性价比最高的优化手段,没有之一。我做过一个项目,加了缓存之后API调用量直接降了40%,成本同步下降。
缓存分几层。第一层是精确缓存,key就是请求的完整hash,命中就直接返回。这一层适合那些重复率高的场景,比如FAQ问答。第二层是语义缓存,把请求向量化,在缓存里找相似度超过阈值的已有结果。这一层能命中那些“问法不同但意思一样”的请求,命中率更高但实现复杂一些,阈值一般设在0.9到0.95之间。
缓存的key设计有讲究。除了请求内容,还要把影响输出的参数都放进去,比如模型名、温度、最大长度。我见过有人只拿prompt做key,结果换了模型还返回旧结果,排查了半天。
缓存的过期策略也要想清楚。知识库类的内容变化慢,可以设长一点,比如一天;实时性要求高的场景,可能几分钟就要过期。我一般用Redis的EXPIRE来做,简单可靠。
4.3 压测与容量规划:别等上线了才发现扛不住
压测这一步很多人跳过,然后上线当天被打挂。我一般用Locust或wrk做压测,重点看三个指标:QPS、P99延迟、错误率。
压测的时候要注意,AI服务的瓶颈往往不在你的服务层,而在推理层。所以压测要分层做:先单独压推理服务,看它的吞吐上限;再压整个链路,看服务层有没有额外瓶颈。我遇到过一次,服务层本身能扛1000 QPS,但推理层只能扛50,结果整体就被卡在50。
容量规划上,我一般按峰值QPS的1.5倍来准备资源,留出缓冲。同时要设计好扩容策略,是加机器还是加卡,是水平扩展还是垂直升级。如果是云端API,要确认账号的配额够不够,我见过有人压测把配额打满,正式上线反而没额度了。
5. 常见问题与排查技巧实录
5.1 检索质量差:从分块到重排的排查路径
RAG场景最常见的问题就是“检索出来的东西不对”。排查这个我一般按顺序走:先看分块,再看embedding模型,最后看重排。
分块问题表现为:检索出来的chunk语义不完整,或者关键信息被切断了。解决办法是调整chunk size和overlap,或者换成语义分块。embedding模型问题表现为:语义相近的文本向量距离却很远。这时候要检查模型是不是适合你的语言和领域,中文场景一定要用中文或多语言模型。重排是最后一道防线,用一个交叉编码器(cross-encoder)对初步检索的结果重新排序,能明显提升精度,代价是增加一点延迟。
我整理了一个排查速查表:
| 现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| 检索结果不相关 | embedding模型不匹配 | 人工看几条向量的相似度 | 换多语言或领域模型 |
| 关键信息检索不到 | 分块切断语义 | 检查chunk边界 | 调整size和overlap |
| 结果同质化 | 数据重复 | 统计重复率 | 去重 |
| 排序不合理 | 缺少重排 | 对比重排前后 | 加cross-encoder |
5.2 推理延迟高:从显存到批处理的逐层定位
延迟高是另一个高频问题。我一般先看显存占用,如果显存快满了,推理会频繁触发换页,延迟飙升。这时候要降低max-model-len或者gpu-memory-utilization。
如果显存没问题,就看批处理。vLLM默认会做连续批处理(continuous batching),但如果你的请求是串行发的,批处理就发挥不出来。要确保并发请求能同时到达推理服务。如果延迟还是高,可能是模型太大,考虑换小模型或者量化。INT8量化一般能降一半显存、提升30%左右速度,精度损失通常在可接受范围。
还有一个容易被忽略的点是首token延迟和总延迟的区别。首token延迟主要受prefill阶段影响,和输入长度强相关;总延迟还受decode阶段影响,和输出长度相关。优化的时候要分清你优化的是哪个。
5.3 成本失控:token消耗的监控与优化
成本失控往往是因为没有监控。我一般会在服务层记录每次请求的输入token、输出token、模型名、耗时,然后按天聚合。有了这些数据,你才能知道钱花在哪了。
优化token消耗有几个方向。一是压缩上下文,把历史对话做摘要而不是全量塞进去。二是限制输出长度,很多场景不需要长篇大论,设个max_tokens能省不少。三是缓存,前面说过了。四是路由,简单问题用小模型,复杂问题才用大模型,这个能省一大笔。
我做过一个对比,同样一批请求,全用大模型和用路由策略,成本差了将近三倍,而用户满意度几乎没差别。路由的实现可以用一个小的分类模型,或者简单的规则判断。
提示:token计数不要自己估算,用官方的tokenizer或者tiktoken,估算误差能到20%以上,做成本核算会失真。
6. 迭代与可观测:让系统越跑越好的闭环
6.1 日志、指标与追踪三件套
一个能持续迭代的AI系统,可观测性是基础。我一般上三件套:日志、指标、追踪。
日志记录每次请求的输入输出、参数、耗时、错误。这里要注意脱敏,用户隐私数据不能明文落盘。指标是聚合数据,比如QPS、P99延迟、缓存命中率、token消耗,用Prometheus加Grafana就能搭起来。追踪是分布式链路追踪,能看到一个请求在各个环节的耗时分布,排查性能问题特别有用,OpenTelemetry是现在的标准方案。
这三件套搭起来之后,你对系统的掌控感会完全不一样。以前出问题靠猜,现在看数据就知道瓶颈在哪。
6.2 反馈闭环:把用户反馈变成迭代燃料
AI系统和其他软件最大的区别是,它的输出质量是概率性的,需要持续迭代。我一般会设计一个反馈机制,让用户能对结果点赞或点踩,这些反馈数据就是迭代的燃料。
反馈数据怎么用?一是用来评估,定期抽样人工评估,看整体质量趋势。二是用来构造测试集,把典型的bad case收集起来,作为回归测试。三是用来微调,积累到一定量之后可以做微调,让模型更贴合你的场景。
我一般会维护一个“黄金测试集”,包含几十到几百个典型问题和期望答案,每次改动之后都跑一遍,看有没有退化。这个习惯帮我避免了好几次“改了一个地方、坏了另一个地方”的事故。
6.3 版本管理与灰度发布
AI系统的版本管理比传统软件复杂,因为你要同时管理代码版本、模型版本、数据版本。我一般用Git管代码,用模型注册表(比如MLflow)管模型,用数据版本工具管数据。三者要能对应上,出问题才能回溯。
发布的时候一定要灰度。先放1%的流量,观察指标,没问题再逐步放大。灰度期间要重点看延迟、错误率、以及业务指标(比如用户满意度)。我见过一次直接全量发布,结果新模型在某个边缘场景下疯狂输出乱码,等发现的时候已经影响了大批用户。
灰度还有一个好处是能做A/B测试,对比新旧版本的效果。这个在AI场景特别有价值,因为模型效果很难离线评估准确,线上数据才是真相。
7. 我个人的一些实操体会
走完这一整套“from scratch”的流程,最大的收获不是学会了某个工具,而是建立了一套判断力。你知道每个环节的瓶颈可能在哪,知道一个方案的成本和收益大概是什么量级,知道出了问题该往哪个方向排查。这种判断力是调包调不出来的。
如果让我给刚入门的朋友一个建议,我会说:先跑通一个最小闭环,再逐步加深。不要一上来就追求完美架构,先用最简单的方案把数据、推理、服务串起来,跑通之后再针对瓶颈优化。我见过太多人卡在“选型纠结”上,半年过去了还没跑通第一个版本。
另外,工具是死的,场景是活的。我在这篇里给的参数和方案都是基于我的经验,但你的数据、你的用户、你的约束可能完全不同。所有参数都要拿你自己的场景验证,这才是工程的本意。踩坑不可怕,可怕的是踩了坑不知道为什么踩、下次还踩。把每次踩坑都变成一条经验,积累下来,你就有了别人拿不走的东西。