💡 本文会带给你
- RAG与LLM的优势
- 如何构建垂域(向量)知识库
- 文本分块的技巧
- 构建一套完整 RAG 系统的步骤
一、RAG的优势
使用 RAG(Retrieval-Augmented Generation)检索增强生成, 核心目的是 结合检索(Retrieval)和生成(Generation)的优势,解决传统大语言模型(LLM)能力不足的问题。
1. 为什么需要 RAG?
传统大语言模型(LLM)存在着知识固化、幻觉、无妨访问企业内部数据等问题,RAG本地部署,可解决这些问题。
(1)解决 LLM 的“知识固化”问题
传统 LLM(如 GPT):依赖训练时的静态知识,无法实时更新。
缺点:如果训练数据中不包含最新信息(如 2024 年的新闻),模型无法正确回答。
RAG:通过检索外部知识库(如本地文档、网页),动态补充最新信息。
示例:
❌ 纯 LLM:问 “2025 年诺贝尔奖得主是谁?” → 可能胡编乱造(幻觉)。
✅ RAG:先检索权威新闻 → 返回真实结果。
(2)减少“幻觉”(Hallucination)
LLM 在缺乏相关知识时容易生成看似合理但错误的答案。
RAG 通过 检索真实文档 提供依据,让生成结果更可信。
示例:
❌ 纯 LLM:“爱因斯坦是如何发明量子计算机的?” → 可能编造故事。
✅ RAG:检索物理学史 → 回答 “爱因斯坦并未直接发明量子计算机”。
(3)支持私有/领域知识
纯 LLM:无法访问企业内部文档、个人笔记等私有数据。
RAG:可直接从本地知识库(如公司 Wiki、PDF 报告)中检索信息。
应用场景:
法律顾问:检索最新法律法规 → 生成合规建议。
医疗诊断:结合医学论文 → 提供参考资料。
(4)低成本 & 可解释性
微调(Fine-tuning):需要大量标注数据和算力,成本高。
RAG:仅需构建检索库,无需重新训练模型,且能提供检索来源(如引用文档)。
对比:
方法 | 成本 | 可解释性 | 更新知识 |
---|---|---|---|
纯 LLM | 低 | 差 | 难 |
微调 | 高 | 中 | 需重新训练 |
RAG | 中 | 高 | 即时 |
2. RAG 的核心优势
(1)动态知识更新
无需重新训练模型,只需更新检索库(如新增 PDF 文件)。
(2)精准答案
生成结果基于检索到的真实文本,而非模型记忆。
(3)模块化设计
可灵活替换组件:
检索器:从 FAISS 切换到 Elasticsearch。
生成模型:从 GPT-4 切换到 Llama3。
(4)适合长尾问题
对冷门、专业问题,RAG 能通过检索补充 LLM 未覆盖的知识。
3. 适用场景
(1)企业知识库
内部文档问答(如员工手册、技术文档)。
客户支持:快速检索产品说明书生成回答。
(2)学术研究
从海量论文中检索相关研究,生成综述。
(3)法律 & 医疗
法律条文、病例报告的精准查询和摘要。
(4)个人知识管理
对个人笔记、收藏文章进行智能搜索和总结。
4. RAG 的局限性
(1)依赖检索质量 如果检索库不完整或噪声多,生成结果也会受影响。
(2)延迟较高 检索 + 生成两步流程,比纯生成慢(可通过缓存优化)。
(3)上下文长度限制 LLM 的输入长度有限(如 GPT-4 Turbo 的 128K),可能截断长文档。
5. 对比其他方案
方案 | 优点 | 缺点 |
---|---|---|
纯 LLM | 响应快,通用性强 | 知识静态,易幻觉 |
微调 | 适应特定任务 | 成本高,难更新 |
RAG | 动态知识,可解释 | 依赖检索质量 |
Agent | 多工具联动 | 复杂度高 |
总之
使用 RAG 的核心原因:
✅ 动态知识:突破 LLM 的训练数据时间限制。
✅ 减少幻觉:答案基于真实文档,而非模型想象。
✅ 私有数据:支持本地/领域知识库。
✅ 低成本:无需微调,快速部署。
适合对 准确性、实时性 要求高的场景(如企业知识管理、专业领域问答)。
二、应用RAG构建知识库
以下是构建本地知识库的完整步骤和基本流程,涵盖从数据准备到部署应用的各个环节:
1. 整体架构
RAG 核心流程 检索(Retrieval):从本地知识库中检索相关文档片段。
生成(Generation):LLM 结合检索到的内容生成回答
2. 详细步骤
2.1 数据准备阶段
(1) 文档收集及预处理
支持格式:PDF/Word/TXT/Markdown/Excel/PPT/Json等,这里也可以自定义文档处理类
存储方式:建议按类别分目录存放(如/data/legal, /data/tech)
文档加载成功后,对文档的文本进行清洗和规范化,包括: 编码标准化、空白字符处理、移除多余空格、理HTML/XML标签(如
,
等)、制表符、换行符、规范化段落分隔(统一为1-2个换行符)、特殊字符清理等操作。
文档加载及预处理示例代码:
In [ ]:
from langchain_community.document_loaders import ( PyPDFLoader, Docx2txtLoader, UnstructuredFileLoader, UnstructuredExcelLoader, UnstructuredPowerPointLoader, CSVLoader, JSONLoader, UnstructuredWordDocumentLoader ) LOADER_MAPPING = { ".json": QAPairLoader, # 动态JSON处理器 ".pdf": PyPDFLoader, ".txt": UnstructuredFileLoader, ".xlsx": UnstructuredExcelLoader, ".pptx": UnstructuredPowerPointLoader, ".csv": CSVLoader, ".docx": UnstructuredWordDocumentLoader, ".doc": UnstructuredWordDocumentLoader } #加载问目录下所有文件 input_dir="./data/" documents = [] for root, _, files in os.walk(input_dir): for file in files: ext = os.path.splitext(file)[1].lower() if ext in LOADER_MAPPING: try: file_path = os.path.join(root, file) loader = LOADER_MAPPING[ext](file_path) docs = loader.load() # 添加文件来源元数据 for doc in docs: doc.metadata["source"] = file_path doc.metadata["file_type"] = ext documents.extend(docs) print(f"成功加载: {file_path}") except Exception as e: print(f"加载失败 {file_path}: {str(e)}") #对数据文本进行清洗 for doc in documents: text = doc.page_content # 1. 去除HTML标签(如果是HTML/XML文件) text = BeautifulSoup(text, "html.parser").get_text() # 2. 标准化空白字符 text = re.sub(r"\s+", " ", text).strip() # 3. 处理特殊字符(保留中文、英文、常见标点) text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9\s,.!?;:,。!?;:、]", "", text) # 4. 处理换行符和段落分隔 text = text.replace("\r\n", "\n").replace("\r", "\n") doc.page_content = text
2.2 文本处理阶段
(1) 文本分割
推荐参数:Chunk大小:300-500字符,重叠窗口:50-100字符(避免信息割裂)
在文档处理或文本分块(Text Chunking)中,chunk_size 和 chunk_overlap 是两个关键参数,用于控制如何将长文本分割成较小的片段。
详细解释如下:
- chunk_size = 500 含义:每个文本块的最大长度(按字符或token计算)
作用:
确保每个分块不超过500个字符(或token)
避免生成过长的文本片段,影响后续处理(如向量化、模型输入等)
实际表现:
In [ ]:
#python # 输入文本(假设共800字符) text = "这是一个很长的文档内容...[共800字符]..." # 分割后: chunk1 = "这是一个很长的文档内容...[500字符]" # 第1个块 chunk2 = "...[剩余300字符]" # 第2个块
- chunk_overlap = 100 含义:相邻文本块之间的重叠字符数(或token数)
作用:
防止关键信息被割裂在不同块中
提升上下文连贯性(尤其在处理句子或段落边界时)
实际表现:
In [ ]:
# 分割效果(接上例): chunk1 = "这是一个很长的文档内容...[500字符]" chunk2 = "文档内容...[最后100字符重叠] + [新400字符]" # 前100字符与chunk1重叠
- 联合工作原理
- 参数选择建议
场景 | 推荐 chunk_size | 推荐 chunk_overlap | 原因 |
---|---|---|---|
通用文档检索 | 300-500 | 50-100 | 平衡信息完整性和效率 |
代码处理 | 800-1200 | 100-200 | 代码块通常较长 |
技术论文解析 | 500-800 | 150-200 | 保持数学公式完整性 |
对话记录分析 | 200-300 | 30-50 | 短文本无需大重叠 |
- 代码示例验证
In [ ]:
from langchain.text_splitter import RecursiveCharacterTextSplitter text = "..." # 假设这里是一个长文本 splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=100, separators=["\n\n", "\n", "。", "!", "?"] ) chunks = splitter.split_text(text) for i, chunk in enumerate(chunks): print(f"Chunk {i+1} (长度: {len(chunk)}):\n{chunk[:100]}...\n")
- ✅注意事项 长度计算方式:
默认按字符计数(中文1字=1字符)
如需按token计算(如LLM输入),需指定tokenizer:
In [ ]:
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese") chunk_size = 500 # 500个token ≈ 375个汉字
重叠部分优化:
避免在单词/术语中间分割:
In [ ]:
splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=100, keep_separator=True # 保留分隔符完整性 )
性能影响:
重叠部分会增加存储和计算量(向量库需存储更多数据)
但对召回率(Recall)有显著提升
通过合理设置这两个参数,可以确保: ✅ 关键信息不被割裂 ✅ 保持语义完整性 ✅ 适配下游任务(如检索、生成等)的需求
In [ ]:
#智能分块与结构化处理 from langchain.text_splitter import ( RecursiveCharacterTextSplitter, MarkdownHeaderTextSplitter ) # 通用文本分块器 text_splitter = RecursiveCharacterTextSplitter( chunk_size=400, chunk_overlap=80, separators=["\n\n", "\n", "。", "!", "?", ";", "……", " ", ""] ) for doc in documents: # Markdown文件按标题分块 if doc.metadata["file_type"] == ".md": headers_to_split_on = [ ("#", "Header 1"), ("##", "Header 2"), ("###", "Header 3"), ] markdown_splitter = MarkdownHeaderTextSplitter( headers_to_split_on=headers_to_split_on ) split_docs = markdown_splitter.split_text(doc.page_content) final_docs.extend(split_docs) # 表格特殊处理 elif doc.metadata["file_type"] == ".csv": table_text = "表格内容:\n" for row in doc.page_content.split("\n"): table_text += "| " + " | ".join(row.split(",")) + " |\n" doc.page_content = table_text #json问答对 elif doc.metadata["file_type"] == ".json": split_docs = [doc] final_docs.extend(split_docs) # 其他文件类型使用通用分块 else: split_docs = text_splitter.split_documents([doc]) final_docs.extend(split_docs)
2.3 向量化阶段
(1) Embedding模型选择
使用text2vec-large-chinese,其是一个专注于中文文本嵌入(Embedding)的预训练模型,从魔塔社区下载到本地使用
(2) 向量化实现
In [ ]:
# 使用HuggingFace本地模型 from sentence_transformers import SentenceTransformer model_name = "./models/text2vec-large-chinese" embedding_model = SentenceTransformer(model_name) # 批量生成向量(全部生成,避免重复计算) texts = [doc.page_content for doc in documents] embeddings = self.embedding_model.encode( texts, normalize_embeddings=True, batch_size=32, # 根据GPU内存调整 show_progress_bar=True )
2.4 向量数据库构建
(1) 数据库选型对比
数据库 | 写入速度 | 查询性能 | 适合场景 |
---|---|---|---|
FAISS | 快 | 极快 | 实验/小规模 |
Chroma | 中 | 快 | 快速原型开发 |
Milvus | 慢 | 快 | 大规模生产环境 |
由于Chroma操作方便,无需安装,直接建立目录即使用,所以选择了Chroma,同时可把向量入库和查询的代码都规划好模块,等以后用方便替换为其他数据库
(2) 向量入库
In [ ]:
#向量入库代码示例 import chromadb persist_dir = "./dbs/chromadb" client = chromadb.PersistentClient(path=persist_dir) # 准备元数据 metadatas = [doc.metadata for doc in documents] ids = [f"doc_{i}" for i in range(len(documents))] # 统一生成ID # 分批次提交(每批100条) batch_size = 100 for i in range(0, len(texts), batch_size): batch_texts = texts[i:i + batch_size] batch_embeddings = embeddings[i:i + batch_size].tolist() # 当前批的embeddings batch_metadatas = metadatas[i:i + batch_size] batch_ids = ids[i:i + batch_size] # 存入当前批次 self.collection.add( documents=batch_texts, embeddings=batch_embeddings, metadatas=batch_metadatas, ids=batch_ids )
2.5 检索增强生成(RAG)
用户提问 → 检索最相关的文本片段 → LLM 生成回答。 推荐 LLM:
云端:GPT-4、DeepSeek-V3
本地:Llama3、ChatGLM3、DeepSeek-R1-Distill-Qwen-1.5B
以DeepSeek-R1-Distill-Qwen-1.5B为例
In [ ]:
#检索最相关的文本片段 from embedding import VectorDatabase query = "上游泳课,泳裤没干"#查询内容 vector_db = VectorDatabase(persist_dir="./dbs/chromadb",model_name=model_name) query_embedding = vector_db.embedding_model.encode( query, normalize_embeddings=True ) # ChromaDB查询 results = vector_db.collection.query( query_embeddings=[query_embedding.tolist()], n_results=5 ) # 格式化输出 formatted_results = [] for i in range(len(results["ids"][0])): doc_id = results["ids"][0][i] doc_text = results["documents"][0][i] metadata = results["metadatas"][0][i] score = 1 - results["distances"][0][i] # 余弦相似度转换 if score >= 0.6: formatted_results.append({ "id": doc_id, "text": doc_text, "metadata": metadata, "score": round(score, 4) })
In [ ]:
#LLM 生成回答,以本地模型DeepSeek-R1-Distill-Qwen-1.5B为例 #生成提示词 content = formatted_results prompt = " [ { "role": "system", "content": "你是一个专业问答助手,根据提供的上下文回答问题。回答时请直接给出最终答案,不要包含思考过程或中间推理。如果不知道答案,请回答'根据已有信息无法确定'。" }, { "role": "user", "content": f"上下文:{context}\n\n问题:{query}\n请用中文回答:" } ] " #调用大模型,使用LMDeploy加载的,下面的代码稍加修改也可以调用云上的模型 from openai import OpenAI # 默认值 DEFAULT_BASE_URL = "http://localhost:23333/v1" DEFAULT_API_KEY = "your-default-api-key" DEFAULT_MODEL_NAME = "DeepSeek-R1-Distill-Qwen-1.5B"#"Qwen2.5-0.5B-Instruct" # 初始化 OpenAI 客户端 client = OpenAI(base_url=DEFAULT_BASE_URL, api_key=DEFAULT_API_KEY if DEFAULT_API_KEY else None) response = client.chat.completions.create( model=DEFAULT_MODEL_NAME, # 使用用户指定的模型名称 messages=[ {"role": "user", "content": prompt} ], max_tokens=100, # 设置生成的最大 token 数 temperature=0.7, # 设置生成文本的随机性 ) print(response.choices[0].message.content)
三. 应用部署
选择使用FastAPI+Nginx 反向代理+html
FastAPI的优点
框架: FastAPI (高性能,自动生成文档)
认证: JWT (适合企业级应用) 或 API Key (快速实现)
文档: Swagger UI (内置于FastAPI)
并发: Uvicorn + Gunicorn (生产环境),Gunicorn 管理多进程,Uvicorn 处理异步请求
四、关键优化点
检索质量优化
多路召回(关键词+向量)
动态chunk大小(重要文档分小块)
性能优化
批量处理文档
建立增量更新机制
安全措施
文档访问权限控制
问答记录审计
五、典型技术栈组合
建议从小规模原型(FAISS+ChatGLM3)开始验证,再逐步扩展到生产级方案(Milvus+LLama3)。