ARTICLE DETAIL

资讯详情

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

AI工程化从零开始:RAG与大模型落地的完整实践指南

AI工程化从零开始:RAG与大模型落地的完整实践指南 1. 从零开始做AI工程到底在做一件什么事这两年“AI工程化”这个词被反复提起但真要说清楚它是什么很多人的理解还是停留在“调接口、套Prompt”。我这个标题起的是ai-engineering-from-scratch翻译过来就是“从零开始的AI工程”说白了就是——不动手动写算法不用复现论文而是站在大模型这个巨人肩膀上用工程手段把AI能力真正落到业务系统里。那为什么“从零开始”值得单独拉出来讲因为大部分开发者和技术管理者碰到的真实痛点并不在于“模型效果不够好”而在于“模型效果有了怎么把它变成一个稳定、可维护、能对外提供服务的系统”。这里面的技术栈、设计模式和踩坑点跟传统后端开发有很大差别网上的资料又特别零散很多人第一阶段就卡住了——不是不会写Python而是不知道该从哪里下手把模型能力“工程化”起来。这篇内容适合谁三类人传统后端/全栈开发者想用大模型能力给现有系统加智能化功能算法工程师需要把实验脚本改造成对外API交给其他团队使用技术负责人想了解AI工程化这条线到底要配什么技术栈、埋什么坑。我写这篇文章的思路就是模拟一次真实的从零落地从技术选型开始到目录搭建再到跑通一个带检索增强RAG的问答API最后把生产环境里最容易出问题的地方列一遍。整个过程我没有用任何云平台也不需要花一分钱全部在本机完成。2. AI工程的核心技术栈与整体设计拆解2.1 AI工程和传统后端开发的本质区别传统后端开发的核心是“确定性”——你写一个函数传入参数A永远得到结果B。但AI应用最大的特征恰恰是不确定性同样的Prompt模型可能给出不同回答同一个问题在不同上下文下质量差异极大。这个差异带来了一系列连锁反应。首先是代码架构的转变——传统业务逻辑是线性调用AI应用则要围绕“模型调用”做大量的上下文组装、结果校验、异常兜底。其次是测试方式——传统后端断言输出等于期望值AI应用没法这样测你需要建立一套评估机制来衡量回答质量。第三是运维监控——传统接口观察QPS、延迟就够了AI应用还得追踪Token消耗、模型幻觉频率、上下文漂移这些指标的组合变化。我见过很多人拿着传统三层架构硬套AI应用结果把Prompt胡乱塞进Service层后来每次改提示词都要重新发版追得运维苦不堪言。AI工程的合理设计至少要把“模型交互层”和“业务逻辑层”彻底分离让Prompt模板、知识库检索、模型参数配置都变成可独立调整的配置项。2.2 技术选型本地模型先跑通再考虑API化我刚做这个项目时的第一决策就是选型模型加载方案。市面上主流的选择其实就三条路方案优点缺点适用场景云端APIOpenAI等部署简单效果稳定数据出域、按量付费、有网络依赖快速验证、非敏感数据本地模型Ollama等数据不出域、无网络依赖、可控性强受机器性能限制、效果弱于主流商用开发调试、私有化部署混合模式灵活、可切换架构复杂度更高生产环境过度阶段我的建议非常明确先从本地模型起步。原因有三个开发期需要高频迭代Prompt和上下文逻辑如果用云端API每改一次配置就要等网络请求Token费用也在无形消耗调试期经常需要反复换模型、改参数本地模型可以毫秒级切换云端API的版本兼容问题会在这个阶段浪费大量时间现在 Ollama、llama.cpp 这些工具已经做得相当成熟一条命令就能启动支持OpenAI格式的本地API服务开发体验和云端几乎一样。我本机的配置是MacBook Pro M1 Pro16GB内存跑7B参数量化模型能稳定在每秒15-20 token的生成速度做开发调试完全够用。如果你机器只有8GB内存跑3B-7B的量化模型也没问题生成速度会慢一些但足够验证程序逻辑。切入到工程视角还有一个关键点模型加载方式必须做成可配置的。我建议配置项里写MODEL_BACKENDollama或者MODEL_BACKENDopenai程序内部统一走 OpenAI SDK 的接口格式这样将来换模型服务商时只需改配置、不用动业务代码。Ollama 本身就提供了 OpenAI 兼容端点默认http://localhost:11434/v1这个迁移成本几乎为零相当于你从一开始就在为将来保留退路。2.3 向量数据库与检索增强的整体设计做AI应用绕不开RAG检索增强生成。这个名称听着学术落地到工程上的理解其实很直接用户提问时先从知识库/资料库里找出与问题最相关的几段文本把这几个片段连同用户问题一起组装成上下文交给模型生成回答。RAG解决的是大模型“不知道你专属业务信息”的困境。大模型的训练数据是公开互联网上采集的它不了解你公司的内部流程、你产品的使用方法、你历史订单的细节。而RAG本质上是给模型一个“参考资料的抽屉”让它根据问题现场把相关的几页纸抽出来读一遍再回答。我对RAG在2025年的一个最新认知是——它已经从“锦上添花”变成了“AI工程的骨架”。纯粹靠模型本身的知识去回答问题在产品上是走不通的用户问一个私域问题就露馅RAG是让AI应用具备“内部知识主权”的最低成本方案也是当前性价比最高的方案它的整体架构可以拆成三个环节文档入库阶段把PDF、Word、Markdown、网页抓取数据切块向量化后存入向量数据库查询召回阶段用户提问时把问题向量化在库里找语义最接近的Top-K个片段生成组装阶段把召回的片段压进Prompt模板连同问题一起送给模型生成答案。这里的核心难点在于切块策略和召回质量。切块太大会把无关信息混进上下文太小则可能切断完整语义这两个问题直接决定回答质量。我通常从800-1000个字符的块大小起步重叠控制在块大小的15%左右再根据实际检索效果微调。向量数据库选型方面我的建议是开发阶段用 Chroma——它是纯本地的一个命令就能跑起来Python接口直接操作如果将来数据量到了百万级向量再切换 Milvus 或 Qdrant 也不迟因为通过统一的向量存储接口比如LangChain的VectorStore抽象来做这件事成本并不高。在这个阶段没必要过度设计跑通了链路比什么都有说服力。2.4 项目目录结构设计好架构不是写出来的是从第一天就把骨架立对了。下面这个目录结构是我实操验证过、且会持续沿用下去的模板每一步都有它存在的理由ai-engineering-from-scratch/ ├── app/ # 核心应用代码 │ ├── main.py # FastAPI入口 │ ├── config.py # 全局配置模型、向量库、Prompt路径 │ ├── api/ # API路由层 │ │ └── chat.py # 问答接口 │ ├── core/ # 核心逻辑层 │ │ ├── llm.py # 模型调用封装支持本地/云端切换 │ │ ├── chains.py # LangChain链式编排 │ │ └── retriever.py # 检索模块 │ ├── prompts/ # Prompt模板独立管理 │ │ └── chat.txt │ └── schemas/ # Pydantic入参出参模型 ├── data/ │ ├── documents/ # 原始文档 │ └── vector_store/ # Chroma本地向量数据 ├── scripts/ │ ├── ingest.py # 文档入库脚本 │ └── test_chain.py # 链路自测 ├── tests/ # 单元测试 ├── requirements.txt ├── README.md └── .env.example # 环境变量示例这个结构的好处是边界清晰api/只负责HTTP层core/负责核心逻辑Prompt模板作为独立文件放在prompts/下面——这意味着产品同事改了话术直接改文件就行不用动代码。scripts/里放一次性任务脚本比如首次入库清洗这样它们不会污染应用代码。我见过太多人把RAG流程全部揉进一个main.py里前期确实爽但迭代到第三周改Prompt时需要四处找字符串拼接的位置那时候再来重构的代价就大了。3. 核心细节解析与实操要点3.1 文档入库链路怎么实现最稳文档入库是RAG系统的“地基”。地基没打好后面检索和回答的质量都会受影响。入库链路的实操要点我拆成了四步第一步确定文档来源建议把资料统一放到data/documents/目录下。第一步先别追求格式丰富就从Markdown和纯文本开始因为它们的切块最干净。第二步文档加载器LangChain仓库中有对应加载器它能把文档读成统一的Document对象这个对象的page_content存文本、metadata存来源等信息。实际建议加载文档后先把metadata里的source信息补上。这一条在后续回答溯源里极其重要——你至少要能回答“这个回答是模型编的还是来自哪个文档的哪一段”。没有溯源能力的回答在生产环境没人敢信。第三步切块策略选择。这一块是最多新手翻车的地方。我做过一组对照实验直接用固定字符数切块的话如果块大小设800字符很多语义完整的段落会被拦腰切断设1500字符上下文信息冗余容易把无关内容捎带进来模型的注意力被稀释。实操中用RecursiveCharacterTextSplitter来切更稳它的切法是这样的按优先级用分隔符尝试切分先尝试用章节标题这种大分隔符切如果块太大再往下用段落、句号、字符号逐级细切而不是粗暴地按固定长度截。from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.document_loaders import DirectoryLoader # 1. 加载文档 loader DirectoryLoader(data/documents/, glob**/*.md) documents loader.load() # 2. 切块chunk_size1000overlap150 splitter RecursiveCharacterTextSplitter( chunk_size1000, chunk_overlap150, separators[\n\n, \n, 。, , , , ] ) chunks splitter.split_documents(documents) print(f文档切块完成共 {len(chunks)} 个片段)chunk_overlap这个参数很多人不理解我解释一下它是让相邻片段保留一小段重复文本。比如前一块的末尾是“公司成立于2010年”后一块的开头是“总部在上海”如果没有重叠前一块丢失了逗号后半句后一块开头就是无头之尾两块的语义都残了。有了重叠检索时无论命中哪一块都能找到相对完整的语义边界。第四步向量化入库。用Embedding模型把每个切块转成向量数组再把向量写入Chroma。这里有个容易被忽略的性能细节Embedding模型加载后要对几千个块逐条向量化不用循环里每条都调用模型效率太低。from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import Chroma # 用nomic-embed-text做向量化 embeddings OllamaEmbeddings(modelnomic-embed-text) vectorstore Chroma.from_documents( documentschunks, embeddingembeddings, persist_directorydata/vector_store ) print(向量化入库完成)Chroma.from_documents会批量处理整个文档列表而不是逐条处理。完成这一步你的知识库就算建立起来了。3.2 检索与Prompt组装回答质量的“胜负手”向量库里存好了数据接下来要解决“怎么查”和“怎么用查到的内容”这两个问题。检索函数的核心代码很简洁但它在整个链路里的地位极高。我给出的检索函数如下def retrieve_context(query: str, k: int 4) - list: 检索最相关的K个文档片段 docs vectorstore.similarity_search_with_score(query, kk) results [] for doc, score in docs: results.append({ content: doc.page_content, source: doc.metadata.get(source, 未知), score: float(score) }) return results这段代码的similarity_search_with_score返回相似度分数。在实际工程使用中我给每段召回结果都打上分数是用来做“防幻觉”的一道闸门只有召回分数达到阈值的内容才允许拼进Prompt否则就干脆告诉用户“知识库中没有找到相关内容”。我之前遇到过的情况是无论用户问什么系统都会硬从库里捞几段相关性最低的文本拼进上下文模型就基于这些无关内容强行编造答案。这个问题一旦出现靠换Prompt根本解决不了只能在检索端加质量闸门。接下来是Prompt组装这是一个值得单独占位为独立服务的环节def build_prompt(query: str, context_docs: list) - str: 组装Prompt带引用来源 context_text \n\n---\n\n.join( f[来源: {d[source]}]\n{d[content]} for d in context_docs ) return f 你是公司的智能助理。请基于以下参考资料回答问题。 参考资料 {context_text} 用户问题{query} 要求 1. 如果参考资料中没有相关信息直接说“根据我目前掌握的资料无法回答这个问题”。 2. 回答尽量简洁控制在200字以内。 3. 在回答最后用#来源标注引用到的文档名称。 为什么Prompt模板必须单独放文件因为这玩意儿在业务迭代时会高频调整产品听说“改一下Prompt就能提升效果”之后大概率会每天提修改需求。如果Prompt拼接逻辑散落在代码里每次改动都要走代码发布流程效率极低。独立文件化管理之后想怎么调就怎么调还可以做A/B对照。3.3 API服务层把调用链暴露成标准接口前面这些逻辑最终都要暴露成HTTP接口给前端、机器人或其他服务调用。我用FastAPI来实现这个服务层它自带Pydantic类型校验、OpenAPI文档对AI应用这种需要调试的场景极其友好。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class ChatRequest(BaseModel): query: str model: str | None None # 可选覆盖默认模型 temperature: float | None 0.7 # 可选覆盖默认温度 class ChatResponse(BaseModel): answer: str sources: list[dict] model: str app.post(/chat, response_modelChatResponse) async def chat(req: ChatRequest): retrieved retrieve_context(req.query) prompt build_prompt(req.query, retrieved) answer call_llm(prompt) return ChatResponse(answeranswer, sourcesretrieved, modelqwen2.5:7b)这里有个需要强调的细节回答里的“来源引用”清单必须独立给前端展示。用户看到“这个回答是基于哪些文档给出的”信任感会完全不同。不要小看这个设计一个AI应用能不能在一个团队里被持续使用很多时候靠的是这份“透明度”。调用模型的方式建议封装在core/llm.py里用配置项控制后端是走Ollama还是OpenAIfrom langchain_openai import ChatOpenAI def get_llm(): return ChatOpenAI( base_urlos.getenv(LLM_BASE_URL, http://localhost:11434/v1), api_keyos.getenv(LLM_API_KEY, no-key-needed), modelos.getenv(LLM_MODEL, qwen2.5:7b), temperaturefloat(os.getenv(LLM_TEMPERATURE, 0.7)) )这套封装的价值在于开发期用本地Ollama将来业务量上来了或需要更强模型时把.env里的LLM_BASE_URL改成云端的服务地址、填上真实API Key即可业务代码一行都不用动。这种“可迁移性”就是工程化区别于纯脚本玩法的关键差异。4. 实操过程与核心环节实现4.1 环境准备从零把依赖装齐这一节我按步骤走一遍让刚了解AI工程的新手也能不出错地搭起环境。第一步为项目建立独立的虚拟环境。这一步不可跳过——如果不隔离你会被全局Python环境搞疯python3 -m venv .venv source .venv/bin/activate第二步安装依赖。我把requirements.txt整理成了下面这个最小可用集fastapi0.115.6 uvicorn0.34.0 langchain0.3.14 langchain-community0.3.14 langchain-openai0.3.14 langchain-text-splitters0.3.4 chromadb0.5.23 pydantic2.10.4 python-dotenv1.0.1安装命令pip install -r requirements.txt这里我特意锁了版本号。LangChain是迭代极快的库不同小版本的API差异很大网上很多教程的代码跑不通原因就是版本不一致。建议后期逐步放开版本限制。第三步安装并启动Ollama。Ollama官网一键安装然后拉取两个模型ollama pull qwen2.5:7b # 对话生成模型 ollama pull nomic-embed-text # 向量化模型拉取qwen2.5:7b之前建议先ollama list看下有没有旧版本残留。我第一次拉模型时没注意已经下过一个旧版本重复下了一遍占用了好几GB磁盘。第四步验证模型服务正常。启动服务后请求curl http://localhost:11434/api/version能返回类似{version:0.x.x}就说明Ollama已经在后台待机了。首次调用模型时会有一段模型加载时间后面就快了。4.2 小数据集端到端跑通验证完整链路验证我用了一个很小的知识集——我把自己博客里几篇关于“个人知识管理”的文章存成Markdown放进data/documents/然后跑入库脚本python scripts/ingest.py输出文档加载完成共 3 篇文章 切块完成共 42 个片段 向量化入库完成耗时 3.2 秒然后启动API服务uvicorn app.main:app --reload --port 8000用一个请求验证完整的RAG链路curl -s http://localhost:8000/chat \ -H Content-Type: application/json \ -d {query: 如何建立知识管理流程}我实际跑出来的回答摘录已经能看到引用来源说明要建立知识管理流程核心是三步 1. 定期收集分散的信息统一存放在一处 2. 每周做一次整理把信息归类并补充自己的理解 3. 定期回顾把知识变成可执行的行动。 #来源: data/documents/knowledge-management.md这个输出的意义在于从文档入库到检索、组装、生成、溯源整条链路通了。接下来的工作就是在这个骨架上演进迭代。4.3 性能优化首次真正把这些模块调配到位后的感受当这条链路真正跑通之后才开始进入“工程优化”环节。这个体验我想特别说明一下因为很多教程到“跑通”就戛然而止但实际项目中最重要的功力恰恰在后面。第一处优化是启动慢。import chromadb这个库在启动时会做一堆初始化检查第一次启动可能耗掉3-5秒。但FastAPI的--reload模式会在源码变更时自动重启控制台不断刷出“Running on http//localhost:8000”这行字——如果你忍受不了这种等待可以把Chroma的初始化封装成懒加载模式只在首个请求进来时才加载。这也是小细节但正是一个个小细节叠加出工程感的。第二处优化是上下文裁剪。我上面组装Prompt时把k4设成固定值但实际收到的查询中有些问题本身很短比如“您好”有些问题很长很具体。一个k4无法适配所有情况。我的实用做法是先从k3入手根据Query长度动态调整短Query加一个兜底的“系统提问”改写环节长Query则重点保障相关性。def retrieve_with_length_k(query: str) - list: base_k 3 if len(query) 150: # 问题很长时多召回一些上下文 base_k 5 return retrieve_context(query, kbase_k)这种处理原则很像后端开发里“根据请求参数选择默认策略”的思路借过来降低了AI环节特有的不确定感。第三处优化是缓存。如果两个用户在短时间内问完全一样的问题这在企业内部知识问答里很常见重复做一遍向量检索模型生成毫无意义。我用了一层简单的Redis缓存以Query的哈希为Key命中直接返回之前的结果未命中才走完整链路。这个优化能把热点问题响应时间从3-5秒降到毫秒级。上面这三点优化做完这个小项目才算得上“工程化”而不是“能跑的脚本”。它揭示的规律是AI工程里90%的复杂度不在模型参数而在系统集成中那些细节性的性能、成本和可维护性问题。5. 常见问题与排查技巧实录AI工程从零起步最大的问题是报错真的多而且报错信息往往不直观。我把实训阶段最常碰到的问题整理成一张速查表现象可能原因排查思路解决方案ollama连接被拒绝Ollama服务没启动curl http://localhost:11434/api/version启动Ollama服务模型生成中文乱码/吞字量化模型表达能力弱换更大的模型用qwen2.5:7b-instruct替代3b检索结果完全无关Embedding模型用错检查向量库中的Embedding模型是否与查询时一致ollama list查看统一模型回答指代不清、不引用资料上下文组装有问题打印最终Prompt检查调整上下文拼接格式、加“引用格式”要求API启动后 import 报错依赖版本冲突pip list查看版本对着requirements.txt锁版本首次调用模型很慢模型从磁盘加载到内存等待几秒首次请求前可预热调用一次空对话Token消耗过快云端API没有控制上下文长度打印Prompt字节数给上下文设置最大长度截断这些坑看起来零散背后其实是一个共性问题**链路里的任意一环出错最终表现都会“翻译”成回答质量问题或API异常而不是直接告诉你哪里出了问题。**所以排查时的起点总是链条任一端的日志尤其是Prompt组装前后的完整日志。建议在开发期加一个环境开关能打印出每次请求最终送给模型的Prompt以及检索到的片段这是最快定位问题的手段。一个独家技巧我给这个项目加了个“检索联调检查模式”——启动时设置环境变量DEBUG_LANGCHAIN1LangChain会自动输出链路上每一个步骤的执行日志包括召回片段、分数、耗时。上生产时关掉它就行它在开发期几乎能替代所有手动断点。这个功能是LangChain自带的但大量教程都不会特意提它。再补充一个需要特别提醒的点任何AI应用上线前都要确认模型的输出经过了合规审查。如果你做的是面向公众的服务回答内容中不含敏感、违法或风险信息是底线。技术上的做法是在Prompt里明确约束回答范围流程上则是设置人工抽检机制。这里是AI工程最严肃的一环没有商量的余地。6. 项目延伸从demo到产线的三个关键升级如果你把前面的骨架跑通了我想额外展开几点“从demo到产线”的经验因为这三件事是让项目从“能用”跨到“好用、敢用”的分水岭。6.1 数据权限隔离怎么做企业内部AI应用几乎必然面临权限问题销售部的AI不应该能查到财务部的数据。RAG架构天然具备这个能力但需要你在入库时把权限标签写进metadata查询时把用户身份一起传入检索条件vectorstore.similarity_search_with_score( query, kk, filter{permission_group: user_group} # Chroma支持按metadata过滤 )在Chroma这种轻量级向量库里做权限过滤原理是在向量相似度检索之外加一层过滤条件通俗理解就是先按相似度粗筛出一批候选片段再通过filter把没有权限标签的片段剔除。如果数据量大或权限模型复杂就需要换到支持权限感知的更重型解决方案但这个思路是通用的。6.2 模型评估与回归测试怎么做传统应用上线前跑测试用例AI应用同样需要——只是断言方式改为质量评估。我建议在下线新Prompt版本时建立一个包含50-100条问答对的小评估集每条记录包含“问题、参考回答、期望引用来源”。改完Prompt跑一遍如果回答能对应上参考内容的关键点记1分如果引用了错误文档扣1分全部跑完后算平均分跟上一版对比分数掉了就不发布。做法再简单一点可以把评估集直接写道一个Excel里由同事人工打分。这个流程也许不“高级”但它让Prompt优化变得有据可依而不是“感觉效果更好了”。6.3 成本与延迟的持续追踪AI应用的成本跟传统应用不在一个维度上。调用云端API时一次问答消耗的Token费用是动态变化的不监控会失控。我的做法的core/llm.py里记录每次调用的输入Token数、输出Token数和总耗时写进日志{event:llm_call,model:qwen2.5:7b,prompt_tokens:642,completion_tokens:183,total_seconds:4.2}一周汇总后才看得出哪些查询占了70%的Token消耗是不是可以把那些被频繁查询的高频知识块直接缓存或者做成预置回答这套数据显示出来后优化方向自然就清楚了不用靠感觉去做优化。这三件事做下来这个从零搭建的AI工程项目就真正具备了上线的自信。落地的过程其实是一个把不确定性不断收敛为确定性的过程而工程化的全部功夫都在这份确定性的收敛里。7. 踩坑后的经验小结整个从零搭建的过程跑下来我最想留给后来人的经验就三条。第一别迷信“效果”先跑通链路。我见过太多新手花了一周调Prompt试图把回答效果调到完美结果发现检索环节的相似度阈值设置错了Prompt再好也白搭。开局阶段的目标不是效果而是把整条链路每个环节的最小可用版本跑通。第二版本和锁依赖要舍得花时间。如果所有代码都写好了才发现LangChain大版本升级把API改得面目全非重改一遍的代价远大于最初花半小时锁定版本。建议在requirements.txt里锁住版本号网上教程里出现的、实测报错的绝大多数是依赖版本不一致引起的。第三从第一天就把可观测性考虑进去。日志输出、Prompt组装预览、召回片段展示、Token计数——这些在开发阶段就要配上。哪怕丑一点也别等上了生产再补。有多少次我差点放弃一个看起来奇慢无比的RAG系统最后打开日志才发现痛苦的根源是某一步在循环里反复重建向量连接。这套ai-engineering-from-scratch的骨架是完全可以作为公司内部智能化项目的启动模板来用的。上手跑通它之后无论再把架构往哪个方向演进你对AI工程的“手感”已经建立起来了。好的开始是成功的百分之八十这句话放在AI工程里同样成立——只要你选择从正确的起点开始。
返回列表