ARTICLE DETAIL

资讯详情

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

从零手搓AI工程闭环:RAG系统底层实现与调优实战

从零手搓AI工程闭环:RAG系统底层实现与调优实战 1. 从零搭建AI工程能力为什么“手搓”比调包更值得投入这两年AI应用层的工具链成熟得吓人LangChain、LlamaIndex、各种Agent框架几乎把能封装的都封装了。你打开一个开源仓库三行代码就能跑起一个RAG问答五行代码就能接一个大模型做对话。表面上看门槛低到尘埃里谁都能说自己“做过AI项目”。但真到了线上出问题的时候能定位到根因的人少得可怜——检索召回率掉了是embedding模型的问题还是分块策略的问题模型输出不稳定是prompt写得太松还是temperature参数没调对上下文超了是截断逻辑有bug还是token计算方式不对我见过太多人卡在这一层会用框架但不懂框架底下发生了什么。一旦框架不支持的场景出现或者性能、成本、稳定性出问题就彻底抓瞎。所以当我看到“ai-engineering-from-scratch”这个方向的时候第一反应是——这才是真正该花时间的地方。它不是让你重新造轮子而是让你亲手把轮子拆一遍再装回去知道每个齿轮咬合在哪里。这篇文章适合两类人一类是已经会用现成框架做AI应用但想搞清楚底层原理、提升排查问题能力的开发者另一类是刚入门AI工程不想一上来就被各种抽象层绕晕希望从最朴素的实现开始建立直觉的人。我会围绕一个完整的AI工程最小闭环——从文本处理、向量化、检索、到生成和评估——把每个环节的核心逻辑、实操要点、以及我踩过的坑全部摊开讲清楚。所有代码和参数你都可以直接抄作业但更重要的是理解每一步为什么这么做。2. 整体架构设计一个最小可用的AI工程闭环长什么样2.1 为什么选择“从零实现”而不是“直接调框架”先把这个核心问题说清楚。调框架和从零实现本质上不是对立的而是不同阶段的选择。框架的价值在于快速验证想法但它的代价是隐藏了细节。当你需要优化成本、提升效果、或者排查诡异bug的时候这些被隐藏的细节就会变成黑盒。我举个真实的例子。之前有个项目用某个流行框架做文档问答检索效果一直不理想。团队换了embedding模型、调了分块大小、改了prompt折腾了一周都没明显改善。后来我让他们把检索结果打出来看发现框架默认的相似度计算方式用的是余弦相似度但他们的文档向量没有做归一化导致长文档的向量模长偏大相似度排序被带偏了。这个问题在框架层面几乎不可见但如果你自己实现过一遍向量检索就会本能地检查归一化这一步。从零实现的核心价值是让你建立“参数直觉”。你知道每个环节有哪些可调参数每个参数影响什么调它的时候预期会发生什么变化。这种直觉是调包调不出来的。2.2 最小闭环的四个核心模块一个完整的AI工程闭环至少包含四个模块文本预处理、向量化与索引、检索与重排、生成与评估。这四个模块串起来就是一个RAG系统的基本骨架。但我想强调的是这个骨架可以扩展到很多场景——不只是问答还包括语义搜索、推荐、分类、摘要等等。文本预处理负责把原始文档变成模型能处理的格式。这里面涉及分块策略、清洗规则、元数据提取。向量化负责把文本转成数值向量核心是选模型和做归一化。检索负责从向量库里找到最相关的片段核心是相似度计算和索引结构。生成负责把检索结果和用户问题一起喂给大模型核心是prompt构造和上下文管理。评估负责衡量整个系统的效果核心是指标设计和测试集构建。这四个模块之间是有数据依赖的所以实现顺序很重要。我的建议是先做向量化和检索因为这部分最容易验证——你可以直接看检索出来的片段是否相关。然后再做生成最后做评估。预处理可以贯穿始终边做边调。2.3 技术选型为什么用这些库而不是那些虽然说是“从零实现”但没必要连矩阵运算都自己写。我的选型原则是底层计算用成熟库业务逻辑自己写。具体来说numpy和scipy用来做向量运算sentence-transformers用来加载embedding模型faiss用来做向量索引transformers用来加载生成模型。这些库足够底层不会隐藏太多细节同时性能也够用。不选LangChain这类高层框架的原因很简单它们抽象太多一个简单的检索操作可能经过五六层封装出问题的时候调用栈长得让人绝望。而且这些框架的API变动频繁今天写的代码下个月可能就跑不通了。从学习的角度直接用底层库反而更稳定。生成模型这块我建议先用一个小模型做实验比如Qwen2.5-1.5B或者Phi-3-mini等流程跑通了再换大模型。这样迭代速度快成本也低。如果你没有GPU用API也行但那样就少了对模型行为的直观感受。3. 文本预处理分块策略决定了检索效果的上限3.1 分块不是切字符串那么简单很多人做RAG的第一步就是按固定长度切分文档比如每500个字符一块。这种做法不能说错但效果通常很差。原因在于固定长度切分很容易把语义完整的段落切断导致检索出来的片段缺少上下文模型看不懂。我试过在一个技术文档问答项目里用固定长度分块检索出来的片段经常是半句话开头、半句话结尾模型回答的时候要么瞎猜要么直接说“根据提供的信息无法回答”。后来改成按语义边界分块效果立刻上了一个台阶。语义分块的核心思路是优先在段落、标题、列表项这些自然边界处切分只有在单个语义单元超过最大长度时才强制切分。具体实现上可以先用正则表达式识别文档结构然后按层级合并或拆分。3.2 分块大小的权衡不是越大越好也不是越小越好分块大小直接影响检索的粒度和生成的质量。块太小检索出来的片段可能缺少必要上下文模型无法理解块太大检索精度下降因为一个块里可能混了多个主题相似度计算被稀释。我的经验值是对于技术文档和说明文块大小在300到500个token之间比较合适对于对话记录和叙事文本可以放宽到500到800个token。这个范围不是拍脑袋来的而是基于embedding模型的训练特性——大多数embedding模型在256到512个token的输入上表现最好超过这个长度向量表示的质量会下降。还有一个容易被忽略的点块之间要有重叠。通常设置10%到20%的重叠比例比如块大小500token重叠50到100token。这样做的目的是防止关键信息刚好落在切分边界上被切断。重叠部分的代价是索引体积增大但相比检索效果的提升这个代价是值得的。3.3 元数据提取让检索多一个维度纯向量检索有个天然缺陷它只关心语义相似度不关心其他维度的信息。比如你问“2024年的营收数据”向量检索可能会返回2023年的相似段落因为两段话的语义结构很像。这时候如果每个块都带了年份元数据就可以在检索时加一个过滤条件把年份不对的排除掉。元数据提取不需要很复杂常见的包括文档来源、创建时间、章节标题、文档类型。这些信息通常在文档的原始格式里就有只需要在预处理阶段解析出来和文本块一起存进索引。检索的时候先按元数据过滤再按向量相似度排序效果会稳很多。我一般会在预处理阶段做三件事第一用正则提取标题层级构建章节路径第二从文件属性或内容里提取时间信息第三给每个块生成一个唯一ID方便后续追溯和更新。这三件事花不了多少时间但后面排查问题的时候会感谢自己。4. 向量化与索引embedding模型选型和归一化实操4.1 embedding模型怎么选看榜单不如看场景选embedding模型的时候很多人直接看MTEB榜单哪个排名高选哪个。榜单有参考价值但不能全信。因为榜单的评测数据集和你的实际场景可能差很远。比如榜单上排名很高的模型可能是在通用语义相似度任务上表现好但你的场景是法律文书检索术语密集、句式复杂表现可能完全不一样。我的做法是先选三到五个候选模型用你自己的数据做一个小规模评测。具体来说准备50到100个查询和对应的正确文档分别用每个模型做检索看召回率和MRR。这个评测半天就能做完但能避免选错模型带来的长期痛苦。候选模型方面英文场景我一般会试all-MiniLM-L6-v2、bge-small-en、gte-small这几个它们体积小、速度快效果也不差。中文场景会试bge-small-zh、text2vec-base-chinese、m3e-small。如果对效果要求高可以上bge-large或者gte-large但推理速度会慢不少需要权衡。4.2 归一化一个容易被忽略但影响巨大的步骤向量归一化这件事说起来简单就是让每个向量的模长变成1。但就是这一步很多人会漏掉导致相似度计算出现偏差。为什么归一化这么重要因为余弦相似度的定义是向量点积除以模长乘积。如果两个向量都归一化了模长乘积就是1余弦相似度就等于点积计算量小很多。更重要的是如果不归一化模长大的向量会在点积中占优势即使方向不太对齐也可能得到较高的相似度分数。这就会导致检索结果偏向长文档或高频词多的文档。归一化的实现很简单用numpy一行代码就能搞定import numpy as np def normalize(vectors): norms np.linalg.norm(vectors, axis1, keepdimsTrue) norms np.where(norms 0, 1, norms) # 防止除零 return vectors / norms注意那个防止除零的判断。我遇到过零向量导致nan的情况排查了半天才发现是某个空文档被编码成了零向量。虽然概率很低但加上这个判断不亏。4.3 索引结构Flat、IVF、HNSW怎么选faiss提供了多种索引结构常用的有IndexFlatIP、IndexIVFFlat、IndexHNSWFlat。选哪种取决于你的数据规模和延迟要求。IndexFlatIP是最简单的暴力检索把所有向量加载到内存逐个计算相似度。优点是结果精确缺点是数据量大时速度慢。我的经验是10万条以下用Flat完全没问题查询延迟在毫秒级。超过10万条就要考虑IVF或HNSW了。IndexIVFFlat是倒排索引先把向量聚类成若干个桶查询时只搜索最近的几个桶。优点是速度快缺点是需要训练而且召回率不是100%。关键参数是nlist桶的数量和nprobe查询时搜索的桶数。nlist一般设为sqrt(N)N是向量总数。nprobe越大召回率越高但速度越慢。我一般从nprobe10开始调看召回率和延迟的平衡点。IndexHNSWFlat是基于图的索引查询速度快召回率高而且不需要训练。缺点是内存占用大构建索引慢。如果内存不是瓶颈HNSW是很好的选择。关键参数是M每个节点的连接数一般设16到64之间。M越大召回率越高内存占用也越大。索引类型适用规模优点缺点关键参数IndexFlatIP10万以下精确、简单大数据慢无IndexIVFFlat10万到1000万速度快需训练、召回率非100%nlist, nprobeIndexHNSWFlat10万到1000万速度快、召回率高内存大、构建慢M5. 检索与重排从向量相似度到最终结果5.1 相似度计算余弦、点积、欧氏距离的区别向量检索的核心是相似度计算。常用的有三种余弦相似度、点积、欧氏距离。如果向量已经归一化余弦相似度和点积是等价的。欧氏距离和余弦相似度在归一化向量上也有单调关系所以排序结果是一样的。但如果不归一化这三种方式的结果会不一样。点积会偏向模长大的向量欧氏距离会偏向模长小的向量余弦相似度只看方向。所以我的建议是统一归一化然后用点积或余弦结果稳定且计算快。faiss的IndexFlatIP就是点积索引配合归一化向量使用效果等同于余弦相似度。如果你用IndexFlatL2那是欧氏距离需要把向量归一化后再用否则结果会有偏差。5.2 重排用交叉编码器提升精度向量检索是双编码器架构查询和文档分别编码然后算相似度。优点是快缺点是不够准因为查询和文档之间没有交互。重排用的是交叉编码器把查询和文档拼在一起输入模型输出一个相关性分数。因为有了交互精度会高很多但速度慢所以只适合对少量候选做精排。典型的流程是先用向量检索召回top 50到100个候选然后用交叉编码器对这50到100个重新打分取top 5到10个送给生成模型。这样既保证了速度又提升了精度。交叉编码器我一般用bge-reranker-base或者bge-reranker-large。base版本速度快large版本精度高根据延迟要求选。实测下来加了重排之后检索的准确率能提升10到20个百分点效果非常明显。5.3 混合检索向量加关键词效果更稳纯向量检索有个弱点对精确匹配不敏感。比如用户搜一个产品型号“XR-2000”向量检索可能返回一堆语义相似但型号不同的文档。这时候如果加上关键词检索用BM25或者简单的倒排索引就能把精确匹配的结果捞回来。混合检索的做法是同时跑向量检索和关键词检索然后合并结果。合并策略有两种一种是加权求和给两种检索的分数各一个权重另一种是RRFReciprocal Rank Fusion按排名融合不依赖分数绝对值。RRF更简单也更鲁棒我一般用RRF。RRF的公式很简单对于每个文档分数等于它在各个检索结果中排名的倒数和。比如一个文档在向量检索中排第3在关键词检索中排第5那它的RRF分数就是1/(603) 1/(605)其中60是一个平滑常数。按这个分数重新排序取top结果。6. 生成与上下文管理让模型答得准、答得稳6.1 prompt构造把检索结果用对检索出来的片段怎么放进prompt直接影响生成质量。我见过两种极端一种是把所有检索结果一股脑塞进去导致上下文超长模型注意力被分散另一种是只放一个片段信息不够模型答不全。我的做法是按相关性排序取top 3到5个片段每个片段前面加上来源标记比如“[文档1]”。然后在prompt里明确告诉模型只根据提供的文档回答如果文档里没有相关信息就说“根据现有信息无法回答”。这样能有效减少幻觉。prompt模板我一般这样写你是一个严谨的问答助手。请根据以下文档回答用户问题。 如果文档中没有相关信息请直接说“根据现有信息无法回答”不要编造。 文档 [文档1] {doc1} [文档2] {doc2} [文档3] {doc3} 用户问题{question} 回答这个模板的关键是“不要编造”这句话。实测下来加上这句话之后模型在信息不足时胡编的概率明显降低。6.2 上下文长度管理截断策略和token计算大模型的上下文窗口是有限的虽然现在动辄128K但实际用的时候太长的上下文会导致推理变慢、成本上升而且模型对中间部分的注意力会下降。所以需要做上下文管理。我的策略是先按相关性排序从高到低往prompt里塞直到接近上下文窗口的80%。留20%的余量给系统prompt和用户问题。如果单个片段太长就截断但保留开头和结尾因为关键信息通常在两端。token计算不要用字符数估算误差太大。用模型对应的tokenizer来算准确度高。transformers库的AutoTokenizer就能做这件事一行代码的事别偷懒。6.3 生成参数调优temperature、top_p、max_tokens生成参数对输出质量影响很大。temperature控制随机性值越高输出越多样但也越容易跑偏。问答场景我一般设0.1到0.3保证输出稳定。top_p控制采样范围一般设0.9到0.95和temperature配合使用。max_tokens限制输出长度根据场景设问答一般256到512就够了。还有一个参数容易被忽略repetition_penalty。模型有时候会重复输出同一句话加上这个参数能缓解。一般设1.1到1.2太高会导致输出不流畅。这些参数没有万能值需要根据你的场景调。我的建议是先用保守值跑通然后逐步调整每次只调一个参数观察输出变化。这样能建立参数直觉。7. 评估与迭代怎么知道系统好不好7.1 检索评估召回率、MRR、NDCG检索效果是RAG系统的上限。检索不准生成再好也没用。评估检索的核心指标有三个召回率、MRR、NDCG。召回率衡量的是在所有相关文档中检索结果覆盖了多少。比如有10个相关文档检索返回了7个召回率就是70%。MRR衡量的是第一个相关文档出现在第几位。如果第一个结果就相关MRR是1如果第二个才相关MRR是0.5。NDCG考虑了排序位置和相关性等级更全面但计算复杂。我的做法是构建一个测试集包含50到100个查询和对应的相关文档标注。然后跑检索算这三个指标。召回率优先因为召回不够后面都白搭。召回率达标后再看MRR和NDCG优化排序。7.2 生成评估忠实度和相关性生成评估比检索评估难因为答案的质量很主观。我一般从两个维度看忠实度和相关性。忠实度是指答案是否基于检索到的文档没有编造。相关性是指答案是否切题有没有答非所问。忠实度可以用NLI模型来判断把检索文档作为前提生成答案作为假设看模型判断是蕴含、中立还是矛盾。蕴含比例越高忠实度越好。相关性可以用embedding相似度来粗略衡量或者人工抽检。我通常会做人工抽检每次迭代后随机抽20到30个查询人工看答案质量。虽然费时间但能发现自动指标发现不了的问题。比如模型可能忠实度很高但答案太啰嗦用户体验不好。这种问题只有人看得出来。7.3 迭代节奏小步快跑每次只改一个变量AI工程优化最忌讳一次改多个地方。你改了分块大小又换了embedding模型还调了prompt效果变好了但你不知道是哪个改动起的作用。下次遇到问题也不知道该调哪个。我的节奏是每次只改一个变量跑评估看指标变化。如果变好保留如果变差回滚。这样虽然慢但每一步都扎实积累下来的经验是可靠的。我一般一周做两到三次迭代每次改动后记录指标变化形成实验日志。这个日志后面会变成团队的知识资产。8. 常见问题与排查技巧实录8.1 检索结果不相关从五个方向排查检索不相关是最常见的问题。我一般按这个顺序排查第一看embedding模型是否适合你的语言和领域不行就换第二看分块是否合理有没有把完整语义切断第三看是否做了归一化没做的话补上第四看相似度计算方式是否匹配索引类型第五看是否需要加重排。这五个方向覆盖了90%的检索问题。我遇到过最诡异的一次是分块大小设成了100token导致每个块信息量太少检索出来的片段都是零碎的短语。改成400token后立刻正常了。8.2 生成答案胡编三个手段降低幻觉幻觉是生成模型的固有问题只能降低不能消除。我的三个手段是第一prompt里明确要求“只根据文档回答”第二降低temperature减少随机性第三加一个后处理步骤用NLI模型检查答案是否被文档蕴含不蕴含就重新生成或返回“无法回答”。第三个手段会增加延迟但对准确性要求高的场景值得。我一般只在医疗、法律这类高风险场景用普通问答用前两个就够了。8.3 上下文超长截断和压缩两种策略上下文超长有两种处理方式截断和压缩。截断简单按相关性排序从低到高丢弃直到长度达标。压缩复杂一些用模型把长文档摘要成短文本再放进prompt。压缩能保留更多信息但会增加一次模型调用延迟和成本都上升。我的建议是先用截断简单可靠。如果截断导致信息丢失严重再考虑压缩。压缩可以用小模型做比如用1.5B的模型做摘要速度快成本低。8.4 常见问题速查表问题现象可能原因排查方法解决方案检索结果不相关embedding模型不匹配换模型做小规模评测选场景匹配的模型检索结果不相关分块不合理检查分块边界调整分块大小和重叠检索结果不相关未归一化检查向量模长加归一化步骤生成答案胡编prompt太松检查prompt模板加“不要编造”约束生成答案胡编temperature太高检查生成参数降低temperature上下文超长检索结果太多检查top_k设置减少top_k或加截断推理速度慢索引类型不合适检查索引结构换IVF或HNSW推理速度慢模型太大检查模型参数量换小模型或量化9. 一些实操心得和后续扩展方向这套从零实现的流程我在三个项目里跑过最深的体会是AI工程的核心不是模型而是数据流。模型是现成的但数据怎么切、怎么存、怎么取、怎么喂这些才是决定效果的关键。把数据流理顺了换个模型效果也不会差太多数据流有问题用再好的模型也救不回来。另一个体会是评估比优化重要。没有评估优化就是盲人摸象。我见过太多团队花大量时间调参但连一个像样的测试集都没有调来调去全靠感觉。这种工作方式运气好能碰对运气不好就是原地打转。后续扩展的话有几个方向值得深入一是多路召回除了向量和关键词还可以加知识图谱检索二是查询改写用模型把用户问题改写成更适合检索的形式三是自适应检索根据问题类型决定检索策略简单问题少检索复杂问题多检索。这些方向都需要在基础流程跑通之后再考虑不然容易贪多嚼不烂。最后分享一个小技巧把每次实验的配置和结果都记下来用一个简单的表格就行。时间长了你会发现自己对哪些参数敏感、哪些改动有效有了非常清晰的直觉。这个直觉才是从零实现最大的收获。
返回列表