多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从零实现RAG语义搜索系统:原理、避坑与实战指南

从零实现RAG语义搜索系统:原理、避坑与实战指南 1. 项目缘起当AI听不懂“人话”时我们该做什么如果你最近尝试过用大模型来查询自己公司的内部文档、或者想让它帮你从一堆技术报告里找点灵感大概率会遇到这样的场景你问“我们上个季度的市场表现怎么样”它给你回了一段关于“市场”的通用论述你问“那个用Python写的、处理JSON日志的脚本在哪”它可能直接给你现场编一段代码出来跟你实际要的那个完全不是一回事。这就是当前大模型应用中最典型也最让人头疼的“幻觉”问题。模型很聪明但它“知道”的仅限于训练时喂给它的通用数据。对于你私有的、最新的、非公开的“领域知识”它一无所知只能靠“猜”和“编”。于是一个能精准理解你问题意图并严格从你指定的知识库中寻找答案的技术就成了刚需。这就是RAG检索增强生成技术爆火的核心原因。但市面上的RAG教程和框架往往一上来就教你装LangChain、配向量数据库、调Embedding模型把流程跑通就结束了。这就像只给了你一把锤子却没告诉你钉子在哪、墙是什么材质、怎么敲才不伤手。结果就是你搭建的系统看似能跑但实际用起来搜索精度差、回答不相关、甚至“一本正经地胡说八道”用户体验还不如直接CtrlF。所以我决定抛开那些重型框架从最本质的原理出发和你一起“从零实现”一个RAG语义搜索系统。我们的目标不是简单地复现一个流程而是要亲手摸清每一个环节的“弦外之音”——为什么文本要这样切分向量检索的“相似”到底是什么意思重排序为什么能大幅提升精度只有把这些底层逻辑吃透你才能真正驾驭RAG让它听懂你的言外之意给出精准可靠的回答。2. 庖丁解牛拆解RAG系统的核心三要素一个完整的RAG系统远不止是“检索生成”的简单拼接。它是一个精密的流水线任何一个环节的粗糙处理都会导致最终结果的失真。我们可以将其核心拆解为三个相互咬合的齿轮知识库的预处理与索引、查询的意图理解与检索、答案的生成与校验。下面我们就来逐一拆解看看每个环节的“弦外之音”是什么。2.1 知识库预处理如何把一本书变成AI能理解的“乐高积木”这是整个流程的基石也是最容易被轻视的一环。很多人直接把整篇PDF或长文档扔给系统结果就是检索效率低下返回的文本块要么太大包含无关信息要么太小丢失关键上下文。核心挑战在于“切片”的粒度与策略。想象一下你要教AI读一本技术手册。如果你按页撕开固定长度切片可能会把一张完整的流程图拦腰截断如果你按章节撕开按标题分割又可能让某个关键概念的解释过于冗长。这里的“弦外之音”是没有一种通用的切片策略最优策略取决于你的数据形态和查询意图。在我的实践中我摸索出了一套“混合切片策略”它比单一方法有效得多基于语义的粗分割首先利用自然语言处理中的句子边界检测和段落识别将文档按自然段落进行初步划分。这保留了最基本的语义完整性。对于Markdown、HTML等结构化文档则优先依据标题H1, H2, H3进行分割。滑动窗口的精细控制对上述分割后的段落应用滑动窗口。例如设置窗口大小为500个字符重叠率为100个字符。这样即使一个关键概念恰好落在段落边界重叠部分也能确保它被完整地包含在至少一个切片中。关键实体的特殊保护在切片前后加入一个简单的规则如果检测到切片边界切分了一个完整的URL、代码块以包裹、或由特定标点如破折号、括号包裹的名词短语则自动调整边界将这些实体完整地保留在一个切片内。# 一个简化的混合切片示例使用Python import re from typing import List def hybrid_chunking(text: str, chunk_size: int500, overlap: int100) - List[str]: 混合切片策略先按段落分再对长段落应用滑动窗口。 # 1. 按换行符分割成初步段落 paragraphs [p.strip() for p in text.split(\n\n) if p.strip()] chunks [] for para in paragraphs: if len(para) chunk_size: # 短段落直接作为一个块 chunks.append(para) else: # 长段落应用滑动窗口 start 0 while start len(para): end start chunk_size chunk para[start:end] # 简单调整边界避免在单词中间切断此处为简化逻辑 if end len(para) and para[end] not in .,;!?\n: # 向前找到最近的空格或标点 adjust_point chunk.rfind( ) if adjust_point ! -1: end start adjust_point 1 chunk para[start:end] chunks.append(chunk) start end - overlap # 设置重叠 return chunks注意这个示例非常基础实际生产中你需要使用更健壮的库如langchain.text_splitter中的RecursiveCharacterTextSplitter并考虑编码、特殊字符等问题。但它的价值在于揭示了核心思想切片是为了在检索精度块小和信息完整性块大之间寻找动态平衡。2.2 向量化与索引让文字拥有“空间感”切片后的文本是字符序列计算机无法直接理解其含义。我们需要将其转化为一种数学表示——向量或称嵌入Embedding。这个过程的“弦外之音”在于一个好的向量应该能让语义相似的文本在向量空间中彼此靠近。这里有两个关键选择Embedding模型这是将文本转为向量的“翻译官”。开源模型如BGE、text2vec、OpenAI的text-embedding-ada-002都是不错的选择。选择时需权衡效果通常更大的模型更好、速度、本地部署成本以及对中文的支持程度。向量数据库这是存储和快速检索海量向量的“图书馆”。ChromaDB轻量易用适合快速原型Milvus、Qdrant、Weaviate则专为生产环境设计支持分布式、持久化等高级特性。一个容易被忽略的细节是向量的归一化Normalization。许多Embedding模型默认输出的向量并非单位向量模长为1。在进行相似度计算如余弦相似度时我们必须先将向量归一化否则计算结果会有偏差。余弦相似度的计算基于向量的夹角而归一化能消除向量长度的影响让相似度分数纯粹反映方向上的接近程度。import numpy as np from sentence_transformers import SentenceTransformer # 初始化模型 model SentenceTransformer(BAAI/bge-small-zh-v1.5) # 一个优秀的中文开源模型 # 生成嵌入向量 texts [什么是人工智能, 机器学习是AI的一个子领域。] embeddings model.encode(texts) # 关键步骤归一化 def normalize_vectors(vectors): norms np.linalg.norm(vectors, axis1, keepdimsTrue) # 防止除零错误 norms np.where(norms 0, 1e-10, norms) return vectors / norms normalized_embeddings normalize_vectors(embeddings) print(f归一化前第一个向量的模长: {np.linalg.norm(embeddings[0]):.4f}) print(f归一化后第一个向量的模长: {np.linalg.norm(normalized_embeddings[0]):.4f}) # 输出应非常接近 1.0将归一化后的向量及其对应的原始文本块存入向量数据库我们的“知识图书馆”就建好了。2.3 查询与检索从“关键词匹配”到“意图理解”当用户提出一个问题时传统的搜索是进行关键词匹配。而RAG的“语义搜索”其“弦外之音”在于理解问题的意图。例如用户问“如何给手机省电”其意图可能是“关闭后台应用”、“降低屏幕亮度”、“开启省电模式”等一系列具体操作的集合。我们的系统会做以下几步查询向量化使用与知识库相同的Embedding模型将用户问题也转化为一个向量。这是保证“苹果和苹果比”的前提不同模型生成的向量空间不同无法直接比较。相似度检索在向量数据库中快速找出与查询向量余弦相似度最高的前K个文本块例如K5。数据库内部通常使用近似最近邻ANN算法如HNSW来在精度和速度间取得平衡。重排序Reranking—— 精度提升的关键第一步的向量检索是“粗筛”它可能因为语义表示的细微偏差而漏掉或误选。重排序是“精筛”它使用一个更精细、但通常也更耗时的交叉编码器Cross-Encoder模型直接计算查询和每一个候选文本块之间的相关性分数并重新排序。# 假设我们已经从向量数据库得到了top_k个初步候选文本 preliminary_results [ {text: 关闭不必要的后台应用可以节省电量。, score: 0.85}, {text: iPhone的电池健康度低于80%建议更换。, score: 0.78}, {text: 调整屏幕自动锁定时间为30秒。, score: 0.75}, ] # 使用一个重排序模型例如 cross-encoder/ms-marco-MiniLM-L-6-v2 from sentence_transformers import CrossEncoder reranker CrossEncoder(cross-encoder/ms-marco-MiniLM-L-6-v2) query 如何给手机省电 candidate_texts [res[text] for res in preliminary_results] # 模型会为每一对 (query, candidate) 打分 pairs [[query, cand] for cand in candidate_texts] rerank_scores reranker.predict(pairs) # 根据新分数重新排序 reranked_results sorted(zip(preliminary_results, rerank_scores), keylambda x: x[1], reverseTrue) print(重排序后结果) for original, new_score in reranked_results: print(f文本: {original[text][:50]}... | 原始向量分: {original[score]:.3f} | 重排序分: {new_score:.3f})在这个例子中虽然第二个结果提到了“电池”但与“如何省电”的操作性意图匹配度可能不如第一个和第三个。重排序模型能更好地捕捉这种细微的语义差别将最相关的结果排到最前面。这是提升最终答案质量性价比最高的一步强烈建议在生产系统中加入。3. 生成与组装让AI“引经据典”而非“凭空捏造”检索到最相关的文本块后我们终于来到了最后一步让大模型生成答案。这里的“弦外之音”是我们必须严格约束模型让它仅基于我们提供的上下文检索到的文本块来回答问题并明确拒绝回答上下文之外的问题。这主要通过精心设计的“提示词工程”来实现。一个糟糕的提示词会让模型忽略上下文重新回到“幻觉”模式一个好的提示词则能引导模型成为一名严谨的“引述者”。# 一个经过实战打磨的RAG提示词模板 def build_rag_prompt(query: str, context_chunks: list) - str: context_str \n\n.join([f[出处 {i1}]: {chunk[text]} for i, chunk in enumerate(context_chunks)]) prompt f你是一个专业的问答助手将严格根据提供的参考资料来回答问题。 如果资料中没有足够信息来回答问题请直接说“根据提供的资料我无法回答这个问题”不要编造信息。 请遵循以下步骤思考 1. 仔细阅读用户问题和所有参考资料。 2. 判断参考资料中是否包含回答问题所需的信息。 3. 如果包含请组织语言用简洁明了的方式回答并尽可能引用出处的编号例如 [出处1]。 4. 如果不包含请直接告知无法回答。 参考资料 {context_str} 用户问题{query} 请开始你的回答 return prompt这个提示词有几个关键点明确角色和规则开头就定下基调强调“严格根据资料”。结构化思考过程引导模型进行逻辑推理而不是直接跳转到生成。格式化上下文给每个文本块编号方便模型引用和溯源也方便我们后期验证。安全的拒绝机制明确给出了无法回答时的标准话术避免了模型强行编造。将构建好的提示词发送给大模型如通过OpenAI API、本地部署的Qwen、ChatGLM等就能得到一个基于可信来源的答案了。4. 实战避坑指南那些教程里不会告诉你的“暗礁”按照上面的流程一个基础的RAG系统就能跑起来了。但要让它在实际业务中稳定、可靠地运行还有无数细节需要打磨。下面是我在多个项目中踩过坑后总结出的核心经验。4.1 检索效果不佳问题可能出在“提问方式”上很多时候我们责怪Embedding模型不好或者切片策略不对但忽略了最前端的一点用户的问题Query本身可能就不适合检索。问题可能太短“代码”、太模糊“那个东西”、或者包含大量指代“他上次说的方案”。解决方案查询重写与扩展。在将用户查询向量化之前先让一个小模型或规则对其进行优化。同义词扩展将“手机”扩展为“智能手机、移动电话”。问题澄清对于模糊查询可以尝试生成几个可能的澄清问题或者直接基于对话历史补充上下文。HyDE假设性文档嵌入这是一个非常巧妙的技术。先让大模型根据问题生成一个假设性的答案然后用这个假设答案的向量去检索。因为假设答案和知识库中的真实答案在语义上会更接近。例如对于“如何给手机省电”模型可能生成“可以通过降低屏幕亮度、关闭后台应用等方式给手机省电。”用这句话去检索效果往往比用原问题更好。# 一个简单的查询扩展示例概念性代码 def query_expansion(query, conversation_history[]): expanded_terms [query] # 1. 同义词扩展可使用词林等资源或小模型 # 此处简化假设有一个同义词字典 synonym_dict {手机: [智能手机, 移动电话], 省电: [节电, 降低功耗]} for word, syns in synonym_dict.items(): if word in query: expanded_terms.extend(syns) # 2. 结合历史上下文如果是多轮对话 if conversation_history: # 简单地将最近一两轮历史拼接 context .join(conversation_history[-2:]) expanded_terms.append(context query) # 3. 返回扩展后的查询列表可以分别检索后合并结果 return expanded_terms # 实际中更复杂的扩展可以使用LLM来生成4.2 答案还是“胡言乱语”检查你的上下文是否“超载”了即使检索到了相关文档如果一次性喂给模型的上下文太长、太杂乱模型也可能无法抓住重点或者被无关信息带偏。这就是“上下文窗口污染”。解决方案动态上下文选择与摘要。设置相关性阈值只将相似度分数高于某个阈值如0.7的文本块放入上下文。低于阈值的宁可不用也不要引入噪声。上下文压缩对于较长的相关文本块可以先让模型对其进行摘要再将摘要放入最终上下文。这尤其适用于检索到了整章内容但只有几段相关的情况。分步问答Multi-Step RAG对于复杂问题不要试图一步到位。可以先检索出高层级的大纲或目录第一轮根据用户反馈或模型判断再针对性地检索具体章节的细节第二轮。4.3 系统延迟太高瓶颈分析与优化策略一个RAG系统的延迟可能来自Embedding模型推理、向量数据库检索、重排序模型推理、大模型生成。需要定位瓶颈。使用轻量级Embedding模型对于中文场景BAAI/bge-small-zh系列在效果和速度上取得了很好的平衡。如果对精度要求极高可以考虑bge-large-zh但需承受更长的推理时间。向量检索优化确保向量数据库的索引类型如HNSW参数设置合理。ef_construction影响索引质量ef_search影响搜索精度和速度需要在你的数据集上做权衡测试。缓存机制对常见的、不变的查询及其结果进行缓存。甚至可以缓存热门文档的Embedding向量避免重复计算。异步处理如果架构允许可以将Embedding计算、检索等步骤设计为异步提升整体吞吐。4.4 评估体系缺失如何量化你的RAG系统好坏“感觉回答得还行”是不可靠的。必须建立评估体系。除了人工抽查可以关注几个核心指标检索相关度检索到的Top K个文档中有多少是真正与问题相关的可以用人工标注或小模型打分答案忠实度生成的答案在多大程度上源自提供的上下文而不是模型自己的知识可以通过让模型在答案中引用出处并检查引用的准确性来衡量。答案有用性答案是否直接、清晰地解决了用户的问题这更主观但可以通过人工评分或设计一些规则来判断如是否包含关键实体、是否回答了疑问词等。建立一个包含各种类型问题事实型、推理型、总结型和边缘案例的测试集定期跑分是迭代优化系统的最佳指南。5. 从RAG到智能体系统的自我进化之路当我们搭建的RAG系统能稳定运行后下一个自然的想法是它能不能更“智能”一点比如用户问“我们产品最近三个月的用户反馈主要集中在哪里”一个基础的RAG系统需要你提前准备好“用户反馈报告”文档。而一个更智能的系统应该能自动理解这个问题需要“获取用户反馈数据”、“按时间过滤最近三个月”、“进行主题归纳”等多个步骤。这就是当前热门的Agentic RAG或RAG Agent的概念。其核心思想是引入一个“智能体”作为大脑它来理解复杂意图、制定计划、调用不同的工具其中一个核心工具就是RAG检索来完成任务。在这个架构中RAG退居二线成为一个可靠的“事实记忆库”或“文档工具”。智能体负责决策“什么时候该去查资料”、“查什么资料”、“查到资料后怎么用”。例如智能体解析问题发现需要“最近三个月的用户反馈”。智能体调用“文档检索工具”即我们的RAG系统查询关键词“用户反馈”、“满意度报告”、“客诉记录”等。RAG系统返回相关的文档片段。智能体发现返回的是原始数据需要分析。于是它调用另一个“数据分析工具”将检索到的文本输入要求进行“情感分析”和“主题聚类”。智能体汇总分析结果生成最终答案“最近三个月用户反馈主要集中在‘登录速度慢’35%、‘界面操作复杂’28%和‘客服响应不及时’20%三个方面。”实现这样一个系统复杂度远高于基础RAG。你需要定义智能体的决策逻辑可以用LLM驱动如使用ReAct范式封装各种工具RAG、计算器、API调用等并设计一个稳定的执行循环。但这是RAG技术发展的必然方向让AI从被动的“问答机”转向主动的“问题解决者”。6. 手把手项目实战构建一个本地知识问答助手理论说了这么多我们动手实现一个最简单的、可在本地运行的RAG问答助手。我们将使用轻量级的工具避免复杂依赖。技术栈选择文本嵌入模型BAAI/bge-small-zh-v1.5(Hugging Face Sentence Transformers)。效果好支持中文本地运行。向量数据库ChromaDB。纯Python无需外部服务内存/持久化两便。大语言模型考虑到本地部署的便利性和中文能力我们使用Qwen2.5-7B-Instruct的量化版本如用ollama运行。你也可以使用OpenAI API需网络和API Key。框架为了清晰展示原理我们尽量少用高级框架核心部分自己写。6.1 第一步环境准备与知识库加载首先安装必要的库并准备你的知识库文档。假设我们有一个knowledge_base.txt文件。# 安装核心库 pip install sentence-transformers chromadb pypdf2 python-dotenv # 如果你使用Ollama运行Qwen # 请先安装Ollama并在终端运行ollama pull qwen2.5:7b# 1. 加载文档 import os from PyPDF2 import PdfReader # 如果是PDF import docx # 如果是Word需要 python-docx def load_documents(file_path): 支持加载文本和PDF文件 text if file_path.endswith(.txt): with open(file_path, r, encodingutf-8) as f: text f.read() elif file_path.endswith(.pdf): reader PdfReader(file_path) for page in reader.pages: text page.extract_text() \n # 可以继续扩展支持 .docx, .md 等格式 else: raise ValueError(fUnsupported file format: {file_path}) return text # 加载你的知识库 kb_text load_documents(./knowledge_base.txt)6.2 第二步文本切片与向量化使用我们之前讨论的混合切片策略并对切片生成向量。from sentence_transformers import SentenceTransformer import numpy as np # 初始化嵌入模型 embed_model SentenceTransformer(BAAI/bge-small-zh-v1.5) # 改进的混合切片函数增加对代码块等特殊内容的保护 def chunk_text_with_overlap(text, chunk_size500, overlap100): # 先按双换行分段落 paragraphs [p.strip() for p in text.split(\n\n) if p.strip()] chunks [] for para in paragraphs: if len(para) chunk_size: chunks.append(para) else: # 使用递归字符分割作为保底但优先按句子分割 sentences para.replace(。, 。\n).replace(, \n).replace(, \n).split(\n) temp_chunk for sent in sentences: if len(temp_chunk) len(sent) chunk_size: temp_chunk sent else: if temp_chunk: chunks.append(temp_chunk) temp_chunk sent if temp_chunk: chunks.append(temp_chunk) # 最终确保没有块过大并应用重叠 final_chunks [] for chunk in chunks: if len(chunk) chunk_size: final_chunks.append(chunk) else: # 对仍然过大的块进行简单滑动窗口 for i in range(0, len(chunk), chunk_size - overlap): final_chunks.append(chunk[i:ichunk_size]) return final_chunks # 切片 chunks chunk_text_with_overlap(kb_text, chunk_size400, overlap80) print(f文档被切分为 {len(chunks)} 个块。) # 生成向量并归一化 chunk_embeddings embed_model.encode(chunks, normalize_embeddingsTrue) # SentenceTransformer 内置归一化选项6.3 第三步构建向量数据库ChromaDB将文本块和对应的向量存入ChromaDB。import chromadb from chromadb.config import Settings # 初始化Chroma客户端持久化到磁盘 chroma_client chromadb.PersistentClient(path./chroma_db) # 创建或获取一个集合类似数据库的表 collection chroma_client.get_or_create_collection( namemy_knowledge_base, metadata{hnsw:space: cosine} # 使用余弦相似度 ) # 准备数据ChromaDB需要ID、嵌入向量和元数据/文档 ids [fchunk_{i} for i in range(len(chunks))] # ChromaDB 接受列表的列表作为嵌入 embeddings_list chunk_embeddings.tolist() documents chunks # 原始文本 # 添加到集合 collection.add( idsids, embeddingsembeddings_list, documentsdocuments ) print(知识库已成功存入向量数据库。)6.4 第四步实现检索与重排序逻辑from sentence_transformers import CrossEncoder # 初始化重排序模型可选但强烈推荐 reranker CrossEncoder(cross-encoder/ms-marco-MiniLM-L-6-v2, max_length512) def retrieve_and_rerank(query, collection, embed_model, top_k5, rerank_top_k3): 1. 将查询向量化 2. 从向量数据库检索top_k个初步结果 3. 使用重排序模型对初步结果精排返回top rerank_top_k个 # 1. 查询向量化 (同样要归一化) query_embedding embed_model.encode([query], normalize_embeddingsTrue)[0] # 2. 向量检索 results collection.query( query_embeddings[query_embedding.tolist()], n_resultstop_k ) # 3. 组织初步结果 prelim_docs results[documents][0] prelim_distances results[distances][0] # ChromaDB返回的是距离相似度1-距离对于余弦 prelim_scores [1 - d for d in prelim_distances] prelim_results [ {text: doc, score: score} for doc, score in zip(prelim_docs, prelim_scores) ] # 4. 重排序 pairs [[query, res[text]] for res in prelim_results] rerank_scores reranker.predict(pairs) # 合并分数并排序可以加权平均这里简单用重排序分 for i, score in enumerate(rerank_scores): prelim_results[i][rerank_score] float(score) # 按重排序分数降序排列 reranked_results sorted(prelim_results, keylambda x: x[rerank_score], reverseTrue) # 返回前 rerank_top_k 个 return reranked_results[:rerank_top_k]6.5 第五步集成大模型生成答案这里演示如何通过ollama调用本地Qwen模型。确保你已安装并运行了Ollama且拉取了qwen2.5:7b模型。import requests import json def generate_answer_with_ollama(query, context_chunks): 调用本地Ollama服务的Qwen模型生成答案 # 构建提示词 context_str \n\n.join([f[资料片段 {i1}]: {chunk[text]} for i, chunk in enumerate(context_chunks)]) prompt f你是一个严谨的助手请严格根据以下资料回答问题。如果资料中没有答案请直接说“资料中未提供相关信息”。 资料 {context_str} 问题{query} 请根据资料回答 # Ollama API 请求 url http://localhost:11434/api/generate payload { model: qwen2.5:7b, prompt: prompt, stream: False, options: { temperature: 0.2, # 低温度减少随机性 num_predict: 500 # 最大生成token数 } } try: response requests.post(url, jsonpayload) response.raise_for_status() result response.json() return result[response].strip() except Exception as e: return f调用模型时出错{e} # 也可以使用OpenAI API需要配置API Key # from openai import OpenAI # client OpenAI(api_keyyour-key) # def generate_answer_with_openai(query, context_chunks): # ...6.6 第六步组装完整流程并测试def rag_qa_pipeline(query): print(f用户问题{query}) print(- * 40) # 1. 检索与重排序 relevant_chunks retrieve_and_rerank(query, collection, embed_model, top_k5, rerank_top_k3) print(f检索到 {len(relevant_chunks)} 个相关片段) for i, chunk in enumerate(relevant_chunks): print(f 片段{i1} (重排序分: {chunk[rerank_score]:.3f}): {chunk[text][:100]}...) print(- * 40) # 2. 生成答案 answer generate_answer_with_ollama(query, relevant_chunks) print(fAI 回答\n{answer}) print( * 60) return answer # 运行测试 if __name__ __main__: # 示例问题 test_queries [ 文档中主要讨论了哪些内容, # 根据你的知识库内容提出具体问题 # 什么是RAG, # 文本切片时要注意什么 ] for q in test_queries: rag_qa_pipeline(q)运行这个脚本你就拥有了一个完全本地化的、具备基本语义检索能力的问答助手。你可以通过丰富知识库、优化切片参数、调整提示词、甚至加入查询扩展来持续提升它的表现。7. 总结与展望RAG是起点而非终点通过这个从零开始的过程我们亲手触摸了RAG的每一个核心部件。你会发现它不是一个神秘的黑盒而是一系列可理解、可调试、可优化的技术组合。关键在于理解每个环节的“弦外之音”预处理的弦外之音是尊重数据的原始结构在信息完整性和检索粒度间找到平衡。向量化的弦外之音是在同一个语义空间里对话确保查询和文档的向量可比。检索的弦外之音是从“相似”到“相关”粗筛后必须要有精排。生成的弦外之音是用提示词给模型戴上“紧箍咒”让它忠于上下文。当前RAG技术仍在飞速演进。Graph RAG尝试利用知识图谱来建模文档间的关系实现更复杂的推理Ontology RAG引入领域本体来提升查询理解的精度Agentic RAG则让系统具备了自主规划和处理复杂任务的能力。但无论技术如何变化其核心目标从未改变让AI更可靠、更精准地为我们服务成为连接人类知识与智能应用的坚实桥梁。当你下次再看到“RAG”、“语义搜索”、“AI Agent”这些热词时希望你能会心一笑因为你知道它们背后是一套你可以亲手搭建、细致调优的系统。而这正是从“使用AI”到“创造AI应用”的关键一步。
返回列表