免费获取学习方案
ARTICLE DETAIL

资讯详情

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

从零搭建RAG智能助理:实战“茴香豆”项目全流程解析

从零搭建RAG智能助理:实战“茴香豆”项目全流程解析 1. 项目概述从“茴香豆”到RAG智能助理的实践之路最近在整理学习笔记正好翻到之前研究RAG技术时的一个实践项目标题就叫“茴香豆”。这名字听起来有点趣味其实它指向的是一个非常具体的技术实现如何从零开始搭建一个属于自己的检索增强生成智能助理。RAG也就是检索增强生成现在可以说是大模型应用落地的标配技术了。它核心要解决的就是大模型“一本正经胡说八道”和知识更新不及时的痛点。简单来说RAG通过外挂一个专属的知识库让大模型在回答问题时先从这个知识库里找到最相关的信息片段作为参考再组织语言回答这样既能保证答案的准确性又能让模型掌握你私有的、最新的知识。这个“茴香豆”项目就是一个典型的RAG系统搭建实战。它不只是一个Demo而是涵盖了从文档处理、向量检索到与大模型集成的完整链路。对于想入门AI应用开发特别是希望将大模型能力与自身业务数据结合的朋友来说走通这样一个项目意义远大于单纯调用API。你会深刻理解数据如何变成模型能“理解”的格式查询如何精准命中知识以及整个流程中那些影响效果的关键“旋钮”都在哪里。接下来我就结合自己的实操笔记把这个过程的思路、步骤和踩过的坑系统地梳理一遍。2. RAG系统核心架构与“茴香豆”设计思路拆解2.1 为什么是RAG核心价值与问题域界定在动手之前我们必须先想清楚为什么要用RAG。直接使用大模型对话比如问它“我司2024年最新的产品政策是什么”它大概率是无法回答的因为这些信息不在它的训练数据里。即使是一些公开知识模型也可能因为训练数据截止日期或“幻觉”问题给出错误答案。RAG的价值就在于它为大模型装上了一双“眼睛”和一个“外部记忆体”。这双眼睛检索器负责在你提供的文档库中快速扫描找到与问题最相关的段落这个记忆体向量数据库则高效存储和索引这些文档内容。“茴香豆”项目的设计目标很明确构建一个轻量级、可复现、效果可控的RAG智能助理原型。它不追求一步到位的企业级复杂功能而是聚焦于打通核心链路让你能清晰地看到数据是如何流动的。整个系统可以抽象为三个核心模块文档处理与索引模块、检索与排序模块、提示工程与生成模块。第一个模块解决“知识怎么存”的问题第二个模块解决“知识怎么找”的问题第三个模块解决“找到了怎么用”的问题。这个清晰的划分是后续一切工作的基础。2.2 “茴香豆”技术栈选型背后的考量技术选型往往决定了项目的上手难度和天花板。在这个项目中我们的选型遵循“轻量、主流、可控”的原则。1. 文档加载与切分LangChain 自定义切分器LangChain几乎是当前大模型应用开发的事实标准框架其DocumentLoader支持PDF、Word、TXT、HTML等多种格式能省去大量解析文件的脏活累活。但LangChain自带的RecursiveCharacterTextSplitter递归字符切分器有时不够灵活。在“茴香豆”里我采用了基于语义的切分策略作为补充。例如对于技术文档我会优先按章节标题Markdown的##或###进行切分以保持上下文的完整性对于普通段落再辅以固定长度重叠overlap的字符切分。这样能更好地平衡检索精度和上下文信息量。2. 向量化模型与向量数据库Sentence Transformers Chroma文本转化为向量嵌入是检索的基石。我选择了all-MiniLM-L6-v2这个模型它来自Sentence Transformers库。选它的理由很实在模型大小仅80MB左右在CPU上也能跑出不错的速度并且在MTEB等通用语义相似度评测榜上表现均衡。对于入门和大多数中文场景它完全够用。如果追求更高精度可以升级为text2vec系列或bge系列的模型。 向量数据库方面ChromaDB以其极简的API和内存/持久化两种模式脱颖而出。它无需单独部署服务几行代码就能集成特别适合原型开发和中小规模知识库万级文档以内。它的核心接口就是add_documents、query直观易懂让我们能把精力集中在效果优化上而不是数据库配置上。3. 大模型接口OpenAI API 或 本地开源模型为了快速验证流程初期直接使用OpenAI的GPT-3.5/4 API是最佳选择稳定且效果有保障。但在“茴香豆”的后期我尝试接入了本地部署的开源模型如ChatGLM3、Qwen等通过FastChat或vLLM提供兼容OpenAI的API接口。这一步的意义在于实现数据闭环和成本可控毕竟长期调用商用API是一笔不小的开销且敏感数据不出本地更安全。4. 前端交互Gradio 或 Streamlit一个可视化的界面能极大提升演示和调试体验。Gradio和Streamlit都能快速构建Web界面。Gradio更轻量专注于机器学习Demo几行代码就能创建一个带聊天框的界面Streamlit则更像一个数据应用框架布局能力更强。在“茴香豆”项目中我选择了Gradio因为它与LangChain的集成更无缝ChatInterface组件开箱即用。3. 从文档到向量知识库构建的魔鬼细节3.1 文档预处理清洗、格式化与结构化很多人以为RAG就是简单地把文档扔进去切分但预处理的质量直接决定了检索的上限。垃圾进垃圾出在这里同样适用。首先格式统一与噪音去除。从不同渠道获得的文档扫描PDF、网页爬虫、Word文件含有大量噪音页眉页脚、页码、无关的广告链接、特殊字符等。我的做法是先用pdfplumber或pypdf2提取PDF文本用python-docx处理Word用BeautifulSoup清理HTML。一个常见的坑是扫描版PDF需要用OCR工具如Tesseract先转文字但这一步会引入大量识别错误需谨慎评估。其次文档结构化解析。这是提升效果的关键。对于技术手册、产品文档这类有明确层级结构的文本我会先用正则表达式或基于规则的解析器识别出章节标题如“1.1 概述”、“第二章 安装”并以此作为元数据metadata记录下来。这样在后续切分时可以尽量保证一个切片包含一个完整的小节避免将一个问题和一个答案切到两个不同的片段中。注意元数据metadata是RAG中的“黄金信息”。除了章节标题还可以包括文档来源、更新时间、作者等信息。在检索时不仅可以按向量相似度排序还可以按元数据过滤比如“只检索2024年更新的产品文档”这能大幅提升答案的时效性和准确性。3.2 文本切分策略长度、重叠与语义边界切分是门艺术。切得太碎检索到的片段缺乏足够上下文模型看不懂切得太长片段会包含无关信息稀释核心内容同时增加模型处理负担和成本。1. 固定长度重叠切分这是最基础的方法。在“茴香豆”中我设置chunk_size500字符数chunk_overlap100。500字符大约是一个自然段到两个自然段的长度能容纳一个相对完整的观点。100字符的重叠是为了防止一个完整的句子或关键信息恰好被切在边界上导致上下文断裂。这个重叠区域就像一个“缓冲区”确保了信息的连续性。2. 语义切分仅按字符长度切分会破坏语义完整性。我引入了semantic-text-splitter库的启发尝试基于句子边界如中文句号、问号、感叹号进行切分并尽量保证每个切片的句子是语义上相对独立的。更高级的做法是使用小型模型计算句子间的语义变化在语义发生较大转折处进行切分但这会显著增加处理时间在原型阶段性价比不高。3. 混合切分策略我的实战经验是先按结构切再按长度微调。例如对于一份API文档首先识别出每个独立的“接口说明”板块通常由接口名称、URL、方法等标题标识将每个板块作为一个大单元。然后在这个大单元内部如果内容很长再使用固定长度重叠的方式进行二次切分。最后为每个切片记录其所属的“接口名称”作为元数据。这样当用户问“用户登录接口的返回值是什么”时检索系统不仅能找到语义相似的片段还能通过元数据快速定位到“用户登录接口”这个章节下的所有相关内容精度更高。3.3 向量化嵌入与索引构建文本切分后就来到了核心的向量化步骤。这里使用的是之前选定的all-MiniLM-L6-v2模型。from sentence_transformers import SentenceTransformer # 加载嵌入模型 embed_model SentenceTransformer(‘sentence-transformers/all-MiniLM-L6-v2‘) # 假设 docs 是切分好的文本片段列表 doc_texts [doc.page_content for doc in docs] # 生成向量嵌入 doc_embeddings embed_model.encode(doc_texts, normalize_embeddingsTrue)关键参数normalize_embeddingsTrue非常重要。它将向量归一化为单位长度这样后续计算余弦相似度就简化为向量点积计算效率最高这也是大多数向量数据库的默认做法。接下来是将向量存入ChromaDB。这里有一个细节连同向量一起存储的还有原始的文本片段chunk和它的元数据metadata。ChromaDB会为每个文档分配一个唯一ID。import chromadb from chromadb.config import Settings # 创建或连接到持久化的ChromaDB client chromadb.PersistentClient(path“./my_chroma_db“) collection client.get_or_create_collection(name“my_knowledge_base“) # 准备批量添加的数据 ids [f“doc_{i}“ for i in range(len(docs))] metadatas [doc.metadata for doc in docs] # 之前准备好的元数据 documents [doc.page_content for doc in docs] # 原始文本 # 添加文档和其嵌入向量 collection.add( idsids, embeddingsdoc_embeddings.tolist(), # 注意转换为list metadatasmetadatas, documentsdocuments )实操心得在构建索引时建议对输入文本进行一次简单的清洗比如去除首尾空白符、合并多个换行符。有时候PDF解析会带来奇怪的换行导致“用户\n登录”和“用户登录”在向量化后产生不必要的差异。另外对于大规模知识库分批batch进行encode和add操作并加入进度提示能更好地管理内存和掌控进程。4. 检索、重排与生成智能问答链路的实现4.1 检索器相似度计算与多路召回当用户提出一个问题Query时第一步是将其转化为向量然后在向量数据库中进行相似度搜索相似度计算通常使用余弦相似度。# 将用户问题转化为向量 query_embedding embed_model.encode([user_question], normalize_embeddingsTrue)[0] # 在集合中进行相似度搜索 results collection.query( query_embeddings[query_embedding.tolist()], n_results5 # 返回最相似的5个片段 )这里的n_results是一个关键超参数。返回太少可能遗漏关键信息返回太多会引入噪音并增加后续处理和模型成本。通常我会设置一个较大的初始值如10然后根据效果调整。单纯的向量相似度检索Dense Retrieval有时会漏掉一些关键词匹配但语义表述不同的重要文档。因此在“茴香豆”中我引入了混合检索的思路稠密检索如上所述基于向量相似度擅长理解语义。稀疏检索如BM25算法基于关键词匹配擅长处理专有名词、术语。 可以将两者的检索结果取并集或按分数融合实现“多路召回”提高召回率。4.2 重排序从“找到”到“找对”检索系统返回了Top K个相关片段但它们的顺序完全基于向量相似度分数这个分数不一定与“对生成最终答案最有帮助”的程度完全一致。这时就需要重排序。重排序器Reranker是一个更精细、通常也更耗资源的模型它会对检索到的候选片段和问题进行一次更深入的交互式打分。一个流行的选择是bge-reranker系列模型。from FlagEmbedding import FlagReranker reranker FlagReranker(‘BAAI/bge-reranker-large‘, use_fp16True) # 使用半精度节省内存 pairs [[user_question, doc] for doc in retrieved_docs] scores reranker.compute_score(pairs, normalizeTrue) # 计算每个问题文档对的得分 # 根据重排序得分对文档重新排序 reranked_docs [doc for _, doc in sorted(zip(scores, retrieved_docs), reverseTrue)]重排序后排名靠前的片段质量通常会有显著提升。在实践中对于精度要求高的场景重排序几乎是必选项。但它会带来额外的延迟因此一种折中方案是先用向量检索召回较多的候选如20个再用重排序器精选出最相关的3-5个送入大模型。4.3 提示工程与答案生成组装上下文与提问这是RAG链路的最后一环也是直接面向用户的环节。我们需要将检索到的最相关文档片段作为上下文Context和用户问题Question一起构造一个提示词Prompt发送给大模型。一个经典且有效的Prompt模板如下你是一个专业的智能助理请严格根据以下提供的上下文信息来回答问题。如果上下文中的信息不足以回答问题请直接说“根据已知信息无法回答该问题”不要编造信息。 上下文信息 {context} 用户问题{question} 请根据上下文信息回答在代码中我们这样实现def build_prompt(context_docs, question): # 将多个文档片段合并为上下文 context “\n\n“.join([doc.page_content for doc in context_docs]) prompt_template “““你是一个专业的智能助理请严格根据以下提供的上下文信息来回答问题。如果上下文中的信息不足以回答问题请直接说“根据已知信息无法回答该问题”不要编造信息。 上下文信息 {context} 用户问题{question} 请根据上下文信息回答”“” return prompt_template.format(contextcontext, questionquestion) # 使用重排序后的前3个文档 top_k_docs reranked_docs[:3] final_prompt build_prompt(top_k_docs, user_question) # 调用大模型 response openai_chat_completion(final_prompt) # 或调用本地模型这里有几个至关重要的细节上下文长度合并的上下文总长度不能超过大模型的上下文窗口限制如GPT-3.5的4K或16K。需要在构建Prompt时计算token数必要时截断最不重要的片段。指令遵循Prompt中必须明确强调“严格根据上下文”这是抑制模型幻觉的关键。引用标注在答案中可以要求模型注明答案来源于哪个文档片段通过元数据中的ID或标题增加可信度。例如在Prompt中加入“请在答案末尾用【来源文档标题】的格式注明出处”。5. 效果评估与迭代优化让“茴香豆”更聪明搭建完基础流程只是第一步要让RAG智能助理真正可用必须进行效果评估和持续优化。5.1 构建测试集与评估指标不能凭感觉说“好像还行”。需要建立一个小的测试集QA对例如从知识库中抽取20-50个问题并准备好标准答案或关键信息点。评估指标可以包括检索精度Top K检索结果中是否包含了能回答问题的正确片段可以计算Hit RateK。答案准确性模型的回答与标准答案在事实层面上是否一致这需要人工或借助更强大的模型如GPT-4进行评判。答案相关性答案是否紧扣问题没有答非所问幻觉率答案中是否出现了上下文未提供的、编造的信息5.2 常见问题排查与优化技巧在实际运行“茴香豆”的过程中我遇到了不少典型问题以下是排查思路和优化方法问题1检索不到相关文档。检查用户问题的向量表示是否合理可以尝试将问题用更完整、更书面化的语言重新表述后检索。优化查询扩展对原始问题进行同义词扩展、或者让大模型生成几个相关的问题用这组问题去检索然后合并结果。优化切分回顾文档切分策略。是不是切得太碎导致关键信息被割裂尝试增大chunk_size或采用语义切分。调整嵌入模型对于专业领域如医学、法律通用嵌入模型可能表现不佳。尝试使用在该领域数据上微调过的嵌入模型或者像bge-large-zh这样在中文上表现更优的模型。问题2检索到了相关文档但答案还是不对或包含幻觉。检查查看最终送入模型的上下文。是不是包含了无关或矛盾的片段Prompt指令是否足够强硬优化引入重排序这是解决此问题最有效的手段之一确保送给模型的是最精华、最相关的片段。优化Prompt在Prompt中增加更严格的约束例如“你必须且只能使用以下上下文中的信息。上下文中的信息是真实可信的请忽略你已有的任何可能与之冲突的知识。”上下文压缩/摘要如果检索到的片段很长且包含冗余可以先用一个较小的模型或大模型本身对每个片段进行摘要再将摘要作为上下文送入减少噪音。问题3回答“根据已知信息无法回答”但明明知识库里有。检查这是典型的“语义鸿沟”问题。用户的问题表述和知识库中的文档表述差异太大。优化对知识库进行数据增强在构建索引时除了原始文本还可以为每个片段人工或自动生成几个可能的问题Question Generation将“问题-片段”对一起存入向量数据库。检索时不仅用用户问题去匹配片段内容也去匹配这些生成的问题。使用HyDE技术让大模型根据用户问题“幻想”一个假设性答案Hypothetical Document Embedding然后用这个假设答案的向量去检索。因为假设答案的表述风格可能更接近知识库文档从而能更好地检索到相关内容。5.3 高级进阶Agentic RAG 与 查询路由当基础RAG跑通后可以探索更高级的模式让“茴香豆”变得更智能。Agentic RAG将RAG系统作为一个工具嵌入到一个智能体Agent的循环中。例如当用户提出一个复杂、多步骤的问题时Agent可以自主规划先检索A文档了解概念再根据结果检索B文档获取具体数据最后综合生成答案。这需要引入如LangChain的Agent框架并定义好RAG工具的调用方式。查询路由不是所有用户查询都需要走RAG流程。系统可以设计一个路由层先判断问题类型如果是简单的问候或通用知识如“你好”、“太阳为什么东升西落”直接让大模型基于自身知识回答。如果是需要最新信息或私有信息的问题如“我司Q3财报要点”、“项目X的架构图在哪里”则触发RAG流程。如果是需要计算或执行某个操作如“计算一下我的报销总额”、“创建一个会议邀请”则路由到相应的工具或函数。 这可以通过训练一个简单的文本分类器或者使用大模型本身进行意图识别来实现。6. 项目部署与工程化思考6.1 从脚本到服务API封装与前端集成开发阶段的代码可能是零散的脚本。为了实用需要将其封装成服务。一个简单的架构是使用FastAPI构建RESTful APIfrom fastapi import FastAPI, HTTPException from pydantic import BaseModel app FastAPI(title“茴香豆RAG智能助理API“) class QueryRequest(BaseModel): question: str top_k: int 5 class QueryResponse(BaseModel): answer: str sources: list[str] # 引用来源 app.post(“/ask“, response_modelQueryResponse) async def ask_question(request: QueryRequest): # 这里集成前面实现的所有步骤检索、重排、生成 # ... answer, source_docs rag_chain.invoke(request.question) return QueryResponse(answeranswer, sources[doc.metadata.get(‘title‘, ‘N/A‘) for doc in source_docs])这样前端如Gradio、微信小程序、企业内部系统就可以通过调用这个API来获取智能问答服务。Gradio的集成非常简单几乎就是一个函数调用。6.2 知识库的更新与维护知识不是静态的。当有新文档加入或旧文档更新时需要支持知识库的增量更新。全量重建最简单但最耗时删除旧集合重新处理所有文档并构建索引。适用于知识库较小或更新不频繁的场景。增量更新更优雅的方式。为每个文档切片计算一个哈希值如MD5当文档更新时只需处理哈希值发生变化的文档并更新向量数据库中对应的条目。这需要更精细的数据管理逻辑。删除处理同样需要支持从知识库中删除特定文档。在ChromaDB中可以根据文档的ID或元数据进行删除操作。6.3 性能、成本与监控性能主要瓶颈在嵌入模型推理和向量检索。对于大规模知识库需要考虑将向量数据库如Chroma部署为独立服务并使用GPU加速嵌入模型。检索时使用近似最近邻搜索ANN算法如HNSW来平衡精度和速度。成本如果使用商用大模型API成本主要来自Token消耗。优化策略包括优化Prompt减少冗余、压缩上下文、对简单问题使用更便宜的模型如GPT-3.5 Turbo、设置使用频率限制等。监控记录每一次问答的日志包括用户问题、检索到的文档、生成的答案、耗时、Token使用量。这有助于分析效果瓶颈、发现常见错误问题Bad Cases并为后续的优化提供数据支持。走完“茴香豆”这个完整的项目你对RAG的理解就不再停留在概念上了。你会清楚地知道一个简单的问答背后是数据预处理、向量化、检索、重排、提示工程等一系列环节的精密协作每一个环节都有优化的空间。这套方法论和实操经验是构建任何更复杂AI应用如智能客服、企业知识中枢、AI编程助手的坚实基础。最重要的是你拥有了一个完全受自己掌控的智能助理原型可以根据需要不断喂养它新的知识让它持续成长真正成为你工作或学习中的得力帮手。
返回列表