免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

从零构建RAG问答系统:原理、实践与避坑指南

从零构建RAG问答系统:原理、实践与避坑指南 1. 从一个“翻车”案例看RAG的必要性去年我团队的一个实习生用当时最先进的GPT-4 Turbo API做了一个内部知识库问答的Demo。他信心满满地演示输入“我们公司最新的差旅报销标准中国内高铁的报销限额是多少”大模型流畅地生成了一段回答引用了“二等座”、“实报实销”、“需提前审批”等看似专业的词汇甚至给出了一个具体的金额。在场的产品经理频频点头觉得AI真厉害。直到财务同事皱着眉头打断“等等这个金额是去年的标准而且我们从来没有‘实报实销’这个说法高铁报销一直需要发票不是审批。”现场一度非常尴尬。这就是一个典型的LLM“幻觉”案例模型基于其训练数据中的通用模式自信地编造了一个看似合理但完全错误的答案。它“知道”差旅报销大概是怎么回事但它对我们公司这份上周刚更新的、只有内部员工才能看到的PDF文档一无所知。这个“翻车”事件恰恰是检索增强生成最生动的开场白。RAG不是凭空冒出来的复杂架构它要解决的就是如何让“无所不知但可能胡说八道”的大语言模型变得“在特定领域内可靠且精准”。简单说RAG的核心思想是当模型需要回答一个问题时先不让它“即兴发挥”而是让它去“翻书”检索相关文档然后基于“书”里的内容检索到的知识来组织答案。所以今天我不讲那些复杂的架构图也不堆砌晦涩的术语。我们就从一个最简单的、几乎零代码的案例出发亲手搭建一个能准确回答“公司制度”的问答系统把RAG里“检索”、“增强”、“生成”这三个词掰开了、揉碎了看看每一步到底在干什么以及我们可能会在哪些意想不到的地方踩坑。2. 案例定义用RAG构建“员工手册”问答机器人为了让讲解足够聚焦我们设定一个极其具体的场景你手头有一份50页的《XX公司员工手册2024版》PDF文件。现在你需要构建一个问答应用让新员工能像问同事一样用自然语言提问并得到基于手册原文的准确答案。这个案例“简单”在哪知识源单一只有一份PDF没有多个数据库、网站等复杂来源。领域封闭问题范围基本限定在手册内容内不涉及外部常识或推理如“根据劳动法公司这样做是否合法”这类需要外部知识的复杂问题暂不处理。目标明确核心目标是事实准确性回答必须忠实于原文不需要创意写作或复杂逻辑链。这个案例“不简单”在哪它几乎涵盖了RAG最核心、也最容易出问题的所有环节知识切片50页的PDF怎么切成一段段“可检索”的文本向量化与检索怎么让机器理解“年假怎么请”和“申请带薪年假的流程”是同一个问题生成怎么让模型“照本宣科”而不是自己发挥下面我们就用这个案例串起RAG的全流程。3. 第一步知识准备——把PDF“切”成AI能理解的片段拿到PDF第一反应可能是直接把整个文件扔给AI不行吗对于50页的文档大多数主流模型的上下文窗口比如128K确实能吞下。但这样做的弊端极大成本高每次问答都需要将整个手册作为提示词输入Token消耗巨大。噪声多对于“年假有几天”这种简单问题模型需要从全部文本中定位信息容易受到无关内容干扰。精度低长文档中关键信息可能被淹没模型未必能精准找到。因此我们必须进行文本切分也叫分块。这步的目标是将长文档分解为语义完整、长度适中、便于检索的片段。3.1 切分策略比想象中复杂最简单的切分是按固定字符数比如500字滑动窗口切分。但这样会粗暴地切断句子和段落。更合理的做法是采用递归切分首先尝试按“\n\n”双换行通常代表段落切分。如果切分后的段落仍然太长如超过800字则按句子切分符。等进行二次切分。如果句子还是太长再按标点或固定长度进行最终切分。同时我们还可以设置一个较小的重叠窗口比如100字。这样即使一个概念恰好被切在边界它也会在相邻的两个片段中同时出现提高被检索到的概率。针对我们《员工手册》的切分实践# 伪代码示意递归切分逻辑 def split_document(text, chunk_size500, overlap50): # 第一级按章节标题切分假设手册有明确的##标题格式 sections split_by_markdown_header(text) chunks [] for section in sections: if len(section) chunk_size: chunks.append(section) else: # 第二级按段落切分 paragraphs split_by_double_newline(section) for para in paragraphs: if len(para) chunk_size: chunks.append(para) else: # 第三级按句子切分并添加重叠 sentences split_by_sentence(para) current_chunk for sentence in sentences: if len(current_chunk) len(sentence) chunk_size: chunks.append(current_chunk) # 创建重叠块取当前块末尾overlap长度的内容 下一句 current_chunk current_chunk[-overlap:] sentence if overlap 0 else sentence else: current_chunk sentence if current_chunk: chunks.append(current_chunk) return chunks踩坑点1切分粒度的权衡块太大包含信息多但检索精度下降可能引入无关噪声。例如把“考勤制度”和“报销制度”切在一起问考勤时也会检索到报销内容。块太小语义不完整。例如“年假天数根据工龄计算1-3年5天3-5年7天...”如果被切成两块只检索到“1-3年5天”的块答案就不完整。我的经验对于制度类文档以“一个完整的问答对”为单位来思考块大小。通常一个制度条款包括条件、操作、例外就是一个天然的块。实践中300-800字的块配合10%-20%的重叠是一个不错的起点需要根据文档特点调整。4. 第二步建立“记忆”——向量化与索引切分好的文本块对计算机来说只是一堆字符串。我们需要把它变成一种便于“按意思查找”的形式——这就是向量化。4.1 向量化把文字变成数学通过一个嵌入模型我们将每一段文本转换成一个高维向量比如768或1536维。这个向量就是这段文本的“数学指纹”。关键特性是语义相似的文本其向量在空间中的距离通常用余弦相似度衡量会更近。例如“如何申请年假”的向量“请带薪年假的流程是什么”的向量《员工手册》中“年假申请流程”章节的向量这三者的向量距离会很近尽管它们的字面表达不同。模型选择对于中文场景我们可以选用text-embedding-ada-002的替代品如开源模型BAAI/bge-large-zh或moka-ai/m3e-base。它们对中文语义理解更好且本地部署免费。# 伪代码使用Sentence Transformers库 from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-large-zh) chunks [文本块1内容, 文本块2内容, ...] chunk_embeddings model.encode(chunks, convert_to_tensorTrue) # 得到向量列表4.2 索引构建知识的“抽屉柜”得到所有文本块的向量后我们需要把它们存储起来并建立一个能快速查找的索引。这就是向量数据库的工作。你可以把它想象成一个特殊的图书馆。传统图书馆按书名或作者索引而向量数据库按“内容的意思”索引。当你提问“年假怎么请”它不会去找包含“年假”、“请”这些关键词的书籍而是计算你问题的向量然后找出整个图书馆里和这个向量最“像”距离最近的那些书页。轻量级选择对于我们这个单文档案例完全不需要动用Milvus、Pinecone这类重型数据库。Chroma或FAISS这类轻量级、内存或本地存储的库就足够了。# 伪代码使用Chroma import chromadb chroma_client chromadb.PersistentClient(path./handbook_db) # 数据持久化到本地 collection chroma_client.create_collection(nameemployee_handbook) # 将文本块、其对应的向量以及元数据如来源页码存入集合 collection.add( embeddingschunk_embeddings.cpu().numpy(), # 向量 documentschunks, # 原始文本 metadatas[{page: i//101} for i in range(len(chunks))], # 假设元数据记录页码 ids[fchunk_{i} for i in range(len(chunks))] # 每个块的唯一ID )踩坑点2向量模型的领域适配性通用嵌入模型在特定领域如医疗、法律、金融可能表现不佳。例如“期权”在员工手册里指“股票期权”在通用语料中可能更接近“选择权”向量表示会有偏差。解决方案如果对精度要求极高可以考虑用公司内部数据对开源嵌入模型进行微调或者使用领域专用的嵌入模型。5. 第三步问答流程——检索、增强、生成现在我们的“图书馆”建好了。当一个新问题到来时RAG的完整流程开始运转。5.1 检索找到最相关的“书页”用户提问“我入职满一年了能休多少天年假”向量化问题使用同样的嵌入模型将用户问题转换为查询向量。相似度搜索在向量数据库中搜索与查询向量最相似的K个文本块例如Top-3或Top-5。这就是“检索”环节。query 我入职满一年了能休多少天年假 query_embedding model.encode([query], convert_to_tensorTrue) results collection.query( query_embeddingsquery_embedding.cpu().numpy(), n_results3 # 返回最相关的3个文本块 ) retrieved_chunks results[documents][0] # 取回的相关文本块列表5.2 增强给模型“划重点”检索到的文本块就是我们要“增强”给模型的知识。我们需要把它们和用户问题一起精心组装成一个“提示词”送给大模型。一个经典的提示词模板如下你是一个专业的员工助手请严格根据提供的公司制度内容回答用户问题。 如果提供的材料中没有相关信息请直接回答“根据现有资料无法回答此问题”不要编造信息。 提供的相关制度内容如下[这里插入检索到的文本块1][这里插入检索到的文本块2][这里插入检索到的文本块3]用户的问题是{用户问题} 请根据上述材料回答这个环节的关键是上下文管理。检索到的文本块总长度可能超过模型上下文限制我们需要进行截断或筛选。同时清晰的指令“严格根据...”、“不要编造”对于抑制幻觉至关重要。5.3 生成让模型“有据可依”最后将组装好的提示词发送给大模型如GPT-4、Claude或开源的Qwen、ChatGLM让它生成最终答案。因为答案的“素材”已经限定在提供的几个文本块中模型的任务更像是“信息整合与转述”而非“凭空创造”从而大大提高了答案的准确性和可靠性。踩坑点3检索结果不相关这是RAG系统失败的最常见原因。可能因为问题表述与文档表述差异大用户问“病假扣钱吗”文档写的是“病假期间工资发放标准”。虽然语义相近但向量匹配可能不理想。解决方案引入查询重写或查询扩展。例如用大模型将原始问题“我入职满一年了能休多少天年假”重写或扩展为“年假天数规定”、“工龄与年假天数关系”、“入职一年年假标准”。用多个查询去检索然后合并结果。多路召回不要只依赖向量检索。可以结合关键词检索如BM25。向量检索负责语义匹配关键词检索保证字面匹配。两者结果融合如加权平均能有效提升召回率。6. 第四步超越简单RAG——让系统更健壮一个基础的RAG管道已经搭建完成。但对于生产环境我们还需要考虑更多。6.1 重排序给检索结果“排座次”向量检索返回的Top-K个结果是按相似度分数从高到低排列的。但这个分数可能不准排第一的未必是最相关的。重排序模型就是一个更精细的“裁判”它专门判断“问题”和“一个文本块”的相关性通常比嵌入模型更准确。流程变为向量检索召回Top-10 - 重排序模型对这10个结果重新打分排序 - 取Top-3送入生成环节。 常用的重排序模型如BAAI/bge-reranker-large。这步能显著提升最终注入上下文的资料质量。6.2 评估你的RAG系统真的靠谱吗不能只靠人工测试。需要建立评估体系检索评估命中率是否检索到了正确答案所在的文本块、平均排名正确答案排第几。生成评估忠实度答案是否严格源自检索内容有没有“无中生有”答案相关性答案是否直接回答了问题信息完整性是否包含了所有必要信息我们可以构造一批测试问题并标注标准答案所在的文档块用来自动化评估检索效果。对于生成效果可以结合基于规则的检查如答案中是否包含文档未出现的实体和基于模型的评估用GPT-4等判断答案质量。6.3 Agentic RAG让RAG“主动思考”这是更前沿的方向。传统的RAG是被动的用户问系统检索-生成。Agentic RAG引入了智能体Agent的概念让系统能主动决策。 例如面对复杂问题“对比一下年假和病假在申请流程和薪资待遇上的区别”子问题分解Agent将问题拆解为“年假申请流程”、“病假申请流程”、“年假薪资”、“病假薪资”。计划与执行为每个子问题分别执行检索。综合将各子答案汇总生成一个对比性的最终答案。这相当于给RAG系统加了一个“大脑”使其能处理多跳问答和复杂查询。7. 实战避坑指南与心得走完整个流程你会发现RAG的每个环节都有“魔鬼在细节中”。心得1文本切分是“地基”地基歪了楼就塌。不要迷信固定长度。一定要结合文档结构。对于手册优先按章节、条款切分。切完后人工抽样检查看看关键信息是否被切断每个块的语义是否独立完整。这是一个需要反复调试的过程。心得2检索不等于回答相关不等于正确。检索到相关文档只成功了60%。剩下的40%在于1检索到的文档是否包含精确答案2模型是否正确理解并提取了答案。因此评估必须拆分为“检索评估”和“生成评估”两部分。心得3提示词工程是“指挥棒”。指令必须清晰、强硬。除了要求“严格依据材料”还可以加入“如果材料中没有请说不知道”、“请引用材料中的具体描述”等。对于事实性问答甚至可以要求模型以“根据《员工手册》第X章...”开头强制其锚定来源。心得4简单场景轻量上阵。我们这个案例完全可以用LangChain/LlamaIndex这类框架快速搭建原型它们封装了切分、向量化、检索的通用流程。但在理解原理后对于定制化要求高的生产系统往往需要脱离框架自建管道以获得更好的控制和性能。心得5RAG不是银弹。它主要解决的是知识更新和减少幻觉的问题。但如果你的知识本身矛盾、模糊或者问题需要大量外部常识和复杂推理RAG也会力不从心。它最适合的还是基于清晰、结构化文档的精确事实问答。最后回到我们开头的案例。如果当时用了RAG流程会是问题 - 在向量化的手册片段中检索 - 找到“差旅报销标准”最新版章节 - 将该章节文本增强给GPT - GPT生成答案。结果将是精准的、有出处的财务同事也就不会摇头了。构建一个可靠的RAG系统更像是一个数据工程和提示词工程的精细活而不是魔法。理解数据文档的特性精心设计每一个处理环节持续地评估和迭代才能让AI真正成为你业务中可信的知识伙伴。
返回列表