十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零搭建RAG知识库问答系统:原理、代码与工程落地

从零搭建RAG知识库问答系统:原理、代码与工程落地 之前在做知识库问答类功能时最头疼的问题就是大模型“一本正经地胡说八道”。明明内部资料里写得清清楚楚模型却经常给出一个看起来合理、实际上完全错误的答案。后来接触并落地了 RAGRetrieval-Augmented Generation检索增强生成方案才真正把这个问题从根上解决了一部分。本文就围绕 RAG 原理、系统搭建、代码实战和工程落地展开从零开始带大家完成一套完整的 RAG 项目。内容比较长建议先收藏再慢慢看。1. RAG 是什么它解决了什么问题1.1 大模型的三个“天生缺陷”先聊一个很多人都会遇到的场景你问大模型一个公司内部制度问题比如“年假怎么休”模型回复了一段看起来很专业的答案但和你们公司的制度完全对不上。这不是模型笨而是它本来就不“知道”你们公司的内部资料。大模型本质上是一个基于海量公开语料训练出来的概率模型。训练结束之后它的知识就固定在了某个时间点这带来三个问题知识时效性差训练数据有截止时间之后发生的新事件、新政策、新产品信息它都不知道。缺乏私有领域知识企业内部文档、专业数据库、个人笔记等非公开数据模型从未见过。容易产生幻觉当模型不确定答案时它会根据概率“编”一个看起来合理的回答而不是承认自己不知道。这些缺陷在通用对话场景下还能忍受但在企业知识库、客服问答、法律文书、医疗辅助等场景下错误答案的代价非常高。1.2 RAG 的核心思路先检索再生成RAG 的思路其实很朴素不要逼模型凭记忆回答而是先从一个外部知识库中检索出与问题最相关的资料片段把这些片段作为“参考资料”一起丢给模型让模型基于这些资料来生成回答。这样做的好处非常明显答案有据可依显著降低幻觉。知识库可以随时更新不需要重新训练模型。内部数据不会进入模型训练隐私相对可控。相比微调Fine-tuningRAG 的成本更低、迭代更快。你可以把 RAG 理解成“开卷考试”模型不再闭卷背诵而是带着一堆参考书进考场先翻书找到相关段落再组织语言回答问题。1.3 RAG 的典型应用场景RAG 并不是只能做问答机器人它的应用范围比很多人想象中更广企业知识库问答把内部文档、制度、SOP 变成可检索的问答系统。智能客服结合产品手册和售后资料自动回答用户问题。文档分析助手对 PDF、Word、网页等长文档进行摘要、信息抽取和对比分析。专业领域辅助医疗、法律、金融等场景下基于权威资料提供参考建议。大模型 Agent 的记忆与工具增强让 Agent 具备访问私有数据的能力。下面我们先把 RAG 的工作原理拆解清楚再动手实现一套可运行的项目。2. RAG 系统工作原理拆解2.1 RAG 整体流程一个标准的 RAG 系统可以分成两个阶段知识库构建索引阶段和问答阶段检索生成阶段。知识库构建阶段做的事情是加载文档读取 PDF、TXT、Markdown、Word 等格式的资料。文本切分把长文档切成适合检索的文本块chunk。向量化用 Embedding 模型把每个文本块转成向量。存入向量数据库将向量和原始文本一起存储建立索引。问答阶段做的事情是问题向量化把用户问题用同一个 Embedding 模型转成向量。相似度检索在向量库中找出与问题向量最相似的 Top-K 个文本块。构造 Prompt把检索到的文本块和原始问题组装成提示词。生成回答把 Prompt 交给大模型生成最终答案。2.2 Embedding 模型是什么Embedding嵌入是 RAG 中承上启下的关键环节。它的作用是把一段文本映射成一个固定维度的向量例如 768 维或 1024 维的浮点数数组。语义相近的文本在向量空间中的距离也更近。举个例子“今天天气怎么样” 和 “今天会不会下雨” 的向量距离会比较近。“今天天气怎么样” 和 “红烧肉怎么做” 的向量距离会比较远。这样当用户提问时系统就可以通过计算向量相似度找到语义上最相关的知识片段。常见的中文 Embedding 模型有text2vec、bge-small-zh、bge-large-zh、m3e等。英文场景常用的有OpenAI text-embedding-ada-002、nomic-embed-text等。如果希望在本地环境完整跑通使用Ollama拉起nomic-embed-text是很方便的做法。2.3 向量数据库的作用向量数据库用于存储 Embedding 向量并提供高效的相似度检索能力。常见的向量数据库和工具包括Chroma轻量级支持本地持久化适合学习和中小项目。FAISSFacebook 开源的向量检索库性能高但偏向底层库。Milvus / Zilliz Cloud分布式向量数据库适合大规模生产环境。pgvectorPostgreSQL 的向量扩展适合已有 PostgreSQL 体系的团队。Elasticsearch通过向量插件支持混合搜索适合已有 ES 体系的团队。项目初期用 Chroma 或 FAISS 就够了等数据量上来再迁移到 Milvus 或 pgvector。2.4 文本切分的策略文本切分直接影响检索质量。切得太粗一个 chunk 包含太多无关信息检索精度会下降切得太细单个 chunk 语义不完整模型拿到的上下文不足。常用的切分策略有以下几种固定长度切分按字符数或 token 数切分实现最简单。递归字符切分按段落、句子、标点逐级切分尽量保留语义完整性。语义切分根据句子向量之间的相似度变化确定切分点效果更好但计算成本更高。结构化切分针对 Markdown、HTML、PDF 按标题、表格结构切分。切分时需要设置两个关键参数chunk_size每个文本块的最大长度通常按 token 数或字符数计算。chunk_overlap相邻文本块之间的重叠长度目的是避免一句话被截断到两个块里。一般来说chunk_size 在 300 到 800 token 之间overlap 在 50 到 100 token 之间具体需要根据文档类型和检索效果调优。3. 环境准备与项目初始化3.1 环境要求本文示例以 Python 为主核心依赖是 LangChain、Chroma、Ollama。具体版本请根据项目实际情况调整下面只给出依赖名称和用途。建议环境操作系统Windows 10/11、macOS、Linux 均可。Python3.9 及以上版本。本地大模型运行工具Ollama用于加载 Embedding 模型和 LLM。开发工具VS Code 或任意 Python IDE。3.2 安装依赖先创建一个虚拟环境python -m venv rag_env # Windows rag_env\Scripts\activate # macOS / Linux source rag_env/bin/activate然后安装 Python 依赖pip install langchain langchain-community langchain-text-splitters pip install chromadb pip install pypdf说明langchain-community里包含了我们需要的文档加载器、向量库封装等组件chromadb提供向量存储和检索能力pypdf用于解析 PDF 文件。3.3 用 Ollama 准备本地模型如果你暂时没有大模型 API 的预算或者不希望把私有数据传到外部服务用 Ollama 在本地跑模型是很合适的方案。安装 Ollama 后拉取需要的模型# 本地运行 LLM示例用 qwen2.5 ollama pull qwen2.5:7b # 本地运行 Embedding 模型 ollama pull nomic-embed-text拉取完成后可以通过下面的命令验证服务是否正常ollama list如果你的开发机器 GPU 显存不够也可以选择更小的模型例如qwen2.5:3b或tinyllama。RAG 对 LLM 的推理能力有一定要求模型太小时即使检索到了正确答案也可能组织不好语言。3.4 项目结构规划我们用一个简单的目录来组织代码rag-project/ ├── docs/ │ └── demo.txt ├── config.py ├── ingest.py ├── query.py └── requirements.txt各文件职责如下docs/demo.txt示例知识文档。config.py统一管理模型名称、向量库路径等配置。ingest.py文档加载、切分、向量化、入库。query.py检索并生成回答。requirements.txtPython 依赖清单。4. 手把手完成一套 RAG 实战项目4.1 准备示例知识文档在docs/demo.txt中写一段模拟的企业制度文档内容要包含几个可以被检索到的明确知识点公司考勤管理制度 第一条 工作时间 公司实行每周五天工作制工作时间为周一至周五上午 9:00 至下午 18:00其中 12:00 至 13:00 为午休时间。 第二条 请假流程 员工请假需提前一天在 OA 系统中提交申请三天以内的假期由部门负责人审批三天以上需同时报人力资源部备案。 第三条 年假政策 员工入职满一年后每年享有 5 天带薪年假。入职满三年后年假天数增加到 10 天。年假应在当年内休完未休完部分不累计到下一年度。 第四条 加班与调休 工作日加班超过 1 小时的部分可以申请调休或按加班工资计算。法定节假日加班按国家规定支付三倍工资。这段文档包含了多个知识点方便后面验证检索效果。4.2 编写配置文件config.py用于集中管理配置。集中管理的好处是后续调整模型或向量库路径时不需要改业务代码。# config.py import os # 知识文档目录 DOCS_DIR os.path.join(os.path.dirname(__file__), docs) # 向量数据库持久化目录 CHROMA_DIR os.path.join(os.path.dirname(__file__), chroma_db) # Embedding 模型名称Ollama 本地模型 EMBEDDING_MODEL nomic-embed-text # 生成回答使用的 LLM 名称Ollama 本地模型 LLM_MODEL qwen2.5:7b # 检索返回的文本块数量 TOP_K 4 # 文本切分参数 CHUNK_SIZE 500 CHUNK_OVERLAP 504.3 编写知识库构建代码ingest.py负责把文档变成向量并入库。这是 RAG 系统最核心的代码之一。# ingest.py from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import Chroma import config def build_knowledge_base(): # 1. 加载 docs 目录下的 txt 文档 loader DirectoryLoader( config.DOCS_DIR, glob**/*.txt, loader_clsTextLoader, loader_kwargs{encoding: utf-8} ) documents loader.load() print(f加载文档数量: {len(documents)}) # 2. 递归字符切分 text_splitter RecursiveCharacterTextSplitter( chunk_sizeconfig.CHUNK_SIZE, chunk_overlapconfig.CHUNK_OVERLAP, separators[\n\n, \n, 。, , , , , ] ) chunks text_splitter.split_documents(documents) print(f切分文本块数量: {len(chunks)}) # 3. 向量化并写入 Chroma embeddings OllamaEmbeddings(modelconfig.EMBEDDING_MODEL) vectorstore Chroma.from_documents( documentschunks, embeddingembeddings, persist_directoryconfig.CHROMA_DIR ) print(f向量库存放完成路径: {config.CHROMA_DIR}) if __name__ __main__: build_knowledge_base()这里有几个值得注意的点DirectoryLoader可以批量加载目录下所有匹配的文件避免为每个文档写加载逻辑。RecursiveCharacterTextSplitter的分隔符数组是有优先级的它先尝试按\n\n切分再按句子标点切分这样能最大程度保留语义完整。Chroma.from_documents会同时完成向量化和入库数据会持久化到persist_directory指定的目录。Embedding 模型必须和后面查询阶段使用同一个模型否则检索结果会完全错乱。运行入库python ingest.py正常情况下会输出类似下面的信息加载文档数量: 1 切分文本块数量: 8 向量库存放完成路径: .../rag-project/chroma_db4.4 编写问答检索代码query.py实现完整的检索增强生成流程。# query.py from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import Chroma from langchain_community.llms import Ollama from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate import config def create_qa_chain(): # 初始化 Embedding 和向量库 embeddings OllamaEmbeddings(modelconfig.EMBEDDING_MODEL) vectorstore Chroma( persist_directoryconfig.CHROMA_DIR, embedding_functionembeddings ) # 构造检索器 retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: config.TOP_K} ) # 初始化本地 LLM llm Ollama( modelconfig.LLM_MODEL, temperature0.1, num_predict1024 ) # 构造 Prompt prompt_template 你是一个严谨的知识库问答助手。请仅根据下面提供的资料回答问题。 如果资料中没有相关内容请直接回答“资料中未找到相关内容”禁止编造。 资料 {context} 问题{question} 请用中文回答 PROMPT PromptTemplate( templateprompt_template, input_variables[context, question] ) # 组装 RAG 问答链 qa RetrievalQA.from_chain_type( llmllm, retrieverretriever, chain_typestuff, chain_type_kwargs{prompt: PROMPT}, return_source_documentsTrue ) return qa if __name__ __main__: qa create_qa_chain() question 入职满两年年假有几天 result qa.invoke(question) print(问题:, question) print(回答:, result[result]) print(\n--- 检索到的参考资料 ---) for i, doc in enumerate(result[source_documents]): print(f[{i 1}] {doc.page_content[:100]}...)代码中的chain_typestuff表示把所有检索到的文本块直接拼接进 Prompt。当 Top-K 数量较少、每个 chunk 不大时这个方式最简单有效。temperature0.1让模型输出更稳定减少随机发挥。知识库问答场景下稳定优先于多样性。运行问答python query.py预期输出类似问题: 入职满两年年假有几天 回答: 根据公司考勤管理制度员工入职满一年后每年享有 5 天带薪年假入职满三年后年假增加到 10 天。入职满两年的员工年假仍为 5 天。 --- 检索到的参考资料 --- [1] 第三条 年假政策... [2] 第二条 请假流程...看到这样的输出就说明这套 RAG 项目已经跑通了。模型不是凭记忆回答而是基于我们提供的文档内容给出的答案。4.5 用原生方式理解 RAG 内部流程LangChain 封装了大量细节方便是方便但如果不理解内部原理出了问题很难排查。下面用最原始的方式演示一遍检索过程# simple_retrieve_demo.py from langchain_community.embeddings import OllamaEmbeddings from chromadb import PersistentClient import config def simple_similarity_search(query: str, top_k: int 2): # 1. 加载向量库 client PersistentClient(pathconfig.CHROMA_DIR) collection client.get_or_create_collection(langchain) # 2. 向量化问题 embeddings OllamaEmbeddings(modelconfig.EMBEDDING_MODEL) query_vector embeddings.embed_query(query) # 3. 在向量库中检索 results collection.query( query_embeddings[query_vector], n_resultstop_k, include[documents, metadatas, distances] ) # 4. 打印结果 for i, doc in enumerate(results[documents][0]): distance results[distances][0][i] print(f第 {i 1} 条距离: {distance:.4f}) print(doc) print(- * 50) if __name__ __main__: simple_similarity_search(年假政策, top_k2)这里的distance表示向量之间的相似度距离距离越小代表语义越相关。理解这层逻辑之后再看 LangChain 的RetrievalQA你就会明白它只是把“向量化、检索、拼 Prompt、调 LLM”这几件事封装起来了并没有额外的魔法。5. 检索质量评估RAG 效果好不好必须用指标说话很多同学做完 RAG 后凭感觉判断“看起来还行”。但在企业级项目中必须用可量化的指标评估效果否则上线后出了问题很难定位。5.1 检索阶段指标检索阶段评估的是系统能否找到正确答案对应的文本块。命中率Hit Rate在 Top-K 结果中是否包含包含正确答案的文本块。命中率越高说明检索越有效。MRRMean Reciprocal Rank衡量正确答案排在结果中第几位。如果正确答案排得越靠前MRR 越高。召回率RecallK在 Top-K 结果中相关文档占全部相关文档的比例。这些指标需要提前构建测试集。最简单的方法是从知识文档中抽取若干条“问题-正确答案片段”对然后逐个测试检索结果。5.2 生成阶段指标生成阶段评估的是最终回答的质量。常见的评估维度包括忠实度Faithfulness回答是否严格基于检索到的资料有没有编造内容。答案相关性Answer Relevancy回答是否针对用户问题有没有答非所问。上下文相关性Context Relevance检索到的上下文是否与问题相关。这部分评估可以使用 LLM 作为裁判LLM-as-a-Judge也可以人工抽样评估。企业级项目中建议两者结合先用 LLM 自动评测大批量数据再人工抽检重点场景。5.3 评估工具简介目前 LLM 应用评估工具比较多常见的有RAGAS、TruLens中文项目也可以使用Agile-Data等。RAGAS 提供了一套开箱即用的指标计算框架适合快速接入。RAGAS 的核心思路是把测试集构造成question、answer、contexts、ground_truth四个字段然后计算上述指标。评估代码很简单from ragas import evaluate from ragas.metrics import faithfulness, answer_relevancy, context_precision # 需要准备测试数据集fields 包含 question/answer/contexts/ground_truth result evaluate( datasettest_dataset, metrics[faithfulness, answer_relevancy, context_precision] ) result.to_pandas()注意RAGAS 的评估依赖一个可用的 LLM 来打分所以使用前需要配置好 LLM 接口。6. 企业级 RAG 落地的常见问题与排查思路下面整理几个高频问题都是实战中容易踩的坑。问题现象常见原因解决思路检索结果完全不相关Embedding 模型不适合领域或语言换用中文/领域专用的 Embedding 模型例如 bge-zh 系列回答内容太泛没有引用资料细节chunk 太大检索粒度太粗调小 chunk_size增大 chunk_overlap答案包含资料中没有的内容Prompt 没有约束“只能基于资料回答”在 Prompt 中明确禁止编造并设置低温参数同一个问题多次回答不一致temperature 设置过高将 temperature 调低如 0.1 以下向量检索耗时长数据量过大或使用了不合适的向量库引入索引优化、批量检索或迁移到 Milvus新增文档后查不到新文档没有重新执行入库流程实现增量入库对新增文档单独向量化并写入中文切分把句子截断分隔符只用了空格增加中文标点分隔符例如。Ollama 调用时报连接错误Ollama 服务未启动或端口不通执行ollama serve启动服务检查 11434 端口对于最常遇到的“检索结果不相关”问题建议按下面的顺序排查查看检索到的原始文本块内容确认是不是 chunk 切分的问题。报告问题的原文测试不同提问方式确认是不是问题表述的问题。尝试换一个 Embedding 模型比较命中率。检查知识库里是否存在相关文档。如果文档本身缺失再好的检索也没用。检查 Top-K 值是否太小。K 太小时可能会漏掉正确答案。7. 企业级 RAG 最佳实践与工程建议7.1 检索质量优先于生成调优RAG 的效果上限由检索决定。如果检索到的内容本身就不相关再好的 LLM 也无法生成正确答案。所以建议把所有精力优先放在高质量的数据清洗。合理的文本切分。合适的 Embedding 模型。针对性的检索调优。数据质量是 RAG 的地基。文档里如果存在大量错误信息、重复信息或格式混乱的内容最终回答质量一定受影响。在上线前需要对知识库做一轮清洗和去重。7.2 选择合适的检索策略基础版 RAG 只做向量相似度检索但企业级场景中往往不够。推荐以下几种增强策略混合检索Hybrid Search同时使用向量检索和关键词检索再融合结果。关键词检索擅长处理专业术语、产品型号等冷门词汇向量检索擅长处理语义相近但字面不同的表达。重排序Rerank第一轮先用向量检索召回较多的候选例如 20 条再用 Rerank 模型精排取前几名的结果。重排序能显著提升精度但对性能有一定消耗。多路召回从不同数据源或不同检索方式分别召回再合并去重。如果使用了 Elasticsearch 或 OpenSearch可以原生支持 BM25 向量的混合检索。对于 Milvus 这类专用向量库通常需要结合外部搜索服务实现混合检索或者干脆用 Milvus 自身的能力做两种检索再进行分数融合。7.3 权限与数据安全企业知识库中通常包含敏感数据RAG 系统上线前必须想清楚权限问题。最小权限原则不同角色只能检索到自己有权限访问的文档。实现方式是在检索阶段根据用户身份过滤文档或知识库集合。数据不出内网若对数据安全要求严格应该使用本地部署的 Embedding 模型和 LLM避免把私有文档上传到外部 API。敏感信息脱敏在入库前对身份证号、手机号、银行卡号等敏感信息做脱敏处理。访问审计记录用户查询、检索结果和生成回答的日志便于安全审计和问题回溯。7.4 建立评测闭环企业级 RAG 系统不能“上线即不管”。建议搭建一个持续评测机制建立测试集包含问题、标准答案、参考片段。每次修改切分策略、换模型、调参数后都跑一遍评测。比较检索指标和生成指标用数据决定是否更新。评测集也需要定期更新融入线上用户提出的真实问题让测试集本身更贴近实际。7.5 性能与缓存RAG 的性能瓶颈主要在两个地方Embedding 计算和 LLM 推理。Embedding 阶段可以批量计算避免循环调用。入库时一次性批量向量化比逐条处理快很多。高频问题可以做缓存。用户问过的问题如果再次出现直接返回缓存答案减少 LLM 调用成本。搜索阶段如果数据量大建议在向量库中建好索引并合理设置nprobe等参数。不同向量数据库参数不同一般来说nprobe越大检索越准但耗时越长需要根据实际检索延迟做权衡。7.6 从 RAG 到 Agentic RAG当 RAG 变得复杂后会有人引入 Agent 的思想。所谓 Agentic RAG就是让大模型自主决定“什么时候需要检索、检索什么、检索多少次”本质上能给 RAG 增加规划能力。举个例子用户问“对比一下 A 方案和 B 方案的优缺点”传统 RAG 可能只会检索一次拿到的上下文不够全面Agentic RAG 可以先拆解问题检索 A 方案相关资料再检索 B 方案相关资料最后汇总回答。但如果刚入门建议先扎实掌握基础 RAG不要急着上 Agent。基础链路稳定之后再根据业务复杂度决定是否引入 Agent 规划。8. 总结与下一步学习路线到这里我们已经完整实现了一个 RAG 系统从文档加载、文本切分、向量化入库到检索、构造 Prompt、生成回答以及评估方法和企业级落地建议。回顾一下核心关键点RAG 的核心是“先检索再生成”用外部知识库约束大模型的生成过程。Embedding 模型和文本切分策略直接决定检索质量。向量数据库是存储和检索的载体小项目用 Chroma / FAISS大规模场景用 Milvus / pgvector。Prompt 中要明确“只能基于资料回答”这是降低幻觉的最直接手段。上线前必须建立评测指标和测试集用数据评估效果而不是靠感觉。如果你接下来想继续深入建议按照下面的路线学习尝试替换不同的 Embedding 模型对比检索命中率。加入混合检索和 Rerank观察效果和耗时变化。从 LangChain 的封装中跳出来用原生 API 实现一次完整的加载、切分、向量化、检索流程。学习如何设计增量入库和文档更新策略。研究如何对不同角色做文档权限隔离。等基础链路稳定后再探索 Agentic RAG 和复杂多轮交互场景。RAG 是一个实践性很强的方向代码可以复制本文的示例跑通但真正理解它、用好它还是需要不断拿真实业务数据去测试和调优。希望这篇文章能帮你在 RAG 落地的路上少踩一些坑。有不清楚的细节欢迎在评论区留言交流。
返回列表