☰
从零开始学AI工程:手写RAG系统全流程与工程实践指南
2026/9/30 12:26:27 网站建设 项目流程

“ai-engineering-from-scratch”这个项目名,翻译过来就是“从零开始的AI工程”。如果你现在打开招聘软件,会发现越来越多岗位在要求“AI工程化能力”,但真正能把这几个字讲清楚的人其实不多。在我看来,AI工程从来不是“跑通一个demo,再往上部署”那么简单,它是把数据清洗、文档切分、向量化、模型选型、检索召回、上下文组装、效果评估、成本控制这一整条链路的每个环节都摸透,并且能稳定支撑线上流量的一套方法论。

这个项目想做的事,就是把这套东西从头到尾手动搭一遍,而不是直接套用别人封装好的框架。它适合两类人:一是刚入门、打算往AI应用方向走的开发者,你现在缺的不是另一个教程,而是把完整链路串起来的机会;二是已经在用现成服务和工具,但一遇到问题就抓瞎,只能靠重启和换prompt来“硬碰运气”的工程师。跟着这条路线走完,你会得到的不是一个只会“照着资料念”的问答机器人,而是“当它答错时,你知道定位到哪个环节去改”的判断力。

1. 先把“AI工程”这个概念拆对了

1.1 它和传统软件工程的区别在哪

很多人写后端很熟练,一进入AI工程就水土不服,根源在于两种工程的思考方式完全不同。传统软件是确定性系统:同样的输入,代码会给出同样的输出,出了问题可以用断点、日志、单测去精确定位。AI应用是概率性系统:同样的 prompt,模型今天和明天答得可能不一样;同样的检索条件,向量召回的结果也可能随切分方式、Embedding模型版本而变化。

这种不确定性带来两个连锁反应。第一,你不能用“单元测试覆盖到100%就放心上线”的思路去做AI工程,你必须在系统外面加一层“评估机制”来兜底,持续监控回答质量和效果变化。第二,AI系统的故障粒度和传统系统完全不同。传统系统挂掉通常是“接口报500”“数据库连不上”,新系统挂掉往往是“回答很流畅,但内容是错的”“检索出来的资料跟问题完全不沾边”,这些故障靠监控告警很难发现,只能靠人工观察和链路追踪去定位。

所以我一直认为,AI工程的核心不是“会用某个模型”,而是“能不能把不确定性管住”。你做的每一步——切分、召回、重排、拼上下文、约束输出——本质上都是在降低系统的不可控风险。理解了这个底层逻辑,后面学任何工具都会快很多。

1.2 从零开始不是重复造轮子,而是造完轮子再选轮子

2024年到2025年,AI工程领域的工具链变化快得离谱。LangChain、LlamaIndex 这些框架把RAG流程封装成几行代码,但代价是黑盒化,出了问题不好定位。比如你用了 LangChain 的VectorStoreRetriever,问答效果不好时,你能想到的排查手段只有调 chunk_size、换 top_k,再往上就是换模型,再深层的机制你根本不了解。

自己从零手写一遍,最大的价值不是让你以后永远手写,而是让你建立起对每个环节运作机制的“体感”。你在这一步会碰到大量细节:PDF解析时表格提取得乱七八糟怎么办?中文文本用什么Embedding模型效果才好?召回回来的段落要怎么去重和排序?不同业务的文档权限如何映射到检索里?当你亲自动手处理过这些问题,你会知道哪些环节是框架帮你兜了底,哪些环节其实什么东西都可能漏。以后再遇到封装好的工具,你一眼就能判断它帮你做了什么、没做什么、哪些地方还需要你自己加逻辑。

说句实在话,这也是面试官比较认的东西。你会用LangChain,很多人也会用;但你能说清楚“Embedding相似度检索在十万条向量里为什么快”“为什么中文分词对召回率有这么大影响”“Rerank模型到底在解决什么问题”,这才能体现工程深度。

1.3 从零开始必备的知识地图(直接抄这张清单)

这里先给一张知识清单,不用一步到位,但每项都应该花时间去碰一下。

  • Python基础:至少能写类、装饰器、类型注解和异步IO,因为AI工程大量代码是胶水代码,你需要在各个服务之间搬数据。
  • 数据预处理基础:会使用正则做初步清洗,了解PDF、HTML、Markdown、DOCX 的区别,知道哪些格式可以直接解析,哪些格式只能用OCR或版面分析模型。
  • 线性代数基础只需要懂到“向量”、“点积”、“余弦相似度”三个概念即可,后面向量检索原理全靠它们。
  • SQL基础:很多知识库数据都存在关系型数据库里,你需要会查数、做简单的清洗聚合。
  • Embedding与向量检索基础:知道“把文本映射成高维向量,然后用余弦距离衡量语义相似度”这一点,后面用FAISS、Chroma、Qdrant都会顺利很多。
  • 大模型API的使用:建立对“上下文、token、temperature、system prompt”这些基本概念的直觉,至少要亲手跑过两次带流式输出的对话接口。
  • 基础Linux运维:会看CPU、内存、GPU占用,会启动服务、看日志、用Docker,因为本地模型推理总要落到服务器上。

这七项里有任何一项是零基础,不需要焦虑。实际操作中,你完全可以在做一个完整项目的过程中按需学习,用到哪块补哪块。但你要有一个观念:AI工程师首先是工程师,工程能力越扎实,AI应用落地越顺利,反过来只背模型参数和技术名词,一到出问题就无计可施。

2. 零基础第一课:手写一个能跑的RAG系统

2.1 为什么第一个项目选RAG而不是微调

“从零开始AI工程的第一课选什么?”我推荐RAG(检索增强生成),原因很朴素:门槛低、见效快、场景覆盖面广。

很多人一上来就惦记微调,觉得微调才显得“专业”。实际上,微调是给模型“注入新的行为模式”用的,比如让模型模仿某种文风、学会按特定格式输出。如果你只是想让模型回答公司内部的制度问题,正确做法是让模型每次都能拿到相关制度原文,然后基于原文作答。这就叫检索增强生成。就算你真的去微调了模型,到生产环境它没有最新制度文本,同样会答错。

RAG另一个好处是天然能解释。用户问一个问题,系统回答完可以把召回的参考资料一并展示出来,对还是不对,人工一看便知。这在企业应用里非常重要,合规部门、业务人员需要能追溯。试想一下,你微调出一个效果不错的模型,被业务方追问“你凭什么这么答”,你根本拿不出证据。但RAG可以直接展示“我根据这份文档第X页的内容回答”,逻辑就顺了。

还有个现实因素:微调模型需要GPU、需要高质量标注数据、需要做效果对比验证,一套跑下来周期至少两三周。RAG最快当场就能跑通。对于绝大多数想入门AI工程的人来说,RAG做到及格线,已经足以解决公司里大量“文档太多找不到答案”的痛苦。先把RAG做扎实,再考虑要不要微调,路径更健康。

2.2 手写RAG用到的基础组件

一个RAG系统从零开始,最少需要下面五类组件:

  1. 文档加载器:把PDF、Word、网页、数据库记录等原始素材读成纯文本。不同的文件类型有不同的解析策略,PDF要区分文字版和扫描版,扫描版要用OCR。
  2. 文本切分器:把长文档按一定规则拆成一小块一小块。切分粒度过大会导致检索精度低、浪费上下文;切分粒度过小则容易丢失语义的完整性。
  3. Embedding模型:把每一块文本转换成一个向量,让语义相近的文本在向量空间里距离更近。现在主流方案是用开源的BGE系列或闭源的OpenAI Embedding接口。
  4. 向量数据库:存放文本对应的向量,并提供快速的相似度检索。常见工具包括Chroma(上手快)、FAISS(轻量级、性能好)、Qdrant和Milvus(适合生产环境、功能更全)。
  5. 大模型API:负责最后一步生成,把召回的文本块和用户问题合并成上下文,由模型生成自然语言答案。它通常还需要配合一个合适的 prompt 模板来约束输出。

这五样东西组合起来,就构成了一条完整的 RAG 链路,你可以用它们组装几乎所有文档问答类应用。为了满足不同场景的需要,链路中通常还会出现意图识别模块、多路召回、Rerank、缓存等组件,但这些是后面的事,第一版先跑通,再逐步加复杂度。

2.3 核心环节实现:嵌入、入库、召回、生成

下面这段代码是一个最小可用的RAG,我给它做了简化,只保留核心逻辑。我用的是sentence-transformers里的中文Embedding模型和Chroma作为向量库,这两个都是现阶段很主流的选择。

from sentence_transformers import SentenceTransformer import chromadb # 你要离线管理模型文件,不然每次启动都会去联网拉权重 embed_model = SentenceTransformer("BAAI/bge-small-zh-v1.5") # 假设这是从文档解析并切分好的文本块 chunks = [ "公司年假制度:入职满一年后享有5天带薪年假,每增加一年工龄增加一天,上限15天。", "加班申请流程:员工如需加班,必须在OA系统提前一天提交申请,由直属主管审批。", "报销规则:差旅报销需在行程结束后两周内提交票据,逾期不予受理。", ] # 初始化向量库(Chroma是嵌入式,开箱即用) client = chromadb.Client() collection = client.create_collection(name="staff_handbook") # 文本向量化并入库 vectors = embed_model.encode(chunks).tolist() collection.add( ids=[str(i) for i in range(len(chunks))], embeddings=vectors, documents=chunks ) # 用户提问 question = "我工作三年了,年假有几天?" question_vec = embed_model.encode([question]).tolist() # 召回top2相关文本块 hits = collection.query( query_embeddings=question_vec, n_results=2 ) retrieved_docs = hits["documents"][0]

到这一步,你已经把用户问题和知识库里的文本块做了语义匹配,拿到了最相关的内容。接下来把检索结果和问题组装成prompt,交给大模型:

from openai import OpenAI client = OpenAI() # 把原始prompt拼接逻辑拆开看,它本质上就是在做“约束上下文” context = "\n\n---\n\n".join(retrieved_docs) prompt = f"""请基于以下资料回答问题。 要求: 1. 只能根据资料内容回答,不要编造 2. 如果资料中没有相关信息,直接回答“资料中未找到相关内容” 3. 回答时尽量简洁、准确 资料: {context} 问题:{question} """ resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], temperature=0 # 正式场景建议设为0,减少随机性 ) print(resp.choices[0].message.content)

这段代码里有两个关键设置我要单独说一下。第一是temperature=0,很多人忽略这一点。RAG场景下我们希望尽量稳定输出,模型每次复述同样的资料,结论不要飘,所以要把随机性降到最低。如果你做的是创意写作、头脑风暴,temperature 才需要调高。第二是 prompt 里明确写了“资料不足时直接说不知道”,这能有效减少模型强行编造答案的概率。

我还想提醒你一个实操细节:Embedding模型文件不要每次启动时现下载。国内网络环境下载 HuggingFace 权重经常超时,建议先用git clone或官方工具把模型下载到本地,然后在代码里指定本地路径。我第一次做这个项目时没注意,在演示现场等了十几分钟下载权重,非常尴尬。

2.4 从demo走向能用:加路由与重排

上面这段代码能跑,但它离“能用”还有距离。真实用户问的问题五花八门,有闲聊,有递归提问,也有跨文档的复杂问题。这时候你需要给系统加两个关键模块:意图路由和重排。

所谓意图路由,就是先判断用户的输入属于哪一类,再决定走哪条处理链路。比如你收到一句“你好”,走检索链路只会浪费token,直接回复“你好,我是部门制度助手,你可以问我关于年假、报销、考勤等问题”即可。收到“年假有几天”这种具体问题,才进入RAG链路。实现方式很简单,用一个支持function calling的大模型,或者写一套关键词规则,把输入分成“闲聊”、“文档问答”、“需要多轮追问”三个入口。规模大了以后,还可以把不同入口接入不同的Embedding模型和知识库。

重排解决的是另一个问题:向量检索只能保证“大致相关”,不能保证“关键信息排在最前面”。它基于语义相似度打分,但文档中的重要程度、权威性和信息密度并没有被建模。解决这个问题的方法是在召回结果后面再跑一次Rerank模型,比如开源的bge-reranker-base或者商业接口,它对“问题+文档块”做更精细的相关性打分,然后将Top 3的文本块作为最终上下文。

我习惯把RAG链路拆成“召回-粗排-精排-生成”四段。召回阶段用向量检索取回Top 20甚至Top 50;粗排阶段过滤掉明显无关的候选;精排阶段用Rerank模型打高分排序,只取Top 3到Top 5喂给大模型。这套做法的优点是,向量检索的召回率可以放宽到极高,但最终真正进入上下文的每一条文本都是经过验证的,上下文质量高了,回答质量自然高,幻觉也少。生产系统中,你还可以同时加一路BM25关键词检索,和向量检索做混合召回,覆盖更全面。

3. 进阶工程化:评估、缓存与可观测

3.1 先给系统上一套评估基线

从零手写RAG,很多人会在“跑通”之后立刻陷入迷茫:明明demo看着不错,丢到一堆真实文档里就拉胯。这个阶段的根源通常是“没有评估标准”。不知道现在系统答得怎么样,也就不知道该往哪个方向调优。

一个系统性的评估要从离线评估开始。第一步,准备一套Golden Set,也就是一组包含标准答案的测试问题。数量不需要特别多,三十到五十条高质量样本就够第一个版本使用。每一条包含:问题、标准答案、期望检索到的文档块ID。Golden Set通常需要你和业务方一起整理,每个问题都要确认“正确答案是客观事实、有据可查”。第二步,在Golden Set上跑完整链路,记录两组指标。一组是检索指标:召回率、准确率,看相关文档有没有被找到、排到前面;另一组是生成指标:可以参考RAGAS框架的忠实度(答案是否严格基于上下文)、答案相关性(是否直接回应问题)、上下文相关度(检索出来的资料是否切题)。

这里我强烈建议把评估流程代码化,做成一个脚本,每次改动切分策略、Embedding模型、重排策略,都跑一遍回归。没有基线就调整系统,等于闭眼开车。我有一个很深的教训:之前把chunk_size从300改成500,以为只是参数微调,结果上线后用户投诉增加,就是因为没有回归评估,检索精度掉了不少。

在线评估也不能忽略。给每一条回答的下面放“有帮助/没帮助”的反馈按钮,记录用户的点击、复制、点赞行为,配合会话日志,能比较直观地看到系统在真实场景下的表现。离线评估解决“是否改坏”的问题,在线反馈解决“是否真的对用户有用”的问题。

3.2 缓存设计:把重复计算的钱省下来

AI工程有成本控制问题。大模型API按token收费,向量检索虽然便宜但也不是免费,花钱最容易失控的场景就是大量重复查询。现实中,员工问“年假有几天”“报销流程是什么”这类问题比例非常高,命中缓存的收益非常可观。

缓存分为两类。第一类是精确缓存:相同的问题直接走缓存返回结果,key可以用用户ID加上问题文本的哈希值。实现容易,但收益有限,因为用户几乎不会用完全一样的字眼提问。第二类是语义缓存:对用户的问题做向量化,然后在缓存库里找相似度超过阈值的历史问题,命中后直接复用当时的答案。阈值建议设置在0.85到0.93之间,太高容易漏,太低容易错。这一步实现时,缓存向量不放在主向量库,免得和大文档向量混杂影响检索,单独开一个collection就行。

我自己的经验是,上线缓存后,相同的重头问题可以减少40%到60%的模型调用量。再加上批量处理和流式输出,成本下降非常明显。另一个不那么起眼但同样实用的缓存维度是文档解析结果:同一批PDF每个用户访问时都重新解析一遍,纯属浪费,建议把解析后的文本块做成文件或数据库记录,只解析一次,后续直接从缓存读取。

3.3 链路追踪、日志和权限控制

AI工程和传统工程一样,线上系统必须有链路追踪。用户问一个问题,你要知道这个问题经过了哪些环节、召回了哪些文档块、模型收到了什么上下文、最终输出了什么、token消耗了多少、总延迟是多少。没有这些记录,用户反馈答错了,你连复现都复现不了。

你可以用 Langfuse 或 LangSmith 这类专门为LLM应用设计的观测工具,它们会记录prompt、模型响应、检索内容、评分等明细。如果不想引入外部依赖,自己写日志也可以,但一定要把以下几项固化下来:request_id、用户ID、时间戳、问题原文、检索命中的文档ID列表、重排后的分数、使用的模型和参数、最终答案、token用量、延迟。此外还需要一条关键日志:最终进入上下文的文本块。很多问题排查到最后,根源往往是“检索的内容里混进了一条不相关的记录”,有这条日志可以直接看到。

还要强调权限控制。内部知识库不是所有内容对所有人开放。简单的做法是给每个文档块打metadata标签,例如部门、保密等级、可见角色,在检索时用元数据过滤把无权访问的文档块提前剔除。注意:权限过滤必须发生在检索之前,不能在生成阶段靠提示词约束模型“不要透露机密信息”,因为只要你把资料放进上下文,模型就可能读到,你还告诉它不能说,这本身就是一个越权的风险点。权限策略不复杂,但一定要理解“防线要前移”这个原则。

4. 上线前的常见问题与排障实录

4.1 召回率奇低,问题出在哪

“明明文档里有这个内容,但机器人就是答不上来”——这是RAG项目最常见的翻车现场。我把它当作第一个最容易掉进去的坑来讲。

优先检查三件事:切片粒度、Embedding模型、检索参数。

切片粒度如果设置得太大,比如每个chunk有2000字,这段文本的向量会被大量无关内容稀释,用户问一个小众细节,整体相似度就被拉低了。切片太小也不行,只有50字的chunk经常截断语义,表现为“相关文本一直在Top 10之外”。我的通用起点是 chunk_size=400到500,chunk_overlap=80到100,然后结合Golden Set做回归调优。注意中英文的token比例不同,同样400字,中文差不多就是400到600token,英文则可能翻倍。

Embedding模型的匹配性也很关键。中文文档用text-embedding-3-large这类通用模型不一定是最优的,开源社区中BAAI/bge-large-zh-v1.5和BAAI/bge-m3在中文语义匹配上有不错的性价比,如果你的语料专业性强,比如医疗、法律、工程规范,标准模型的效果可能不够,可以用领域文本做无监督的继续预训练来提升效果。

检索参数方面,最容易踩的是top_k设得太小,比如只有1或2,相关文档被挤出去就彻底找不回来。建议先放宽到10到20,让Rerank阶段去决定最终进上下文的内容。此外,很多向量数据库对中文没有做分词,直接按字符或者按整句的Embedding做检索,效果与使用jieba等工具预先分词后的差异不算大,因为Embedding模型本身已经能在一定程度上理解中文短语,这一点不用过度纠结。

4.2 有时召回正确,但回答依然离谱

这是比召回率低更让人崩溃的问题:你检查日志,召回回来的文档块是对的,上下文明明包含答案,模型输出的结论还是错的。

首当其冲要排查上下文冗余。Top 5路线中,真正对回答有用的可能只有两三条,其余几条虽然相似但讲的是别的内容。这些干扰片段会带偏模型。解决办法是把召回范围调大后,用一个强一点的Rerank模型把最相关的两三条挑出来,大幅减少噪声。另外,可以对召回的文本块做预处理,如果多个文本块内容高度重复,只保留信息最完整、发布日期最新的一块。

其次是prompt设计问题。有的prompt模板只是把资料拼在问题前面,没有明确告诉模型“严格基于资料回答”。模型在资料不足时,会倾向于用自己预训练的知识补全。要让模型在资料不足时敢于说“不知道”,这需要明确写进prompt,甚至可以加“如果资料中未找到答案,请告知用户补充资料后重试”这样的兜底话术。

最后一条是输出约束。如果允许模型自由发挥,再强的prompt也拦不住输出发散。你在工程上可以增加一层输出校验:对模型输出的答案做关键词匹配检查,看看答案中是否真的覆盖了资料里的关键信息。这一步不能百分之百解决问题,但是一个不错的约束。如果业务要求高,可以在prompt中要求模型给出引用编号,比如“根据资料[1][3]”,然后程序校验引用编号是否存在且有效,这样能显著提升答案的可追溯性。

4.3 模型又慢又贵怎么办

纯RAG链路中,成本大头通常在大模型生成阶段,检索阶段占比很小。常见的优化手段有三个:模型降级、prompt瘦身、结果缓存。

模型降级是说不要所有请求都走最强模型。类似“公司年假有几天”这种事实型查询,用一个轻量模型足够;复杂推理、多文档对比、总结归纳才需要更强的模型。我常用的分层策略是:意图路由阶段用轻量模型做判断,文档问答阶段默认用中型模型,确实复杂的问题再升级到大型模型。前端做个按钮或后端自动路由,用户无感,费用下降不少。

prompt瘦身也很重要。有人习惯把大量示例和说明塞进prompt,token消耗成倍增加。RAG场景的prompt不需要展示太多few-shot,因为资料本身已经是强约束。我建议为每个环节的prompt做一个token预算,比如系统说明控制在一百五十token以内,资料部分维持在两千token以内,问题部分单独拼。这样既保证模型聚焦,又将成本压在可控范围。

缓存这块前面已经专门讨论过,它在慢和贵这两个维度上双赢。命中的请求直接返回历史结果,不需要调模型、不需要推理,延迟几乎为零。综合这几个手段,一个中等规模的内部问答机器人在成本上完全能做到日用可控。有一点要记住:优化是一个迭代过程,不要指望一步到位,上线后根据日志的token统计和延迟分布去调,比拍脑袋改参数靠谱得多。

4.4 排障速查表

把这几年在RAG项目里踩过的大坑整理了一下,做成速查表,方便你排查时直接对照。

现象优先排查项常用手段
答非所问,内容与问题无关检索召回是否返回无关文档检查召回日志、降低top_k靠前的相关性阈值、引入Rerank
上下文有答案但输出错误上下文是否被无关块干扰减少喂给模型的文本块数量、优化prompt约束、要求模型引用资料编号
回答总是“不知道”召回率偏低或切片粒度过大换Embedding模型、调整chunk_size、扩大召回候选集
同一问题不同答案temperature过高或未固定temperature设为0、检查prompt模板是否确定
线上延迟高模型调用链路过长或模型负载高加缓存、模型降级、流式输出、批量处理
用户觉得越权权限过滤未在检索前生效给文档块打metadata权限标签、检索前过滤、日志审计
token消耗异常高检索结果大量冗余或prompt过胖压缩上下文、限制召回数量、精排后只留高相关文本块

这张表不保证覆盖所有场景,但覆盖了80%的常见故障。一旦你遇到新问题,就把日志翻出来,从“检索内容是否准确”开始排查,再逐步向上下游扩散。RAG链路说起来环节多,但实际上有问题时,大多是数据预处理和检索阶段出了问题,生成阶段反而是最稳定的。

5. 写在最后:从零开始,最后学到的是“判断力”而不是“跑通”

如果让我归纳“ai-engineering-from-scratch”这门手艺最终给我的东西,我首先想到的其实不是具体技术清单,而是一种判断力。你亲手从文档解析写到向量入库再写到prompt组装,你就会非常清楚每个环节该被放在什么位置。以后遇到一个新技术,你不会被它的宣传词带走,而是会问:“它在链路里替代哪个环节?它解决了什么问题?它引入了什么新的不确定性?”这些问题没有标准答案,但判断者做过或没做过,说出来的话完全不一样。

另外一个实实在在的建议是:把你自己搭好的最小系统整理成一个模板,后续新项目从这个模板起步。每当你往上面加一个模块——Rerank、缓存、多路召回——都强制自己跑一遍上一轮预设的评估基线,对比新老效果再决定合并。这条习惯我坚持了快两年,帮我挡住了很多“看起来很美但实际会拖垮效果”的伪优化。

最后送给你一个小技巧。任何AI工程项目的复盘,都值得把“资料准备环节”单独拎出来过一遍。这个环节往往决定了项目的上限。文档质量差、覆盖不全、切分混乱,后面再好的模型和再聪明的prompt都救不回来。你在资料上多花的时间,翻倍地回报在问答效果上。这也是我推荐一切从零开始的AI工程学习路线时,始终坚持的第一出发点。

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

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

立即咨询