1. 从零搭建AI工程体系,为什么我劝你别一上来就调包
这两年“AI工程”这个词被说得太多了,多到有点变味。打开任何一个技术社区,满屏都是“三行代码调用大模型”“十分钟搭建RAG”“零基础微调自己的模型”。我不否认这些工具确实把门槛拉低了,但如果你真打算把AI能力落到一个能扛住真实流量的业务里,光会调API是远远不够的。ai-engineering-from-scratch这个标题之所以值得单独拿出来聊,是因为它戳中了一个被大多数人跳过的环节——从底层把AI工程这条链路自己走一遍。
我自己带过几个从零起步的AI项目,也见过太多团队在“能跑demo”和“能上生产”之间反复摔跤。摔跤的原因往往不是模型不够强,而是工程链路里那些不起眼的地方出了问题:数据管道断了、推理服务的内存泄漏了、向量检索的召回率莫名其妙掉了、评测集和线上分布对不上了。这些问题,调包调不出来,只有你亲手从零搭过一遍,才知道每个环节的边界在哪里。
这篇内容适合三类人看。第一类是有一定编程基础、想系统理解AI工程全貌的开发者,你可能已经会写Python,但对“一个AI系统到底由哪些部分组成”还没有完整的认知。第二类是正在做AI项目、但总觉得哪里不对劲的工程师,你手里的东西能跑,但你不确定它是不是“对”的。第三类是对AI感兴趣、想找一个靠谱路径入门的技术爱好者,你不想只停留在调包层面,想真正搞明白背后的机制。
我会按照一条完整的工程链路来讲:从环境与依赖管理开始,到数据处理管道、模型推理服务、向量检索与RAG、评测体系、可观测性,最后聊部署和迭代。每一块我都会说清楚“为什么这么设计”“关键参数怎么定”“我踩过哪些坑”。这不是一篇教程,更像是一个做过几轮的人坐下来跟你复盘。
2. 整体架构设计:先想清楚边界,再动手写代码
2.1 从零搭建的核心思路与分层原则
很多人一上来就开始写推理代码,结果写到一半发现数据格式不对、依赖版本冲突、评测没法做,只能推倒重来。我的建议是,动手之前先把整个系统分成几层,每层只负责一件事,层与层之间通过明确的接口通信。这样做的好处是,任何一层出问题,你可以单独替换或调试,而不会牵一发动全身。
我通常会把一个AI工程系统分成五层。最底层是基础设施层,包括运行环境、依赖管理、硬件资源。往上是数据层,负责数据的采集、清洗、切分、存储。再往上是模型层,包括推理服务、模型版本管理、提示词管理。然后是应用层,也就是RAG、Agent、工作流这些面向业务逻辑的部分。最上面是观测与评测层,负责监控、日志、评测、反馈闭环。
这个分层不是教科书上的标准答案,而是我在实际项目里摸索出来的。它的核心原则是:每一层只依赖它下面那一层,不允许跨层调用。比如应用层不能直接去读原始数据文件,必须通过数据层提供的接口。这样做看起来麻烦,但当你的系统从demo长成一个有几十个模块的项目时,你会发现这种约束救了你无数次。
提示:分层不是为了好看,是为了让每一层的变更不会波及全局。你在数据层换一个清洗逻辑,不应该影响模型层的推理代码。
2.2 技术选型背后的取舍逻辑
选型这件事,我的原则是“用最无聊的技术”。什么意思?就是优先选那些社区成熟、文档齐全、你团队里有人用过的方案,而不是最新最酷的。AI工程这个领域变化太快了,如果你在基础设施上也追新,你会发现自己一半的时间在解决工具本身的bug,而不是业务问题。
具体来说,运行环境我推荐用容器化方案,把依赖锁死在一个镜像里。Python的依赖管理是个老大难,pip、conda、poetry各有各的坑。我的做法是用pyproject.toml声明依赖,用锁文件固定版本,然后在容器里安装。这样本地开发和线上部署的环境是一致的,不会出现“我本地能跑”的经典问题。
推理框架的选择要看你的场景。如果你只是调用外部API,那没什么好选的,做好超时和重试就行。如果你要自己部署模型,vLLM和TGI是目前比较主流的选择,前者吞吐量高,后者对HuggingFace生态支持好。如果你要做本地轻量推理,llama.cpp系列是不错的选择。这里没有银弹,关键看你的延迟要求、并发量和硬件条件。
向量数据库这块,我踩过的坑最多。早期我用过FAISS,它轻量、快,但不支持持久化和分布式,适合原型阶段。后来换到Milvus和Qdrant,前者功能全但运维重,后者轻量且API友好。我的建议是,如果你的数据量在百万级以下,Qdrant或pgvector就够了,别一上来就上重型方案。
| 层级 | 推荐方案 | 适用场景 | 主要坑点 |
|---|---|---|---|
| 环境管理 | 容器 + 锁文件 | 所有场景 | 镜像体积膨胀 |
| 推理服务 | vLLM / TGI | 自部署模型 | 显存管理复杂 |
| 向量检索 | Qdrant / pgvector | 百万级以下 | 索引参数调优 |
| 评测框架 | 自建 + 开源工具 | 所有场景 | 评测集污染 |
2.3 目录结构与工程规范
从零搭建的另一个好处是,你可以从一开始就把工程规范定好。我见过太多项目,代码写到后面目录乱成一锅粥,新人接手要花一周才能找到入口。我的做法是,在项目根目录下按层划分文件夹,每个文件夹里再按功能细分。
一个典型的目录结构大概是这样:infra/放容器配置和部署脚本,data/放数据处理管道,models/放推理服务和模型管理,app/放业务逻辑,eval/放评测代码,observability/放监控和日志配置。每个文件夹里都有一个README.md说明这一层的职责和接口。
命名规范也很重要。我坚持用动词+名词的方式命名函数,比如load_documents、clean_text、build_index,而不是documents、text这种含糊的名字。变量名要能看出它的类型和用途,比如raw_docs、chunked_docs、embedded_vectors。这些细节看起来琐碎,但当你的代码超过几千行,它们决定了你还能不能读懂自己三个月前写的东西。
3. 数据管道:AI工程里最脏最累但最重要的部分
3.1 数据采集与清洗的实操要点
数据是AI系统的地基,但大多数人把80%的精力花在模型上,只给数据留了20%。这是个巨大的误区。我的经验是,一个AI系统的效果上限,在数据阶段就基本决定了。模型再强,喂进去的是垃圾,出来的也是垃圾。
数据采集的第一步是明确数据源。你的数据可能来自文档、网页、数据库、API、日志等等。每种来源的处理方式不同。文档类数据要处理格式转换,PDF、Word、Markdown各有各的解析库。网页数据要处理HTML清洗和正文提取。数据库数据要处理字段映射和去重。
清洗环节是最考验耐心的。我通常会把清洗分成几个步骤:去重、去噪、格式化、分块。去重不只是完全相同的文本,还包括近似重复的内容,这时候要用到MinHash或SimHash这类算法。去噪要处理HTML标签、特殊字符、乱码、页眉页脚。格式化要统一编码、统一换行符、统一标点。分块要根据语义边界切分,而不是简单地按字数切。
注意:分块策略直接影响后续的检索效果。按固定字数切分会切断语义,导致检索到的片段不完整。我推荐按段落或标题层级切分,再根据模型上下文长度做二次合并。
3.2 分块策略与元数据设计
分块这件事,看起来简单,实际上有很多讲究。块太大,检索时噪声多,模型处理慢。块太小,语义不完整,检索到的信息碎片化。我的经验值是,对于中文文本,每块控制在300到500字之间比较合适,英文可以到500到800词。但这只是个起点,具体要看你的文档类型和查询模式。
更重要的是元数据设计。每个块除了文本内容,还应该带上来源、标题、层级、时间、作者等信息。这些元数据在检索时可以用于过滤和排序。比如用户问的是某个产品的最新政策,你就可以用时间元数据过滤掉旧版本。用户问的是某个章节的内容,你就可以用层级元数据缩小检索范围。
元数据的设计要遵循“够用就好”的原则。我见过有人给每个块加了二十几个字段,结果维护成本极高,实际用到的没几个。我的建议是,先确定你的检索场景需要哪些过滤条件,再倒推需要哪些元数据。通常来源、标题、时间、类型这四个字段能覆盖大部分需求。
3.3 数据版本管理与可复现性
数据版本管理是很多人忽略的一环。你的模型效果变差了,你怎么知道是模型的问题还是数据的问题?如果没有数据版本管理,你根本无从查起。我的做法是,每次数据管道跑完,都给输出打一个版本号,记录输入数据的哈希、处理参数的配置、输出数据的统计信息。
这个版本号要贯穿整个系统。模型训练时记录用了哪个数据版本,评测时记录用了哪个评测集版本,线上推理时记录用了哪个索引版本。这样当问题出现时,你可以沿着版本号一路回溯,快速定位是哪一环变了。
可复现性还要求你的数据管道是确定性的。同样的输入,跑两次应该得到同样的输出。这意味着你要固定随机种子、固定排序规则、固定分块边界。任何引入随机性的地方,都要显式地控制住。这一点在调试阶段尤其重要,否则你连问题都复现不了。
4. 模型推理服务:从能跑到能扛的关键细节
4.1 推理服务的封装与接口设计
把模型跑起来不难,难的是让它稳定地跑、高效地跑、可维护地跑。我见过太多项目,推理代码和业务代码混在一起,改一个提示词要重新部署整个服务。正确的做法是把推理服务封装成一个独立的模块,对外暴露清晰的接口。
接口设计上,我推荐用HTTP或gRPC。HTTP简单通用,适合大多数场景。gRPC性能好,适合高并发低延迟的场景。接口的输入输出要用结构化的格式,比如JSON,并且要有明确的字段定义和校验规则。不要用裸字符串传参,那样后期维护是灾难。
推理服务内部要做好几件事:请求队列管理、批处理、超时控制、错误重试、限流。请求队列管理是为了应对突发流量,批处理是为了提高GPU利用率,超时控制是为了防止慢请求拖垮整个服务,错误重试是为了应对偶发的网络或硬件问题,限流是为了保护后端不被压垮。这些机制缺一不可。
4.2 批处理与显存管理的参数计算
批处理是提升推理吞吐量的关键手段,但批大小不是越大越好。批太大会导致显存溢出,批太小又浪费算力。怎么定这个值?我的方法是先估算单个请求的显存占用,再根据可用显存反推最大批大小,然后留20%的余量。
单个请求的显存占用主要包括模型权重、KV Cache和中间激活值。模型权重是固定的,比如一个7B参数的模型,用FP16存储大概占14GB。KV Cache跟序列长度和批大小成正比,公式大概是2 * 层数 * 头数 * 头维度 * 序列长度 * 批大小 * 精度字节数。中间激活值跟模型结构有关,通常占比较小。
举个例子,假设你的GPU有40GB显存,模型权重占14GB,系统预留2GB,剩下24GB给KV Cache和激活值。如果序列长度是2048,每请求的KV Cache大概是1GB,那理论上最多能批24个请求。但实际中要考虑碎片和波动,我一般会设成16左右,然后根据监控数据动态调整。
提示:批大小不是固定值,要根据实际负载动态调整。我通常会在服务里加一个自适应逻辑,根据队列长度和显存使用率自动扩缩批大小。
4.3 提示词管理与版本控制
提示词是AI工程里最容易被忽视的“代码”。很多人把提示词硬编码在业务逻辑里,改一次要发一次版。正确的做法是把提示词当成配置来管理,独立存储、独立版本、独立测试。
我的做法是,每个提示词模板都有一个唯一ID和版本号,存储在配置文件或数据库里。业务代码通过ID引用提示词,而不是直接写字符串。这样改提示词不需要改代码,只需要更新配置。同时,每次改提示词都要记录变更原因和测试结果,方便回溯。
提示词的测试也很重要。我会为每个提示词准备一组测试用例,覆盖正常情况、边界情况和异常情况。每次修改提示词,都要跑一遍测试,确保没有回归。这个习惯帮我避免了很多“改了一个词,效果全崩”的事故。
5. 向量检索与RAG:召回质量决定一切
5.1 向量化模型的选择与评估
RAG系统的效果,七成看召回,三成看生成。而召回的质量,又很大程度上取决于向量化模型。选向量化模型不能只看榜单,要看你的数据分布和查询模式。通用榜单上的第一名,在你的垂直领域可能表现平平。
我的做法是,先准备一批真实的查询和对应的正确文档,然后拿几个候选模型分别跑一遍,看召回率、MRR、NDCG这些指标。不要只看一个指标,要综合看。召回率高但排序差的模型,用户体验也不好。另外要注意模型的维度,维度越高存储和计算成本越大,但效果不一定线性提升。
中文场景下,我试过不少模型,有些在通用语料上表现好,但在专业术语上就拉胯。这时候可以考虑用领域数据做微调,或者用混合检索的方式,把关键词检索和向量检索结合起来。混合检索往往比单一向量检索更稳,尤其是在专有名词和数字查询上。
5.2 索引构建与检索参数调优
索引构建不是把向量塞进去就完事了。不同的索引类型有不同的适用场景。扁平索引精度最高但速度慢,适合小数据集。IVF索引速度快但精度有损,适合大数据集。HNSW索引在速度和精度之间平衡得比较好,是我最常用的。
调优的关键参数有几个:nlist控制聚类数量,nprobe控制搜索的聚类数,ef_search控制HNSW的搜索范围。这些参数直接影响召回率和延迟。我的调优方法是,先固定其他参数,单独调一个,画出召回率-延迟曲线,找到拐点。通常nprobe设在nlist的10%到20%之间比较合适。
还有一个容易被忽视的点是重排序。向量检索出来的Top-K结果,可以用一个交叉编码器做重排序,把最相关的排到前面。这一步能显著提升最终效果,代价是增加一点延迟。我的经验是,如果延迟预算允许,重排序几乎总是值得做的。
5.3 RAG流程的组装与上下文管理
RAG的流程看起来简单:检索、拼接、生成。但每一步都有细节。检索阶段要决定检索多少个文档,太多会超出上下文长度,太少可能漏掉关键信息。我的做法是检索一个较大的候选集,比如20个,然后重排序取前5个。
拼接阶段要注意上下文的组织方式。不要把检索到的文档随便堆在一起,要按相关性排序,并且加上来源标注。这样模型在生成时能更好地利用信息,也方便用户溯源。如果文档之间有冲突,要在提示词里说明如何处理冲突。
上下文管理还要考虑长度限制。不同模型的上下文窗口不同,你要根据模型的能力来裁剪。我的做法是,先算好系统提示词、用户查询、检索文档各占多少token,然后动态调整检索文档的数量。如果超了,就按相关性从低到高丢弃。
6. 评测体系:没有评测就没有迭代
6.1 评测集构建与标注规范
评测是AI工程里最容易被跳过的一环,也是最不该跳过的一环。没有评测,你根本不知道你的改动是变好了还是变差了。我见过太多团队凭感觉调参,结果越调越差。
评测集要覆盖真实场景的分布。不要只用简单问题,要包含难例、边界例、对抗例。标注规范要明确,什么算正确、什么算部分正确、什么算错误,都要有清晰的定义。标注人员要经过培训,保证一致性。我通常会做双人标注,然后计算一致性指标,低于阈值就重新标注。
评测集的规模不用很大,几百条高质量的就够用了。关键是质量,不是数量。我见过有人搞了几万条评测数据,结果一半是重复的,一半是标注错误的,这种评测集还不如没有。
6.2 自动化评测与人工评测的结合
自动化评测适合快速迭代,人工评测适合深度分析。我的做法是,日常迭代用自动化评测,每周做一次人工抽检。自动化评测可以用规则匹配、模型打分、指标计算等方式。人工评测则关注那些自动化覆盖不到的地方,比如语气、逻辑、安全性。
自动化评测的一个坑是评测集污染。如果你用同一个评测集反复调参,你的模型会过拟合到这个评测集上。解决办法是准备一个“开发集”用于日常迭代,一个“测试集”只在关键节点用。测试集不能频繁看,看了就失去意义了。
人工评测的另一个价值是发现新问题。自动化评测只能测你想到的东西,人工评测能发现你没想到的。我每次人工评测都会记录新发现的问题类型,然后补充到自动化评测里。这样评测体系会越来越完善。
6.3 线上反馈闭环的搭建
线上反馈是最真实的评测数据。用户的点击、停留、追问、点赞、举报,都是信号。要把这些信号收集起来,定期分析,反哺到评测集和模型迭代里。
我的做法是在应用层埋点,记录每次交互的输入、输出、用户行为。然后定期抽样,人工判断质量,把有问题的样本加入评测集。同时,对于用户明确反馈不好的case,要单独分析原因,是检索问题、生成问题还是提示词问题。
反馈闭环的关键是快。从用户反馈到问题定位到修复上线,周期越短越好。我通常会把反馈处理流程自动化,用户反馈后自动分类、自动通知、自动创建任务。这样能把响应时间从几天缩短到几小时。
7. 可观测性与部署:让系统在真实环境里活下去
7.1 日志、指标与追踪的三位一体
系统上线后,你需要知道它在干什么。日志记录事件,指标记录趋势,追踪记录链路。三者缺一不可。
日志要结构化,用JSON格式,包含时间戳、级别、模块、请求ID、关键参数。不要用print,要用日志库。日志级别要合理,DEBUG用于开发,INFO用于正常事件,WARN用于异常但可恢复,ERROR用于需要关注的错误。
指标要覆盖几个维度:延迟、吞吐、错误率、资源使用率。延迟要看P50、P95、P99,不要只看平均值。吞吐要看QPS和并发数。错误率要分类,是超时、是模型错误还是业务错误。资源使用率要看CPU、内存、GPU、显存。
追踪要能串起一次请求的完整链路。从用户请求进来,到检索、到推理、到返回,每一步的耗时和状态都要记录。这样当延迟变高时,你能快速定位是哪一环慢了。
7.2 灰度发布与回滚机制
AI系统的更新不能一把梭。模型换了、提示词改了、索引重建了,都可能引入回归。灰度发布是控制风险的关键手段。
我的做法是,新版本先放1%的流量,观察指标。如果指标正常,逐步扩大到5%、10%、50%、100%。每一步都要有明确的观察期和回滚条件。回滚条件要提前定好,比如错误率超过阈值、延迟超过阈值、人工抽检发现严重问题。
回滚机制要能快速执行。我通常会把模型、提示词、索引都做成可切换的配置,回滚只需要改配置,不需要重新部署。这样能把回滚时间从几十分钟缩短到几秒钟。
7.3 成本控制与资源优化
AI系统的成本主要来自推理和存储。推理成本跟模型大小、请求量、批处理效率有关。存储成本跟向量维度、数据量、索引类型有关。
控制成本的手段有几个。第一是模型分级,简单请求用小模型,复杂请求用大模型。第二是缓存,相同或相似的请求直接返回缓存结果。第三是批处理,把多个请求合并处理。第四是量化,用INT8或INT4降低显存占用和计算量。
我的经验是,缓存能省掉30%到50%的推理成本,尤其是对于重复查询多的场景。批处理能提升2到5倍的吞吐。量化能降低一半的显存,但可能损失一点效果,要评估后再用。
8. 常见问题与排查技巧实录
8.1 检索效果差的排查路径
检索效果差是最常见的问题。排查时我会按这个顺序走:先看查询本身有没有问题,再看向量化模型适不适合,再看索引参数有没有调好,最后看分块策略合不合理。
查询问题包括拼写错误、语义模糊、超出知识范围。向量化模型问题包括维度不匹配、领域不适应、版本不一致。索引参数问题包括nprobe太小、ef_search太小、索引没重建。分块问题包括块太大、块太小、边界切错。
我遇到过一个典型案例:用户问“最新的退款政策是什么”,检索总是返回旧政策。排查后发现是元数据里没有时间字段,检索时无法按时间过滤。加上时间元数据后,问题解决。这个案例说明,很多检索问题不是模型问题,是数据问题。
8.2 推理延迟高的优化手段
推理延迟高,先定位是哪一段慢。是网络传输慢、是排队慢、是推理慢还是后处理慢。定位方法就是看追踪数据,每一段的耗时都记录下来。
如果是排队慢,说明并发太高,要加机器或限流。如果是推理慢,要看是模型太大还是批处理没做好。如果是后处理慢,要看是不是重排序或格式化耗时太多。
优化手段包括:模型量化、批处理、缓存、异步处理、预计算。我通常会先上缓存和批处理,这两个见效最快。如果还不够,再考虑量化和模型替换。
8.3 效果不稳定的归因方法
效果不稳定,时好时坏,是最难排查的问题。可能的原因有:数据分布变化、模型版本不一致、提示词被改动、缓存污染、并发竞争。
我的排查方法是,先固定所有变量,只改一个,看效果变化。如果固定不了,就加详细的日志,记录每次请求的完整上下文。然后对比好case和坏case的差异,找到共同点。
我遇到过一次效果波动,最后发现是缓存key设计有问题,不同用户的请求命中了同一个缓存。修复key的设计后,问题解决。这个教训是,缓存key要包含所有影响输出的变量,不能偷懒。
| 问题类型 | 典型表现 | 排查方向 | 解决手段 |
|---|---|---|---|
| 检索差 | 召回不相关 | 查询、模型、索引、分块 | 换模型、调参数、改分块 |
| 延迟高 | 响应慢 | 网络、排队、推理、后处理 | 缓存、批处理、量化 |
| 效果波动 | 时好时坏 | 数据、版本、提示词、缓存 | 固定变量、加日志、对比分析 |
| 成本高 | 账单超预算 | 模型、请求量、存储 | 分级、缓存、量化 |
8.4 我踩过的几个典型坑
第一个坑是依赖版本冲突。早期我没用容器,本地装了一堆包,结果线上部署时各种版本不兼容。后来全部容器化,问题解决。教训是,环境一致性要从第一天就抓。
第二个坑是评测集泄露。我用同一个评测集调了两周参数,效果看起来很好,上线后一塌糊涂。后来才知道模型过拟合到评测集了。教训是,评测集要分开发集和测试集,测试集不能频繁看。
第三个坑是提示词硬编码。提示词写在代码里,改一次要发一次版,效率极低。后来把提示词抽出来做成配置,改提示词不用发版。教训是,提示词是配置,不是代码。
第四个坑是没有监控。系统上线后没有监控,出了问题只能等用户反馈。后来加了日志、指标、追踪,问题能在几分钟内发现。教训是,可观测性不是可选项,是必选项。
9. 迭代与扩展:从能用到好用的路
系统上线只是开始,不是结束。真正的挑战在于持续迭代。迭代的方向来自三个方面:用户反馈、评测结果、线上监控。用户反馈告诉你哪里不好用,评测结果告诉你哪里不够准,线上监控告诉你哪里不够稳。
迭代的节奏要控制好。太慢,问题积累;太快,风险失控。我的做法是,小改动随时发,大改动走灰度。每次改动都要有明确的预期和验证方法。改完要看数据,不要凭感觉。
扩展的方向有几个。一是支持更多数据源,二是支持更多模型,三是支持更复杂的查询,四是支持多模态。每扩展一个方向,都要重新评估架构是否撑得住。我见过太多系统,一开始设计得太窄,扩展时只能推倒重来。
最后分享一个小技巧:定期做“故障演练”。故意关掉一个服务、故意注入延迟、故意让模型返回错误,看系统能不能扛住。这种演练能暴露很多平时发现不了的问题。我每次演练都能找到几个隐患,修完之后系统稳定性明显提升。
这个内容后续还可以这样扩展:把评测体系做成自动化的CI流程,每次提交代码自动跑评测;把提示词管理做成可视化的平台,让非技术人员也能调;把可观测性做成统一的dashboard,一屏看全所有指标。这些都是我下一步打算做的事,等做完再跟大家复盘。