ARTICLE DETAIL

资讯详情

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

从零构建个人知识库问答机器人:RAG、向量库与大模型实战

从零构建个人知识库问答机器人:RAG、向量库与大模型实战 1. 为什么我要做个人知识库问答机器人我平时的工作状态大概是这样的浏览器常年开着几十个标签页微信收藏夹里躺着几百篇稍后阅读的文章本地硬盘里散落着各种 PDF 白皮书、会议纪要、技术文档还有一堆自己写的 Markdown 笔记。每次想找某个具体信息比如上次那个向量检索的召回率优化方案是怎么写的我就要在四五个工具之间来回翻翻到最后往往放弃了干脆重新搜一遍。这个痛点其实很普遍。信息不是不够而是太散。传统的文件夹分类和关键词搜索本质上都要求你记得住文件名或者记得住关键词但人的记忆是靠语义关联的不是靠精确字符串匹配的。这就是我决定动手做一个个人知识库问答机器人的直接原因——我想用自然语言问它它去我的资料堆里找答案然后用人话回给我。这个项目属于Agent 实践系列的第一个练手项目核心用到的技术栈是RAG检索增强生成、向量库和大模型。说白了它解决的就是我有一堆私有资料我想用对话的方式把它们用起来这个问题。适合谁来参考我觉得有三类人一是像我这样资料多但整理懒的职场人二是想入门 Agent 开发但不知道从哪下手的开发者三是已经在用大模型但觉得它不懂我的东西的进阶用户。哪怕你之前没碰过向量数据库跟着走一遍也能跑起来。我先把整体思路讲清楚再拆细节最后把踩过的坑都摊开说。整个过程我会尽量给出可以直接抄的配置和参数而不是停留在概念层面。2. 整体架构设计与技术选型思路2.1 为什么是 RAG 而不是微调一开始我也纠结过到底是把资料喂给大模型做微调还是用 RAG 做外挂检索这两个路线经常被拿来对比我实际评估后的结论是——个人知识库场景RAG 几乎是唯一合理的选择。原因很直接。微调的本质是让模型记住知识它适合的是风格迁移、特定任务格式对齐这类需求比如让模型学会用某种口吻写文案。但知识库问答的核心诉求是准确检索到某条具体信息而微调有两个致命问题一是知识更新成本极高我加一篇新文档就得重新训练一轮二是微调后的模型容易产生幻觉它会把训练时见过的东西编出来而且你没法追溯它到底是从哪条资料里得出的结论。RAG 就不一样了。它把知识和模型解耦知识存在向量库里模型只负责理解和组织语言。我新增文档只需要把新文档切片、向量化、入库几分钟搞定模型完全不用动。而且 RAG 的答案可以附带引用来源我能点开看它到底参考了哪段原文这对建立信任特别重要。提示如果你的需求是让模型学会我的写作风格那微调更合适如果是让模型回答我资料里的具体问题别犹豫直接上 RAG。两者也可以结合但对个人项目来说先把 RAG 跑通性价比最高。2.2 核心链路的四个环节整个系统拆开看其实就是一条流水线我把它分成四个环节文档加载与解析把 PDF、Markdown、TXT、网页等各种格式统一读成纯文本。文本切片Chunking把长文档切成适合检索的小块这是最容易被低估但影响最大的一步。向量化与存储用 Embedding 模型把每个文本块转成向量存进向量库。检索与生成用户提问时把问题也向量化去库里找最相似的几块连同问题一起塞给大模型生成答案。这条链路里切片策略和检索策略是决定效果的两个关键点模型本身反而没那么重要——用个中等能力的模型只要检索准答案就不会差。我见过太多人一上来就追求最强模型结果检索召回的全是无关内容再强的模型也只能一本正经地胡说八道。2.3 技术选型的取舍选型这块我踩过一些坑最后定下来的方案是轻量优先、本地优先。下面这张表是我实际对比后的结论环节我的选择备选方案选择理由编排框架LangChainLlamaIndex、手写生态成熟文档多出问题好搜向量库ChromaFAISS、Milvus轻量、零配置、支持持久化个人够用EmbeddingBGE-small-zhOpenAI Embedding中文效果好可本地跑无调用成本大模型本地部署 云端 API 双通道纯云端敏感资料走本地日常问答走云端文档解析PyPDF Unstructured纯 PyPDFUnstructured 对复杂版式更友好这里重点说两个决策。第一向量库为什么选 Chroma 而不是 MilvusMilvus 是工业级的性能强但要单独部署服务对个人项目是杀鸡用牛刀。Chroma 可以直接嵌入到 Python 进程里pip install完就能用还支持本地持久化重启不丢数据。第二Embedding 为什么不用云端因为知识库文档动辄几百上千条每次重建索引都要调用上千次 API成本不说速度也慢。本地跑 BGE 这类小模型一次索引几分钟搞定还不用担心资料外泄。注意选型没有绝对的对错关键是匹配你的规模。如果你要处理的是几十万份文档、要扛高并发那 Chroma 就不够了得上 Milvus 或 Qdrant 这类专业向量库。个人知识库通常几千到几万条切片Chroma 完全扛得住。3. 核心细节解析与实操要点3.1 文本切片决定成败的隐形环节切片这件事看起来就是把长文本按字数切开但里面的门道特别多。我一开始图省事直接按固定 500 字切结果检索效果惨不忍睹。问题出在哪固定长度切分会把一句话、一个段落从中间劈开导致每个切片都语义不完整。比如一个方案的关键结论在切片 A 的末尾而它的前提条件在切片 B 的开头检索时只召回 A模型就理解偏了。后来我改成了递归字符切片策略是这样的优先按段落切\n\n段落太长再按句子切。句子还长才按字符硬切。同时设置一个重叠区overlap让相邻切片共享一部分内容避免边界信息丢失。具体参数我调了很久最后定下来这套chunk_size 500单个切片的目标长度中文按字符算。chunk_overlap 80相邻切片重叠 80 字大约是 chunk_size 的 15%。分隔符优先级[\n\n, \n, 。, , , , , ]为什么 overlap 是 15% 左右这个比例是我实测出来的平衡点。太小比如 5%起不到衔接作用太大比如 30%会导致大量重复内容入库检索时召回一堆相似切片反而稀释了有效信息。15% 左右既能保证语义连续又不会造成明显冗余。实操心得切片大小不是越小越好。切片太小单块信息量不足模型拿到的上下文不够切片太大一块里混了多个主题检索精度下降。500 字左右对中文技术文档是个比较稳的起点你可以根据自己文档的特点微调但别偏离太远。3.2 向量化把语义变成可计算的距离向量化的本质是把一段文本映射到一个高维空间里的点语义相近的文本它们的点距离就近。这样找相似内容就变成了找最近的点数学上可计算了。我用的是 BGE-small-zh 这个中文 Embedding 模型输出 512 维向量。选它是因为它在中文语义相似度任务上表现不错模型体积小几百 MBCPU 上也能跑不需要显卡。如果你有 GPU可以上 BGE-large效果会更好但速度慢一些。这里有个容易忽略的点查询和文档必须用同一个 Embedding 模型。因为不同模型把文本映射到的向量空间是不一样的用 A 模型编码文档、用 B 模型编码查询算出来的距离毫无意义。我见过有人图省事混用结果检索全乱套。还有一个细节是归一化。BGE 这类模型输出的向量通常要做 L2 归一化让每个向量长度都是 1。这样计算相似度时余弦相似度和点积就等价了检索更快。Chroma 默认用的是余弦距离所以入库前我会手动归一化一遍保证一致性。3.3 检索策略从能查到到查得准检索这块我经历了三个阶段。第一阶段是纯向量检索就是拿问题向量去库里找最相似的 Top-K。这个方案简单但有个明显短板它对关键词精确匹配不敏感。比如我问XX-2024 这个型号的参数向量检索可能召回一堆讲参数的文档但把具体型号忽略了。第二阶段我加了BM25 关键词检索和向量检索做混合。BM25 是传统的信息检索算法擅长精确匹配关键词。把两路结果融合既能抓住语义又能抓住关键词。融合方式我用的是 RRF倒数排名融合简单说就是把两路结果的排名做加权排名越靠前的权重越高。第三阶段我加了Rerank 重排。初步检索召回 Top-20然后用一个专门的重排模型我用的是 BGE-reranker对这 20 条重新打分排序取 Top-5 给大模型。重排模型比向量检索更精细它会把问题和每个候选切片拼在一起做深度匹配精度提升很明显。这套组合拳下来检索准确率比最初的纯向量方案提升了一大截。下面是我实测的对比检索方案召回准确率我的测试集响应速度纯向量 Top-5约 62%快向量 BM25 混合约 78%中混合 Rerank约 89%稍慢注意Rerank 会带来额外延迟因为要对每个候选做一次模型推理。如果候选有 20 条延迟可能增加几百毫秒到一秒。对个人问答场景这点延迟完全可以接受换来的是准确率的大幅提升非常值。3.4 提示词设计让模型只答资料里的内容RAG 最容易出的问题是模型自由发挥明明资料里没有它硬编一个答案。解决办法是在提示词里把规则写死。我的提示词模板大致是这样的你是一个严谨的知识库助手。请严格根据下面提供的参考资料回答问题。 规则 1. 只使用参考资料中的信息不要引入外部知识。 2. 如果参考资料中没有相关信息直接回答根据现有资料无法回答不要猜测。 3. 回答时尽量引用原文关键句并在末尾标注来源编号。 4. 用简洁的中文回答不要啰嗦。 参考资料 {context} 用户问题{question}这套提示词的关键在于第 2 条——明确告诉模型不知道就说不知道。很多人怕模型答不上来显得笨故意不给这条约束结果模型就开始编。宁可它说无法回答也不要它给一个看似合理实则错误的答案后者危害大得多。4. 完整实操流程与关键环节实现4.1 环境准备与依赖安装先把环境搭起来。我用的是 Python 3.10这个版本对各类 AI 库的兼容性最好。建议用虚拟环境隔离避免污染全局。python -m venv kb-env source kb-env/bin/activate # Windows 用 kb-env\Scripts\activate pip install langchain langchain-community chromadb pip install sentence-transformers pip install pypdf unstructured pip install rank-bm25这里解释下每个包的作用langchain是编排框架chromadb是向量库sentence-transformers用来加载 BGE 模型pypdf和unstructured负责解析文档rank-bm25提供关键词检索能力。装完之后第一次运行会自动下载 BGE 模型大概几百 MB耐心等一会。实操心得如果你在国内网络环境下载模型慢可以提前把模型文件下好放到本地目录加载时指定本地路径。这一步能省掉很多等待时间。4.2 文档加载与清洗加载环节我写了个统一入口根据文件后缀走不同的解析器。PDF 用 PyPDFMarkdown 和 TXT 直接读网页内容用 Unstructured 的 HTML 解析器。from langchain_community.document_loaders import PyPDFLoader, TextLoader import os def load_documents(folder_path): docs [] for filename in os.listdir(folder_path): filepath os.path.join(folder_path, filename) if filename.endswith(.pdf): loader PyPDFLoader(filepath) elif filename.endswith((.md, .txt)): loader TextLoader(filepath, encodingutf-8) else: continue docs.extend(loader.load()) return docs清洗这一步很多人会跳过但我强烈建议做。PDF 解析出来的文本经常带一堆页眉页脚、页码、乱码字符这些噪声会污染向量。我一般会做三件事去掉连续空白、去掉纯数字行多半是页码、去掉过短的碎片行。清洗完的文本质量直接决定后续检索效果。4.3 切片与向量入库切片用 LangChain 的递归分割器参数按前面说的设置from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, separators[\n\n, \n, 。, , , , , ], length_functionlen, ) chunks splitter.split_documents(docs)然后加载 Embedding 模型并入库from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma embedding HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True}, ) vectorstore Chroma.from_documents( documentschunks, embeddingembedding, persist_directory./kb_db, ) vectorstore.persist()注意normalize_embeddingsTrue这一行这就是前面说的归一化一定要开。persist_directory指定持久化目录下次启动直接加载不用重新索引。4.4 检索与问答链路检索部分我把向量检索和 BM25 结合起来再加重排。核心逻辑是两路各召回 Top-20用 RRF 融合取融合后的 Top-20 交给重排模型最后取 Top-5 拼进提示词。def retrieve(query, vectorstore, bm25, reranker, top_k5): # 向量检索 vec_results vectorstore.similarity_search(query, k20) # BM25 检索 bm25_results bm25.get_top_n(query, all_chunks, n20) # RRF 融合 fused rrf_fusion(vec_results, bm25_results) # 重排 reranked reranker.rerank(query, fused, top_ntop_k) return rerankedRRF 融合的公式很简单每个文档的得分等于它在各路结果中排名的倒数之和。排名越靠前得分越高。这个方法的妙处在于它不需要归一化两路的分值直接看排名鲁棒性很好。最后把 Top-5 切片拼成 context套进提示词模板调用大模型生成答案。整个链路跑通后我拿自己的资料库测了几十个问题大部分都能准确回答而且能给出引用来源。4.5 参数调优的实测记录调参这块我做了不少实验记录几个关键发现。第一top_k从 3 调到 5答案完整度明显提升但调到 8 以上就开始引入噪声模型反而容易被无关内容带偏。第二重排模型的候选数从 10 调到 20准确率有小幅提升但延迟增加明显20 是个平衡点。第三切片 overlap 从 50 调到 80边界问题的召回率提升约 10%再往上收益递减。这些数字不是绝对的跟你的文档特点强相关。但调参的思路是通用的每次只动一个参数固定测试集记录指标别凭感觉。5. 常见问题与排查技巧实录5.1 检索召回不准怎么办这是最高频的问题。排查顺序我一般是这样的先看切片质量把召回的几个切片打印出来看它们是不是语义完整、有没有被切断再看 Embedding 模型是否匹配确认查询和文档用的是同一个模型最后看是否需要加重排。大部分召回问题根源都在切片策略上而不是模型不行。5.2 模型答非所问或编造答案如果模型开始编先检查提示词里有没有不知道就说不知道的约束。如果没有加上。如果加了还编那多半是检索召回了错误内容模型拿着错误资料自然答错。这时候要回到检索环节排查而不是怪模型。还有一种情况是 context 太长模型注意力被稀释可以适当减少 top_k。5.3 索引速度慢或内存占用高索引慢通常是 Embedding 模型在 CPU 上跑导致的。如果文档量大建议换 GPU或者分批索引。内存占用高一般是切片数量太多Chroma 把向量全加载进内存了。可以开启 Chroma 的持久化模式或者换用支持磁盘索引的方案。下面这张表是我整理的常见问题速查现象可能原因解决方向召回内容不相关切片语义不完整调整 chunk_size 和 overlap关键词查不到纯向量检索不敏感加入 BM25 混合检索答案编造提示词无约束加无法回答规则答案不完整top_k 太小适当增大 top_k响应慢重排候选太多减少重排候选数索引慢CPU 跑 Embedding换 GPU 或分批处理5.4 几个我踩过的坑第一个坑是中文标点分隔符。我一开始分隔符只写了英文标点结果中文文档切得乱七八糟。中文的句号、问号、分号都要加进去。第二个坑是PDF 表格解析。PyPDF 对表格支持很差表格内容会被打散成乱序文本。如果资料里表格多建议用 Unstructured 的 high_res 模式或者干脆手动把表格转成 Markdown 再入库。第三个坑是重复文档。我资料库里有几份内容高度相似的文档导致检索时召回一堆重复切片浪费了 context 额度。后来我加了个去重步骤用文本哈希判断相似度超过阈值的只保留一份。实操心得知识库不是越大越好。与其塞一堆低质量、重复的资料不如精选一批高质量文档。检索质量的上限取决于你入库内容的质量下限。6. 后续可以怎么扩展跑通基础版之后我陆续加了一些扩展。一个是多轮对话让机器人记住上下文支持追问。这个需要在提示词里带上历史对话同时注意控制长度。另一个是来源高亮答案里标注引用编号点开能看原文这个对建立信任很有用。再往深了走可以试试知识图谱增强。纯向量检索擅长语义相似但对实体之间的关系处理较弱。比如问A 和 B 是什么关系向量检索可能召回两段分别讲 A 和 B 的内容但拼不出关系。这时候引入知识图谱把实体和关系结构化能补上这块短板。不过这是另一个量级的工程个人项目可以先不碰。我个人的体会是RAG 这套东西跑通不难调好很难。真正拉开差距的不是用了多强的模型而是切片、检索、提示词这些脏活累活做得够不够细。我前后调了两周效果才从能用变成好用。如果你也在做类似的东西别急着换模型先把检索链路打磨透收益比换模型大得多。
返回列表