news 2026/9/10 13:59:46

从零手搓RAG:深入理解检索增强生成的核心架构与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零手搓RAG:深入理解检索增强生成的核心架构与工程实践

1. 项目缘起:为什么从零开始做RAG?

最近几年,AI应用开发的热度居高不下,尤其是RAG(检索增强生成)技术,几乎成了大模型落地的“标配”。网上教程很多,框架也层出不穷,像LangChain、LlamaIndex、Dify这些,号称能让你“三行代码”搞定一个智能问答。但说实话,跟着这些“快餐式”教程走一遍,你大概率还是懵的:向量化到底在干什么?为什么我的答案总是不准?所谓的“工程化”到底包含了哪些脏活累活?

这正是我决定抛开所有现成框架,从零手搓一个RAG问答应用的原因。我想弄明白,在那些高级抽象的API和封装良好的Pipeline背后,每一个环节到底是如何运作的,又会遇到哪些“坑”。这个过程,远比调用pip install然后复制粘贴代码要痛苦,但也远比那样做要收获巨大。它让我对RAG的认知,从一个模糊的“检索+生成”概念,变成了一个由数据准备、向量化、检索、重排序、生成等多个精密齿轮咬合而成的系统工程。

如果你也厌倦了当“调包侠”,想真正理解RAG的筋骨,或者你是一名开发者(无论是前端、Java还是其他背景),正考虑转向AI应用开发,觉得前景迷茫不知从何下手,那么这篇从零开始的实践记录,或许能给你一些实实在在的参考。我们不止要做出一个能跑的Demo,更要搞清楚每一个选择背后的“为什么”,以及那些只有踩过坑才知道的“怎么办”。

2. 核心架构拆解:RAG不是“检索+生成”那么简单

很多人把RAG简单理解为“先用向量数据库搜一下,再把结果喂给大模型”。这个理解没错,但过于粗糙,无法指导工程实践。一个健壮、可用的RAG系统,其内部是一个分层级的架构。结合业界共识和我的实践,可以将其梳理为以下几个核心层级:

2.1 数据层:一切始于“知识”的预处理

这是整个RAG系统的基石,也是最容易被轻视的环节。你的原始数据(PDF、Word、网页、数据库)不是直接就能用的。

知识切片(Chunking):这是第一个技术难点。怎么切?按固定长度?按段落?按语义?这里没有银弹。

  • 固定长度切片:最简单,比如每256个token切一段。但问题很明显:很可能把一个完整的句子或概念从中间切断,导致检索到的片段信息不完整。
  • 按段落/标题切片:利用文档的天然结构(如Markdown的#, PDF的段落)。这比固定长度好,但依赖文档本身格式良好。
  • 语义切片:更高级的方法,目标是让每一个切片在语义上尽可能独立、完整。这通常需要先用一个轻量模型(如BERT)计算句子的嵌入向量,然后根据向量间的相似度或变化点来划分边界。实操心得:对于技术文档、知识库,我推荐“重叠式滑动窗口切片”。比如,设置切片大小为500字符,重叠部分为100字符。这样既能保证每个片段有足够上下文,又能避免因切在关键位置而丢失信息,是效果和复杂度的一个很好平衡。

元数据附加:切片时,不要光存文本内容。一定要把来源(文件名、章节标题)、在原文中的位置(页码、行号)等信息作为元数据(Metadata)附加到每个切片上。这在后续的引用溯源和混合检索中至关重要。

2.2 索引层:让知识“可被检索”

处理好的文本切片,需要转换成一种便于快速查找的形式,这就是索引。

向量化(Embedding):这是核心中的核心。我们使用一个嵌入模型(Embedding Model)将文本切片转换为高维空间中的向量(一组浮点数)。语义相似的文本,其向量在空间中的距离(如余弦相似度)也更近。

  • 模型选型:选对模型事半功倍。对于中文,text2vecBGE系列是很好的开源选择。对于这个实践项目,我选择了BGE-large-zh-v1.5,它在中文语义相似度任务上表现稳健,且社区活跃。为什么不直接用OpenAI的Embedding?成本、延迟和隐私。自托管开源模型,一次部署,无限次使用,数据不出私域。
  • 向量维度:比如BGE模型输出1024维的向量。维度越高,表征能力越强,但存储和计算成本也越高。需要权衡。

向量数据库(Vector Database):用于存储和高效检索这些向量。市面上有Pinecone、Weaviate等云服务,也有Chroma、Qdrant、Milvus等可以自部署的开源方案。

  • 我的选择:为了彻底掌控,我选择了本地部署的ChromaDB。它轻量、简单,Python集成友好,对于中小规模知识库完全够用。你当然可以选更强大的Milvus,但Chroma能让我们的注意力更集中在RAG流程本身,而非数据库的运维上。
  • 索引算法:Chroma默认使用HNSW(近似最近邻搜索)算法来构建索引。你不需要手动实现,但要知道它的存在:它通过构建一个图结构来加速检索,在精度和速度之间取得了很好的平衡,是当前向量检索的主流算法。

2.3 检索与重排层:找到“最相关”的知识

当用户提问时,系统需要从海量切片中找到最相关的几个。

检索(Retrieval / Recall):将用户问题也向量化,然后在向量数据库中搜索与之最相似的N个文本切片(比如Top 10)。这就是经典的“向量检索”或“语义检索”。

  • 多路召回(Hybrid Search):单一向量检索可能不够。例如,用户问“2023年公司的营收是多少?”,其中“2023”、“营收”是关键词。这时可以结合关键词检索(如BM25算法)。向量检索负责语义匹配(“营收”可能匹配“收入”、“销售额”),关键词检索负责精确匹配。将两路召回的结果合并,能显著提升召回率。这就是“混合检索”的工程价值。

重排序(Re-ranking):从向量库召回的可能有10个片段,但它们与问题的相关度排序不一定准确。重排序就是一个“精排”过程,使用一个更精细的(通常是交叉编码器)模型,对“问题-候选片段”对进行打分,重新排序。

  • 为什么需要?向量检索基于“向量相似度”,而重排序模型能更好地理解问题和答案之间的“相关性”,尤其是对于需要复杂推理或存在语义鸿沟的情况。例如,问题“如何解决内存泄漏?”,向量检索可能返回一堆讲“内存管理”的片段,而重排序模型能识别出其中真正在讲“泄漏检测工具使用”的片段更相关。
  • 模型选择:可以使用专门的重排序模型,如BGE-Reranker。也可以直接用一个大语言模型(LLM)来做,让它给相关性打分,但成本较高。在本项目中,为了流程完整,我引入了BGE-Reranker-large模型,对Top 10的召回结果进行重排,选出最终的Top 3作为上下文。

2.4 生成层:基于知识的“安全”作答

这是最后一步,也是用户直接感知的一步。

提示工程(Prompt Engineering):将重排序得到的最相关文本片段(上下文),连同用户的问题,按照一定的模板组织起来,形成给大模型的“提示词”(Prompt)。 一个经典的模板如下:

请基于以下上下文信息回答问题。如果上下文信息不足以回答问题,请直接回答“根据已知信息无法回答该问题”,不要编造信息。 上下文: {context} 问题:{question} 请给出答案:
  • 设计要点:这个模板明确了角色、任务边界(防止幻觉)、输入格式。“不要编造信息”这个指令对于RAG的可靠性至关重要。

大语言模型(LLM)调用:将组装好的Prompt发送给LLM,得到最终答案。这里可以选择云端API(如GPT-4、文心一言、通义千问)或本地部署的模型(如Qwen、ChatGLM)。

  • 我的选择:为了项目的完整性和可控性,我选择了在本地部署Qwen2.5-7B-Instruct模型。它性能足够,对中文友好,并且完全免费、隐私安全。使用vLLMllama.cpp这样的推理框架可以高效地部署和服务化这个模型。
  • 事实性(Grounding):RAG的核心优势就在于,模型的回答被“锚定”在你提供的上下文里。你需要确保模型严格基于上下文生成,并在答案中引用片段的来源元数据(例如,“根据《2023年度报告》第5页…”),这能极大提升可信度。

3. 从零到一的实战:手把手搭建核心流水线

理论讲完了,我们进入实战环节。我会用代码片段展示核心步骤,并解释关键参数和设计理由。

3.1 环境准备与依赖安装

首先,创建一个干净的Python环境(推荐3.9+),并安装核心库。

# 创建虚拟环境 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 pypdf # 用于解析PDF,比PyPDF2更活跃 pip install langdetect # 用于语言检测(可选) pip install torch # 深度学习框架,sentence-transformers依赖 # 如果需要本地LLM,例如用ollama pip install ollama # 或者用vLLM部署Qwen # pip install vllm

3.2 第一步:文档加载与智能切片

假设我们有一个knowledge_base.pdf文件。我们来实现加载和切片。

import PyPDF2 from typing import List, Dict import re class DocumentProcessor: def __init__(self, chunk_size: int = 500, chunk_overlap: int = 100): self.chunk_size = chunk_size # 每个切片的目标字符数 self.chunk_overlap = chunk_overlap # 切片间的重叠字符数 def load_pdf(self, file_path: str) -> str: """加载PDF文件,提取所有文本""" text = "" with open(file_path, 'rb') as file: reader = PyPDF2.PdfReader(file) for page_num, page in enumerate(reader.pages): page_text = page.extract_text() if page_text: text += f"[Page {page_num+1}] {page_text}\n" # 附加页码元数据 return text def smart_chunking(self, text: str, source: str) -> List[Dict]: """ 使用重叠滑动窗口进行切片。 返回一个字典列表,每个字典包含‘text‘和‘metadata‘。 """ # 基础清洗:去除多余空白字符 text = re.sub(r'\s+', ' ', text).strip() chunks = [] start = 0 text_length = len(text) while start < text_length: # 计算切片结束位置 end = start + self.chunk_size # 如果没到文本末尾,尝试在句末、分号或逗号处截断,避免切碎句子 if end < text_length: # 查找最近的句子边界 for break_point in range(end, start, -1): if text[break_point] in '.。!!??;;': end = break_point + 1 # 包含标点 break # 如果没找到标点,就在空格处截断 else: for break_point in range(end, start, -1): if text[break_point] == ' ': end = break_point break else: end = text_length chunk_text = text[start:end].strip() if chunk_text: # 忽略空切片 metadata = { "source": source, "start_char": start, "end_char": end, } # 尝试提取切片内的标题作为更细粒度的元数据 title_match = re.search(r'^(#{1,6}\s*.+)$', chunk_text, re.MULTILINE) if title_match: metadata["section"] = title_match.group(1) chunks.append({ "text": chunk_text, "metadata": metadata }) # 移动窗口,考虑重叠 start = end - self.chunk_overlap return chunks # 使用示例 processor = DocumentProcessor(chunk_size=500, chunk_overlap=50) full_text = processor.load_pdf("knowledge_base.pdf") doc_chunks = processor.smart_chunking(full_text, source="knowledge_base.pdf") print(f"共切分出 {len(doc_chunks)} 个文本片段。")

关键点解析:

  1. chunk_size=500, overlap=50:这是一个经验值。对于技术文档,500字符能容纳一个中等长度的概念说明。50字符的重叠确保了上下文连贯。
  2. 智能断句:代码中尝试在句末标点处截断,这是为了保持语义完整性,比粗暴地按固定字符数切割效果好得多。
  3. 元数据丰富化:除了来源和位置,我们还尝试提取Markdown风格的标题作为section元数据。这为后续的混合检索提供了可能:你不仅可以按语义搜,还可以按“章节标题”过滤。

3.3 第二步:向量化与索引构建

接下来,我们将文本片段转换为向量,并存入ChromaDB。

from sentence_transformers import SentenceTransformer import chromadb from chromadb.config import Settings class VectorIndexer: def __init__(self, embedding_model_name: str = "BAAI/bge-large-zh-v1.5"): # 加载嵌入模型 self.embedding_model = SentenceTransformer(embedding_model_name) # 初始化Chroma客户端,持久化到本地目录 self.client = chromadb.PersistentClient(path="./chroma_db") # 获取或创建集合(类似数据库的表) self.collection = self.client.get_or_create_collection( name="knowledge_base", metadata={"hnsw:space": "cosine"} # 使用余弦相似度 ) def create_index(self, chunks: List[Dict]): """将文本切片向量化并存入数据库""" texts = [chunk["text"] for chunk in chunks] metadatas = [chunk["metadata"] for chunk in chunks] ids = [f"chunk_{i}" for i in range(len(texts))] # 为每个切片生成唯一ID # 批量生成向量嵌入 print("正在生成向量嵌入...") embeddings = self.embedding_model.encode(texts, normalize_embeddings=True).tolist() print(f"已生成 {len(embeddings)} 个向量,维度为 {len(embeddings[0])}。") # 批量添加到集合 self.collection.add( embeddings=embeddings, documents=texts, metadatas=metadatas, ids=ids ) print("向量索引构建完成。") # 使用示例 indexer = VectorIndexer() indexer.create_index(doc_chunks)

关键点解析:

  1. normalize_embeddings=True:将向量归一化为单位长度。这样,向量间的点积就等于余弦相似度,是语义相似度计算的常用方法。
  2. hnsw:space: “cosine”`:指定Chroma使用余弦相似度作为距离度量。对于归一化后的向量,余弦相似度范围是[-1,1],值越大越相似。
  3. 批处理encode方法支持批量文本输入,一次性生成所有向量,效率远高于循环单条处理。
  4. 持久化PersistentClient将数据和索引保存在本地./chroma_db目录,下次启动无需重新索引。

3.4 第三步:实现混合检索与重排序

当用户提问时,我们需要执行检索流程。

# 首先,需要安装重排序模型库(如果使用BGE Reranker) # pip install FlagEmbedding from FlagEmbedding import FlagReranker class Retriever: def __init__(self, indexer: VectorIndexer, reranker_model_name: str = "BAAI/bge-reranker-large"): self.collection = indexer.collection self.embedding_model = indexer.embedding_model # 初始化重排序模型 self.reranker = FlagReranker(reranker_model_name, use_fp16=True) # 使用半精度加速 def hybrid_retrieve(self, query: str, top_k: int = 10, keyword_weight: float = 0.3): """ 混合检索:结合向量检索和简单关键词匹配。 keyword_weight: 关键词检索结果的权重,用于融合分数。 """ # 1. 向量检索 query_embedding = self.embedding_model.encode([query], normalize_embeddings=True).tolist()[0] vector_results = self.collection.query( query_embeddings=[query_embedding], n_results=top_k * 2, # 多取一些,供后续融合和重排序筛选 ) # 简单模拟关键词检索(实际可用whoosh、elasticsearch或BM25库) # 这里仅作演示,在实际项目中应替换为真正的关键词检索引擎 all_docs = self.collection.get()['documents'] all_metadatas = self.collection.get()['metadatas'] keyword_scores = [] for i, doc in enumerate(all_docs): # 简单计算查询词在文档中出现的频率作为分数 score = sum([doc.count(word) for word in query.split() if len(word) > 1]) keyword_scores.append((i, score)) keyword_scores.sort(key=lambda x: x[1], reverse=True) keyword_top_indices = [idx for idx, _ in keyword_scores[:top_k]] # 2. 结果融合(简化版) # 为向量检索结果赋予初始分数(这里用1 - 距离,因为chroma返回的是距离) fused_results = {} for i, (doc, meta, dist) in enumerate(zip(vector_results['documents'][0], vector_results['metadatas'][0], vector_results['distances'][0])): vector_score = 1 - dist # 将距离转换为相似度分数 # 检查该文档是否也在关键词检索结果中 keyword_boost = 1.0 if i in keyword_top_indices else 0.0 combined_score = vector_score + keyword_weight * keyword_boost fused_results[i] = { "document": doc, "metadata": meta, "score": combined_score, "from_vector": True } # 按融合分数排序 sorted_fused = sorted(fused_results.items(), key=lambda x: x[1]['score'], reverse=True) candidates = [item[1] for item in sorted_fused[:top_k * 2]] # 取前 top_k*2 作为重排序候选 return candidates def rerank(self, query: str, candidates: List[Dict], top_n: int = 3): """使用重排序模型对候选文档进行精排""" if not candidates: return [] # 准备重排序模型需要的输入对 (query, document) pairs = [[query, cand["document"]] for cand in candidates] # 批量计算相关性分数 scores = self.reranker.compute_score(pairs) # 将分数附加到候选文档上 for cand, score in zip(candidates, scores): cand["rerank_score"] = score # 按重排序分数降序排列 candidates.sort(key=lambda x: x["rerank_score"], reverse=True) return candidates[:top_n] def retrieve(self, query: str): """完整的检索流程""" print(f"用户查询: {query}") # 1. 混合检索,获取较多候选 candidates = self.hybrid_retrieve(query, top_k=10) print(f"混合检索得到 {len(candidates)} 个候选片段。") # 2. 重排序,获取最相关的少数几个 final_contexts = self.rerank(query, candidates, top_n=3) print(f"重排序后选取 top {len(final_contexts)} 个上下文。") return final_contexts # 使用示例 retriever = Retriever(indexer) contexts = retriever.retrieve("公司2023年的主要营收来源是什么?") for i, ctx in enumerate(contexts): print(f"\n--- 上下文 {i+1} (得分: {ctx['rerank_score']:.4f}) ---") print(f"来源: {ctx['metadata']['source']}, 位置: {ctx['metadata'].get('start_char', 'N/A')}") print(f"内容摘要: {ctx['document'][:200]}...")

关键点解析:

  1. 混合检索模拟:上述代码中的关键词检索是极简模拟。在生产环境中,你需要集成如rank_bm25这样的库,或者使用Elasticsearch。核心思想是并行执行向量检索和关键词检索,然后按一定策略(如加权分数、RRF)融合结果。
  2. 重排序的价值:注意看,hybrid_retrieve返回的candidates是按初步融合分数排序的。但经过rerank模型后,顺序很可能发生变化。重排序模型(交叉编码器)会同时编码问题和文档,进行深度的交互匹配,其相关性判断通常比单纯的向量相似度更精准。
  3. Top-K策略:通常,第一轮检索(召回)会取一个较大的K(如50),保证召回率;重排序后,再取一个较小的N(如3-5)作为最终上下文,保证精确率。这被称为“召回后重排序”范式。

3.5 第四步:提示词构建与大模型生成

最后,我们将精选的上下文送给大模型,让它生成答案。

# 假设我们使用本地部署的Ollama服务,运行了Qwen2.5模型 # 启动命令:ollama run qwen2.5:7b import requests import json class AnswerGenerator: def __init__(self, llm_api_url: str = "http://localhost:11434/api/generate"): self.llm_api_url = llm_api_url def build_prompt(self, query: str, contexts: List[Dict]) -> str: """构建包含上下文和指令的Prompt""" context_str = "" for i, ctx in enumerate(contexts): source = ctx["metadata"]["source"] # 可以加入更多元数据,如章节 context_str += f"[上下文片段 {i+1}, 来源: {source}]:\n{ctx['document']}\n\n" prompt_template = """你是一个专业的问答助手。请严格根据以下提供的上下文信息来回答问题。如果上下文信息中没有包含问题答案所需的信息,或者信息不充分,请明确回答“根据已知信息无法回答该问题”,不要编造任何信息。 上下文信息: {context} 用户问题:{question} 请基于上下文信息,给出准确、简洁的答案。如果答案涉及具体数据或观点,请注明其来源上下文片段的编号。 """ return prompt_template.format(context=context_str.strip(), question=query) def generate_answer(self, prompt: str) -> str: """调用LLM API生成答案""" payload = { "model": "qwen2.5:7b", # 指定模型 "prompt": prompt, "stream": False, "options": { "temperature": 0.1, # 低温度,减少随机性,让答案更确定 "top_p": 0.9, } } try: response = requests.post(self.llm_api_url, json=payload, timeout=60) response.raise_for_status() result = response.json() return result.get("response", "LLM返回结果为空。") except requests.exceptions.RequestException as e: return f"调用LLM API时出错: {e}" def answer(self, query: str, contexts: List[Dict]) -> str: """完整的答案生成流程""" prompt = self.build_prompt(query, contexts) print("--- 构建的Prompt (前500字符) ---") print(prompt[:500] + "...") print("--- 正在生成答案 ---") answer = self.generate_answer(prompt) return answer # 使用示例 generator = AnswerGenerator() final_answer = generator.answer("公司2023年的主要营收来源是什么?", contexts) print("\n=== 最终答案 ===") print(final_answer)

关键点解析:

  1. Prompt设计:这是控制LLM行为的关键。我们的模板强调了三点:角色定义(专业助手)、指令约束(严格基于上下文,不胡编乱造)、输出要求(准确简洁,注明来源)。temperature=0.1使得模型输出更确定、更忠实于上下文。
  2. 上下文格式化:将每个上下文片段与其元数据(如来源)一起格式化,不仅有助于模型理解,也方便在最终答案里做引用。
  3. 错误处理:网络请求总是可能失败,必须有基本的try-except来处理超时或连接错误,给用户友好的反馈。
  4. 本地LLM替代方案:除了Ollama,你也可以使用vLLM部署并调用,或者使用transformers库直接加载模型。Ollama的优势是简单易用,适合快速原型验证。

4. 超越Demo:RAG工程化的核心挑战与优化

一个能跑的Demo和一个能在生产环境服务的RAG系统之间,隔着巨大的鸿沟,这就是“工程化”要解决的问题。以下是几个你必须面对的挑战和优化方向。

4.1 知识切片的质量:最大的变量

切片策略直接决定检索的上限。不好的切片会导致“信息碎片化”或“信息冗余”。

  • 动态切片:不要对所有文档都用一种切片方式。对于API文档,可以按函数/方法切;对于长篇文章,可以按章节切;对于QA对,可以保持一对一切片。可以训练一个分类器,先判断文档类型,再选择切片策略。
  • 小颗粒度,大上下文:一种先进实践是“小切片存储,大上下文检索”。即存储时用较小的颗粒度(如200字),但在检索时,如果相邻切片来自同一来源或章节,则将它们合并后作为上下文送给LLM。这平衡了检索精度和上下文完整性。
  • 结构化信息提取:在切片前,先用模型(如UIE、LayoutLM)从文档中提取出表格、键值对、标题层级等结构化信息,并将其作为元数据存储。检索时,这些元数据可以作为强大的过滤条件。

4.2 检索效果的评估与迭代:没有度量,就没有优化

你怎么知道你的RAG系统变好了还是变坏了?你需要一套评估体系。

  • 构建测试集:收集一批真实用户可能问的问题(Q),并人工标注每个问题对应的标准答案(A)以及支撑答案的文档片段(C,即Ground Truth Context)。这就是你的“黄金测试集”。
  • 核心评估指标:
    • 检索召回率(Retrieval Recall):系统检索到的Top K个片段中,是否包含了人工标注的标准上下文(C)?这是衡量检索模块能力的核心。
    • 答案相关性(Answer Relevance):LLM生成的答案是否直接回答了问题?可以用另一个LLM(如GPT-4)来打分,或者用基于嵌入的相似度计算(如答案与标准答案的向量相似度)。
    • 答案事实性/忠实度(Answer Faithfulness/Groundedness):答案中的每一个陈述,是否都能在提供的上下文中找到依据?这是防止“幻觉”的关键。同样可以用LLM判断或规则匹配。
  • 持续迭代:当你更换嵌入模型、调整切片大小、启用重排序后,跑一遍测试集,看这些指标的变化。只有数据才能告诉你,什么优化是真正有效的。

4.3 处理“未知问题”与拒答

一个成熟的RAG系统必须知道什么时候该说“我不知道”。

  • 置信度阈值:可以为检索和生成环节设置阈值。
    1. 检索置信度:如果重排序后,Top 1片段的相关性分数低于某个阈值(例如,rerank_score < 0.5),可能意味着知识库中没有相关信息。
    2. 生成置信度:在Prompt中明确要求模型在无法回答时输出特定语句(如“根据已知信息无法回答”),并在后端解析答案。如果模型输出了这句话,就触发拒答。
    3. 一致性检查:让LLM对自己生成的答案进行评估,判断其是否严格基于上下文。这虽然增加了开销,但能进一步降低幻觉率。

4.4 性能、成本与扩展性

  • 缓存:对常见问题(FAQ)的问答对进行缓存,可以极大降低检索和LLM调用的开销。
  • 异步与流式:对于耗时的重排序或LLM生成,采用异步处理。对于长答案,支持流式输出(Server-Sent Events),提升用户体验。
  • 多模态RAG:如果知识库包含图片、图表,需要多模态嵌入模型(如CLIP)来为图像生成向量,实现“以图搜图”或“图文联合检索”。
  • 图增强RAG(Graph RAG):对于知识之间存在复杂关系(如人物关系、事件脉络)的场景,可以先将知识抽取成图结构(知识图谱)。检索时,先在图上游走找到相关实体和关系,再获取对应的文本片段。这能极大提升对复杂、关联性问题的回答能力。

从零实现一个RAG应用,就像亲手组装一台精密仪器。你不仅知道了每个零件的名字,更清楚了它们如何咬合,哪里容易出故障,以及如何调试。这个过程赋予你的,是对AI应用开发生态更深的理解和更强的掌控力。当你再看到“Agentic RAG”、“Graph RAG”这些新概念时,你看到的将不再是一个黑盒术语,而是一系列可拆解、可实现的工程组件的有机组合。这才是从“使用工具”到“创造工具”的关键一步。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 2:20:45

ARM MCU车门控制面板设计:电容触摸按键与LIN总线实战解析

最近在做一个车门控制面板的项目&#xff0c;核心诉求很明确&#xff1a;用电容触摸按键替代传统机械按钮&#xff0c;配一块小尺寸屏做状态显示&#xff0c;再通过LIN总线和车身控制器通信。这个方向在汽车电子里很常见&#xff0c;但真正动手做的时候我发现&#xff0c;很多人…

作者头像 李华
网站建设 2026/9/2 1:51:01

博途PLC单容水箱PID仿真:变积分与变增益实战指南

1. 项目概述&#xff1a;为什么单容水箱是PID控制的“教科书级”入口 博途PLC PID仿真——这个标题里藏着西门子自动化工程师日常最常打交道、也最容易栽跟头的一类典型控制任务。我带过十几届自动化专业实习生&#xff0c;第一课永远不是画梯形图&#xff0c;而是打开博途&…

作者头像 李华
网站建设 2026/8/30 4:30:09

基于SSE与BFF架构实现大模型流式输出:从原理到实践

1. 项目缘起&#xff1a;为什么我们需要“三件套”来实现流式输出&#xff1f; 最近在做一个内部知识库问答的Demo&#xff0c;核心需求就是模仿ChatGPT那种“一个字一个字往外蹦”的流式回答体验。一开始想得很简单&#xff0c;不就是个HTTP请求吗&#xff1f;前端发个问题&am…

作者头像 李华
网站建设 2026/8/30 23:23:19

Wolfram小波建模实战:数模国赛信号去噪与多尺度分析

1. 项目概述&#xff1a;为什么小波分析是数模国赛里“藏得最深的利器” 如果你正在准备数模国赛&#xff0c;尤其是看到2024年B题涉及非平稳信号去噪、2025年C题预告中提到“多尺度特征提取”或“时频局部化建模”&#xff0c;那“小波”这个词绝不是偶然出现的术语——它是真…

作者头像 李华
网站建设 2026/8/31 9:46:21

AI自主编程循环实践:从零构建完整后端项目的Loop Engineering工作流

1. 项目概述&#xff1a;当AI学会“自我驱动”编程最近在折腾AI编程工具链的朋友&#xff0c;可能都听过一个词叫“Loop Engineering”&#xff0c;或者更直白点&#xff0c;叫“AI自主编程循环”。这玩意儿听起来有点科幻&#xff0c;但核心思想其实很朴素&#xff1a;我们能不…

作者头像 李华
网站建设 2026/8/30 14:56:39

2026AI论文工具深度解析[特殊字符]别盲目用!内行才懂的选型逻辑

现在写论文没人不用AI论文工具&#xff0c;但2026双检严查时代&#xff0c;90%的人都用错了&#xff01; 很多同学以为随便找个AI写写、降个重就能定稿&#xff0c;最后却栽在AI痕迹超标、虚假文献、模板同质化、查重虚高上。今年高校对AI论文的审核不再只查重复率&#xff0c…

作者头像 李华