简介:面向希望搭建私有知识库问答系统的开发者,这份源代码以ChatGLM、MOSS等大模型为底座,解决文档检索增强生成的实际问题,适合用于企业内训、学术资料问答等场景,也可用于个人知识管理。压缩包共73个文件,包含39个pickle预处理数据、22个Python脚本、4个docx知识库示例文档(含基础与多份补充主题),另附readme、png流程示意图、txt依赖说明等,整体大小约17.3MB。目前已有216人学习下载。代码按agent、configs、loader、textsplitter、chains等模块组织,loader支持PDF、图片及文本加载,从文档加载、中文文本分割、向量数据库存储到大模型生成回答的链路完整,命令行与Web演示程序均可直接启动体验。模型模块提供MOSS、ChatGLM两套大模型调用封装,pickle文件便于快速加载文本向量,docx文档可直接充当知识库语料,既适合验证效果,也方便替换语料或接入私有数据,是一份很适合上手的RAG落地参考。 这些年接手过不少知识库问答的项目,从最早的基于关键词匹配的检索系统,到后来引入向量化检索,再到如今直接对接大模型做生成式问答,技术路线换了一茬又一茬。这次要拆解的这套"基于大模型的知识库问答源代码",本质上是一个完整的RAG(检索增强生成)落地实现,解决的问题很直接:让企业内部文档、产品手册、专业资料这些非结构化数据,能够通过自然语言对话的方式被快速查询和利用。
这套代码适合谁?三类人。第一类是刚接触RAG的开发者,想看看一个最小可用的问答系统到底由哪些模块组成;第二类是已经在用Dify、AnythingLLM这类现成工具、但想知道底层原理、准备自己定制流程的人;第三类是做技术选型的技术负责人,需要评估从零搭建的成本和可行性。下面我从架构设计讲到具体实现,再聊聊我实际踩过的坑。
1. 项目概述与整体思路
1.1 核心需求拆解
知识库问答这个需求看起来很直白,但真正落地时你会发现它隐含了三个层面:数据层面,要处理格式各异的文档,可能是PDF、Word、Markdown,也可能是网页抓取下来的HTML;检索层面,要能从海量文本中快速找到和问题相关的片段,这直接决定答案质量的上限;生成层面,要让大模型基于检索到的片段组织出通顺、准确、有据可查的回答,而不是凭空编造。
这套源代码围绕这三层展开,整体流程可以概括为:文档加载 -> 文本切分 -> 向量化 -> 向量存储 -> 相似度检索 -> 提示词组装 -> 大模型生成。我把每一步都做成了独立的模块,方便单独替换和调试。比如你想把默认的向量库从Chroma换成Milvus,只需要改存储层一个接口,不需要动其他代码。
1.2 为什么是RAG而不是微调
很多人在做知识库问答时都会纠结一个问题:到底是微调大模型,还是用RAG?我的经验是,绝大多数场景下RAG是更合理的选择。
微调的本质是改变模型的参数,让模型"记住"特定领域的知识。但知识库里的内容往往是高频更新的——产品文档改版、政策条款变更、技术方案迭代,如果都靠微调去跟,意味着每次更新都要重新准备训练数据、重新跑训练流程,成本高且周期长。RAG则不同,它的核心思想是"检索+生成":模型本身的知识能力不变,我们只是把最新的文档切碎、索引、存起来,问答时先检索出相关内容塞进上下文,让模型基于这些内容作答。文档更新了,只需要重新跑一遍索引流程,分钟级搞定。
另外从效果角度看,RAG天然具备可解释性——模型回答时引用了知识库里的哪段原文,是可以追溯到具体位置的。这在企业场景里太重要了,总不能模型给客户报了个错误参数,你连来源都说不清楚。所以除非你的场景是"模型需要内化某种能力或风格",否则我建议优先考虑RAG。
2. 技术选型与原理解析
2.1 嵌入模型的选择逻辑
嵌入模型(Embedding Model)负责把文本转换成向量,这是RAG链路中最容易被低估的环节。很多人随便选一个模型就跑,结果检索效果一塌糊涂,还以为是检索逻辑写错了。
选择嵌入模型时我主要看三个指标:第一是语义理解能力,能不能区分近义词在不同语境下的差异;第二是向量维度,维度越高理论上表达能力越强,但存储和计算开销也越大;第三是中文支持程度,这一点尤其重要,很多英文模型在中文语料上表现大打折扣。
以这套源代码默认使用的通义千问text-embedding-v3为例,它的向量维度是1024,在中文语义理解上表现不错,同时也兼容英文内容,适合中英文混合的知识库。如果你完全在本地运行、不想调用外部API,也可以换成BGE系列或者M3E这类开源嵌入模型,代码里我留了统一的Embedding接口,切换成本很低。
2.2 向量数据库的选型对比
向量数据库负责存储嵌入向量并提供相似度检索。这个领域现在非常卷,FAISS、Chroma、Milvus、Qdrant、Weaviate,各有各的定位。我给这套代码设计了可插拔的存储层,默认接Chroma,因为它在轻量级场景下最省事,pip安装就能跑,支持持久化,适合单机部署和原型验证。
但如果你要上生产环境,我建议认真评估一下Milvus或Qdrant。Chroma在数据量超过百万级向量时,检索延迟和稳定性会明显下降,而且它的事务能力和多租户支持都比较弱。选型时有一个简单的判断标准:如果知识库文档总量在10万篇以内、单机部署、追求快速上线,Chroma足够;如果数据量更大、需要分布式扩展或高并发查询,直接上Milvus,省得后面迁移。
2.3 大模型推理方案的权衡
生成环节的大模型选择,直接决定了回答质量的下限和单次调用的成本。这套源代码支持两种模式:一种是调用云端API,比如通义千问、智谱GLM的接口,优点是效果稳定、无需维护推理环境,缺点是数据要出域,对数据安全要求高的场景不适合;另一种是通过Ollama部署本地开源模型,比如Qwen2.5、Llama系列,数据完全在内网流转,但需要一台配置不错的GPU服务器。
注意:如果你选择本地部署模式,8B以下的小模型在复杂推理和长文本理解上的表现会明显弱于云端大模型。做知识库问答时,我建议至少用7B~14B参数量级的模型,并且开启量化(如Q4_K_M),在效果和显存占用之间找一个平衡点。
3. 核心实现与实操细节
3.1 文档加载与切分策略
文档加载是RAG链路的第一环,也是最容易被忽视的一环。不同类型的文档有不同的解析方式:PDF需要考虑布局和表格,Word需要处理分页和样式,HTML需要剥离标签提取正文。我在代码里封装了统一的DocumentLoader,用LangChain的文档加载器做底层解析,遇到扫描版PDF时会自动尝试OCR兜底。
文本切分是整个流程中对最终效果影响最大的环节之一,这里我踩过不少坑。最初我按固定长度500字硬切,结果经常把一个完整的段落切断,导致语义不完整。后来改成分隔符优先策略:先按章节标题切,再按段落切,最后按句子切,每一步控制块大小。默认参数是块大小800字符、重叠200字符,这个配置在大多数业务文档上表现都不错。块大小的选择有个基本原则:太小则上下文信息不足,模型容易答偏;太大则检索粒度太粗,还可能把多段不相关的内容揉在一起,干扰模型的判断。
3.2 向量化与检索链路
切分完成后,每段文本会通过嵌入模型转换成向量并写入向量库。检索阶段,用户的提问同样会做一次向量化,然后在库里做相似度查找。这里有一个关键细节:直接拿用户的原话去检索,效果往往不好。口语化提问和文档里的书面表达之间存在明显的语义鸿沟,比如用户问"产品支不支持并发访问",文档里写的是"系统具备高并发处理能力",两者向量相似度可能并不高。
我在代码里加了一个查询改写模块,先让大模型把用户的问题改写成适合检索的形式,提取核心实体和关键词,再做向量检索。实测下来,这个改写步骤能让命中率提升20%以上。检索时我同时使用向量相似度和关键词BM25加权,做混合检索再合并排序。纯向量检索在专有名词和编号类查询上经常翻车,混合检索能有效弥补这个短板。
检索的核心实现大致如下:
def search(query: str, top_k: int = 5) -> list[Document]: # 查询改写:提取核心关键词,生成检索式 rewritten = rewrite_query(query) # 向量检索 query_vec = embed_model.embed(rewritten) vec_results = vector_store.similarity_search(query_vec, top_k) # 关键词检索(BM25) bm25_results = bm25_index.search(rewritten, top_k) # 结果融合:加权合并,去重后返回 merged = fusion(vec_results, bm25_results) return merged[:top_k]3.3 问答生成与提示词设计
检索到相关片段后,接下来就是组装提示词,把上下文交给大模型生成回答。提示词的设计直接影响回答质量,我一开始的提示词写得非常简陋,就是"根据以下内容回答问题",结果模型经常脱离给定的上下文自由发挥。后来我调整了策略,提示词里明确了几件事:回答必须基于给定的上下文片段,不能凭空编造;如果上下文不足以回答问题,要明确说"知识库中没有相关信息";回答需要标注引用的文档片段编号。
代码里对应的提示词模板大概是这个风格:
PROMPT_TEMPLATE = """ 你是企业内部知识库的智能助手。 请基于以下检索到的文档片段回答用户问题: 1. 只使用给定的片段作为依据,不得使用片段以外的知识。 2. 如果片段不足以回答问题,请明确回答"知识库中暂未找到相关信息"。 3. 回答末尾标注引用的片段编号,格式如[来源1][来源2]。 文档片段: {context} 用户问题:{question} """这个提示词模板是一个很好的起点,但它更像是基线版本。实际使用中你还要根据领域特点调整。比如我做法律合同场景时,会额外要求模型"区分事实描述与法律结论";做客服场景时,会要求模型"先给结论再给依据,语气简洁友好"。
4. 常见问题排查与优化
4.1 检索质量差的定位思路
检索不到正确答案是知识库问答最让人头疼的问题,而且原因往往不在检索本身。排查时我的习惯是先做模块隔离:跳过向量检索,把数据库里某段已知的原文直接塞给大模型,如果回答正确,说明生成链路没问题,问题出在检索;如果回答还是不对,那要先检查提示词和模型。
确认是检索问题后,再逐层排查。先看切分是否合理,比如长文档是否被均匀切分、重要内容有没有被拦腰截断;再看嵌入模型是否适合当前语料,中文文档用了英文优化的模型会很吃亏;最后看检索参数,top_k设置太小可能漏掉关键信息,设置太大又会引入噪声。我把这个排查路径总结成一个检查清单,每次调优直接照着过一遍,效率高很多。
| 排查环节 | 常见问题 | 检查方法 |
|---|---|---|
| 文本切分 | 块过小导致语义不全 | 随机抽检10个块,看内容是否完整 |
| 嵌入模型 | 语种不匹配或模型过弱 | 用标准测试集跑一遍召回率 |
| 检索参数 | top_k不合适 | 对比不同top_k下的回答质量 |
| 查询改写 | 改写丢失关键实体 | 打印改写结果人工核对 |
4.2 幻觉问题的缓解措施
幻觉是生成式问答绕不开的话题。模型给出了看起来很像那么回事、但知识库里根本没这个说法——这种错误在企业场景里是致命的。我总结了几条有效的缓解手段,按优先级排列:第一是前面提到的提示词约束,明确禁止编造,这个成本最低,但效果有限;第二是检索增强,确保喂给模型的上下文足够贴题,上下文相关度越高,模型越不容易自由发挥;第三是来源标注,让回答携带引用信息,用户在业务使用时会自然形成监督;第四是阈值拒绝,当检索结果的相关度分数低于某个阈值时,直接不调用生成模型,回答"未找到匹配信息"。
这套源代码里我实现了前三种措施,阈值拒绝逻辑也预留了接口。实际项目里我建议组合使用,单靠任何一种都很难根除幻觉问题。另外可以做一个离线评测集,存放几十组真实问答对,每次改动后自动跑一遍,对比回答质量的得分变化,防止优化一个问题的同时破坏另一个问题。
4.3 性能优化与工程化建议
性能问题通常在从原型走到生产时集中爆发。我遇到过两种典型情况:一是文档量上来后,全量索引时间太长;二是并发查询时,单机向量库撑不住。索引慢的解决办法是增量索引,只处理新增和变更的文档,配合定时任务在业务低峰期执行。并发问题的处理思路更直接:把向量库和大模型推理拆开,向量库可以加只读副本分担查询压力,大模型如果走本地推理则需要考虑多卡部署或上推理框架做并发调度。
另外有一点容易被忽略:Embedding操作频繁调用外部API时,网络延迟会成为瓶颈。单篇文档几百个块,串行向量化可能耗时几十秒。我建议用线程池做并发向量化,同时做好失败重试机制。实测下来,并发度设为8时能在不影响准确率的前提下把索引速度提升5倍以上。
5. 实际操作中的体会
代码写到这里,整套知识库问答系统已经具备了一个线上可用项目应有的完整度。回顾下来,如果说有什么特别值得分享的经验,我觉得是"不要一上来就追求全流程自动化"。最初我自己做RAG项目时,总想着端到端自动化,文档扔进去答案就出来,结果在检索质量上反复碰壁。后来调整了思路,先做成"半自动":索引流程跑完后,抽样检查切分质量和检索命中情况,确认无误后再开放问答。看起来多了一步人工环节,实际上省去了大量反复调试的时间。
另外一个小建议,把回答的历史记录和用户反馈都存下来。知识库问答系统的优化永远依赖真实使用数据,你收集到的"这个问题答得不好"的反馈,比任何评测集都更宝贵。有了这些反馈,后续无论是调整检索策略、优化提示词,还是补充知识库文档,都有了明确的方向。这套系统的代码我后续也会持续迭代,目前计划中的改进包括支持多轮对话的上下文管理,以及从知识库内容中自动挖掘高频问题、生成推荐问题列表,感兴趣的朋友可以顺着这个方向继续扩展。
本文还有配套的精品资源,点击获取