ARTICLE DETAIL

资讯详情

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

从零搭建个人知识库问答机器人:RAG、LangChain与FAISS实战

从零搭建个人知识库问答机器人:RAG、LangChain与FAISS实战 1. 为什么我要从零搭一个个人知识库问答机器人我自己平时有大量零散资料技术笔记、产品文档、会议纪要、收藏的文章、PDF 手册散落在本地文件夹、笔记软件和浏览器书签里。想找某个具体结论时关键词搜出来的是一堆文件还得逐个打开翻。通用大模型虽然能对话但它不知道我这些私有资料问它等于白问甚至还会一本正经地编。所以我动手做了一个个人知识库问答机器人。核心思路就是现在很火的RAG检索增强生成先把我的资料切块、向量化、存进向量库用户提问时先检索出最相关的几段原文再把这些原文作为上下文交给大模型组织答案。这样回答有据可依能落到我自己的资料上而不是靠模型瞎猜。这个项目适合谁如果你有一点 Python 基础想搞明白Agent、RAG、LangChain、FAISS这几个词到底怎么落地或者你手头有一堆私有文档想变成能对话的知识库那这篇就是给你写的。我会把整体设计、关键参数、踩过的坑、排查思路都摊开讲代码和配置尽量给到能直接抄的程度。整条链路我实测跑通过单机、纯本地、不依赖外部付费服务也能跑起来。2. 整体架构设计与技术选型思路2.1 这个机器人到底由哪几块组成先把骨架说清楚不然后面看代码会晕。一个最小可用的知识库问答机器人本质是四段流水线文档加载与切分把 PDF、Markdown、TXT 等读进来切成一段段合适大小的文本块chunk。向量化与存储用 Embedding 模型把每个 chunk 转成向量存进向量库建立索引。检索用户提问也转成向量在向量库里找最相似的 Top-K 个 chunk。生成把检索到的 chunk 拼进提示词交给大模型生成最终答案。这四步里前两步是“离线建库”后两步是“在线问答”。很多人一上来就纠结用哪个大模型其实真正决定效果上限的是切分策略和检索质量模型只是最后一步的“嘴”。2.2 为什么选 LangChain FAISS 这套组合选型我对比过几套方案最后定的是LangChain FAISS 本地 Embedding 本地或在线 LLM理由很实在组件选型为什么选它替代方案与取舍编排框架LangChain文档加载器、切分器、检索链都是现成的省掉大量胶水代码手写也行但重复造轮子向量库FAISS单机、零依赖、纯内存/本地文件几万条数据毫秒级检索Chroma 更易用但重一些Milvus 适合大规模但部署复杂Embedding本地小模型数据不出本地免费隐私可控在线 Embedding 效果好但按量计费、有隐私顾虑LLM本地或在线可切换灵活测试用本地追求质量用在线纯在线成本高纯本地对硬件有要求FAISS 是 Facebook 开源的向量检索库它的定位就是“把向量相似度搜索做到极致快”。个人知识库通常也就几千到几万条 chunkFAISS 用IndexFlatL2或IndexFlatIP这种暴力检索索引召回率是 100%因为真的全比了一遍速度完全够用。数据量再大再考虑 IVF、HNSW 这些近似索引。提示个人知识库规模不大时别一上来就上分布式向量库。FAISS 单机方案简单、可控、调试方便是性价比最高的起点。2.3 离线建库和在线问答为什么要分开这是新手最容易混的地方。建库是一次性或低频操作资料更新了才重建。问答是高频操作每次提问都要跑。两者分开的好处是问答时不用重新加载和向量化所有文档直接查索引就行响应快很多。我的做法是把 FAISS 索引持久化到本地磁盘问答时直接load_local加载。这样重启程序不用重建库几秒钟就能开始问答。如果资料有更新再跑一次建库脚本覆盖索引即可。3. 核心细节解析与实操要点3.1 文档切分决定成败的第一步切分Chunking是 RAG 里最容易被低估、却最影响效果的环节。切太大检索出来的块里混了大量无关内容模型容易被干扰切太小一个完整语义被拆散检索到了也拼不出完整答案。我的经验参数是chunk_size 取 500~800 字符chunk_overlap 取 50~100 字符。overlap 的作用是让相邻块有重叠避免一句话正好被切断导致语义丢失。中文场景下按字符切比按 token 切更直观LangChain 的RecursiveCharacterTextSplitter会优先按段落、句子、标点这些自然边界切比硬切好得多。from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size600, chunk_overlap80, separators[\n\n, \n, 。, , , , , , ], )注意separators里我把中文标点加进去了这是中文文档切分的关键。默认分隔符是英文的中文长句会被硬切加上中文标点后切分点自然很多。注意Markdown 和代码文件不要用同一套切分参数。代码块被切断会彻底失去意义建议对代码类文档单独用更大的 chunk_size或者按函数/类边界切。3.2 Embedding 模型怎么选Embedding 决定了“语义相似”判得准不准。中文场景我推荐用支持中文的模型比如 BGE 系列的中文小模型。选它的理由体积小、CPU 也能跑、中文语义表现好、完全本地。这里有个坑建库用的 Embedding 模型和查询时必须用同一个。换了模型向量空间就变了旧索引直接失效必须重建。我一开始图省事换过一次模型结果检索全乱排查了半天才反应过来。3.3 检索策略Top-K 和相似度阈值检索时k取多少太小可能漏掉关键信息太大则塞进太多噪声。我实测k3~5比较平衡。同时我会加一个相似度阈值过滤低于阈值的块直接丢掉避免“硬凑”出无关上下文。retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: 4, score_threshold: 0.3}, )score_threshold的具体值跟 Embedding 模型和距离度量方式有关需要自己试。做法是拿几个已知答案的问题跑一遍看正确块排在第几、分数多少再定阈值。3.4 提示词模板让模型“只依据资料回答”RAG 最容易翻车的地方是模型不看资料、自己发挥。解决办法是在提示词里明确约束from langchain.prompts import PromptTemplate prompt PromptTemplate.from_template( 你是一个严谨的知识库助手。请仅根据下面提供的资料回答问题。 如果资料中没有相关信息直接回答“资料中未提及”不要编造。 资料 {context} 问题{question} 回答 )“资料中未提及”这句兜底非常重要。没有它模型遇到知识库覆盖不到的问题时会强行编答案这在个人知识库里是致命的——你没法判断它说的是真是假。4. 完整实操流程与关键环节实现4.1 环境准备与依赖安装先把环境搭起来。我用的是 Python 3.10依赖不多pip install langchain langchain-community faiss-cpu sentence-transformers pypdf说明一下每个包的作用langchain是编排框架langchain-community提供各种加载器和向量库封装faiss-cpu是 CPU 版向量检索库有 GPU 就装faiss-gpusentence-transformers用来加载本地 Embedding 模型pypdf用来读 PDF。提示faiss-cpu和faiss-gpu不要同时装会冲突。个人知识库数据量小CPU 版足够检索延迟通常在几十毫秒。4.2 第一步加载并切分文档我写了一个统一的加载函数按扩展名分发from langchain_community.document_loaders import PyPDFLoader, TextLoader from pathlib import Path def load_documents(folder: str): docs [] for path in Path(folder).rglob(*): if path.suffix.lower() .pdf: docs.extend(PyPDFLoader(str(path)).load()) elif path.suffix.lower() in (.md, .txt): docs.extend(TextLoader(str(path), encodingutf-8).load()) return docs这里用rglob递归遍历子目录保证不漏文件。TextLoader一定要指定encodingutf-8否则中文文档在部分系统上会乱码。加载完接切分raw_docs load_documents(./knowledge) chunks splitter.split_documents(raw_docs) print(f共切出 {len(chunks)} 个文本块)这个数字要留意。如果切出来只有个位数说明文档没读进来如果几万条可能切太碎了。我一般几千字的文档切出几十到上百块是正常的。4.3 第二步向量化并构建 FAISS 索引from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS embedding HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True}, ) vectorstore FAISS.from_documents(chunks, embedding) vectorstore.save_local(./faiss_index)normalize_embeddingsTrue是个关键参数。归一化后向量长度为 1内积就等于余弦相似度检索更稳定。BGE 系列模型官方也建议对查询加指令前缀中文检索场景可以给 query 加上为这个句子生成表示以用于检索相关文章能小幅提升召回。建库这一步是 CPU 密集的几千个 chunk 大概跑几分钟。跑完save_local存盘下次直接加载。4.4 第三步组装问答链from langchain.chains import RetrievalQA from langchain_community.llms import Ollama llm Ollama(modelqwen2.5:7b) # 本地模型示例 qa_chain RetrievalQA.from_chain_type( llmllm, retrievervectorstore.as_retriever(search_kwargs{k: 4}), return_source_documentsTrue, chain_typestuff, chain_type_kwargs{prompt: prompt}, ) result qa_chain.invoke({query: 我的项目里向量库用的什么索引}) print(result[result]) for doc in result[source_documents]: print(来源, doc.metadata.get(source))return_source_documentsTrue是我强烈建议开的。它会把检索到的原文块一起返回你能核对答案是不是有据可依。chain_typestuff表示把所有检索块直接塞进提示词简单直接适合块数不多的情况。块特别多时可以换map_reduce但会慢很多。4.5 第四步把问答包成一个 Agent到这一步其实已经能问答了但我想让它更“Agent”一点——能自己判断该不该查知识库、能不能调用工具。这就是Agent和普通 RAG 链的区别普通链是固定流程Agent 是让模型自己决定下一步动作。from langchain.agents import Tool, initialize_agent tools [ Tool( name知识库检索, funclambda q: qa_chain.invoke({query: q})[result], description当问题涉及我的个人资料时使用输入是一个完整问题, ), ] agent initialize_agent( tools, llm, agentzero-shot-react-description, verboseTrue )description写得好不好直接决定 Agent 会不会正确调用工具。要写清楚“什么时候用”而不是“这是什么”。我一开始写得太笼统Agent 经常该查不查改成明确触发条件后就稳了。5. 常见问题与排查技巧实录5.1 检索不到正确内容怎么办这是最高频的问题。排查顺序我总结成一张表现象可能原因排查方法解决答案完全跑偏建库和查询用了不同 Embedding检查两处 model_name统一模型并重建索引检索块不相关切分太碎或太大打印 chunk 内容看语义是否完整调整 chunk_size/overlap正确块排在第 5 名开外k 太小把 k 调到 10 看正确块排名适当增大 k 或加 rerank中文检索差用了英文 Embedding换中文模型用 BGE 中文系列分数普遍很低没归一化检查 normalize_embeddings开启归一化我踩过最深的一个坑是文档里同一段话被切成了三块检索只召回其中一块模型拿到半截信息答得不完整。后来我把 overlap 从 0 调到 80问题明显缓解。5.2 模型不看资料、自己编答案两个原因一是提示词约束不够二是检索到的资料本身不相关。先确认检索块是否相关如果相关但模型还是编就加强提示词把“仅根据资料”“未提及就说未提及”写死。如果检索块不相关那是检索问题回到上一节排查。5.3 回答太慢慢通常慢在 LLM 生成不在检索。FAISS 检索几万条数据也就几十毫秒。如果用的是本地大模型7B 参数在 CPU 上生成一段话要十几秒很正常。优化方向换更小的模型、用 GPU、或者把在线模型作为可选项。检索侧能优化的空间其实很小。5.4 资料更新了怎么办我的做法是维护一个建库脚本资料变动后重跑一次覆盖./faiss_index。不要试图增量更新 FAISS 的IndexFlat删除和更新很麻烦个人场景直接全量重建最省心。几千条数据重建也就几分钟。注意重建索引前先备份旧的万一新参数效果更差能快速回滚。这个习惯帮我省过好几次事。5.5 几个容易忽略的实操心得先小样本验证再全量建库。拿 5 篇文档跑通全流程确认检索和回答都对再灌全量资料能省大量等待时间。给文档加 metadata。加载时把文件名、路径写进 metadata回答时能显示来源方便溯源。问题改写能提升召回。用户口语化提问和文档书面语差距大时先让模型把问题改写成几个检索友好的查询再分别检索召回率会明显提升。别迷信大 k。k 从 4 加到 10噪声也翻倍模型反而更容易被带偏。宁可用小 k 加 rerank。6. 后续可以怎么扩展这套骨架跑通后扩展空间很大。我目前在做和打算做的几个方向一是加rerank用交叉编码器对召回的块重新排序精度提升明显二是做多路召回向量检索加关键词检索BM25融合兼顾语义和精确匹配三是把知识库做成多轮对话带上下文记忆追问时不用重复背景四是接结构化知识把 FAQ、表格这类内容单独处理和向量检索互补。如果你也想动手我的建议是别一上来追求大而全。先把“加载-切分-向量化-检索-生成”这条最小链路跑通用几篇自己的文档验证效果再逐步加东西。RAG 的瓶颈往往不在框架而在数据质量和切分策略这两块值得多花时间调。
返回列表