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

资讯详情

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

从零搭建RAG语义搜索系统:原理、实践与优化指南

从零搭建RAG语义搜索系统:原理、实践与优化指南 1. 从一盘菜到一套系统为什么“语义搜索”是RAG的灵魂最近在折腾一个内部知识库项目想把公司历年来的技术文档、会议纪要和项目复盘都塞进去让新来的同事能快速找到“当年我们是怎么解决那个诡异的线上OOM问题的”。一开始我天真地以为这不就是个搜索框吗把关键词“OOM”、“内存溢出”、“Full GC”输进去不就完事了结果发现文档里写的是“堆内存持续增长导致频繁Full GC最终服务不可用”而新同事搜的是“Java程序跑着跑着就卡死了”。你看这中间差了多少个“酸辣土豆丝”和“马铃薯做法”的距离这就是传统关键词搜索的尴尬。它像是一个严格的图书管理员你必须说出书名里确切的字眼他才能从书架上给你找到那本书。你说“土豆”他找不到“马铃薯”你说“酸辣”他理解不了“开胃下饭”。而语义搜索更像是一个精通厨艺的老饕。你跟他说“我想吃个酸辣口、脆生生的素菜”他脑子里立马就能映射到“酸辣土豆丝”这道菜甚至还能联想到“凉拌土豆丝”、“炝炒土豆片”是不是也符合你的要求。语义搜索的核心就是让机器理解语言背后的意图和概念而不是机械地匹配字符。RAG检索增强生成技术正是为了解决“让AI回答得更准、更有依据”这个问题而生的。你可以把它想象成一个超级助理。当用户问了一个问题Query这个助理不会凭空捏造答案而是会先冲进一个庞大的资料库你的知识库里快速找到几份最相关的文档Retrieval然后结合这些文档里的信息Augmentation组织成一段通顺、准确的回答Generation。而决定这个助理能否快速找到“对”的文档的关键一步就是检索。如果检索回来的都是一堆“番茄炒蛋”的菜谱那无论后面的生成模型多强大它也做不出“酸辣土豆丝”。所以在RAG的架构里语义搜索的质量直接决定了整个系统的天花板。今天我就抛开那些复杂的框架和术语手把手带你从零搭建一个最核心、最本质的语义搜索模块。我们不依赖LangChain、LlamaIndex这些“全家桶”就用最基础的组件把“从马铃薯到酸辣土豆丝”这条语义理解的路打通。你会发现理解了底层原理再去用那些高级框架感觉会完全不一样。2. 庖丁解牛拆解一个RAG语义搜索系统的核心部件要搭建一个可用的语义搜索系统我们得先搞清楚它需要哪些“器官”。一个最小化的、不依赖现成向量数据库的RAG语义搜索流程可以拆解为下图所示的几个核心环节flowchart TD A[“原始知识文档brPDF/Word/TXT等”] -- B[“文本预处理与切片br清洗、分段、标准化”] B -- C[“文本转向量brEmbedding 模型”] C -- D[“向量存储br内存/文件/向量数据库”] E[“用户提问Query”] -- F[“Query转向量br同款Embedding模型”] F -- G[“向量相似度检索br如余弦相似度”] G -- H[“召回Top-K相关片段”] D -- G H -- I[“可选重排序br精排模型”] I -- J[“最终检索结果br送入LLM生成答案”]接下来我们就像解剖一样把图中每个环节的“为什么”和“怎么做”讲清楚。2.1 知识切片为什么不能把整本《中华菜谱》直接扔进去想象一下你的知识库是一本厚厚的《中华菜谱》。用户问“酸辣土豆丝要放醋吗” 如果你的搜索单元是整本书那么系统需要计算“酸辣土豆丝要放醋吗”和整本《中华菜谱》的相似度。这就像大海捞针不仅计算量大而且即便找到了最相关的“川菜”章节返回的也是一整章几十页的内容里面包含了鱼香肉丝、宫保鸡丁等无关信息这会严重干扰后续生成答案的LLM。因此我们必须把“书”切成适合入口的“段落”或“句子”这个过程叫文本切片Chunking。切片的目标是让每个切片在语义上尽可能独立、完整并且大小适中。常见的切片策略有固定长度重叠切片这是最常用、最稳妥的方法。比如设定每个切片500个字符切片之间重叠50个字符。重叠是为了防止一个完整的句子或概念被生硬地切在两段导致语义断裂。就像切香肠每一片大小均匀且中间有一点重叠保证完整性。按语义分割利用标点、换行或更高级的NLP句子分割模型在自然段落或句子边界处切割。这更符合人类阅读习惯但对文档格式要求高。递归分割先尝试按大段落分如果段落太长再按句子分直到满足大小要求。这是一种混合策略平衡了完整性和长度。实操心得对于技术文档、Wiki固定长度重叠切片如chunk_size500, chunk_overlap50基本够用简单有效。但对于法律合同、小说等强逻辑连贯文本需要更精细的语义分割。一个关键技巧是切片后一定要人工随机抽查一些切片看看它们是否还保有独立的、可理解的语义。如果发现一个切片以半句话开头或者一个核心概念被拆得七零八落就需要调整你的切片策略了。2.2 文本转向量让机器“读懂”文字的魔法Embedding这是语义搜索的“灵魂”步骤。计算机不认识文字只认识数字。我们需要一种方法把一段文字无论是知识切片还是用户问题转换成一串有意义的数字即向量Vector或嵌入Embedding。一个好的Embedding模型会让语义相似的文本其对应的向量在数学空间里的“距离”也很近。比如“酸辣土豆丝”和“马铃薯做法”的向量就应该比“酸辣土豆丝”和“红烧排骨”的向量更接近。这个“距离”通常用余弦相似度Cosine Similarity来衡量值越接近1表示越相似。如何选择Embedding模型本地部署 vs. 在线API如果你追求数据隐私、离线可用和低成本选本地模型如BGE、text2vec系列。如果怕麻烦追求效果和易用性可以用OpenAI的text-embedding-ada-002或智谱、百度的Embedding API。模型维度常见的有384维、768维、1024维等。维度越高通常表征能力越强但计算和存储开销也越大。对于千万级以下的数据量768维是一个很好的平衡点。中文适配性如果你的资料主要是中文务必选择针对中文优化过的模型如BGE-zh、text2vec-large-chinese。这里我们以开源的BGE模型为例展示如何将文本转化为向量。# 示例使用 sentence-transformers 库背后是BGE等模型生成Embedding from sentence_transformers import SentenceTransformer # 1. 加载模型首次运行会自动下载 # 这里选用 BAAI/bge-small-zh-v1.5一个轻量级且效果不错的中文模型 model SentenceTransformer(BAAI/bge-small-zh-v1.5) # 2. 准备我们的“知识”和“问题” knowledge_chunks [ 酸辣土豆丝是一道经典川菜主要原料是土豆、辣椒和醋口感酸辣脆爽。, 马铃薯又称土豆是一种全球广泛种植的块茎类粮食作物富含淀粉。, 红烧排骨的做法需要先将排骨焯水然后用糖色炒制最后加水慢炖。 ] user_query 土豆能做哪些开胃的菜 # 3. 生成向量 # 模型会将所有文本一次性编码为向量效率很高 knowledge_embeddings model.encode(knowledge_chunks) query_embedding model.encode([user_query])[0] # 注意query是单个文本需要包装成列表 print(f知识片段向量形状: {knowledge_embeddings.shape}) # (3, 384) 表示3个片段每个是384维向量 print(f问题向量形状: {query_embedding.shape}) # (384,)运行后knowledge_embeddings就是一个NumPy数组里面存放着三个知识片段的向量。机器现在“看懂”了这些文字并以向量的形式记住了它们。2.3 存储与检索为向量建一个“图书馆”有了向量我们需要把它们存起来以便快速查找。这就是向量数据库如Milvus, Pinecone, Qdrant干的事。但为了理解本质我们先自己实现一个最简单的“内存向量库”。检索的本质就是计算用户问题向量与所有知识向量之间的相似度然后找出最相似的Top K个。import numpy as np from numpy.linalg import norm # 计算余弦相似度 def cosine_similarity(vec_a, vec_b): 计算两个向量的余弦相似度 dot_product np.dot(vec_a, vec_b) norm_a norm(vec_a) norm_b norm(vec_b) return dot_product / (norm_a * norm_b) # 进行检索 def simple_retrieve(query_embedding, knowledge_embeddings, knowledge_chunks, top_k2): 简单的暴力检索适用于数据量不大的情况 :param query_embedding: 用户问题的向量 :param knowledge_embeddings: 所有知识片段的向量矩阵 :param knowledge_chunks: 对应的知识片段文本 :param top_k: 返回最相关的K个结果 :return: 排序后的相似度 文本列表 similarities [] for idx, kb_vec in enumerate(knowledge_embeddings): sim cosine_similarity(query_embedding, kb_vec) similarities.append((sim, knowledge_chunks[idx])) # 按相似度从高到低排序 similarities.sort(keylambda x: x[0], reverseTrue) return similarities[:top_k] # 执行检索 top_results simple_retrieve(query_embedding, knowledge_embeddings, knowledge_chunks, top_k2) print(检索结果) for sim, text in top_results: print(f[相似度: {sim:.4f}] {text})当你运行这段代码你会看到类似这样的输出检索结果 [相似度: 0.6453] 酸辣土豆丝是一道经典川菜主要原料是土豆、辣椒和醋口感酸辣脆爽。 [相似度: 0.3201] 马铃薯又称土豆是一种全球广泛种植的块茎类粮食作物富含淀粉。神奇的事情发生了系统并没有匹配到“开胃”这个词但它通过语义理解知道“酸辣土豆丝”酸辣脆爽符合“开胃的菜”这个描述而“红烧排骨”则完全不相关。这就是语义搜索的魅力。避坑指南暴力检索的局限。上面的simple_retrieve函数遍历了所有向量这叫“暴力搜索”Brute-force。当你的知识库有几十万、上百万条数据时这种方法是不可行的会慢到无法接受。这时就需要专业的向量数据库。它们内部使用了诸如HNSW近似最近邻搜索等高级索引算法能在海量数据中实现毫秒级的检索。所以在实际生产环境中当数据量超过几千条后一定要引入向量数据库。3. 进阶优化让你的搜索从“能用”到“好用”基础的语义搜索跑通了但离“好用”还有距离。用户可能会问得很模糊或者知识库里有大量相似内容。我们需要引入更多策略来提升召回结果的质量。3.1 混合检索关键词搜索与语义搜索的“双保险”语义搜索虽然智能但也有弱点。比如当用户搜索一个非常特定的、罕见的专业术语或产品型号如“LSTM-2024-Pro型号的故障码E105”这个词的Embedding可能训练得不充分导致语义搜索失效。而传统的关键词搜索如BM25算法在这方面却很擅长。混合检索Hybrid Search结合了两者的优点并行执行同时用语义搜索和关键词搜索去查找。结果融合将两者的结果列表按照一定规则如加权分数、RRF进行合并和重新排序。# 假设我们有关键词搜索函数 keyword_search (可以用 whoosh, elasticsearch 或简单的倒排索引实现) # 和语义搜索函数 semantic_search (即我们上面实现的) def hybrid_search(query, knowledge_chunks, knowledge_embeddings, model, alpha0.5, top_k5): 简单的加权分数混合检索 :param alpha: 语义搜索分数权重关键词搜索权重为 (1-alpha) # 1. 语义搜索 query_vec model.encode([query])[0] sem_results simple_retrieve(query_vec, knowledge_embeddings, knowledge_chunks, top_ktop_k*2) # 多取一些 sem_scores {text: score for score, text in sem_results} # 2. 关键词搜索 (这里用简化的词频匹配模拟) def simple_keyword_search(query, chunks): from collections import Counter query_terms set(query) scores {} for chunk in chunks: chunk_terms set(chunk) overlap len(query_terms chunk_terms) scores[chunk] overlap / len(query_terms) if query_terms else 0 return scores kw_scores simple_keyword_search(query, knowledge_chunks) # 3. 分数归一化与融合 all_chunks set(sem_scores.keys()) | set(kw_scores.keys()) combined_scores {} for chunk in all_chunks: sem_score sem_scores.get(chunk, 0) kw_score kw_scores.get(chunk, 0) # 简单加权求和 combined_score alpha * sem_score (1 - alpha) * kw_score combined_scores[chunk] combined_score # 4. 按融合分数排序返回 sorted_results sorted(combined_scores.items(), keylambda x: x[1], reverseTrue) return sorted_results[:top_k] # 测试混合检索 query_test 土豆 淀粉 含量 results hybrid_search(query_test, knowledge_chunks, knowledge_embeddings, model, alpha0.7) print(\n混合检索结果查询‘土豆 淀粉 含量’) for text, score in results: print(f[分数: {score:.4f}] {text})在这个例子中对于查询“土豆 淀粉 含量”语义搜索能抓住“马铃薯...富含淀粉”的语义关键词搜索也能精准匹配“土豆”、“淀粉”这两个词。混合后最相关的结果分数会更高。3.2 重排序让最相关的答案站上“C位”经过初步检索无论是语义、关键词还是混合我们得到了一个Top-K的候选列表。但这个列表的排序可能还不够精准。重排序Re-ranking就像决赛圈的评委用一个更精细、但通常也更耗资源的模型对这几个候选片段进行“一对一”的精细打分重新排列座次。重排序模型通常是交叉编码器Cross-Encoder它会把用户问题和候选文档拼接在一起输入模型让模型直接输出一个相关度分数。这比只用向量点积的语义搜索更准确但速度慢很多所以只用于对少量候选进行精排。# 示例使用一个简单的重排序思路可以用专门的Re-rank模型如bge-reranker # 这里我们用原始的Embedding模型做一个更精细的交互计算来模拟 def simple_rerank(query, candidate_chunks, model, top_k3): 一个简单的重排序模拟将query和每个chunk拼接后再编码计算与query本身的相似度 这比双塔点积更“精细”一些但非标准重排序。 query_embedding model.encode([query])[0] rerank_scores [] for chunk in candidate_chunks: # 将query和chunk拼接 combined_text query [SEP] chunk # 使用分隔符 combined_embedding model.encode([combined_text])[0] # 计算拼接后向量与原始query向量的相似度模拟交互强度 # 注意这是一个非常简化的模拟真正的重排序模型是端到端训练的。 sim cosine_similarity(query_embedding, combined_embedding) rerank_scores.append((sim, chunk)) rerank_scores.sort(keylambda x: x[0], reverseTrue) return rerank_scores[:top_k] # 假设我们第一轮检索到了两个候选 first_round_candidates [chunks[0], chunks[1]] # ‘酸辣土豆丝...’ 和 ‘马铃薯...’ reranked simple_rerank(user_query, first_round_candidates, model) print(\n重排序后结果) for sim, text in reranked: print(f[精排分数: {sim:.4f}] {text})经验之谈重排序是提升RAG答案质量性价比很高的一个环节。对于大多数应用第一轮用高效的向量检索召回10-20个候选再用一个轻量级的重排序模型如BGE-Reranker选出Top-3效果会有显著提升。不要把重排序模型用于第一轮海量检索那是杀鸡用牛刀会严重拖慢系统响应。3.3 查询理解与改写当好用户的“翻译官”用户的问题往往是口语化、不完整的。比如“咋做土豆丝”、“马铃薯有哪些吃法”。直接拿这样的问题去搜索效果可能打折扣。查询理解/改写Query Understanding/Reformulation就是在检索前对用户Query进行预处理。纠错”士豆“ - ”土豆“。扩展”土豆做法“ - ”土豆 烹饪 方法 菜谱“。改写”咋做土豆丝“ - ”土豆丝的制作方法“。生成假设性问题HyDE这是一个高级技巧。让LLM根据用户问题生成一个假设性的答案段落然后用这个段落的向量去检索。因为生成的段落和知识库中的真实答案在语义和表述上可能更接近。# 示例使用LLM进行简单的查询改写需要接入一个LLM API此处为伪代码 def query_rewrite_with_llm(original_query, llm_client): 使用LLM将口语化查询改写成更规范、更适合检索的语句。 prompt f 请将以下用户问题改写成一句更正式、信息更完整的检索查询语句用于在知识库中搜索相关答案。 只输出改写后的句子不要任何解释。 用户问题{original_query} 改写后的查询 # 调用LLM例如 OpenAI GPT, 智谱GLM, 通义千问等 # rewritten_query llm_client.complete(prompt) rewritten_query 土豆有哪些常见的烹饪方法和菜谱 # 模拟输出 return rewritten_query # 在检索前加入改写步骤 original_user_query 咋做土豆丝 better_query query_rewrite_with_llm(original_user_query, llm_clientNone) print(f原始查询{original_user_query}) print(f改写后查询{better_query}) # 然后用 better_query 去进行后续的Embedding和检索4. 工程化落地从Demo到可服务的系统把上面的代码片段拼凑起来我们能跑通一个流程。但要成为一个真正可用的服务还需要考虑很多工程细节。4.1 向量数据库选型与实践当数据量超过内存限制或者需要持久化、高并发检索时必须引入向量数据库。选型考量的核心点特性/数据库MilvusQdrantWeaviatePinecone (托管)部署方式可自建复杂可自建较简单可自建简单全托管无需运维性能极高功能丰富高性能API友好性能好集成GraphQL稳定易用生态与语言丰富Python/Java等Rust/Python 简洁Go/Python 内置模块多Python/JS 最易上手适用场景超大规模、复杂场景对性能和易用性有平衡要求需要结合语义与图检索快速原型、中小规模生产以Qdrant为例一个极简的接入流程部署docker run -p 6333:6333 qdrant/qdrant插入向量from qdrant_client import QdrantClient, models client QdrantClient(hostlocalhost, port6333) client.create_collection(...) # 创建集合类似数据库表 client.upsert(...) # 插入带向量和元数据如原始文本的点检索hits client.search( collection_namemy_knowledge, query_vectorquery_embedding.tolist(), # 输入查询向量 limit5 )向量数据库内部会高效地完成近似最近邻搜索返回结果。4.2 构建完整的RAG检索服务链路一个完整的检索服务不仅仅是搜索还包括数据管道的构建、更新和监控。1. 数据管道Data Pipeline 这是离线的、周期性的任务。你需要编写脚本完成以下流程文档加载从各种来源本地文件、Confluence、Notion、网页读取文档。文本提取与清洗用PyPDF2、python-docx、BeautifulSoup等库提取纯文本去除乱码、无关页眉页脚。切片与向量化应用前面讲的切片策略并用Embedding模型批量生成向量。导入向量库将(向量, 文本片段, 元数据)三元组批量导入向量数据库。元数据可以包括来源文档、页码、创建时间等便于追溯。2. 检索服务Retrieval Service 这是在线的、实时响应的API服务。它接收用户Query并返回相关片段。接收Query通过HTTP API如FastAPI接收请求。查询预处理可选的纠错、改写、扩展。生成Query向量使用与构建时相同的Embedding模型。检索调用向量数据库的搜索接口获取Top-K候选。后处理可选的混合检索分数融合、重排序。返回结果将排序后的文本片段列表通常附带相似度分数和元数据返回给下游的LLM生成模块。3. 更新与监控增量更新当有新文档加入时只处理新文档生成向量后插入数据库。注意有些向量数据库支持动态更新索引。监控指标需要关注检索延迟P99、召回率检索到的相关片段占所有相关片段的比例、准确率检索结果中相关片段的比例。可以人工构造一些测试用例进行定期评估。4.3 效果评测如何知道你的搜索系统好不好搭建好了不能凭感觉说“好像还行”。需要有一些量化的评估手段。人工评估最可靠构造一批有标准答案的问题QA对让系统去检索人工判断返回的片段是否包含了问题的答案。计算命中率。自动化指标检索召回率Retrieval Recall对于一个问题标准答案涉及的知识片段集合是A系统检索到的片段集合是B召回率 |A ∩ B| / |A|。这需要你有标注好的“问题-相关片段”对。MRRMean Reciprocal Rank第一个正确答案出现在检索结果列表中的排名的倒数再对所有问题求平均。这个指标衡量系统把正确答案排在前面的能力。端到端评估直接看最终生成的答案质量。可以用LLM如GPT-4作为裁判对比标准答案和生成答案在事实一致性、信息完整性、相关性等方面打分。踩坑实录评测时最容易犯的错误是数据泄露。确保你的评测问题集Test Set没有在构建向量库的训练数据或Embedding模型的训练数据中出现过否则评测结果会虚高。最好使用自己业务场景下新构造的、从未见过的问题。从“酸辣土豆丝”到“马铃薯做法”我们走完了语义搜索从概念到实践的全过程。这套系统是RAG的基石它的稳健与否直接决定了你的AI助手是“靠谱的专家”还是“胡言乱语的复读机”。理解每个环节背后的“为什么”你就能在遇到问题时快速定位在需要优化时有的放矢。记住没有银弹最好的系统永远是贴合自己业务和数据特点一步步迭代出来的。
返回列表