尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从零构建RAG问答系统:核心架构、代码实现与优化实战

从零构建RAG问答系统:核心架构、代码实现与优化实战 1. 项目概述为什么RAG是当前AI应用开发的核心最近和不少同行交流发现一个挺有意思的现象无论是刚入行的新人还是从Java、前端转过来的资深工程师大家聊到AI应用开发第一个想动手实践的项目几乎都绕不开RAG。这背后其实反映了一个很实在的需求——我们手里有大量的文档、知识但怎么让大模型真正“理解”并“用好”这些知识而不是让它凭空瞎编RAG检索增强生成技术恰好就是解决这个“最后一公里”问题的钥匙。简单来说RAG就是一个“先查资料再回答问题”的智能流程。它不像传统的聊天机器人那样完全依赖模型训练时学到的、可能已经过时的知识而是实时地从你指定的知识库比如公司内部文档、产品手册、法律条文里检索出最相关的信息然后把这些信息作为上下文喂给大模型让它基于这些确凿的依据来生成答案。这样做出来的应用回答的准确性、专业性和可信度都会大幅提升特别适合做智能客服、企业知识库问答、法律咨询、教育辅导这类对事实准确性要求高的场景。我之所以想从零开始带大家走一遍RAG应用的开发全流程是因为我发现网上很多教程要么只讲概念要么直接丢出一个复杂的框架让你配置中间的关键决策点和踩坑经验却很少提及。这次我们就抛开那些厚重的框架用最核心的组件一步步搭建一个能跑起来、且效果不错的RAG问答应用。你会清晰地看到从文档处理、到向量检索、再到与大模型交互的每一个环节理解每个技术选型背后的“为什么”并掌握那些只有真正做过项目才会知道的实操技巧。2. 核心架构设计拆解RAG的四大支柱要搭建一个健壮的RAG应用不能只盯着“检索”和“生成”这两个词。我们需要一个清晰的架构把整个过程模块化。一个典型的、可工程化的RAG系统可以分解为四个核心支柱它们环环相扣共同决定了最终应用的效果。2.1 知识切片与向量化把“书”变成“卡片”这是整个流程的起点也是最容易埋下隐患的一步。你的原始知识PDF、Word、网页就像一本厚厚的书直接扔给模型是没法高效“阅读”的。我们需要把书拆成一张张有意义的“卡片”即文本切片并为每张卡片生成一个“数字指纹”即向量嵌入。知识切片Chunking这里的关键在于平衡。切片太大检索时可能包含无关信息干扰模型切片太小又会丢失完整的上下文语义。我常用的策略是“重叠滑动窗口法”。比如设置切片大小为500个字符但让相邻切片重叠100个字符。这样能保证即使一个概念被切在了边界也能在相邻片段中找回上下文。对于结构清晰的文档如Markdown可以优先按标题进行语义切片这比单纯按固定长度切分效果要好得多。注意千万不要忽视文档的预处理。PDF中的换行符、多余空格、页眉页脚都会污染你的文本。一个简单的正则表达式清洗步骤能为你后续省去大量调试时间。向量化Embedding这是将文本转化为计算机可理解、可计算的形式。你需要选择一个合适的嵌入模型。对于中文场景text-embedding-3-small、BGE-M3或M3E都是经过广泛验证的选择。选择时主要考虑几点支持的语言、生成的向量维度影响存储和计算成本、以及在你的领域数据上的表现。将切片后的文本通过嵌入模型转换得到的就是一串高维度的数字向量它表征了这段文本的语义。2.2 向量数据库与检索建立高效的“图书馆”生成了海量的向量“卡片”后我们需要一个专门的“图书馆”来存放和快速查找它们这就是向量数据库。向量数据库选型市面上选择很多比如Chroma轻量、易上手、Qdrant性能强劲、功能丰富、Weaviate自带向量化模块、Milvus适合大规模生产环境。对于从零开始的实践项目我推荐从Chroma入手它几乎零配置用几行代码就能跑起来让你快速聚焦于RAG流程本身而不是折腾数据库部署。检索召回当用户提出一个问题时我们首先将这个问题也转化为向量使用同样的嵌入模型然后去向量数据库中寻找与它语义最相似的文本切片。这个过程通常使用余弦相似度或点积来计算向量间的距离。数据库会返回相似度最高的前k个结果比如前5个这就是“多路召回”中的“一路”——基于向量的语义召回。2.3 多路召回与重排序从“找到”到“找对”单纯依靠向量相似度检索有时候并不够精准。比如用户问“2024年公司年假政策”向量检索可能找到的是2023年甚至更早的政策因为语义很相似。为了提高召回结果的相关性我们引入“多路召回”和“重排序”。多路召回除了向量检索我们还可以并行使用其他检索方式。最典型的就是关键词检索如BM25算法。BM25不关心语义只关心词频它能很好地召回那些包含用户问题中关键字的文档。将向量召回的结果和关键词召回的结果合并就得到了一个更丰富的候选集。有时候还可以加入基于元数据如文档日期、作者、类型的过滤召回。重排序Rerank合并后的候选文档可能多达几十个我们需要一个更精细的模型来对它们进行“精排”选出最相关的那几个送给大模型。这就是重排序模型的任务。它通常是一个专门的、参数较小的交叉编码器模型能够更准确地计算查询和文档之间的相关性分数。比如bge-reranker系列就是常用的选择。经过重排序后我们取Top N如3个最相关的文档片段作为生成答案的上下文。2.4 提示工程与大模型生成交付最终答案这是最后一步也是直接面向用户的一步。我们将精心筛选出的上下文和用户问题按照特定的格式组织成一个“提示词”Prompt发送给大语言模型LLM让它生成最终答案。提示词设计这是影响答案质量的关键。一个糟糕的提示词会让模型忽略上下文自己胡编。一个有效的提示词模板通常包含系统指令定义模型的角色和行为准则。例如“你是一个专业的客服助手请严格根据提供的上下文信息回答问题。”上下文清晰标注出我们提供的参考文档。例如“参考上下文\n{{context}}\n”用户问题明确指出的问题。回答要求给出格式、风格等限制。例如“如果上下文无法回答问题请直接说‘根据已知信息无法回答该问题’不要编造信息。”大模型选型与调用你可以使用OpenAI的GPT系列、Anthropic的Claude或者开源的Qwen、ChatGLM等。对于事实性强的RAG问答我建议优先考虑在“遵循指令”和“拒绝幻觉”方面表现较好的模型。通过API调用这些模型将构造好的提示词传入即可得到生成的答案。3. 从零开始手把手搭建RAG问答应用理论讲得再多不如动手做一遍。下面我将用一个具体的例子——基于一份产品说明书构建问答助手来演示完整的开发流程。我们将使用Python作为主要语言。3.1 环境准备与依赖安装首先创建一个干净的Python虚拟环境是个好习惯。然后安装我们需要的核心库。# 创建并激活虚拟环境可选 python -m venv rag_env source rag_env/bin/activate # Linux/Mac # rag_env\Scripts\activate # Windows # 安装核心依赖 pip install chromadb # 向量数据库 pip install sentence-transformers # 用于本地嵌入模型这里以BGE为例 # pip install openai # 如果需要使用OpenAI的嵌入和生成模型 pip install pypdf2 # 用于解析PDF文档 pip install langchain # 可选但它的文档加载器和文本分割器非常方便 pip install tiktoken # 用于文本分词计数这里我选择了Chroma作为向量数据库sentence-transformers来运行本地的BGE嵌入模型这样整个流程可以完全在本地运行无需API密钥和网络开销更适合学习和初步验证。langchain是一个优秀的框架它封装了很多工具链但为了理解底层原理我们初期会尽量使用其组件而非完整框架。3.2 文档加载与预处理假设我们有一个名为product_manual.pdf的产品说明书。第一步是把它读进来并转换成纯文本。import PyPDF2 import re def load_and_extract_text(pdf_path): 加载PDF并提取文本 text with open(pdf_path, rb) as file: reader PyPDF2.PdfReader(file) for page_num in range(len(reader.pages)): page reader.pages[page_num] text page.extract_text() return text raw_text load_and_extract_text(product_manual.pdf) print(f原始文本长度{len(raw_text)} 字符) # 文本清洗移除多余换行、空格处理特殊字符 def clean_text(text): # 合并因PDF解析导致的错误换行小写字母后跟换行再接小写字母 text re.sub(r([a-z])-\n([a-z]), r\1\2, text) # 将多个连续换行符替换为一个空格 text re.sub(r\n, , text) # 将多个连续空格替换为一个空格 text re.sub(r\s, , text) return text.strip() cleaned_text clean_text(raw_text) print(f清洗后文本长度{len(cleaned_text)} 字符)这个清洗步骤至关重要凌乱的文本会严重影响后续切片和嵌入的质量。正则表达式的模式需要根据你的具体文档格式进行调整。3.3 文本切片与向量化存储接下来我们把清洗后的长文本切成小块并生成向量存入数据库。from sentence_transformers import SentenceTransformer import chromadb from chromadb.config import Settings # 1. 文本切片 def split_text_into_chunks(text, chunk_size500, chunk_overlap100): 使用简单的滑动窗口进行文本切片 chunks [] start 0 text_length len(text) while start text_length: end start chunk_size chunk text[start:end] chunks.append(chunk) start chunk_size - chunk_overlap # 滑动窗口设置重叠 return chunks text_chunks split_text_into_chunks(cleaned_text, chunk_size500, chunk_overlap100) print(f共切分出 {len(text_chunks)} 个文本块。) print(示例块, text_chunks[0][:200]) # 2. 加载嵌入模型 embed_model SentenceTransformer(BAAI/bge-small-zh-v1.5) # 使用一个较小的中文模型 print(嵌入模型加载完毕。) # 3. 生成向量 chunk_embeddings embed_model.encode(text_chunks, normalize_embeddingsTrue) # 归一化便于余弦相似度计算 print(f已生成 {len(chunk_embeddings)} 个向量每个向量维度为 {chunk_embeddings.shape[1]}。) # 4. 创建并持久化向量数据库 client chromadb.PersistentClient(path./chroma_db) # 数据将保存在本地chroma_db文件夹 collection client.create_collection(nameproduct_manual) # 为每个块添加元数据例如来源和序号方便追溯 for i, (chunk, embedding) in enumerate(zip(text_chunks, chunk_embeddings)): collection.add( embeddings[embedding.tolist()], documents[chunk], metadatas[{source: manual.pdf, chunk_id: i}], ids[fchunk_{i}] ) print(向量数据已全部存入Chroma数据库。)这里有几个关键点chunk_size和chunk_overlap需要根据你的文档类型和内容调整。技术文档可能需要更大的chunk_size来保持代码段或逻辑的完整。嵌入模型encode时设置normalize_embeddingsTrue这样生成的向量模长为1后续计算余弦相似度就简化为向量点积效率更高。存储时除了向量和文本还把metadata也存了进去。这在后续调试和展示引用来源时非常有用。3.4 实现检索与重排序逻辑现在我们的“图书馆”建好了。当用户提问时我们需要实现检索和精排的流程。# 首先我们需要一个重排序模型。这里以bge-reranker为例需要额外安装 # pip install FlagEmbedding from FlagEmbedding import FlagReranker reranker FlagReranker(BAAI/bge-reranker-v2-m3, use_fp16True) # 使用FP16加速 def retrieve_and_rerank(query, collection, embed_model, reranker, top_k_retrieve10, top_k_final3): 检索并重排序 :param query: 用户问题 :param collection: Chroma集合 :param embed_model: 嵌入模型 :param reranker: 重排序模型 :param top_k_retrieve: 初步检索数量 :param top_k_final: 最终返回数量 :return: 精排后的相关文档列表 # 1. 将用户问题转换为向量 query_embedding embed_model.encode([query], normalize_embeddingsTrue)[0] # 2. 从向量数据库进行语义检索 results collection.query( query_embeddings[query_embedding.tolist()], n_resultstop_k_retrieve, include[documents, metadatas, distances] ) retrieved_docs results[documents][0] retrieved_metas results[metadatas][0] if not retrieved_docs: return [] # 3. 使用重排序模型对检索结果进行精排 # 构建 (query, document) 对 pairs [[query, doc] for doc in retrieved_docs] # 计算相关性分数 scores reranker.compute_score(pairs, normalizeTrue) # normalizeTrue将分数归一化到0-1之间 # 4. 根据分数排序选取Top N scored_docs list(zip(retrieved_docs, retrieved_metas, scores)) scored_docs.sort(keylambda x: x[2], reverseTrue) # 按分数降序排列 final_docs scored_docs[:top_k_final] return final_docs # 测试检索函数 test_query 这款产品如何进行初次充电 relevant_chunks retrieve_and_rerank(test_query, collection, embed_model, reranker) print(f针对问题{test_query}检索到 {len(relevant_chunks)} 个最相关片段) for i, (doc, meta, score) in enumerate(relevant_chunks): print(f\n--- 片段 {i1} (相关性分数{score:.4f}) ---) print(doc[:300]) # 打印前300字符预览这个retrieve_and_rerank函数是RAG系统的核心引擎。它先做语义召回再做精细排序。重排序模型compute_score返回的分数越高代表该文档与问题的相关性越强。通过这个流程我们就能确保送给大模型的上下文是质量最高、最相关的几段文本。3.5 构建提示词并调用大模型生成答案最后我们将筛选出的上下文和用户问题组合调用大模型生成最终答案。这里我们以调用开源模型Qwen2.5-7B-Instruct的本地API为例假设已通过Ollama或类似工具部署。import requests import json def build_rag_prompt(contexts, question): 构建RAG提示词 context_str \n\n.join([f[参考内容 {i1}]: {ctx} for i, (ctx, _, _) in enumerate(contexts)]) prompt f你是一个专业的产品支持助手。请严格根据以下提供的参考内容来回答问题。如果参考内容中没有足够的信息来回答问题请直接说“根据已知信息无法回答该问题”不要编造任何信息。 参考内容 {context_str} 用户问题{question} 请根据上述参考内容用中文给出准确、清晰的回答 return prompt def generate_answer_with_llm(prompt, api_urlhttp://localhost:11434/api/generate): 调用本地LLM API生成答案 payload { model: qwen2.5:7b, # 根据你本地部署的模型名称修改 prompt: prompt, stream: False, options: { temperature: 0.1, # 低温度使输出更确定更适合事实问答 top_p: 0.9 } } try: response requests.post(api_url, jsonpayload, timeout60) response.raise_for_status() result response.json() return result.get(response, ).strip() except requests.exceptions.RequestException as e: return f调用模型API时出错{e} # 整合流程从问题到答案 def rag_qa_pipeline(question): print(f\n用户问题{question}) # 1. 检索与重排序 relevant_info retrieve_and_rerank(question, collection, embed_model, reranker, top_k_final3) if not relevant_info: return 抱歉在知识库中未找到相关信息。 # 2. 构建提示词 prompt build_rag_prompt(relevant_info, question) # print(调试 - 生成的提示词预览, prompt[:500]) # 调试时可打开 # 3. 调用LLM生成答案 answer generate_answer_with_llm(prompt) # 4. 可选附上引用来源 source_refs [meta.get(chunk_id, N/A) for _, meta, _ in relevant_info] answer f\n\n回答依据自知识库片段{source_refs} return answer # 运行完整的问答流程 final_answer rag_qa_pipeline(这款产品如何进行初次充电) print(\n 最终答案 ) print(final_answer)至此一个具备核心功能的RAG问答应用就完成了。它包含了从文档处理、向量化存储、检索重排序到提示生成和答案合成的完整链路。你可以通过修改generate_answer_with_llm函数中的API端点轻松切换为OpenAI GPT、Claude或其他任何兼容API的大模型。4. 效果优化与工程化考量一个能跑起来的Demo只是起点要让RAG应用真正可用、可靠还需要在以下几个方向深耕。4.1 检索效果优化实战检索是RAG的“粮草”检索不准后续生成再好也是空中楼阁。多路召回策略融合我们之前实现了语义向量召回。可以引入关键词召回如使用rank_bm25库来增强对专有名词、型号数字的命中率。from rank_bm25 import BM25Okapi import jieba # 用于中文分词 # 假设我们已有一个所有文本块的列表all_chunks tokenized_corpus [list(jieba.cut(chunk)) for chunk in all_chunks] bm25 BM25Okapi(tokenized_corpus) def hybrid_retrieval(query, collection, embed_model, bm25_index, all_chunks, top_k_vec10, top_k_bm2510, top_k_merge15): 混合检索向量检索 BM25关键词检索 # 向量检索 query_embedding embed_model.encode([query], normalize_embeddingsTrue)[0] vec_results collection.query( query_embeddings[query_embedding.tolist()], n_resultstop_k_vec, include[documents, metadatas] ) vec_docs vec_results[documents][0] vec_metas vec_results[metadatas][0] vec_set set([doc for doc in vec_docs]) # BM25检索 tokenized_query list(jieba.cut(query)) bm25_scores bm25.get_scores(tokenized_query) top_bm25_indices bm25_scores.argsort()[-top_k_bm25:][::-1] bm25_docs [all_chunks[i] for i in top_bm25_indices] # 这里简化处理假设meta信息可以通过索引关联 bm25_set set(bm25_docs) # 合并并去重 combined_docs list(vec_set.union(bm25_set)) # 需要根据合并后的文档找回对应的元数据这里省略具体实现... return combined_docs[:top_k_merge] # 返回合并后的文档列表混合检索能有效应对“语义相似但关键词不匹配”或“关键词匹配但语义不相关”的情况召回更全面的候选集。查询理解与改写用户的问题可能很模糊或很长。可以对查询进行预处理例如提取关键词、进行同义扩展或者用大模型将问题改写成更利于检索的形式。def query_rewrite(original_query, llm_api): 使用大模型对查询进行改写或扩展 rewrite_prompt f请将以下用户问题改写成更易于从知识库中检索到相关信息的版本。可以提取核心关键词或改写成更正式的表述。直接输出改写后的问题。 原问题{original_query} 改写后的问题 rewritten call_llm_api(rewrite_prompt, llm_api) # 假设有一个调用LLM的函数 return rewritten.strip() if rewritten else original_query4.2 提示工程进阶技巧提示词是引导大模型的“方向盘”细微的调整可能带来效果的显著提升。结构化上下文与角色扮演在提示词中更清晰地分隔系统指令、上下文和问题。给模型一个明确的角色能更好地约束其行为。def build_advanced_prompt(contexts, question): system_role 你是一个严谨的产品技术支持专家。你的所有回答必须基于用户提供的“参考文档”。参考文档中的信息是绝对权威的。你的回答需要做到 1. 准确直接引用文档中的事实和数据。 2. 清晰分点说明语言简洁。 3. 安全如果文档中没有相关信息必须明确告知用户“文档中未提及此内容”切勿推测或编造。 context_str \n---\n.join([f【文档片段{idx1}】\n{ctx} for idx, (ctx, _, _) in enumerate(contexts)]) prompt f{system_role} 以下是本次咨询可参考的文档内容 {context_str} 用户咨询的问题 {question} 请根据以上文档内容开始你的回答 return prompt少样本示例Few-Shot在提示词中提供一两个输入输出的例子能更有效地教会模型我们想要的回答格式和风格。few_shot_prompt ...系统指令和上下文同上... 示例 用户问题产品的保修期是多久 参考文档【文档片段1】提到“本产品享受自购买日起24个月的全国联保”。 助理回答根据文档本产品的保修期为自购买日起24个月支持全国联保。 现在请回答用户的实际问题 用户问题{question} 助理回答4.3 系统评估与迭代如何知道你的RAG系统是好是坏需要建立评估体系。构建测试集收集一批真实或模拟的用户问题并准备好标准答案或至少是包含答案的文档片段。这些问题应覆盖不同类型事实型、概括型、多跳推理型等。设计评估指标检索相关度人工或使用模型判断检索出的Top K个文档中有多少个是真正相关的。答案准确性将系统生成的答案与标准答案对比可以使用BLEU、ROUGE等文本相似度指标但更重要的是人工评估答案是否忠实于上下文、有无事实错误幻觉。答案有用性答案是否清晰、完整地解决了用户问题。持续迭代根据评估结果反推是哪个环节出了问题。是切片方式不对导致上下文不完整还是检索不够准或者是提示词没写好有针对性地调整参数、策略甚至模型形成一个“构建-评估-优化”的闭环。5. 避坑指南与常见问题排查在开发和优化RAG系统的过程中我踩过不少坑。这里总结一些典型问题和解决方案希望能帮你节省时间。5.1 检索相关但答案不佳问题表现系统检索到的文档片段看起来和问题相关但大模型生成的答案却跑偏了或者干脆说“找不到信息”。排查思路与解决检查提示词这是最常见的原因。你的提示词是否足够强硬地要求模型“基于上下文”回答在提示词开头用醒目的方式如## 指令 ##强调规则。尝试加入“如果上下文没有就说不知道”的强制指令。检查上下文长度和格式一次性给模型喂太多上下文比如超过模型上下文窗口它可能会“忽略”中间部分。确保送进去的上下文总长度在模型能力范围内。同时确保上下文文本是干净的没有乱码或奇怪的格式。检查模型本身有些模型在“遵循指令”和“拒绝幻觉”方面能力较弱。可以换一个在这方面口碑更好的模型如GPT-4、Claude 3或特定的开源指令微调模型试试。启用检索引用在生成答案时要求模型同时输出引用的文档编号或片段。这不仅能增加可信度也便于你调试看模型到底用了哪段上下文。5.2 检索结果不相关问题表现向量数据库返回的文档和用户问题风马牛不相及。排查思路与解决检查嵌入模型你用的嵌入模型是否适合你的文本领域用英文模型处理中文文本效果肯定差。尝试更换或微调嵌入模型。可以用一些标准测试集如MTEB中的相关任务来评估。检查文本预处理和切片垃圾进垃圾出。回顾一下你的文本清洗是否彻底切片是否把完整的句子或语义单元切碎了尝试不同的切片策略按句、按段落、按标题和大小观察对检索效果的影响。尝试混合检索如4.1节所述引入BM25等关键词检索方法与向量检索结果融合往往能提升召回率。调整相似度算法Chroma默认使用余弦相似度也可以尝试点积或欧氏距离。对于归一化后的向量余弦和点积等价。5.3 系统响应速度慢问题表现从提问到出答案等待时间过长。排查思路与解决向量检索优化确保向量数据库建立了索引HNSW、IVF等。对于大规模数据百万级以上索引至关重要。检查Chroma的索引配置。限制检索数量不要一次性检索过多文档如top_k50。经过重排序后通常前3-5个最相关的文档就足够了。减少检索量能直接提升速度。模型调用优化如果使用远程API网络延迟可能是瓶颈。考虑使用响应更快的模型或者对答案生成设置超时限制。对于本地模型确保硬件GPU资源充足。异步与缓存对于高并发场景可以考虑异步处理检索和生成步骤。对于常见问题可以引入缓存机制将问答对缓存起来避免重复计算。5.4 处理复杂查询与多跳推理问题表现用户问题需要结合多个文档片段的信息才能回答例如“对比产品A和产品B的充电时间”但系统只能给出基于单一片段的不完整答案。解决思路迭代检索先检索与问题相关的第一轮文档从这些文档中提取可能涉及到的其他实体或概念再以这些概念为新的查询进行第二轮检索如此迭代。图检索增强如果知识内部有很强的关联性如人物关系、事件流程可以考虑将知识构建成图结构利用图数据库进行关系检索与向量检索结合。让LLM参与查询分解在检索前先用大模型将复杂问题分解成几个简单的子问题分别检索最后再综合所有子答案生成最终回答。这属于Agentic RAG的范畴复杂度更高但能处理更复杂的任务。开发RAG应用是一个不断调优的过程没有一劳永逸的“银弹”。最重要的就是建立评估基准大胆假设小心验证用数据驱动每一次迭代。从这一个简单的管道出发你可以逐步加入更复杂的组件如查询路由、智能体Agent工作流、对话历史管理最终构建出强大而智能的AI应用。
返回列表