从零搭建AI工程化项目:RAG问答系统全流程实战 这几年关于AI的讨论很多但我发现一个很有意思的现象很多人会用现成的API、会跑通开源模型的Demo可真要让自己从零搭建一个完整的AI应用却总会卡在某个地方——要么数据不知道怎么处理要么模型调好了不知道怎么打包上线要么部署完一跑就崩、完全没有排查思路。这个“从零到能落地”的跨越其实就是AI Engineering的核心。它不是说把模型训练出来就完了而是涵盖数据处理、模型调用、服务封装、部署运维、效果评测一整条链路。这个项目标题“ai-engineering-from-scratch”简单翻译就是“从零开始搞AI工程化”。它适合的不只是算法工程师反而更适合那些已经会调用AI能力、但没系统走过工程化全流程的开发者。哪怕你只是做后端的、做产品的、做数据分析的只要你想把一个AI想法变成一个能稳定跑起来、能给别人用的服务这篇内容就能给你一条比较完整的路线。我会把这几年在实际项目里趟过的路、踩过的坑、总结出的流程全部拆开来讲尽量让每个环节都能“照着做”。1. 为什么说要亲手搭建一个AI项目才算入门1.1 AI应用与AI工程化之间的差距先区分两个概念AI应用和AI工程化很多人其实把它们混在一起了。写一个Python脚本调用大模型API生成一段文案这是一个AI应用Demo但如果这个应用要面向多个用户、要稳定运行、要能处理各种异常输入、要在模型升级后还能保持效果这就变成工程问题了。举个简单的例子。开发一个基于本地知识库的问答系统Demo阶段只需要读文件、调接口、返回答案。可一旦要真正投入使用问题就全冒出来了文档格式五花八门怎么统一解析用户问的是同一个问题但表述不同怎么处理模型返回的答案格式不稳定怎么校验并发上来之后延迟蹭蹭涨怎么优化这些问题没有一个是“多做几个Demo”能解决的必须在工程架构层面提前设计。我见过太多团队Demo跑得飞起、一上线就拉胯原因恰恰是没把工程化当回事。1.2 一个最小但完整的学习闭环应该包含什么从零开始学AI工程化我不建议一上来就啃庞大的框架或追最新的模型。比较稳妥的方式是先跑通一个“最小闭环”在这个闭环里把核心链路全部走一遍。什么是最小闭环以我经常用的例子来说就是从一份PDF文档开始做到一个能通过网页或API访问的问答机器人。这个闭环天然包含了几大工程模块数据层文档解析、清洗、切片。索引层向量化、索引构建、存储。逻辑层检索、排序、Prompt组装、模型调用。服务层API封装、配置管理、日志监控。部署层容器化、环境隔离、健康检查。别看这个链路小五脏俱全。跑通一遍之后你对AI工程化的理解会从“感觉会了”变成“真的会了”。而且这个闭环后续扩展性很强把文档换成数据库就是Text-to-SQL把单轮问答换成多轮工具调用就是Agent雏形。所以我的核心建议是不要贪多先选择一个能端到端跑通的小项目把它做扎实。2. 选型面向工程化的技术栈怎么定2.1 开发语言与框架选择的底层逻辑很多文章一说技术栈就直接给出某个框架但我更想先聊底层逻辑。AI工程化项目里语言和框架的选择最核心的考量不是“哪个新”、哪个热度高而是三点生态成熟度、团队熟悉度、运维成本。Python在AI工程化领域依然是首选这个没什么争议。它的大模型SDK、数据处理库、向量数据库客户端、部署工具链都是最全的出了问题搜解决方案也最容易。但Python不是万能的如果你的场景对推理延迟极度敏感比如实时的边缘计算场景那可能得考虑用Go或Rust写部分高性能服务再用Python做上层调度。一般起步阶段不用纠结这些先用Python把链路跑通性能瓶颈在哪里之后再针对性优化。框架层面我建议从LangChain或LlamaIndex这类主流框架入手但一定不要“无脑用”。我的做法是先用框架搭脚手架快速跑通然后逐步替换掉封装过深的部分改成自己可控的实现。比如LangChain的文档加载器很好用但如果你只需要处理特定格式自己写五六十行解析代码更可控。工程化的核心诉求是可维护、可调试框架能帮你加速但不能替你思考。2.2 模型、存储、编排组件的取舍建议模型选择看起来只是个“选哪个”的问题实际上它会影响整个架构。如果做通用对话直接用云端大模型API就好省心、效果好如果做垂直领域问答特别是数据敏感的场景可能得考虑开源模型本地部署。我的经验是不要一上来就追求私有化部署先评估领域数据的敏感程度。很多业务场景其实用API完全够用把精力放在RAG链路的优化上效果提升比换模型更明显。存储组件是大头。向量数据库选择很多Milvus、Qdrant、Chroma、pgvector各有侧重。我的建议很简单个人学习阶段用Chroma或Qdrant安装简单、起步快到了需要高并发生产环境再评估Milvus或pgvector。后端业务已有PostgreSQL的pgvector能省一套基础设施数据规模特别大的Milvus更合适。这里有个容易踩的坑把向量数据库当成万能的什么都往里面塞。实际上向量检索只是召回手段最终效果还要靠后面的重排和模型生成。2.3 环境准备与依赖安装环境这块我踩过不少坑简单分享一套比较稳的流程。第一步创建虚拟环境用Python 3.10或3.11版本别用最新版本有些依赖还没适配第二步确定核心依赖版本建议锁版本而不是用latest我一般在requirements.txt里直接写明版本号第三步配置环境变量把API Key、数据库连接串等敏感信息和代码分离。以我常用的技术栈为例一个最小环境的依赖需求大概是这样python 3.10 fastapi0.104.1 uvicorn0.24.0 openai1.3.0 sentence-transformers2.2.2 qdrant-client1.7.0 pypdf3.17.0 python-dotenv1.0.0安装完依赖之后先写一个最小的脚本验证基础组件是否都能正常调用不要直接写业务逻辑。先确认Embedding模型能跑通、向量数据库能连接、API Key有效这三件事确认了后面开发才会顺畅。给自己定一个原则每引入一个新组件先花十分钟验一个最小用例这比最后统一排错节省的时间多得多。3. 从零实现一个检索增强问答系统3.1 数据准备与切片策略整个链路里数据准备和切片策略是最“笨”但也最关键的环节。很多人做RAG效果差第一反应是模型不行实际上八成是数据没处理好。先看文档解析PDF要分扫描版和文字版扫描版必须走OCRWord、PPT、HTML各有不同的解析库。我的习惯是统一先转成纯文本再做结构化处理这样后续切片逻辑只需要面对一种格式。切片策略直接决定检索质量这块没有银弹但有几个经验参数可以参考。第一个是切片大小做过多次对比实验后我一般会控制在400到600个字符之间这个大小既能保留足够上下文又不至于因为语义混杂导致检索不精准。第二个是重叠长度相邻切片之间重叠50到100个字符避免把一个完整语义切到两个切片里导致漏检。第三个是切片逻辑尽量按章节、段落这样的“语义边界”来切而不是硬按字数切。比如你处理的是技术文档每个API说明是一个完整单元硬切就会把“请求参数”和“返回结果”分开检索效果自然差。切片做完之后还有一个关键动作清洗。我处理过很多真实文档里面充斥着页眉页脚、重复标题、无关广告、特殊字符。这些噪声如果不清理检索阶段会召回大量无效内容。清洗规则根据业务来定但有几个通用操作可以参考去重、去特殊符号、统一换行、过滤超短文本。清洗之后再统计一下切片数量和平均长度做到心里有数。3.2 向量化与索引构建向量化就是把文本变成一串数字向量让机器能计算语义相似度。这一步有两个选择用云端的Embedding API或者用本地的开源Embedding模型。我的建议是起步阶段直接用开源的sentence-transformers系列模型比如BAAI/bge-small-zh-v1.5或moka-ai/m3e-small原因是免费、离线可用、中文效果也不错。如果你追求极致效果再考虑云端API或者更大参数的模型。向量化之后要建立索引这个环节有个容易忽视的点向量维度的一致性。你用什么模型生成向量索引就必须匹配对应的维度换模型之后要么重建索引要么做向量映射否则查询会直接报错。我先列一下索引构建的核心代码思路from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct client QdrantClient(hostlocalhost, port6333) # 建立collection维度需与embedding模型输出一致 client.recreate_collection( collection_nameknowledge_base, vectors_configVectorParams(size768, distanceDistance.COSINE), ) # 构造点数据payload里带上原始文本和元信息 points [ PointStruct( idi, vectorembedding_vector, payload{text: chunk_text, source: doc_name} ) for i, (chunk_text, embedding_vector) in enumerate(zip(chunks, embeddings)) ] client.upsert(collection_nameknowledge_base, pointspoints)这里有三个细节值得展开。第一ID生成策略要稳定建议用内容哈希而不是自增ID这样重复写入不会产生重复数据第二payload字段不要塞太多东西只放文本和必要的元信息否则后期更新很麻烦第三相似度度量方式中文场景我一般用余弦距离Cosine欧氏距离在向量归一化之后和余弦效果接近但可解释性差一点。3.3 检索逻辑与Prompt组装检索不做花哨的处理就是查询向量化之后在向量库里做相似度搜索返回Top K个相关切片。但这里面有几个工程经验可以分享。第一个是Top K的选择我的经验值一般是4到8个太多会让Prompt超长或引入噪声太少又可能漏掉关键上下文。第二个是相似度阈值的设置低于阈值的检索结果宁可丢弃也不要硬塞给模型否则会明显“胡说八道”。第三个是重排如果预算允许在前排结果里再用一个rerank模型做精排效果提升非常明显。Prompt组装是整个RAG链路里最值得花时间调优的环节。核心就是把检索到的切片作为上下文加上用户的原始问题组合成一个结构化的Prompt。我的参考格式是这样的system_prompt 你是一个严谨的问答助手请基于给定的资料回答问题。如果资料中没有相关信息请直接说你不知道。不要编造答案。 context \n\n.join([f【资料{i1}】{doc} for i, doc in enumerate(retrieved_docs)]) user_prompt f请基于以下资料回答问题 {context} 用户问题{user_question} 请输出清晰、准确的回答。如果资料无法支撑答案请明确回复“资料不足”。这个组装看起来简单但有几个坑。检索到的资料顺序会影响答案质量相关度最高的应该排在前面Prompt里必须明确“资料不足时怎么处理”的规则否则模型会强行“脑补”上下文总长度要控制在模型输入上限之内切片数量多的时候尤其要注意文本太长就算模型能处理性能也会大幅下降。实测下来把Prompt结构写清晰之后回答的稳定性和可接受度会有质的提升。3.4 服务封装与接口暴露链路跑通之后不能只停在脚本阶段得把它封装成一个服务。我用FastAPI比较多原因无他轻量、异步支持好、自动生成API文档对工程化特别友好。服务封装的核心是把业务逻辑和接口层分离不要让路由函数里塞满RAG逻辑而是把前面的数据加载、向量检索、Prompt组装、模型调用封装成独立的类或函数。一个最小可用的接口层设计如下from fastapi import FastAPI, HTTPException from pydantic import BaseModel app FastAPI() class QueryRequest(BaseModel): question: str top_k: int 4 class QueryResponse(BaseModel): answer: str sources: list[str] app.post(/api/ask, response_modelQueryResponse) def ask(request: QueryRequest): try: answer, sources rag_pipeline.run(request.question, request.top_k) return QueryResponse(answeranswer, sourcessources) except Exception as e: # 记录完整traceback而不是只返回错误信息 logger.exception(query failed: %s, request.question) raise HTTPException(status_code500, detailinternal error)接口设计有一个值得强调的点不要直接把错误堆栈抛给前端。一方面这是安全隐患另一方面对用户毫无帮助。返回值要结构化answer是正常结果sources是引用来源。很多团队不重视source的返回我反而觉得这恰恰是工程化项目该有的东西因为可溯源才能可评估可评估才能持续优化。接口层还要考虑输入校验。空字符串、超长文本、特殊字符这些都要在入口处理掉不要等到调模型才发现问题。一个简单的Pydantic模型加上正则校验就能挡住大部分无效请求这些细节看似小事但生产环境的稳定性就是这么一点一滴堆出来的。4. 工程化落地的关键环节4.1 配置管理与日志体系脚本阶段的配置可以写死在代码里工程化阶段必须把配置、代码、凭证分开管理。我的做法很简单用.env文件存敏感信息用config.py读取并管理配置配置项分类清晰比如模型相关、数据库相关、服务相关分别归类。不要小看这个动作配好之后换环境只需要改.env代码一行不用动。日志体系则是那种“平时想不到、出事才后悔”的部分。我刚开始做项目时日志东一榔头西一棒子出了问题根本无从查起。后来总结了一套固定的日志规范请求进来打一条INFO参数是什么、耗时多久RAG检索完成打一条INFO召回了几个切片、耗时多久模型调用完成打一条INFOPrompt是多少字符、生成用了多少token任何异常打一条ERROR带完整堆栈。这套日志看着简单真实排查问题的时候价值巨大。还有一个容易被忽略的点生产环境与开发环境的日志级别要分开。开发环境可以调试级别、输出详细信息生产环境一般INFO就够了避免日志量过大影响性能。日志别打到控制台就完事要落盘或采集到集中日志系统否则容器一重启问题根因就跟着丢了。4.2 性能优化与缓存AI应用性能瓶颈在哪答案非常明确模型调用。Embedding模型和LLM的生成耗时占了整个请求链路的大头。针对这块最经济且有效的优化手段就是缓存。缓存分两层第一层是向量检索结果缓存完全相同的query在短时间内直接返回之前的结果用Redis或内存缓存都行第二层是Embedding结果缓存相同文本的向量不重复计算。我实际项目中遇到过一个场景用户高频反复查询同一个问题加上缓存之后整体QPS直接翻倍响应延迟从2秒降到300毫秒效果非常明显。要注意的坑是缓存必须带过期时间和大小限制否则缓存数据膨胀会引发内存问题。另外一个性能优化点是向量检索本身。数据量小的时候无所谓数据量大了之后就需要关注。比如Qdrant可以开启hnsw_ef参数来调整检索精度和速度的平衡追求速度就调小追求效果就调大。后续数据量真到百万级还要考虑分区sharding和过滤索引但初期项目没必要过度设计做到够用且规范就行。4.3 Docker部署与健康检查最后一步是部署我用Docker打包整套服务保证开发环境和生产环境一致。Dockerfile不宜太复杂核心就三件事拉一个Python基础镜像、拷贝依赖和代码、启动命令配置好。但有几个细节值得注意一是依赖安装要利用Docker缓存层先拷贝requirements.txt再拷贝代码这样依赖没变化时构建不会重新安装二是镜像里不要包含.env等敏感文件通过环境变量或挂载方式注入三是启动命令不要用--reload那是开发模式生产环境需要的是稳定。部署之后必须做健康检查。FastAPI加一个/health接口返回服务状态和关键依赖的连通性比如向量数据库和模型API是否正常。容器编排配置里配上探针这样服务不可用时会自动重启或摘流量。我遇到过一种常见故障向量数据库挂了但服务还在运行请求进来全部超时。有了健康检查这个问题就能第一时间暴露。健康检查代码很简单但作用很大app.get(/health) def health_check(): # 检查向量库连接和模型可用性 qdrant_ok client.check_connection() model_ok embedding_model is not None return {status: ok if qdrant_ok and model_ok else degraded}这里的关键是把“健康”的定义做清楚。不能只检查进程在就跑通要检查依赖组件的可用性不能只检查API接口通要检查核心链路是否可用。工程化项目的成熟度体现在它对自己“不健康”状态的感知能力。5. 常见问题与排查技巧实录5.1 文档加载了但检索效果差这是RAG项目里最典型的问题。我排查过很多次九成原因都在数据切片上。切片太大导致一个切片包含多个主题切得太小导致语义不完整按固定字数硬切把完整的逻辑切断了。我的排查顺序是先查看召回结果和用户查询的语义相关度再检查对应切片内容是否完整、是否有噪声最后再回头调整切片策略。另外一个容易被忽视的原因是索引没有同步更新。文档修改后如果只更新了文本存储、没有重新生成向量检索用的还是旧向量那自然查不准。遇到检索结果和文档内容对不上的情况优先检查索引和源数据是否一致。5.2 模型输出不稳定、偶尔漏内容这个问题在工程化项目里非常常见。同一个问题几次结果不一样有时候答案完整有时候丢三落四。根源一般在Prompt设计上。你给模型的约束不够明确模型就会“自由发挥”。我的经验是把输出要求写到很具体比如“必须逐条回答”、“每个问题都要给出结论和依据”、“资料不足时明确说明原因”。一次不行就多轮迭代实测下来Prompt的三五轮调整往往比换模型更有效。在代码层面也要做校验和兜底。模型返回结果之后要做后处理检查是否为空、是否包含预期的结构。如果模型返回了非预期格式可以在服务端做一次修复或重试。这里的思路是模型输出不可能100%稳定工程化要做的是在接口层兜住这些不稳定因素。5.3 容器启动慢和资源占用高容器启动慢通常有两个原因一个是启动时加载模型权重中文Embedding模型几十MB到几百MB从磁盘加载要时间另一个是启动时执行了很多初始化逻辑。针对第一个我建议给模型建立独立的持久化挂载首次下载后就不需要反复下载了针对第二个把数据预加载和模型预加载放到后台任务接口可以先响应健康检查处理完再对外提供服务。资源占用高的问题一般在Embedding模型和向量数据库身上。内存不够时优先考虑换更小的Embedding模型效果降不了多少但内存省一大截。向量数据库的容量要提前规划数据增长过快时及时加索引优化或扩展节点。5.4 排查工具与问题速查表这里给一份我实际排查问题时用的速查表希望能帮你少走些弯路问题现象大概率原因排查手段解决方案检索结果乱七八糟切片策略不合理打印召回切片详情按语义边界重新切片调小切片长度回答凭空编造上下文缺失或阈值太低检查相似度分数提高阈值增加相关资料接口响应很慢模型调用耗时长查看链路耗时日志加缓存换更快的模型调低Top K服务突然不可用依赖组件挂了看健康检查状态检查向量库和模型服务加自动重启结果不一致Prompt约束不足对比多次调用输出强化Prompt约束增加后处理校验内存持续上涨缓存或向量数据膨胀监控内存曲线增加缓存过期策略限制缓存数量这六条基本上覆盖了RAG项目从开发到上线后的主要问题类型你可以把它先收藏下来等真的遇到问题再对照排查。6. 从RAG到Agent的扩展经验当RAG链路稳定运行之后自然就会想往上加能力。我比较推荐的下一步是把单轮问答扩展成Agent式的多轮交互。具体来说就是让模型不只是“回答问题”而是“根据用户需求调用工具完成任务”。这里有个工程化的思考方式把每一种请求都抽象成工具调用参数校验、权限控制、日志审计一套体系都复用。从RAG到Agent的演进过程中最困难的地方不是技术实现而是稳定性控制。单轮问答的失败模式很简单Agent的失败模式就复杂多了模型可能调错工具、参数传错、陷入循环。如果你在RAG阶段没有把日志和评测体系打好Agent阶段会非常痛苦。所以我在RAG阶段最后一步一定会建议搭一个简单的评测集准备三五十条典型问题每次调整之后跑一遍回归测试确保没有改坏已有能力。这一点越早做越受益。6.2 评测体系搭建心得如果让我给AI工程化项目排优先级评测体系绝对排前三。没有评测你所有的优化都在“凭感觉”。搭建评测体系的最简方案是准备一组标准问题和期望回答要点跑一遍系统人工或自动比对回答是否覆盖了期望要点。简单但有效。进阶一点做法是把评测指标量化检索召回率、答案相关性、响应延迟、Token消耗等都可以做成报表。有了这些数据之后每次改动都能量化对比而不是靠拍脑袋。特别是模型换版本、Prompt调整之后有没有回退一测便知。这个过程是AI工程化和纯算法Demo之间最大的分水岭。6.3 我的个人感受与建议做“ai-engineering-from-scratch”这条路我最有体会的一点是真正难的不是某个算法或框架而是把一堆组件拼起来还要稳定运转的那种“系统工程感”。这种能力没有捷径靠的就是把一个一个项目从头到尾地做完整。每一次踩坑、排查、修复都会内化成你的工程直觉。最后分享一个我一直在坚持的小习惯每次项目结束我都会花一点时间写一段项目复盘记录哪些组件选对了、哪些环节走了弯路、哪些坑下次要提前规避。技术更新很快但工程化的底层思考方式不会过时。希望这篇内容能在你从零搭建AI工程化项目的路上帮你少踩几个我没能绕开的坑。