
做 LLM 应用开发时最常遇到的尴尬情况就是模型回答得一本正经实际上却是一本正经地胡说八道。尤其是让大模型回答公司内部产品文档、项目经验、离线业务数据这类私有知识单纯靠写死在 Prompt 里的上下文根本不够用——长文本塞不进去塞进去也超出上下文窗口而且每次问答都要重新拼接成本与延迟双双失控。后来系统性学习和实践了 RAGRetrieval-Augmented Generation检索增强生成才意识到这其实是知识类问答场景的标准解法。这篇文章会从 RAG 的原理讲起再带大家从零搭建一套完整可运行的 RAG 项目内容包括文档加载、文本切分、向量库构建、检索问答、效果评估和企业级落地优化方向。新手可以照着一步步操作有经验的开发者也能快速拿到一套可复用、可扩展的代码框架。1. 为什么大模型需要 RAG先搞懂这个问题1.1 大模型的知识困境大语言模型LLM本质上是一个“根据训练数据预测下一段文本”的系统。它记忆的知识来自训练语料而这些语料存在三个天然限制知识截止时间固定模型训练完成后无法自动获取新知识。通用知识多、私有知识少公司内部文档、个人笔记、行业研究报告等内容出现在训练语料中的概率极低。知识仍然可能“记错”模型会以很高的流畅度生成不存在的答案也就是常说的幻觉问题。在企业内部知识问答、法律政策问答、客服工单处理等场景中这三个限制非常致命。用户问一个业务规则模型可能给出“听起来合理但实际不存在”的答案这直接影响业务可靠性。1.2 RAG 是什么一句话理解RAG 的思路很直接先检索再生成。当用户提出一个问题时系统不去直接让大模型“回忆”答案而是先从外部知识库中检索出与该问题最相关的内容片段把这些片段作为参考资料连同用户问题一起交给大模型让大模型基于参考资料生成答案。换句话说RAG 相当于给大模型配了一个“外置知识库”。模型不需要记住所有内容它只需要具备“阅读理解”和“归纳总结”的能力真正的事实性内容由检索系统提供。这种设计带来几个明显优势答案可溯源可以告诉用户“答案来自哪份文档”。知识可以实时更新新增知识只需要更新检索库不需要重新训练模型。减少幻觉模型被约束在给定资料范围内作答。降低训练成本不需要为了新知识微调大模型。1.3 RAG 适合哪些场景RAG 的应用面非常广目前主流场景包括企业知识库问答把产品文档、内部规范、项目资料做成可交互问答系统。客服智能助手基于历史工单、FAQ、售后文档回答问题。法律与医疗辅助阅读对长文本条款、指南做定向检索。个人学习助手把书籍、笔记、论文做成私人知识库。数据报表解读把数据库里的结构化数据转换为大模型可理解的语义内容后回答问题。RAG 并不适合所有任务。如果要让模型完成创意写作、数学推理、代码生成这类“不依赖具体资料”的任务直接使用大模型可能更高效。RAG 的核心价值场景是“答案必须来自某份具体资料”。2. 环境准备与项目结构2.1 运行环境说明本文实战案例以 Python 为例涉及以下核心组件Python 3.10 及以上版本LangChain 生态作为流程编排框架HuggingFace 嵌入模型本地运行不需要访问海外模型服务Chroma 作为向量数据库支持持久化适合学习和小型项目本地大模型推理服务通过 Ollama 启动兼容 OpenAI API 格式如果你的电脑没有 GPU本文案例依然可以运行只是向量化和本地模型推理会慢一些。重点先打通流程性能优化放在后面章节讨论。版本方面目前 LLM 生态依赖更新非常快我建议以实际安装时的版本为准。本文示例代码的主要作用是演示实现思路如果你安装的 LangChain 版本比文中新个别模块导入路径可能需要按官方文档微调。2.2 依赖安装创建项目目录并准备虚拟环境mkdir simple_rag cd simple_rag python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate创建requirements.txtlangchain0.2.0 langchain-community0.2.0 langchain-huggingface0.0.3 langchain-text-splitters0.2.0 langchain-openai0.1.0 chromadb0.5.0 sentence-transformers2.7.0 pypdf4.0.0 faiss-cpu1.8.0安装依赖pip install -r requirements.txt说明一点LangChain 0.2 版本之后很多模块从langchain主包拆分到了独立的命名空间包。例如本文用到的HuggingFaceEmbeddings在langchain_huggingface中文本切分器在langchain_text_splitters中。如果你遇到ModuleNotFoundError先检查包的导入路径是否与版本匹配。2.3 项目目录设计本文案例的完整项目结构如下simple_rag/ ├── requirements.txt ├── config.py # 全局配置 ├── document_loader.py # 文档加载器 ├── text_splitter.py # 文本切分器 ├── build_index.py # 构建向量索引 ├── query_engine.py # 问答链路 ├── run.py # 交互式问答入口 └── data/ ├── docs/ # 放待索引的文档 └── vector_store/ # 向量库持久化目录这里的划分遵循了一个重要原则构建索引和问答查询分开。知识库更新频率较低一次构建后可以多次查询如果每次都重新加载和切分所有文档问答延迟会非常高。3. RAG 核心工作流程拆解在写代码之前需要把 RAG 链路上的每一个环节理解清楚。整个流程可以概括为五个步骤文档加载文本切分向量化与向量存储检索生成回答3.1 文档加载让大模型能“读”文件RAG 系统面临的第一件事是如何把格式各异的原始文件转换成纯文本。常见的文档格式包括Markdown / TXT直接用文本读取。PDF需要解析文本块、表格甚至扫描图像。Word需要解析 docx 格式。HTML需要提取正文去掉标签和脚本。在 LangChain 中每种格式都有对应的 Document Loader。以 PDF 为例本文使用PyPDFLoader它可以按页拆分 PDF并将每页内容封装为一个 Document 对象。Document 对象包含两个核心字段page_content文本内容和metadata来源信息。元数据非常重要。例如 PDF 文件路径、页码、文档标题等信息应该保留到每个文本块中。后续回答问题时可以借助元数据告诉用户“答案来自哪份文档的第几页”。3.2 文本切分决定检索质量的第一道关文档加载后往往是一段很长的文本。如果直接把整篇文档向量化会产生两个问题向量粒度太粗一个向量代表多页内容检索时难以精准定位答案所在段落。超出嵌入模型和生成模型的输入长度限制。因此需要对文本进行切分切分成语义独立、长度适中的“文本块”。切分策略直接影响检索效果chunk_size每个文本块的最大字符数。设置过大文本块内包含多个主题检索精度下降设置过小信息不完整回答时缺乏上下文。chunk_overlap相邻文本块之间的重叠部分。设置重叠可以避免一个问题恰好被切分边界截断导致信息丢失。推荐的做法是先按段落边界切分再调整块大小。中英文混合场景下分隔符需要同时考虑中英文标点和换行。3.3 向量化与向量存储知识的编码方式文本本身不能参与相似度计算需要先转换成向量。嵌入模型Embedding Model的作用就是把一段文本映射成一个固定维度的数值向量。语义相近的文本在向量空间中的距离会更近。例如“如何申请年假” 和 “年假申请流程是什么” 的向量距离会很近。“如何申请年假” 和 “食堂几点开门” 的向量距离会较远。向量存储Vector Store负责完成两件事保存文本块与向量的对应关系。在检索时快速找到与查询向量最相似的向量。常用的向量数据库包括 Chroma、FAISS、Milvus、Qdrant 等。对于学习和小型项目Chroma 足够对于企业级海量数据场景通常需要专业的向量数据库或云服务。3.4 检索与生成从召回到答案的链路检索阶段系统会计算用户问题向量与知识库中所有文本块向量的相似度返回最接近的 Top-K 个结果。这里的 K 值需要根据经验调整K 太小可能漏掉关键信息K 太大会引入无关噪声。生成阶段系统会把检索到的文本块拼接成上下文与用户问题一起放入 Prompt 模板然后交给大模型生成最终答案。一个优秀的 Prompt 模板需要明确约束模型行为只能基于给定资料回答。资料中没有答案时要直接说“无法回答”严禁编造。可以要求模型引用资料来源。可以设定回答的语言、风格和长度。到这里RAG 的主链路已经清楚了。下面进入实战代码。4. 手把手搭建一套可运行的 RAG 项目4.1 编写核心工具模块首先创建config.py把所有可配置项目集中管理# 文件路径simple_rag/config.py # 原始文档目录 DOC_DIR ./data/docs # 向量库持久化目录 VECTOR_DIR ./data/vector_store # 嵌入模型名称 # 使用本地模型无需联网 EMBEDDING_MODEL BAAI/bge-small-zh-v1.5 # 本地 LLM 服务地址通过 Ollama 启动 # 请先执行ollama pull qwen2.5:7b LLM_BASE_URL http://localhost:11434/v1 LLM_MODEL qwen2.5:7b LLM_API_KEY ollama # 检索返回的文本块数量 RETRIEVED_K 4 # 文本切分参数 CHUNK_SIZE 512 CHUNK_OVERLAP 64这里使用了BAAI/bge-small-zh-v1.5作为嵌入模型。它是国内广泛使用的中文嵌入模型体积小、效果稳定。第一次运行时sentence-transformers会自动从 HuggingFace 下载模型文件。如果下载受限需要提前在本地配置好镜像源或提前下载模型到本地目录。LLM_BASE_URL指向http://localhost:11434/v1这是 Ollama 提供的 OpenAI 兼容接口。如果你使用其他本地推理服务如 vLLM、LM Studio或云端 API只需要替换base_url、model和api_key三个配置项。创建document_loader.py# 文件路径simple_rag/document_loader.py from langchain_community.document_loaders import ( DirectoryLoader, TextLoader, PyPDFLoader, ) def load_documents(doc_dir: str): 加载目录下所有支持的文档返回 Document 列表 docs [] # 加载 Markdown 文件 md_loader DirectoryLoader( doc_dir, glob**/*.md, loader_clsTextLoader, loader_kwargs{encoding: utf-8}, show_progressTrue, ) docs.extend(md_loader.load()) # 加载 TXT 文件 txt_loader DirectoryLoader( doc_dir, glob**/*.txt, loader_clsTextLoader, loader_kwargs{encoding: utf-8}, show_progressTrue, ) docs.extend(txt_loader.load()) # 加载 PDF 文件 pdf_loader DirectoryLoader( doc_dir, glob**/*.pdf, loader_clsPyPDFLoader, show_progressTrue, ) docs.extend(pdf_loader.load()) return docs这段代码的意图是扫描doc_dir目录下的三类文件转换成统一的 Document 对象列表。show_progressTrue可以在控制台看到加载进度。创建text_splitter.py# 文件路径simple_rag/text_splitter.py from langchain_text_splitters import RecursiveCharacterTextSplitter def get_text_splitter(chunk_size: int 512, chunk_overlap: int 64): 创建递归字符文本切分器 return RecursiveCharacterTextSplitter( chunk_sizechunk_size, chunk_overlapchunk_overlap, separators[ \n\n, \n, 。, , , , ., !, ?, ;, ], )RecursiveCharacterTextSplitter的切分策略是递归的优先按完整段落切分如果段落仍然太长再依次按换行、中文句号、英文句号等分隔符细分。把中文句号加入separators是为了防止中英文混排时切分边界太生硬。4.2 构建知识库索引创建build_index.py# 文件路径simple_rag/build_index.py from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma from config import DOC_DIR, VECTOR_DIR, EMBEDDING_MODEL, CHUNK_SIZE, CHUNK_OVERLAP from document_loader import load_documents from text_splitter import get_text_splitter def build_index(): print(1. 加载文档...) docs load_documents(DOC_DIR) print(f 原始文档数量: {len(docs)}) if not docs: print( 未发现任何文档请先往 data/docs 目录中添加 .md/.txt/.pdf 文件。) return print(2. 切分文档...) splitter get_text_splitter(chunk_sizeCHUNK_SIZE, chunk_overlapCHUNK_OVERLAP) chunks splitter.split_documents(docs) print(f 切分后文本块数量: {len(chunks)}) print(3. 初始化嵌入模型...) embeddings HuggingFaceEmbeddings(model_nameEMBEDDING_MODEL) print(4. 构建并持久化向量库...) vectorstore Chroma.from_documents( documentschunks, embeddingembeddings, persist_directoryVECTOR_DIR, ) print(f 向量库构建完成已保存到: {VECTOR_DIR}) if __name__ __main__: build_index()运行前先准备一篇测试文档。在data/docs目录下创建一个company_policy.md文件内容可以简单写# 公司考勤与休假制度 员工每年享有 15 天带薪年假。年假需要提前 3 个工作日在 OA 系统中提交申请。 如果员工当月累计迟到 3 次以上将扣除当月绩效评分 5 分。遇到恶劣天气导致交通延误可提供当日交通记录申请免责。 弹性工作时间为上午 9 点到 10 点之间到岗工作满 8 小时后可下班。更多真实文档格式不限但建议先从小规模文档开始验证链路后再增加内容。然后执行索引构建python build_index.py预期输出类似1. 加载文档... 原始文档数量: 1 2. 切分文档... 切分后文本块数量: 3 3. 初始化嵌入模型... 4. 构建并持久化向量库... 向量库构建完成已保存到: ./data/vector_store首次运行嵌入模型需要下载模型文件耗时较长后续再运行会直接使用缓存。4.3 编写问答链路创建query_engine.py# 文件路径simple_rag/query_engine.py from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough from config import ( EMBEDDING_MODEL, VECTOR_DIR, LLM_BASE_URL, LLM_MODEL, LLM_API_KEY, RETRIEVED_K, ) SYSTEM_PROMPT 你是一个严谨的问答助手。请基于以下给定的资料回答问题。 要求 1. 只能根据资料中的内容回答不要编造。 2. 如果资料中没有足够信息请直接回答“根据现有资料无法回答”。 3. 回答时尽量简洁、准确、有条理。 4. 如果资料中提到了来源信息在回答末尾标注参考文档。 参考资料 {context} 用户问题 {question} def build_chain(): 构建 RAG 问答链路 embeddings HuggingFaceEmbeddings(model_nameEMBEDDING_MODEL) vectorstore Chroma( persist_directoryVECTOR_DIR, embedding_functionembeddings, ) retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: RETRIEVED_K}, ) llm ChatOpenAI( base_urlLLM_BASE_URL, modelLLM_MODEL, api_keyLLM_API_KEY, temperature0.3, ) prompt ChatPromptTemplate.from_messages( [ (system, SYSTEM_PROMPT), (human, {question}), ] ) chain ( { context: retriever, question: RunnablePassthrough(), } | prompt | llm | StrOutputParser() ) return chain def query(question: str) - str: 传入用户问题返回 RAG 回答 chain build_chain() return chain.invoke(question) if __name__ __main__: question input(请输入你的问题) answer query(question) print(\n回答, answer)这段代码中有几个关键点需要说明。vectorstore.as_retriever()把向量库包装成一个检索器。search_typesimilarity表示使用普通的相似度检索k4表示返回最相关的 4 个文本块。这四个文本块会被拼接到 Prompt 的{context}中。RunnablePassthrough()的作用是原样传递用户问题。这里需要注意 LangChain 链式调用的输入输出结构context由retriever生成而question直接透传用户输入两者组合后填充到 Prompt 模板中。temperature0.3设置了一个较低的温度目的是让模型的回答更保守、更贴近资料内容减少自由发挥。4.4 运行与验证创建run.py实现一个简单的交互式问答入口# 文件路径simple_rag/run.py from query_engine import query def main(): print(欢迎使用 RAG 问答系统输入 exit 退出) while True: question input(\n问题).strip() if question.lower() in (exit, quit, 退出): break if not question: continue answer query(question) print(f回答{answer}) if __name__ __main__: main()在运行问答程序前需要确保本地 Ollama 服务已经启动并且已经拉取了配置文件中指定的模型。可以在另一个终端执行ollama pull qwen2.5:7b ollama serve然后运行问答python run.py输入示例问题问题员工每年有几天年假 回答员工每年享有 15 天带薪年假。年假需要提前 3 个工作日在 OA 系统中提交申请。再测试一个知识库之外的问题问题公司食堂几点开门 回答根据现有资料无法回答。第二个回答体现了 RAG 的一个关键效果模型没有资料支持时不会编造答案。这是很多直接调用大模型接口的问答系统做不到的。如果你没有 Ollama也可以把LLM_BASE_URL、LLM_MODEL、LLM_API_KEY修改为任何 OpenAI 兼容的 API 服务代码其余部分不需要改动。5. RAG 知识库效果指标与评估方法RAG 系统上线后不能只凭“感觉回答得不错”来评估质量。需要建立一套可量化的评估指标。5.1 为什么要关注指标RAG 的效果取决于多个环节文档切分是否合理、嵌入模型是否匹配业务语言、检索算法是否召回正确答案、Prompt 是否约束住了模型行为。任何一个环节出问题最终答案都会出错。如果不做指标评估就很难判断问题出在哪个环节。上线后也无法持续追踪知识库更新是否带来效果提升。5.2 常用指标详解RAG 评估指标可以分为“检索质量”和“生成质量”两个层面。检索质量指标指标含义理解方式RecallK前 K 个检索结果中包含标准答案的比例衡量系统是否“找得到”正确答案PrecisionK前 K 个检索结果中真正相关结果的比例衡量系统是否“找得准”MRR第一个相关结果在排序中倒数位置的平均值衡量正确答案是否排在前面NDCG考虑排序位置的加权相关性分数越靠前越重要生成质量指标指标含义理解方式Faithfulness忠实度模型的回答是否严格基于给定资料衡量是否“答非所问”或“凭空编造”Answer Relevance答案相关性回答是否真正回应了用户问题衡量回答与问题的匹配度Context Relevance上下文相关性检索到的资料与问题的相关程度衡量检索环节是否有效在实际项目中最直观的是“答案准确率”准备一组包含问题和标准答案的测试集用 RAG 系统逐个回答问题统计回答与标准答案一致的比例。这个指标建立成本低业务人员也能看懂。5.3 如何在项目中评估建议从一个小规模测试集开始具体操作如下收集 50 到 100 个真实业务问题。为每个问题标注标准答案尽量标注答案来源文档。跑一遍 RAG 系统记录回答结果。人工判断回答是否正确、是否有引用、是否出现幻觉。对于检索环节可以单独检查每个问题的 Top-K 召回结果中是否包含正确答案对应的文本块。如果检索结果里没有正确答案问题大概率出在文本切分、嵌入模型或检索算法上而不是生成模型上。社区也有一些自动化评估框架例如 RAGAS可以从忠实度、答案相关性等维度自动打分。你可以把它作为辅助工具但不要完全依赖自动分数业务场景中的最终判断仍然需要人工确认。6. 企业级落地优化与工程化方向一个小型 RAG 项目跑通之后距离“企业级 RAG 系统”还有很长的路要走。这里总结几个核心优化方向。6.1 检索优化混合检索与重排序基于向量相似度的检索有一个天然弱点它对关键词匹配不敏感对专有名词、缩写、精确 ID 的检索效果不稳定。例如用户问“QPS 应该控制在多少”如果知识库中的原文是“每秒查询率”纯向量检索可能无法精准命中。解决方案是混合检索同时使用关键词检索如 BM25和向量检索把两类结果合并后再做重排序Rerank。重排序模型可以更精准地评估“文档片段与问题的相关性”把最相关的结果放到最前面。企业级 RAG 的建议检索链路是向量召回 Top 50。关键词召回 Top 50。合并去重。调用 Rerank 模型精排。取前 K 个送入 Prompt。6.2 生成优化Prompt 与上下文管理生成环节的优化目标不是让模型“更有文采”而是让模型“更忠实”。首先Prompt 要明确限定回答边界。系统提示词中应写明“严禁使用资料以外的知识作答”“当资料不足时直接声明无法回答”。其次检索结果拼接进 Prompt 时需要做内容压缩。大模型上下文窗口有限把 4 个长文本块全部塞进去未必有效。可以考虑使用摘要型重写在保留关键信息的前提下缩短文本块。社区中有不少基于 LLM 的上下文压缩思路核心都是“先粗粒度过滤再精粒度保留”。最后答案可以要求模型输出引用标记例如在回答中标注[1]、[2]并在回答末尾列出对应文档来源。这样可以让用户验证答案也方便业务方发现问题时追踪。6.3 工程化缓存、权限、监控与安全生产环境与本地 Demo 的区别主要体现在工程约束上。缓存方面可以在 RAG 系统前面加一层问答缓存。用户提出重复问题时直接返回缓存答案极大降低模型调用成本和延迟。缓存键可以基于“归一化后的问题 Top-K 检索结果摘要”生成。权限方面企业文档通常有保密等级。RAG 系统必须做到“用户只能检索到权限范围内的文档”。这种控制需要在文档入库时给每个文本块打上权限标签在检索时根据用户身份过滤而不是把所有私有知识都暴露给所有用户。监控方面上线前需要记录三类数据用户问题和回答日志。检索命中的文档来源。LLM 调用费用与延迟。回答日志配合人工抽检可以持续发现检索和生成质量问题。费用与延迟数据用于容量规划和成本控制。安全方面有两点需要强调。第一知识库中的敏感信息在入库前必须做脱敏处理避免私有数据被模型记住或泄露。第二对 LLM 生成内容要保留事后审计能力即使出现错误回答也能追溯到具体原因。企业对生产环境进行任何变更前都应该先在小范围测试环境验证并做好备份和回滚方案。7. 常见问题与排查清单7.1 高频问题对照表问题现象常见原因解决思路ModuleNotFoundError: No module named langchain_huggingface依赖包未安装或版本过旧安装最新依赖pip install -U langchain-huggingface向量库构建很慢嵌入模型过大或没有 GPU换用更小的嵌入模型例如bge-small-zh-v1.5检索结果明显不相关文本切分过大/过小或嵌入模型与业务语言不匹配调整 chunk_size 和 chunk_overlap尝试同领域微调嵌入模型回答总是编造内容Prompt 约束不足在 System Prompt 中明确“资料中没有就回答无法回答”本地 LLM 调用超时Ollama 服务未启动或模型未拉取检查ollama list确认ollama serve在运行同一文档重复出现在检索结果中切分时 overlap 过大或文档未去重检索后按来源文档去重或调低重叠比例中文标点被切碎切分器未识别中文分隔符在separators中加入中文句号、感叹号、问号7.2 排查建议遇到 RAG 效果问题时建议按照“定位环节”的思路排查先检查检索直接打印检索器返回的 Top-K 内容看是否包含正确答案。如果不包含问题在加载、切分、向量化或检索环节。再检查 Prompt检索结果正确但回答错误说明 Prompt 或 LLM 配置有问题。最后检查模型同一条检索内容换一个更强的 LLM 测试排除模型理解能力不足的因素。这个排查顺序可以节省大量时间。大多数 RAG 效果差的问题根源在检索而不是生成。8. 总结与下一步学习建议本文从大模型的知识困境出发解释了 RAG 检索增强生成的核心思想并完整搭建了一套可运行的 RAG 项目。你现在应该掌握了一条完整的链路文档加载 - 文本切分 - 向量化 - 向量存储 - 检索 - Prompt 组装 - LLM 生成 - 结果输出。如果你第一次运行遇到了问题先放下复杂的参数调优准备 3 到 5 条文本的小知识库把整条链路跑通再逐步增加文档规模和优化策略。这一步非常重要因为 RAG 的每行代码之间都有依赖关系链路不通时调参没有意义。下一步可以按以下顺序继续深入学习向量数据库原理理解 ANN 检索与 HNSW 索引。尝试接入混合检索和重排序模型观察检索效果变化。使用 RAGAS 等框架建立自动化评估流程。尝试用 Dify、RAGFlow 等开源框架搭建可维护的 RAG 服务。探索 Agent 与 RAG 的结合让系统能够自主决定何时检索、检索什么内容。RAG 是一个工程属性很强的方向从原型到生产之间需要补的知识还有很多。希望这篇文章能帮你迈出第一步后续的优化和落地需要在真实数据和真实业务中不断打磨。如果本文对你有帮助欢迎收藏备用后续会继续更新更多 RAG 落地实战内容。