ARTICLE DETAIL

资讯详情

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

从零手搓本地知识库问答机器人:LangChain+FAISS+本地模型实战

从零手搓本地知识库问答机器人:LangChain+FAISS+本地模型实战 1. 为什么我要从零手搓一个个人知识库问答机器人先说结论我折腾这个项目的出发点特别朴素——我的笔记和文档散落在四五个地方Obsidian 里一堆 Markdown、本地存了几百个 PDF、浏览器书签里还躺着一堆技术博客每次想找点东西都得靠grep加肉眼翻效率低到令人发指。市面上的云端知识库工具我也试过但要么数据得传上去要么检索效果一言难尽问它一个问题它给我返回一堆看起来相关但实际答非所问的片段。所以我决定自己搭一个。核心目标就三个数据全在本地、检索要准、回答要能溯源。技术选型上我选了 LangChain 做编排、FAISS 做向量检索、再配一个本地跑得动的大模型。这套组合不是拍脑袋定的后面我会详细拆解每个选择背后的考量。这个项目适合谁如果你有大量个人文档需要管理对数据隐私有要求又想顺便学一下 RAG检索增强生成和 Agent 的基本原理那这篇内容应该能帮你少走不少弯路。我踩过的坑、调过的参数、翻过的车都会原原本本写出来。先说清楚一个概念区分很多人容易搞混RAG 知识库和结构化知识库是两回事。结构化知识库比如用 Neo4j 存的图谱适合关系明确、需要多跳推理的场景比如“A 的老板的同事是谁”而 RAG 知识库适合非结构化文本的语义检索比如“我去年写的关于缓存穿透的笔记里提到了哪些解决方案”。我这个项目走的是 RAG 路线因为我的文档大部分是自然语言写的笔记和文章硬抽成图谱性价比太低。2. 整体架构设计与技术选型拆解2.1 为什么是 LangChain FAISS 本地模型这套组合先聊 LangChain。很多人说 LangChain 封装太厚、调试困难这话有一定道理但它的优势在于组件标准化。文档加载器、文本分割器、向量存储接口、检索器、链式调用这些都有现成的抽象。我如果从零写光是把 PDF、Markdown、网页这几种格式统一处理就得花不少时间。LangChain 让我可以把精力放在检索质量和回答效果上而不是重复造轮子。FAISS 的选择更直接它是 Facebook 开源的向量相似度搜索库纯本地运行不需要额外部署服务对个人知识库这种数据量几千到几万条 chunk来说性能绰绰有余。我实测过五万条向量做一次 top-5 检索耗时在毫秒级。相比 Chroma 或 MilvusFAISS 更轻量没有额外的服务进程要管适合个人项目。模型这块我选的是本地部署的方案通过 Ollama 跑一个 7B 级别的模型。原因很简单知识库内容涉及我个人笔记不想往外传。7B 模型在消费级显卡我用的是 12G 显存的卡上跑量化版本速度可以接受回答质量对于“基于给定上下文回答问题”这个任务来说够用了。这里有个关键认知RAG 场景下模型的主要职责是“阅读理解 组织语言”而不是“凭记忆回答”所以不需要特别大的模型把检索做好比把模型换大更重要。2.2 数据流是怎么跑通的整个流程分两个阶段我画不出图也不打算用图表就用文字描述清楚索引阶段文档加载 → 文本分割 → 向量化 → 存入 FAISS 索引。这一步是离线的文档更新时重新跑一次就行。查询阶段用户提问 → 问题向量化 → FAISS 检索 top-k 相关片段 → 拼接上下文 → 交给模型生成回答 → 返回答案和引用来源。这里有个容易被忽略的点查询阶段的问题向量化和索引阶段的文档向量化必须用同一个 embedding 模型否则向量空间不对齐检索结果会完全乱套。我一开始图省事索引用了 A 模型查询时手滑换成了 B 模型结果检索出来的东西驴唇不对马嘴排查了半天才反应过来。2.3 文本分割策略这是 RAG 的命门我见过太多人 RAG 效果差问题根本不在模型而在分割。分割粒度太粗一个 chunk 里混了好几个主题检索时噪声大分割太细上下文断裂模型拿到半句话没法回答。我的策略是递归字符分割 重叠窗口。具体参数chunk_size 设为 500 字符chunk_overlap 设为 100 字符。为什么是这两个数500 字符大约对应中文 250 到 300 字正好是一个完整段落的量级能容纳一个相对完整的论述100 字符的重叠是为了防止关键信息刚好被切在边界上导致两个 chunk 都只拿到半截。对于 Markdown 文件我会优先按标题层级分割这样每个 chunk 天然带有语义边界。LangChain 的MarkdownHeaderTextSplitter就是干这个的配合RecursiveCharacterTextSplitter做二次细分效果比无脑按字数切好很多。3. 核心环节的实操落地3.1 环境准备与依赖安装我用的 Python 3.10太新的版本有些库兼容性还没跟上。依赖清单如下pip install langchain langchain-community langchain-ollama pip install faiss-cpu pip install pypdf unstructured markdown pip install sentence-transformers这里说明一下faiss-cpu是 CPU 版本如果你有 GPU 且数据量大可以换faiss-gpu。embedding 模型我用的是sentence-transformers里的多语言模型对中文支持不错。如果你追求更好的中文效果可以换成专门的中文 embedding 模型但要注意维度变化后索引得重建。注意LangChain 的版本迭代很快不同版本 API 差异不小。我写这篇内容时用的是 0.2.x 系列如果你用的是 0.1.x 或更早版本部分导入路径和参数名可能对不上建议先确认版本。3.2 文档加载与预处理我的文档来源有三类Markdown 笔记、PDF 论文和电子书、以及少量网页存档。分别用不同的加载器处理from langchain_community.document_loaders import ( DirectoryLoader, PyPDFLoader, UnstructuredMarkdownLoader ) # Markdown 笔记目录 md_loader DirectoryLoader( ./notes, glob**/*.md, loader_clsUnstructuredMarkdownLoader ) md_docs md_loader.load() # PDF 文件 pdf_loader DirectoryLoader( ./papers, glob**/*.pdf, loader_clsPyPDFLoader ) pdf_docs pdf_loader.load()预处理阶段我做了两件事一是清洗去掉多余的空行、页眉页脚、PDF 转换产生的乱码字符二是元数据标注给每个文档打上来源路径、文件类型、修改时间等标签。元数据在后面做引用溯源和按来源过滤时非常有用。这里有个实操心得PDF 解析质量参差不齐尤其是扫描版 PDFPyPDFLoader提取出来可能是空的或者一堆乱码。遇到这种情况要么换 OCR 方案要么直接放弃这类文件。我个人的做法是扫描版 PDF 单独走一遍 OCR 预处理转成文本再入库。3.3 向量化与 FAISS 索引构建embedding 模型加载和索引构建的代码大致如下from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS from langchain.text_splitter import RecursiveCharacterTextSplitter # 加载 embedding 模型 embeddings HuggingFaceEmbeddings( model_namesentence-transformers/paraphrase-multilingual-MiniLM-L12-v2, model_kwargs{device: cuda}, encode_kwargs{normalize_embeddings: True} ) # 分割文本 splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap100, separators[\n## , \n### , \n\n, \n, 。, ] ) all_splits splitter.split_documents(md_docs pdf_docs) # 构建 FAISS 索引 vectorstore FAISS.from_documents(all_splits, embeddings) vectorstore.save_local(./faiss_index)关于normalize_embeddingsTrue这个参数我要特别说明它会把向量归一化到单位长度这样内积就等于余弦相似度。FAISS 默认用 L2 距离归一化后用内积检索效果更稳定。这个细节很多教程不讲但不加的话检索质量会有肉眼可见的下降。索引构建的时间取决于文档量。我大约 3000 个 chunk在 GPU 上跑了几分钟。如果文档量特别大建议分批构建再合并避免内存爆掉。3.4 检索与回答生成检索环节我用了MMR最大边际相关性而不是简单的相似度 top-k。原因在于纯相似度检索容易返回一堆内容高度重复的片段浪费上下文窗口。MMR 在保证相关性的同时增加结果多样性能覆盖问题的不同侧面。retriever vectorstore.as_retriever( search_typemmr, search_kwargs{k: 5, fetch_k: 20, lambda_mult: 0.7} )fetch_k20表示先取 20 个候选再从中选 5 个多样性最好的。lambda_mult0.7是相关性和多样性的权衡系数越接近 1 越偏重相关性越接近 0 越偏重多样性。我调到 0.7 是试出来的这个值下检索结果既相关又不重复。回答生成用 Ollama 加载本地模型from langchain_ollama import OllamaLLM from langchain.chains import RetrievalQA llm OllamaLLM(modelqwen2.5:7b, temperature0.1) qa_chain RetrievalQA.from_chain_type( llmllm, retrieverretriever, return_source_documentsTrue ) result qa_chain.invoke({query: 我之前记录的缓存穿透解决方案有哪些}) print(result[result]) for doc in result[source_documents]: print(doc.metadata[source])temperature0.1是为了让回答更稳定、更贴近检索到的内容减少模型自由发挥。RAG 场景下我不建议把 temperature 调高否则模型容易脱离上下文瞎编。4. 踩坑实录与常见问题排查4.1 检索不准的几种典型情况和排查思路这是最高频的问题。我整理了一个排查表现象可能原因排查方法解决方向检索结果完全不相关embedding 模型不一致检查索引和查询是否同一模型统一模型重建索引检索结果沾边但答非所问chunk 粒度过大打印 chunk 内容看是否混主题减小 chunk_size优化分割符关键信息检索不到分割切断语义检查边界处文本增加 overlap按结构分割返回大量重复内容未用 MMR查看结果相似度切换 MMR 检索中文检索效果差模型中文能力弱换中文测试集对比换多语言或中文专用模型我印象最深的一次翻车问“文档里提到的部署方案”检索出来的全是无关内容。后来发现是那个文档的标题层级分割出了问题所有内容被切成了超长 chunk一个 chunk 里塞了七八个主题向量表示被“平均”掉了导致检索时匹配不上任何具体问题。把 chunk_size 从 1500 降到 500 之后问题立刻解决。4.2 模型胡编乱造怎么办RAG 的一个核心价值就是减少幻觉但如果 prompt 没设计好模型照样会编。我的做法是在 prompt 里明确约束你是一个知识库问答助手。请严格根据以下提供的上下文回答问题。 如果上下文中没有相关信息直接回答“根据现有资料无法回答该问题” 不要编造内容。回答时请引用具体的来源片段。 上下文 {context} 问题{question}这个约束看起来简单但效果立竿见影。关键那句“如果上下文中没有相关信息直接说无法回答”能挡掉大部分幻觉。另外return_source_documentsTrue让我可以核对模型回答是否真的有据可依如果它说的内容在检索片段里找不到那就是在编。4.3 性能与并发的一些经验个人知识库一般不需要考虑高并发但有几个性能点值得注意。索引加载这块FAISS 索引每次启动都从磁盘加载如果索引文件大启动会慢。我的做法是常驻一个服务进程索引只加载一次后续查询复用。embedding 计算是查询阶段的主要耗时如果 GPU 可用就挂 GPUCPU 上跑几百毫秒到一秒不等。模型推理是最慢的一环7B 模型在消费级显卡上生成一段回答大概几秒这个没法避免除非换更小的模型或者用量化版本。关于“AI Agent 怎么扛并发”这个问题我的看法是个人知识库场景下与其纠结并发不如先把单次查询的质量做上去。真要做多用户那得考虑索引分片、模型服务化、请求队列这些复杂度完全不是一个量级。4.4 几个容易被忽略的细节第一文档更新后的增量索引。我一开始每次改一个笔记就全量重建索引几千个 chunk 跑一遍要几分钟很烦。后来改成按文件哈希判断是否变更只对变更文件重新向量化然后合并进现有索引。FAISS 支持add_documents增量添加但删除比较麻烦需要重建。所以我的策略是新增和修改走增量删除走全量重建。第二元数据过滤。检索时可以按来源、时间、类型过滤比如“只在我最近三个月写的笔记里找”。这在 LangChain 里通过filter参数实现能显著提升检索精准度。第三chunk 的上下文补全。检索出来的 chunk 是孤立的片段有时候缺少上下文。我的做法是在每个 chunk 前面拼接它所属的文档标题和章节路径这样即使片段被单独取出模型也能知道它在讲什么。5. 后续可以怎么扩展这套东西跑通之后能扩展的方向不少。我目前在做的是加一层查询改写用户的问题往往口语化、指代不清先用模型把问题改写成更适合检索的形式再去做向量检索召回率能提升一截。另一个方向是多路召回向量检索配合关键词检索BM25两路结果融合对专有名词和精确匹配的场景效果更好。再往深了走就是往 Agent 方向演进。现在的系统是“一问一答”的固定流程如果加上工具调用能力让它能自己决定是查知识库、还是查网页、还是执行计算那就更接近真正的 Agent 了。不过这一步复杂度陡增我还在摸索等跑通了再单独写一篇。最后分享一个我调参时的小技巧准备一组测试问题覆盖不同类型事实查询、总结归纳、多跳推理每次改完参数就跑一遍对比检索命中和回答质量。凭感觉调参很容易越调越乱有基准测试才能知道改动到底有没有用。我一开始就是靠感觉结果改来改去反而把效果改差了后来老老实实建了个 20 题的测试集效率高多了。
返回列表