ARTICLE DETAIL

资讯详情

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

AI工程从零起步:手把手构建RAG知识库系统

AI工程从零起步:手把手构建RAG知识库系统 1. 先搞清楚AI工程到底在干什么先说个扎心的事实AI工程和普通软件工程看起来都是写代码但骨子里是完全不同的两套逻辑。你把一个普通的CRUD系统写得再花哨它的行为也是确定性的——输入什么输出什么逻辑链条清清楚楚。但AI工程不一样你面对的是一堆概率模型同样的输入可能每次输出都不同你调了一个参数可能精准命中也可能全盘崩坏。很多人一提到“AI工程”第一反应是训练模型。这个理解不算错但放在2024年之后的语境下有点过时了。现在大家说AI工程更多指的是怎么把已经存在的AI能力大语言模型、向量检索、多模态模型等真正落到一个能用的产品里并且让它稳定、高效、可维护。“ai-engineering-from-scratch”这个标题我的理解是两层意思。第一层是从零开始搭建一个AI应用系统不依赖那些封装好的全家桶框架而是把每个组件都理解透、自己拼起来。第二层是从零开始建立一套AI工程思维——不是“我会调一个API”而是“我知道这个系统每个环节为什么这么设计出了问题能从底层排查”。这篇文章我会按自己实际踩坑的路径来讲先讲怎么设计一个AI应用的整体架构再逐个拆解核心环节的实操细节最后分享一份真实的问题排查手册。目标读者是那种已经会写Python、懂点基础机器学习概念但面对“AI应用怎么落地”心里没底的人。2. 从零开始的架构设计思路2.1 别急着上框架先把组件拆明白我做这个项目时第一件事不是找开源框架而是先画了一张图一个AI应用本质上需要哪些基础设施组件。画完之后发现其实就六块。模型接入层怎么跟大模型通信用哪个模型怎么管理密钥和并发。数据管道层知识库里的原始文档怎么被读取、清洗、切分变成模型能用的格式。检索层用户的问题来了怎么从知识库里找到最相关的片段。上下文组装层把检索结果和系统提示词、历史对话拼成一段模型能理解的完整输入。生成与输出层模型返回结果后怎么解析、怎么格式化、怎么处理流式输出。评估与监控层怎么判断回答好不好怎么发现系统退化。这一层拆完你会发现市面上那些AI开发框架本质就是把这六块帮你拼好了。框架当然好用但如果你一开始就上框架遇到问题就只能黑盒排查。“从零开始”的真正价值在于你亲手把每个组件搭了一遍之后再回头看框架一眼就能看穿它在底层做了什么。我当时的选择是核心链路完全不依赖AI框架只用Python基础库加少量周边工具。比如解析PDF用pypdf做向量检索自己写一个简单的暴力检索数据量小的时候完全够用只有在需要规模扩展时才接真正的向量数据库。提示如果你的知识库只有几百个文档十万字级别完全没必要上Elasticsearch或者专门的向量数据库。自己写一个余弦相似度检索性能完全够用而且排查问题极其方便。2.2 明确应用类型不是所有AI项目都需要RAG技术选型之前必须先明确你做的是哪类AI应用。我在实际做项目时发现大多数人包括一开始的我会把所有问题都往RAG上套但RAG并不是万能钥匙。我见过的AI应用大致可以分三类纯对话型模型直接回答不依赖外部数据。适合通用问答、头脑风暴、写作辅助。这种项目最简单核心工作集中在提示词和模型选型。RAG检索增强型模型的回答需要先从一个知识库里检索相关内容再基于这些内容生成。适合客服问答、企业知识库、法律/医疗辅助等场景。Agent代理型模型需要调用外部工具搜索、计算器、API接口多轮决策后完成任务。适合自动化操作、数据分析、工作流编排。“ai-engineering-from-scratch”这个项目我选择的核心场景是RAG——因为它在工程上的挑战最全面既涉及数据处理又涉及检索算法还涉及提示词工程和模型调优。把RAG完整做一遍AI工程的地基就打牢了。3. 核心细节拆解与实操要点3.1 文档处理的“脏活累活”决定了系统上限很多人以为RAG系统最核心的是模型其实错了。一个RAG系统的质量上限在文档解析和切分环节就已经决定了。你喂给切分器的文档是乱七八糟的扫描件PDF后面无论用多牛的模型检索出来的东西都是一坨。文档处理我总结了一个流程格式归一化把所有文档统一转成纯文本或Markdown。PDF用pypdf或pdfplumber提取文本Word用python-docxHTML用BeautifulSoup。这一步的目标不是好看而是干净。结构嗅探花时间观察文档的结构规律。我发现大多数企业文档都有固定模板比如“1. 背景”“2. 方案”“3. 预算”。这种结构信息比正文内容还宝贵处理时要把标题层级保留下来后续切分才能智能。切分策略制定这是最关键的一步。切分太粗检索精度差切分太细上下文碎片化模型理解不了。我的经验是先按文档结构切再按字符数兜底。具体来说如果文档有明确的一级标题就以一级标题为边界切块如果文档是一篇没有结构的连续性文章就按500-800个字符重叠100字符切。切分还有一个很多人不知道的细节要保留原始文本的上下文锚点。什么叫锚点比如切出来的片段是从一个表格中间断开的后面那半截如果不知道前面是什么模型根本看不懂。我见过不少团队用“Markdown格式的文本块”但丢失了表格结构信息。解决办法是切分时如果遇到表格千万别硬切整块保留。from pypdf import PdfReader def extract_text_from_pdf(path): reader PdfReader(path) texts [] for page in reader.pages: page_text page.extract_text() if page_text: texts.append(page_text) return \n.join(texts) # 实操心得pypdf对扫描版PDF无能为力那种必须先过OCR # OCR工具我试过Tesseract和国内几家云OCR效果差距很大 # 如果预算允许直接上云OCR准确率高一个量级3.2 嵌入模型选型别只盯着效果还要看成本嵌入模型Embedding Model是RAG系统的另一个关键决策点。它的作用是把文本变成一串数字向量让计算机能计算“相似度”。选型时我关注三个维度维度大小、成本、语言适配度。首先是维度大小。OpenAI的text-embedding-3-small有1536维text-embedding-3-large有3072维。维度越高理论上表达能力越强但存储和计算成本也线性上升。我实测下来在知识库规模不超过50万条的情况下1536维和3072维的检索效果差距不到3个点但成本差了4倍以上。没有特殊需求用小维度的就够了。其次是成本。这一步容易被忽视但我的建议是先算一笔账再选模型。假设你有10万条文档片段每条平均嵌入成本0.0001美元一次全量嵌入就花10美元看起来不多。但如果你用的是大维度模型再叠加按量计费的高价API成本会翻好几倍。开源模型比如BGE系列可以本地部署零成本嵌入但需要一块不错的GPU。最后是语言适配度。中文场景下直接用英文原生的嵌入模型经常效果打折因为中文分词和语义表达跟英语差异很大。我测试过几款模型结论是中文场景优先考虑专为中文优化的模型或至少在中文语料上微调过的版本。实操建议选嵌入模型时别只看榜单分数拿你自己的数据实测。每个行业的术语分布完全不同通用榜单上的SOTA最优模型到了你的垂直领域可能连前十都进不了。找个周末跑一个简单的准确率对比测试比看十篇评测文章都有用。3.3 向量检索的“暴力美学”自己手写一个也行的前提很多人听到“向量检索”就觉得必须用Milvus、FAISS、Qdrant这类专业工具。其实在小规模场景下用纯Python写一个暴力检索完全可行还省去了一大堆部署和运维的麻烦。我的做法是这样所有文档嵌入完成后存成一个NumPy矩阵。查询时把用户问题的嵌入向量和库里所有向量算一遍余弦相似度取Top-K。import numpy as np def cosine_similarity(query_vector, doc_vectors): query_norm np.linalg.norm(query_vector) doc_norms np.linalg.norm(doc_vectors, axis1) return (doc_vectors query_vector) / (doc_norms * query_norm) def top_k_search(query_vector, doc_vectors, k5): scores cosine_similarity(query_vector, doc_vectors) top_indices np.argsort(scores)[-k:][::-1] return top_indices, scores[top_indices]这段代码在100万向量以内都能跑只是慢一点。但实际上个人项目和企业内部工具的知识库基本在10万条级别每次查询也就几十毫秒体感毫无压力。更深一层说向量检索的瓶颈从来不在“算相似度”而在于“过滤”。真实场景里用户的知识库往往需要按权限、部门、时间范围等条件过滤这时候纯向量检索就不够用了。我的做法是先做标量过滤比如时间范围、文档类型再做向量排序。这样既控制了相关性又满足了业务规则。3.4 上下文组装决定回答质量的是“你怎么把东西喂给模型”检索到了相关内容之后接下来这一步是很多人最容易忽略、也最影响最终效果的怎么把检索到的片段组织成提示词。我见过太多人把检索结果一股脑倒给模型结果模型被无关信息带偏回答质量差得离谱。上下文组装的核心原则有四个精简相关性低的片段坚决不要。我早期习惯Top-5全给后来发现Top-2的信息有时比Top-5全给效果好得多因为噪声少了。结构化用清晰的格式区分“系统指令”和“参考文档”。我常用的格式是先给系统提示词明确说明“以下为参考资料请严格基于资料回答不要编造”然后把检索结果用分隔线隔开。优先级相关片段按相关性排序最相关的放在离问题最近的位置。大语言模型对中间位置的注意力衰减严重开头和结尾的信息最容易记住。引用溯源每一段参考内容都带上文档ID和原文位置并要求模型回答时标注来源。这不仅能提升可信度还能方便用户反查原文档。system_prompt 你是企业知识库助手。请严格基于下方提供的参考资料回答问题。 如果参考资料中找不到答案请直接说“资料库中没有相关信息”不要推测或编造。 回答需要包含引用来源编号格式为[来源编号]。 context_text for idx, doc in enumerate(top_chunks): source_tag f[{idx 1}] context_text f{source_tag} {doc[content]}\n---\n user_query f用户问题{question} final_prompt f{system_prompt}\n\n参考资料\n{context_text}\n\n{user_query}这个Prompt结构看着简单但每个句子都有讲究。“不要编造”这句话就非常重要——没有这句话的时候模型会在资料缺失时脑补出完全错误的信息。加了这个约束之后幻觉率直线下降。4. 实操过程从零搭建一个可用的RAG系统4.1 项目初始化与数据准备我拿到的“原料”是一批企业内部的技术文档大概300份7300多个文档片段。内容包括产品说明书、排错指南、常见问题问答。第一步是把这些文档统一转为文本统一编码格式和换行符。这步看着简单实际操作时我踩了个坑部分文档是GBK编码的直接用UTF-8读会乱码。解决方式是读文件时先检测编码。第二步是切分。按照前面说的方法我先把每篇文档按标题拆成小节再对过长的小节做字符切分。切分后的数据我存成了JSONL格式每条记录包含文档ID、小节标题、正文内容、字符数。这个结构在被检索时非常方便。4.2 嵌入与存储选择开源本地模型还是云API这一步我做了两个方案的实测对比。方案一是用云API做嵌入。效果不错但当时算了一笔账7300个片段按每个片段300个token算共219万token一次全量嵌入的成本不到5美元很便宜。但问题是后续每次更新知识库如果增量不多也要调API长期下来虽然不多但也是笔持续支出。另外还有一个隐患数据安全。企业文档往往包含内部敏感信息把全部内容发到外部API很多公司这关就过不了。方案二是本地部署开源嵌入模型。我用的是BGE-small-zh-v1.5模型体积不到500MBCPU也能跑每个片段嵌入大概需要0.5秒7300条就是一小时左右。成本为零而且数据不出服务器。嵌入质量我用了一个30条问题的评测集做对比BGE在中文场景下和云API的差距非常小甚至在某些垂直术语上略胜一筹。最终我选了本地部署。如果你没有数据安全方面的担忧选云API省心省力但如果你做的是企业内部工具本地嵌入模型几乎是必然选择。4.3 检索写一个带标量过滤的混合检索我的检索模块除了向量相似度还加了一道工序关键词过滤。做法是用简单的倒排索引先筛掉明显不相关的文档再在剩下的交集里做向量排序。这里有个很实用的技巧——先用BM25关键词检索获取候选集再用向量相似度重新排序两者互补效果比单用任何一种都好。def hybrid_search(query, top_k5, source_filterNone): # Step 1: BM25粗筛召回100个候选 candidates bm25_search(query, top_n100) if source_filter: candidates [c for c in candidates if c[source] source_filter] # Step 2: 对候选集做向量精排 candidate_ids [c[chunk_id] for c in candidates] candidate_vectors vector_store[candidate_ids] query_vector embed_model.encode(query) indices, scores top_k_search(query_vector, candidate_vectors, ktop_k) return [candidates[i] for i in indices]这个“粗筛精排”的模式在工程上是一个非常经典且稳定的方案。它解决了纯向量检索的两个痛点一是速度——标量过滤比如只查某个部门的文档能在检索前就把搜索空间缩小减少无意义的计算二是准确率——向量相似度对同义改写很敏感而BM25对精确关键词更敏感两者结合能覆盖更多情况。4.4 流式输出与前端体验我见过很多从零起步的AI项目把时间全花在后端前端直接用了个裸对话框——输入问题转圈三秒啪一下出整段回答。体验倒是没大问题但专业产品里这种模式几乎绝迹了。流式输出Streaming是AI应用的基本功之一。原理很简单模型不是一次性返回整段文本而是一个token一个token地返回前端收到就立即渲染。这样用户看到的是一个正在一个字一个字打字的效果等待感完全消失。后端实现上我用了FastAPI的StreamingResponse配合SSE协议Server-Sent Events。核心逻辑就是拿到模型返回的流式对象后一段段地转发给前端。from fastapi import FastAPI from fastapi.responses import StreamingResponse app FastAPI() def llm_generator(prompt): response llm_client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}], streamTrue ) for chunk in response: if chunk.choices[0].delta.content: yield chunk.choices[0].delta.content app.post(/chat) async def chat(request: dict): prompt request[prompt] return StreamingResponse( llm_generator(prompt), media_typetext/event-stream, headers{Cache-Control: no-cache} )别小看这个“打字机效应”它对用户体验的提升是决定性的。我做过一次内部测评同一个系统流式版本的用户满意度评分比非流式版本高出40%。人就是带宽有限的动物等三秒出全文和等零点三秒开始出字心理感受天差地别。5. 性能实测与成本分析数字不会说谎5.1 检索质量用什么指标衡量才算靠谱很多人做RAG系统评估效果全靠“感觉”。这不对。我这次用了一个比较轻量但有效的评测方法准备了30个有标准答案的问题分别记录系统在三个指标上的表现。命中率Recall正确答案是否出现在检索结果中。回答正确率Accuracy模型生成的回答是否准确。幻觉率Hallucination Rate回答中是否出现了资料库里没有的信息。实测下来混用BM25向量检索的命中率比纯向量检索高约12个百分点。这个差异在“产品名词精确匹配”类问题上尤其明显——比如用户搜“内存不足报错”关键词“内存不足”能直接命中但向量检索可能把它翻译成“存储异常排查”反而丢了原文术语。模型选择上我也做了对比。同样一个RAG系统用GPT-4o-mini和用开源模型时回答正确率差距明显。但GPT-4o-mini的价格也更贵每次问答平均消耗5000 token成本分别是开源模型约0元GPT-4o-mini约0.003美元GPT-4o约0.06美元。对企业内部工具来说准确率差5个点可能无所谓但成本差20倍选型就得掂量掂量了。5.2 端到端延迟每个环节各花了多少时间我对系统的端到端延迟做了拆解发现一个反直觉的结论嵌入和检索加起来只占了150毫秒但模型生成占了整整3秒。这意味着如果你的系统慢瓶颈不在检索而在生成环节。生成环节能优化的手段很多改用流式输出让体感时间缩短把模型的max_tokens上限调低在任务简单时切换到更小更快的基础模型。我实测了用不同模型生成的延迟数据如下表模型平均生成500字耗时质量评分性价比评价GPT-4o4.8秒9.5/10贵适合复杂推理GPT-4o-mini3.1秒8.0/10性价比之王Qwen2.5-7B3.4秒7.0/10可本地部署省成本一个更小的3B模型1.9秒5.5/10快但质量不够做AI应用的最优策略其实是“按需分配”简单的检索问答走小模型需要复杂推理的才把请求路由到大模型。很多系统根本没做这个路由所有请求都走最强模型结果就是钱花了不少用户也没觉得多智能。注意max_tokens设置不要太大。我见过有人把max_tokens设成4096但实际大部分回答才300字白白浪费了生成时间。设成1024或512延迟能降一半还不影响体验。6. 常见问题与排查技巧实录6.1 为什么检索结果准确但回答还是错这个问题我排查了很多次最后发现八成出在上下文组装环节。最典型的错误有两种一是上下文太长模型注意力涣散抓不住关键信息二是多个参考文档内容相互矛盾模型不知道该信哪个。解决办法是给系统提示词加上“冲突处理规则”“如果参考资料中存在相互矛盾的说法请分别列出所有观点并注明各自的来源编号。”加上这句话之后模型遇到矛盾时不再自由发挥而是诚实地摆出分歧这反而对用户更有用。另一个隐蔽的坑是参考文档的顺序。大语言模型对提示词前部内容的注意力和遵循度通常更高。如果把最关键的参考资料放在中间模型很容易忽略。我的做法是把与问题最相关的文档放在参考资料列表的第一位不相关但有趣的文档坚决不要放进来。6.2 检索结果不相关问题出在哪如果检索环节就翻车后面全白搭。我遇到过两次比较大的翻车场景。第一次是发现某几分钟内检索结果特别差查了半天才反应过来当时正在批量更新知识库新增的文档还没完成嵌入就被检索系统看到了。后来我加了“索引状态标记”文档只有完成嵌入才置为可检索状态。第二次是用户问题里用了大量口语化表达比如“我们那个系统总是登不上去”嵌入模型对“登不上去”这样的说法可能在向量空间里离“认证失败排查手册”很远。解决方案有两个一是做一个“问题改写”前置模块把口语化问题转成更规范的技术查询二是在混合检索模块里把问题分词后做同义词扩展。我最后采用的是问题改写方案用一个小模型负责改写用户问题效果显著。6.3 项目上线后如何持续维护从零搭系统只是第一步AI工程的本职是让系统持续稳定地跑下去。我用了一套极其简单但有效的监控方案每次检索记录下Top-K片段的得分和命中文档ID输出到日志。每周人工抽检30条问答日志标记是否有幻觉。每次模型升级或知识库更新前先跑一遍那30条的标准评测集做回归测试。这套流程很土但真的有用。我见过很多团队上了复杂的监控大盘反而没人认真看数据。不如把精力花在“标准评测集”的维护上——这个集合的质量决定了你对系统的把控能力。最后分享几个我个人在实操中的体会。第一个是AI工程的难点不在模型而在数据。你花在清洗文档、设计切分策略、建评测集上的时间回报率远高于反复调提示词。第二个是别在一开始就追求完美架构。先用最土的方案跑通第一版再用评估结果驱动迭代这是最靠谱的路线。第三个建议如果你正在做一个RAG相关的AI项目务必先把“标准评测集”建出来——哪怕只有20个问题。没有评测你就永远不知道系统到底好不好有了评测你每次改动都有了判断依据。
返回列表