☰
从零搭建AI工程体系:架构设计、核心模块与实操落地
2026/10/3 3:45:57 网站建设 项目流程

1. 从零搭建AI工程体系,为什么我劝你别一上来就调包

"ai-engineering-from-scratch"这个标题,第一次看到的时候我愣了一下。市面上讲AI的教程铺天盖地,但绝大多数都是教你pip install transformers然后三行代码跑个推理,或者调个API做个聊天机器人。真正讲"从零"把AI工程这套东西搭起来的,少之又少。

我自己在这个坑里摸爬滚打了几年,带过几个从传统后端转AI工程的同事,也面试过不少号称"做过大模型应用"的候选人。一个很普遍的现象是:大家会用LangChain,会调OpenAI的接口,会写Prompt,但一旦线上出了延迟抖动、显存OOM、检索召回率暴跌、token成本失控这些问题,就完全不知道从哪下手。因为他们的知识是"调用层"的,不是"工程层"的。

这个项目标题的核心价值,恰恰在于它强调from scratch——不是从零训练一个大模型(那玩意儿没几千万预算别想),而是从零搭建一套能跑、能扛、能观测、能迭代的AI工程体系。它解决的是"我会用模型,但我不会做工程"这个断层问题。适合谁看?适合有一定编程基础、想把AI应用真正推到生产环境的工程师,也适合那些被各种框架惯坏了、想搞清楚底层到底发生了什么的开发者。

我下面要聊的,就是这套体系从零搭起来时,我踩过的坑、做过的取舍、以及那些文档里不会写的实操细节。核心关键词ai-engineering-from-scratch会贯穿始终,因为这不是一个玩具项目,而是一套完整的工程方法论。

2. 整体架构设计:先想清楚边界,再动手写代码

2.1 为什么"从零"不等于"什么都自己造"

很多人对from scratch有个误解,觉得什么都得手写,连HTTP服务器都要自己撸一个。这是典型的用力过猛。AI工程的核心矛盾从来不是"能不能实现",而是"能不能稳定、低成本、可观测地实现"。所以第一步要做的,是划清楚哪些轮子必须自己造,哪些直接用现成的。

我的判断标准很简单:凡是和你的业务逻辑、数据特性、成本模型强相关的部分,自己写;凡是通用基础设施,用成熟方案。比如向量检索的索引结构,如果你只是做个小规模知识库,用FAISS就够了;但如果你要做多租户、带权限过滤、还要支持增量更新,那自己封装一层检索抽象层就是必须的,因为现成方案满足不了你的过滤语义。

再比如推理服务,vLLM、TGI这些推理引擎已经把吞吐优化做得很好了,你没必要自己写CUDA kernel。但推理前面的请求队列、批处理调度、超时降级、多模型路由,这些和你的SLA强相关,必须自己控制。

我见过一个团队,为了"技术自主",自己写了个推理服务,结果吞吐只有vLLM的三分之一,还天天OOM。这就是没搞清楚边界。from scratch的正确姿势是:在正确的抽象层级上从零构建,而不是在错误的层级上重复造轮子。

2.2 分层架构:把AI应用拆成五层

我习惯把一套AI工程体系拆成五层,从下往上分别是:

层级职责自建还是复用关键考量
基础设施层GPU调度、容器编排、存储复用(K8s + 对象存储)成本、弹性
模型服务层推理、批处理、量化复用引擎+自建调度吞吐、延迟、显存
编排层链路编排、工具调用、状态管理自建可控性、可观测
数据层向量库、缓存、特征存储混合一致性、召回率
应用层业务逻辑、Prompt管理、评测自建迭代速度

这个分层的好处是,每一层的问题都能被隔离定位。线上延迟高了,你先看是模型服务层的排队时间,还是编排层的串行调用,还是数据层的检索慢。没有分层,你就是在黑盒里瞎猜。

我特别想强调编排层必须自建这一点。LangChain这类框架在demo阶段很爽,但到了生产环境,它的抽象泄漏会让你痛不欲生。你想加个自定义的重试逻辑、想在某个节点插入埋点、想根据中间结果动态改变链路,框架的抽象就会变成枷锁。自己写一个轻量的编排器,可能就几百行代码,但换来的是完全的可控性。

2.3 成本模型先行:别等账单来了才后悔

从零搭AI工程,最容易失控的就是成本。我在项目启动阶段一定会做一件事:建立token成本模型。具体做法是,把一次完整请求拆解成输入token、输出token、检索token、重排token,分别乘以单价,算出单次请求成本,再乘以预估QPS,得到月度成本。

这个模型会直接反过来影响你的架构决策。比如你发现检索回来的上下文占了总token的70%,那你就得在检索层做更激进的截断和重排,而不是无脑把top-k调大。再比如你发现输出token成本是大头,那你就得考虑用更小的模型做初筛,或者用流式输出配合早停。

我见过太多项目,架构设计得漂漂亮亮,一上线发现一个月烧几十万,然后手忙脚乱地砍功能。成本模型不是财务的事,是架构师的事,必须在写第一行代码之前就建起来。

3. 核心模块拆解:每个环节的坑和取舍

3.1 推理服务:批处理是吞吐的命根子

推理服务这块,我踩过最大的坑就是忽视了批处理。早期我直接用HuggingFace的pipeline,一个请求一个请求地跑,QPS低得可怜,GPU利用率常年个位数。后来才明白,GPU这东西,你不把batch塞满,就是在烧钱。

连续批处理(continuous batching)是现在的标配。它的核心思想是:不等一个batch里所有请求都生成完,而是动态地把新请求插进来,把已经生成完的请求踢出去。这样GPU几乎永远在处理有效计算,吞吐能提升几倍到十几倍。

但连续批处理也带来一个问题:单个请求的延迟会变高,因为它要和别的请求共享计算资源。所以你需要根据业务场景做取舍。如果是实时对话,延迟敏感,batch就不能太大;如果是离线批量处理,那就往死里塞。

我一般的配置是:实时服务用vLLM,开连续批处理,max_num_seqs设成32到64,max_num_batched_tokens根据显存调。离线任务用单独的实例,batch size直接拉满。两套实例分开,互不干扰。

还有一个细节是KV Cache的管理。长上下文场景下,KV Cache会吃掉大量显存。vLLM有PagedAttention来优化,但你仍然需要设置合理的gpu_memory_utilization,一般0.9左右,留一点给其他操作。如果显存实在紧张,就得上量化,INT8或者INT4,但要注意量化对输出质量的影响,尤其是对数值推理和代码生成任务,量化后可能明显变差。

3.2 检索增强:召回率是玄学,但可以工程化

RAG这套东西,demo五分钟,调优五个月。核心难点在于召回率和精度的平衡。

我一开始的做法很朴素:把文档切块,embedding,存FAISS,查询时取top-k。结果发现,有些问题明明答案就在库里,就是召不回来。后来才意识到,问题出在切块策略上。固定长度切块会把一个完整的语义单元切碎,导致embedding表达不完整。

我的改进方案是语义切块+重叠窗口。具体来说,先按段落切,如果段落太长再按句子切,同时相邻块之间保留10%到20%的重叠。这样既能保证语义完整,又不会让块太大导致检索噪声。

切块之后是embedding模型的选择。这里有个反直觉的点:不是越大的模型越好。大模型embedding维度高,检索慢,而且对短查询可能过拟合。我实测下来,中等规模的embedding模型配合好的切块策略,效果往往比大模型配烂切块好。

再往上是重排(rerank)。检索回来的top-k,用cross-encoder重排一遍,精度能提升不少。但重排是串行计算,延迟高,所以一般只对top-20到top-50做重排,再取top-3到top-5喂给生成模型。

这里有个成本陷阱:重排的token消耗容易被忽略。cross-encoder要把query和每个候选文档拼起来算分,如果候选多、文档长,token消耗很可观。所以重排的候选数量要控制,不能无脑调大。

3.3 编排层:状态管理比链路编排更难

编排层大家关注的都是"怎么把多个步骤串起来",但我认为真正的难点是状态管理。

一个多轮对话的AI应用,状态包括:对话历史、检索到的上下文、工具调用的中间结果、用户的偏好设置等等。这些状态怎么存、怎么过期、怎么在多个服务间共享,是个大问题。

我试过几种方案。最简单的是把状态全塞进Prompt里,每次请求都带上完整历史。问题是token会爆炸,而且历史越长,模型越容易迷失。后来改成滑动窗口+摘要:保留最近N轮完整对话,更早的对话用一个小模型做摘要。这样token可控,信息也不丢。

工具调用的状态更麻烦。比如用户让AI查个天气,然后基于天气推荐穿搭,这中间有个工具调用的结果需要传递。如果工具调用是异步的,你还得处理回调、超时、重试。我的做法是给每个会话维护一个状态机,工具调用作为状态转移的触发条件,状态存在Redis里,带TTL。

还有一个坑是并发安全。同一个会话如果有多个请求并发进来,状态可能被覆盖。我一般用乐观锁或者会话级别的串行队列来解决。别小看这个问题,线上真出过用户A的对话历史串到用户B那里的bug,虽然概率低,但一旦发生就是事故。

3.4 可观测性:没有埋点,你就是在盲飞

AI应用的可观测性和传统后端很不一样。传统后端看QPS、延迟、错误率就够了,AI应用还得看token消耗、检索命中率、生成质量、幻觉率。

我的埋点方案是分层的。请求级别记录:输入输出token数、端到端延迟、各阶段耗时、检索到的文档ID和分数、模型版本、Prompt版本。会话级别记录:轮次数、用户反馈、是否触发人工介入。系统级别记录:GPU利用率、显存占用、队列长度、缓存命中率。

这些数据汇总起来,你才能回答一些关键问题。比如"最近延迟涨了,是检索慢了还是生成慢了","成本涨了,是token多了还是调用次数多了","用户满意度降了,是模型换了还是Prompt改了"。

我特别推荐做一个请求回放功能。把线上请求的完整上下文(输入、检索结果、Prompt、输出)存下来,出问题时可以原样回放,快速定位。这个功能在排查偶发问题时简直是救命稻草。

4. 实操落地:从零到一的具体步骤

4.1 环境准备与依赖管理

第一步是把环境搭起来。我的建议是用容器,别在宿主机上裸装。CUDA版本、驱动版本、Python版本、各种库的版本,这套依赖关系能把你逼疯。用Docker把环境固化下来,换机器、扩缩容都方便。

基础镜像我一般选nvidia/cuda:12.1.0-runtime-ubuntu22.04,然后装Python 3.10(3.11也行,但有些库还没跟上)。依赖管理用uv或者poetry,别用裸pip,版本冲突会让你怀疑人生。

# 基础环境 FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 RUN apt-get update && apt-get install -y python3.10 python3-pip git RUN pip install uv COPY requirements.txt . RUN uv pip install --system -r requirements.txt

requirements.txt里我会锁定所有版本,包括间接依赖。AI这块库更新太快,不锁版本,今天能跑的代码明天就崩。

GPU这块,如果是单机多卡,注意设置CUDA_VISIBLE_DEVICES来隔离。如果是多机,那就得上K8s加GPU调度器。我一般先用单机把流程跑通,再考虑分布式,别一上来就搞复杂。

4.2 推理服务搭建:vLLM实战配置

推理服务我用vLLM,启动命令大概长这样:

python -m vllm.entrypoints.openai.api_server \ --model /path/to/model \ --tensor-parallel-size 2 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --max-num-seqs 64 \ --max-num-batched-tokens 8192 \ --served-model-name my-model \ --port 8000

几个参数的解释:tensor-parallel-size是张量并行度,等于GPU数量,两张卡就设2。gpu-memory-utilization是显存利用率,0.9是留10%给其他操作,设太高容易OOM。max-model-len是最大上下文长度,根据你的模型和显存调,别设太大,KV Cache会吃光显存。max-num-seqs是最大并发序列数,这个直接影响吞吐和延迟的平衡。

启动之后,用OpenAI兼容的接口调用,方便后续切换模型。我一般会在前面加一层自己的网关,做鉴权、限流、路由、埋点。

注意:vLLM启动时会预分配显存,如果启动失败报OOM,先把gpu-memory-utilization降到0.8试试,再不行就降max-model-len。

4.3 检索层搭建:从切块到重排的完整链路

检索层的搭建我分四步走。

第一步是文档解析。PDF、Word、HTML各种格式,用unstructured或者pymupdf解析成纯文本。这一步的坑是表格和图片,表格解析不好会丢信息,图片里的文字得用OCR。我一般对表格做特殊处理,转成Markdown格式保留结构。

第二步是切块。我写了个语义切块器,核心逻辑是:

def semantic_chunk(text, max_len=512, overlap=64): paragraphs = text.split("\n\n") chunks = [] current = "" for para in paragraphs: if len(current) + len(para) > max_len: if current: chunks.append(current) # 处理超长段落 if len(para) > max_len: sentences = split_sentences(para) for sent in sentences: if len(current) + len(sent) > max_len: chunks.append(current) current = current[-overlap:] + sent else: current += sent else: current = current[-overlap:] + para if current else para else: current += "\n\n" + para if current else para if current: chunks.append(current) return chunks

这段代码的关键是overlap,保留重叠能避免语义断裂。max_len根据embedding模型的最佳输入长度来定,一般512左右。

第三步是embedding和索引。embedding模型我用过不少,最后稳定在bge-large-zh这类中等规模模型上,性价比高。索引用FAISS的IndexFlatIP做精确检索,数据量大了再换IVF或者HNSW。注意embedding要归一化,用内积当余弦相似度。

第四步是重排。用bge-reranker这类cross-encoder,对top-50重排取top-5。重排的batch size别太大,不然显存扛不住。

4.4 编排层实现:一个轻量状态机

编排层我写了个轻量的状态机,核心是一个Session对象和一组Step。

class Session: def __init__(self, session_id): self.session_id = session_id self.history = [] self.context = {} self.state = "idle" def add_turn(self, role, content): self.history.append({"role": role, "content": content}) # 滑动窗口,保留最近10轮 if len(self.history) > 20: self.history = self.history[-20:] class Pipeline: def __init__(self, steps): self.steps = steps def run(self, session, user_input): session.add_turn("user", user_input) for step in self.steps: result = step.execute(session) if result.should_stop: break return session.history[-1]["content"]

每个Step是一个可插拔的组件,比如RetrieveStep、RerankStep、GenerateStep、ToolCallStep。这样加功能就是加一个Step,改逻辑就是改一个Step,非常清晰。

状态存Redis,key是session:{id},value是序列化的Session对象,TTL设个30分钟。并发控制用Redis的分布式锁,保证同一会话串行处理。

4.5 评测体系:没有评测,迭代就是赌博

从零搭AI工程,评测体系是最容易被忽略但最重要的部分。我见过太多团队,改了个Prompt,感觉"好像好了一点",就上线了,结果用户投诉变多。

我的评测体系分三层。离线评测:准备一批标注好的问答对,每次改动跑一遍,看准确率、召回率、BLEU、ROUGE这些指标。在线评测:用A/B测试,一部分流量走新版本,对比用户反馈、停留时长、转化率。人工评测:定期抽样,人工打分,尤其是对生成质量这种主观指标。

离线评测的数据集要持续维护,把线上bad case加进去,让评测集越来越贴近真实场景。我一般用ragas这类框架做RAG的自动评测,但自动评测只能当参考,最终还得靠人工。

提示:评测集不要太大,几百条高质量的就够了。关键是覆盖各种场景和边界情况,而不是追求数量。

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

5.1 延迟抖动:从现象到根因的排查路径

延迟抖动是AI应用最常见的问题。我的排查路径是:先看是所有请求都慢还是部分请求慢。

如果所有请求都慢,大概率是资源瓶颈。看GPU利用率,如果接近100%,那就是算力不够,得扩容或者优化batch。看队列长度,如果队列堆积,那就是吞吐不够。看显存,如果接近上限,那就是KV Cache管理有问题。

如果部分请求慢,那就要看这些请求有什么共同点。是不是输入特别长?是不是检索返回的文档特别多?是不是触发了工具调用?我一般会在埋点里记录每个阶段的耗时,然后按耗时排序,找出长尾请求的特征。

我遇到过一个经典问题:某些请求延迟特别高,排查发现是检索层的问题。原因是这些请求的query触发了FAISS的全量扫描,因为索引的nprobe设得太高。调低nprobe后,延迟降下来了,召回率只降了一点点。这就是典型的参数没调好。

还有一个坑是冷启动。模型服务刚启动时,第一次推理特别慢,因为要加载权重、初始化CUDA context。解决办法是启动后先跑几个warmup请求,把服务预热。

5.2 显存OOM:预防比抢救重要

显存OOM是另一个高频问题。我的原则是预防为主。启动时就设好gpu-memory-utilization,别贪心设0.95。运行中监控显存,设个告警阈值,到0.85就报警。

如果真的OOM了,抢救措施有几个:降低max-num-seqs,减少并发;降低max-model-len,减少KV Cache;开启量化,INT8能省一半显存;如果还不行,就得上多卡或者换更小的模型。

我踩过最坑的一次是,一个长上下文请求把显存打满了,导致整个服务崩溃。后来我加了个输入长度检查,超过阈值的请求直接拒绝或者截断,避免单个请求拖垮整个服务。

5.3 检索召回率低:从数据到策略的逐层排查

召回率低,先别急着换模型,按这个顺序排查:

第一,数据质量。文档解析有没有丢内容?切块有没有把关键信息切碎?我遇到过PDF解析把表格内容全丢了的情况,导致检索永远召不回表格里的数据。

第二,embedding质量。拿几个典型query,手动算一下和正确文档的相似度,看看是不是embedding本身就没表达好。如果是,换个embedding模型试试。

第三,检索策略。top-k是不是太小?相似度阈值是不是太高?试试调大top-k,或者用混合检索(向量+关键词)。

第四,重排。如果检索召回了但重排没排上来,那就是重排模型的问题。试试换个重排模型,或者调整重排的输入格式。

我一般会做一个召回率分析工具,输入query和期望文档,输出检索排名,直观地看到问题出在哪一层。

5.4 成本失控:token消耗的精细化管控

成本失控的根源是token消耗没管住。我的管控手段有几个:

输入侧:检索上下文做截断,只保留最相关的部分;对话历史做摘要,别全塞进去;Prompt做精简,去掉冗余的指令。

输出侧:设置max_tokens上限,别让模型无限生成;用流式输出配合早停,生成到关键信息就停;简单任务用小模型,复杂任务才用大模型。

缓存:相同或相似的query,缓存结果,直接返回。语义缓存(用embedding判断相似)比精确匹配缓存命中率高得多。

我做过一个统计,加了语义缓存之后,token成本降了40%。这个投入产出比非常高。

5.5 常见问题速查表

问题现象可能原因排查方法解决措施
延迟高资源瓶颈/长尾请求看GPU利用率、队列长度、分阶段耗时扩容、优化batch、限制输入长度
显存OOM并发高/上下文长看显存监控、请求特征降并发、降上下文、量化
召回率低数据/embedding/策略召回率分析工具改切块、换模型、调top-k
成本高token消耗大token成本模型截断、缓存、小模型
输出质量差模型/Prompt离线评测、人工评测换模型、改Prompt、加few-shot
状态串号并发问题看会话ID、锁机制加分布式锁、会话串行

6. 迭代与扩展:让体系能持续进化

6.1 Prompt管理:别把Prompt硬编码在代码里

Prompt是AI应用的核心资产,但很多人把它硬编码在代码里,改一次Prompt就要发一次版。这是大忌。

我的做法是把Prompt抽出来,存在配置中心或者数据库里,带版本号。每次请求记录用了哪个版本的Prompt,方便回溯。改Prompt不用发版,改完即时生效,还能做A/B测试。

Prompt的版本管理我一般用Git,每个Prompt一个文件,改动用commit记录。这样能追溯每次改动的原因和效果。

6.2 模型热切换:别让模型升级变成事故

模型升级是必然的,但直接替换模型风险很大。我的做法是灰度切换。新模型先接10%的流量,对比指标,没问题再逐步放大。

实现上,在网关层做模型路由,根据流量比例或者用户ID哈希来决定用哪个模型。这样切换平滑,出问题也能快速回滚。

模型版本也要记录在埋点里,方便对比不同版本的效果。

6.3 反馈闭环:让用户帮你迭代

用户反馈是最宝贵的迭代信号。我在应用里加了点赞点踩、问题反馈的入口,把反馈和请求ID关联起来。定期分析bad case,找出共性问题,针对性优化。

更进一步,可以把用户反馈作为评测集的一部分,让离线评测更贴近真实需求。这个闭环建起来之后,迭代速度会快很多。

6.4 扩展方向:从单模态到多模态

这套体系搭好之后,扩展性很强。想加多模态,就在检索层加图片embedding,在生成层换多模态模型。想加Agent能力,就在编排层加更多的工具调用Step。想加个性化,就在数据层加用户画像,在Prompt里注入个性化信息。

核心是分层架构带来的解耦,每一层都能独立演进,不会牵一发而动全身。

我个人在实际操作中的体会是,从零搭AI工程最难的不是技术,而是克制。克制住什么都想自己造的冲动,克制住无脑堆功能的欲望,克制住不看数据就改代码的习惯。把边界划清楚,把埋点做扎实,把评测建起来,剩下的就是持续迭代。这套东西搭一次,后面做任何AI应用都能复用,这才是from scratch真正的价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询