简介这套基于大语言模型与RAG的知识库问答系统面向需要快速构建智能问答能力的技术团队与开发者支持直接上传文档或自动爬取在线文档通过文本拆分、向量化与检索增强有效降低大模型幻觉。系统保持模型中立既能接入Llama 3、Qwen 2等本地私有大模型也可对接国内主流大模型及OpenAI、Claude、Gemini等国外接口并内置工作流引擎与函数库支持零编码嵌入第三方业务系统。资源包共937个文件、约31.99MB以484个Python后端、204个Vue前端、110个TypeScript文件为主辅以SQL、SVG图标、Dockerfile等部署配置与文档。已有1403人学习下载适合希望掌握RAG落地、多模型对接及知识库工程化部署的中高级开发者参考。1. 基于大语言模型和 RAG 的知识库问答系统先解决大模型不知道你业务这件事你手头有一批产品手册、售后工单和公司制度文件每天几十个人在群里反复问同样的问题。你试过把文档直接丢给大语言模型结果它要么答得南辕北辙要么在关键参数上胡编。这正是基于大语言模型和 RAG 的知识库问答系统要解决的事情先通过检索把知识库里最相关的片段捞出来再让大模型只依据这些片段作答而不是凭训练记忆凭空发挥。这篇文章面向真正要落地这套系统的人不管是做企业知识库、客服助手还是文档问答我会把链路拆开讲清楚每个环节怎么选型、参数怎么配、会踩什么坑以及效果不行时先从哪儿查起。2. RAG 链路设计与技术选型Embedding、向量库、大模型怎么搭才不白做2.1 一条 RAG 链路从文档到答案要经过哪几步RAG 不是一个独立的算法它是一条链文档加载 → 清洗 → 切分 → 向量化 → 入库 → 检索 → 重排序 → 拼接 Prompt → 大模型生成。每一步都会影响最终答案而且越靠前的环节出错越往后的环节越难补救。我把这条链的职责分成两段来看。前段负责“有没有”切分、向量化、检索决定相关材料能不能被找到这是 RAG 效果的底线。后段负责“好不好”重排序和 Prompt 决定找到的材料能不能被用对大模型只负责把材料组织成通顺的答案。很多团队把精力全花在选大模型上结果检索召回的是些不痛不痒的片段换再强的模型也是无米之炊。实际落地时我一般会先把链路画出来标出每个环节的输入输出再逐个环节做最小验证。不要上来就搭建完整工程。先把一篇文章跑通端到端比搭好一个半成品架构再去迁数据要省时间得多。2.2 Embedding 模型选型中文场景别直接套英文模型Embedding 模型把文本变成向量语义相近的文本在向量空间里距离更近。检索命中率一大半由它决定。中文场景下直接套用纯英文训练的模型效果会很差专有名词、品牌名、长句式的表达经常抓不住重点。常见的几个选择维度可以看这张表方案典型代表部署方式适合场景需要注意的问题本地开源中文模型BAAI/bge-m3、m3e-base单机 GPU/CPU数据不出域、需要自控首次下载量较大需联网拉取权重通用本地多语模型multilingual-e5-large单机多语言混排文档维度高内存占用随之上升商业 API各大模型厂商的 embedding 接口远程调用不想自己维护向量服务每一次调用都有延迟和费用且有数据合规边界选型之后一定要做召回验证不要凭直觉。我的习惯是挑 20 个真实用户在系统里会问的问题逐一拿它们去检索测试库看第一轮召回里有没有包含正确答案的片段算一个简单命中率。如果 20 问里命中不到 15 问先调 Embedding 和切分不要急着动大模型。这一个环节最容易被忽略多数 RAG 项目翻车都翻在 Embedding 和文档质量上而不是大模型本身。2.3 向量库选型先拿 Faiss 跑通再考虑生产级集群向量库负责存储向量和做相似度检索。选型要贴着你的数据量和运维能力走别一上来就上重型架构。Faiss 是 Meta 开源的相似度检索库单机内存就能跑几十万条文本片段以内完全够用最省心。Chroma 更轻适合快速写 demo后续也能平滑迁移。Milvus 是真正的分布式向量数据库支持百万级以上数据量、过滤表达式和在线扩容适合数据规模确定变大后再上。Elasticsearch 自带向量检索能力如果你公司已经有 ES 集群多一套基础设施也可以省下来。我的建议是第一次搭 RAG 用 Faiss 跑完整条链路把精力花在切分策略和检索质量上。等真实数据量上来了再迁到 Milvus。迁库的成本远低于带着错误检索策略去搭集群的排错成本。团队主力是 Java 时也有 LangChain4j 的 RAG 模块接口和 Python 版对齐但底层策略得自己调别指望两边默认行为完全一致。2.4 大模型选型RAG 里生成模型的上限是检索给的在 RAG 链路里大模型只负责“照着提供的上下文把话说顺”它不负责回忆知识。所以选大模型时第一优先级是它能不能严格遵循指令、会不会在没依据时硬编第二优先级才是语言能力。7B 到 14B 量级的本地开源模型足够完成日常知识库问答。启动简单、延迟可控、数据不出域是我做内部系统时的默认选择。商业 API 质量更高但延迟、费用和数据合规需要一并评估。如果在 RAG 之上还要做「大模型微调实战」我建议先让 RAG 跑通再上微调。微调负责风格和输出格式RAG 负责知识供给两个黑匣子同时开启时排错会非常痛苦。3. 搭建最小可用的 RAG 问答系统从文档导入到第一次问答3.1 准备环境依赖清单与安装命令先准备一个干净 Python 3.10 环境安装以下依赖。国内网络环境建议给 pip 换清华源否则大模型权重和依赖包下载都会很慢。pip install langchain langchain-community langchain-huggingface pip install faiss-cpu sentence-transformers pip install ollama然后启动 Ollama 并拉取一个本地模型示例用 qwen2.5:7bollama pull qwen2.5:7b说明langchain 负责把加载、切分、检索、生成串起来faiss-cpu 提供单机向量检索sentence-transformers 是 HuggingFaceEmbeddings 的底层支持ollama 用来跑本地大模型。Miniconda 建一个虚拟环境能省掉很多依赖冲突的麻烦。3.2 文档加载与切分把原始文件变成能被检索的片段新建一个docs目录把要喂给系统的 txt 或 markdown 文件放进去。下面代码会把目录下所有文本文件加载进来再按固定策略切成片段。from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter DATA_DIR ./docs loader DirectoryLoader( DATA_DIR, glob**/*.{txt,md}, loader_clsTextLoader, loader_kwargs{encoding: utf-8}, ) docs loader.load() print(loaded docs:, len(docs))加载后先看一眼原始内容有没有乱码或截断这一步省不得。接着做切分splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , ], ) chunks splitter.split_documents(docs) print(chunks:, len(chunks)) print(chunks[0].page_content[:200])逻辑说明先按段落切段落太长再按句子切尽量保住语义边界。chunk_overlap保留前后文各 50 字避免一个关键句刚好被拦腰切断。中文文档特别要把。、、放进分隔符列表否则切出来经常是一堆半句。3.3 向量化与入库计算 Embedding 并写入 Faiss切好的片段需要变成向量才能检索。这里用 bge-m3 做本地向量化生成一次后把索引保存到磁盘。from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS embedding_model HuggingFaceEmbeddings( model_nameBAAI/bge-m3, encode_kwargs{normalize_embeddings: True}, ) vector_store FAISS.from_documents(chunks, embedding_model) vector_store.save_local(./vector_store) print(saved to ./vector_store, chunks , len(chunks))逻辑说明FAISS.from_documents会自动对每个 chunk 调用 Embedding 并建索引。save_local把向量和原文一起落盘下次不需要重新计算。normalize_embeddings设为 True 后相似度分数会落在 [-1, 1] 区间后面调阈值时好观察。第一次跑会从 HuggingFace 拉取模型权重需要保证网络可达。3.4 最小问答闭环检索 生成一条龙跑通向量库建好后写一个最小问答脚本。这一步把检索和生成串起来先跑通再谈优化。from langchain_community.vectorstores import FAISS from langchain_huggingface import HuggingFaceEmbeddings from langchain_ollama import OllamaLLM embeddings HuggingFaceEmbeddings( model_nameBAAI/bge-m3, encode_kwargs{normalize_embeddings: True}, ) store FAISS.load_local( ./vector_store, embeddings, allow_dangerous_deserializationTrue, ) retriever store.as_retriever(search_kwargs{k: 4}) question 服务条款里关于退款的时间窗口是怎么约定的 hits retriever.invoke(question) context \n\n.join( f[{i1}] {d.page_content} for i, d in enumerate(hits) ) llm OllamaLLM(modelqwen2.5:7b) prompt f请只依据以下上下文回答问题不要使用其他知识。 如果上下文中没有答案直接说资料中没有找到相关内容。 上下文 {context} 问题 {question} 答案 answer llm.invoke(prompt) print(answer)逻辑说明检索阶段用as_retriever把 Faiss 包装成 LangChain 的检索器k4表示取最相似的 4 个片段。拼接阶段把片段带编号放进 Prompt 的上下文区并明确告诉模型“只依据上下文回答”。生成阶段用 Ollama 调用本地 Qwen不依赖云端 API整个过程数据不出机器。到这里一个最小可用的知识库问答系统已经能回答问题了。接下来才是真正的挑战参数怎么调回答才能又准又稳。4. 让问答更准的四个关键参数chunk、Top-K、重排序、Prompt 设计4.1 chunk size 与 overlap切多大才不切断语义切分粒度是 RAG 里最“玄学”的参数。切得太大一条检索结果里包含大量无关内容大模型容易跑偏切得太小一个完整结论被拆成两半哪一半都答不出完整答案。参数组合适用场景常见问题chunk_size200, overlap20碎片化文档、FAQ 类问答答案经常缺后半句chunk_size500, overlap50通用技术文档、制度文件比较均衡默认推荐chunk_size800, overlap100长章节、研究报告混入无关内容干扰明显我一般从 500 开始然后随机挑 10 个真实问题和文档对照判断缺失点。一个实用技巧是把切好的片段打印出来人工看一遍特别关注段落边界处有没有出现“前半段讲 A、后半段讲 B”的情况。如果有就把separators调整为更贴近文档结构的层级或者改用按 Markdown 标题切分。overlap 的作用经常被忽略。没有 overlap 时即使是一个自然段里的转折句只要落在边界前 20 个字这个信息就永久丢了。50 到 100 字是稳妥区间。4.2 Top-K 与相似度阈值检索不是越多越好k决定把多少片段送给大模型。k 太小真实答案可能没被召回到k 太大大模型会被一堆只有“表面相关”的内容干扰。k3到k6是大多数内部知识库的稳定区间。除了硬编码 K还应该加一层相似度阈值过滤。具体阈值的量纲取决于 Embedding 模型和距离度量不要凭感觉写先打印分数观察分布。question 服务条款里关于退款的时间窗口是怎么约定的 for doc, score in store.similarity_search_with_score(question, k10): print(round(score, 4), doc.page_content[:40])逻辑说明similarity_search_with_score返回每个命中的相似度分数打印后你能直观看到“和问题真正匹配”的片段分数在哪个区间以及“看着相关其实无关”的片段分数落在哪里。观察完再设定阈值通常是 0.3 到 0.5 之间视 Embedding 模型而变。注意一条文档里如果有多个同类片段Top-K 会把它们全捞上来把其他真正不同角度的答案挤掉。对于这种场景可以在入库时为每个片段打文档级元数据检索时按来源过滤或限制同源数量但这属于进阶调整先把基础参数稳住再上。4.3 重排序让最相关的片段排到最前面向量检索是粗召回计算的是整体语义相似度。问题是一个 500 字的片段里可能只有一句话和问题真正相关。重排序的思路是先放宽容召回 20 到 30 条候选再用交叉编码器对每条候选做细粒度相关性打分取前 5 条进 Prompt。from FlagEmbedding import FlagReranker reranker FlagReranker(BAAI/bge-reranker-base, use_fp16True) question 服务条款里关于退款的时间窗口是怎么约定的 candidates store.similarity_search(question, k20) pairs [[question, d.page_content] for d in candidates] scores reranker.compute_score(pairs, normalizeTrue) ranked sorted(zip(scores, candidates), keylambda x: x[0], reverseTrue) top_context \n\n.join( f[{i1}] {d.page_content} for i, (_, d) in enumerate(ranked[:5]) )逻辑说明先用 Faiss 召回 20 条粗候选再把问题和每条候选交叉编码得到更精准的相关性分数最后按新分数降序取前 5。重排序会带来额外计算但对回答准确率的提升非常明显尤其适合文档量比较大、初检结果噪音多的场景。FlagReranker 需要单独安装FlagEmbedding包。如果只有 CPU 机器也能跑只是每轮会慢几百毫秒实际项目中可接受。4.4 Prompt 设计让大模型学会说“不知道”Prompt 是 RAG 的最后一站。很多系统检索做得不错但大模型在上下文不充分时依然会引用训练记忆去编这时候就靠 Prompt 约束。我的常用模板有三条硬约束只依据上下文、不编造、不知道就明说。prompt f 你是企业内部知识库问答助手。 你的任务是基于提供的上下文片段回答问题。 规则 1. 只依据上下文片段回答禁止使用训练记忆中的知识。 2. 如果多个片段信息冲突指出冲突并给出每个片段的编号。 3. 如果上下文中完全没有答案直接回答资料中没有找到相关内容。 上下文 {context} 问题 {question} 请给出答案并标注引用片段编号如 [1]。 逻辑说明规则 2 用于处理多文档冲突规则 3 用于对抗大模型“即使没依据也要给个答案”的倾向。加入引用编号要求后答案可追溯使用者能判断信息来自哪份文档这条在 To B 场景几乎属于刚需。5. RAG 落地避坑指南五个必踩的坑与解决办法5.1 坑一切分把语义切断了答案支离破碎现象回答内容前后不连贯上一句还在讲“退款时间”下一句跳到“售后服务电话”模型把两个不相干的片段缝在一起。原因固定长度切分把完整语义块拦腰切断检索到的片段只包含问题相关的那一句话缺少支撑上下文。解决优先按文档结构切分Markdown 文档按标题层级切不要一刀切固定字符数。如果必须固定长度把chunk_overlap加大到 80 到 100 字并加入中文标点分隔符。切完后人工抽查 20 个片段的边界这一步成本很低但价值极高。5.2 坑二召回一堆相似但没用的片段现象检索结果和问题“沾边”但都不是真正的答案。问“退款时间”召回的全是“退款流程”“退款需要材料”。原因向量检索看整体语义相似度同主题下不同子话题容易被混淆。Top-K 调太大时混入的噪音更多。解决先降低 Top-K 到 3观察命中率。把相似度阈值打开过滤低分片段。再加上重排序把事件级相关性提到全局语义之前。如果同一个主题下仍然互相干扰给每个片段打“主题”或“分类”元数据检索时先按元数据过滤。5.3 坑三多轮对话时上下文被污染现象用户先问“退款周期多久”再问“那开发票呢”模型回答里把退款周期又扯了进来而且引用的还是第一轮的检索片段。原因简单把整段历史对话塞进 Prompt模型无法区分哪条检索片段属于哪个问题。解决每一轮只携带当前问题的检索结果历史对话只保留精简的“用户问题 当时的答案摘要”不让历史检索片段进入当前上下文。在多轮场景里历史消息属于对话逻辑检索片段属于事实依据两者要分开处理。5.4 坑四文档更新后旧知识还在回答现象服务条款改了退款时间系统回答的还是旧时间。原因入库时按目录全量追加旧切片没有删除同一文档新旧版本同时在库检索时旧版本被先召回。解决入库时以文档内容哈希作为唯一文档 ID重新入库前先按文档 ID 删除旧切片再插入新切片。元数据里记录更新时间和文档版本号检索时优先采用最新版本。这一条不做知识库会在三个月后变成“新老混答”的黑匣子。5.5 坑五效果不好就换大模型现象换了更大的模型坏答案依旧坏甚至更啰嗦。原因绝大多数坏答案的根因在检索期不在生成期。检索回来没有正确答案后面怎么换模型都是徒劳。解决先把评估做起来。攒 30 条“问题 标准答案片段”的测试集跑一遍计算 hit rate检索到的前 5 条里是否包含标准答案片段。如果 hit rate 低于 70%问题出在切分、Embedding 或重排序上先回头调前文提到的参数不要碰模型。这一步也是说服团队“该往哪儿投入”最有力的证据。6. 进阶从 RAG 到 Agentic RAG 与一套能说服自己的验证方法6.1 从单次检索到 Agentic RAG让系统自己决定下一步传统 RAG 是“一问一检索一答”。遇到复杂问题时比如“对比 A 合同和 B 合同的违约责任差异”单次检索很难同时覆盖两份文档的关键条款。Agentic RAG 把检索从一次调用变成一个决策循环Agent 先拆解问题再决定分别检索哪个范围必要时发起多次检索甚至调用外部工具核对计算结果。GraphRAG 是另一条值得关注的方向。它把文档里的实体和关系抽取成知识图谱检索时先定位到图上的节点再取相关切片在多跳问答场景表现明显优于纯向量检索。对专业领域ontology RAG 通过预先定义实体属性和关系的本体约束让检索在受限语义空间内进行精度更高但搭建成本也更高。这些方向的共同代价是复杂度和调试难度上升适合在基础 RAG 的 hit rate 稳定到 80% 之后再引入。6.2 用命中率和忠实度做回归验证而不是靠感觉我给自己的项目定了一个最小验证口径每月跑一次每次 30 条真实问题统计两个指标。命中率看资料有没有被检索到忠实度看大模型有没有答出检索以外的内容。指标口径合格线hit rate正确答案片段是否出现在检索前 5 条结果中≥ 70%忠实度答案关键信息是否都能在上下文片段中找到来源≥ 90%这个验证最大的价值是回答“值不值得继续投入”。当命中率只有 50% 时问题在检索侧钱应该花在数据清洗和切分策略上当命中率到 85% 而忠实度只有 70% 时问题在 Prompt 约束和模型遵循指令的能力上。方向错了后面所有优化都是在给错误系统补漏。我自己从零搭过三轮 RAG最深的体会是七成工作量在把数据收拾干净三成才轮到模型和参数。只要检索是通的模型哪怕小一点都能把话说明白。先把链路跑通把测试集建好再谈优化。希望帮到你。本文还有配套的精品资源点击获取