ARTICLE DETAIL

资讯详情

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

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

从零搭建个人知识库问答机器人:RAG+LangChain+FAISS实战 1. 为什么我要从零搭一个个人知识库问答机器人我自己平时有大量碎片化的资料——技术笔记、会议纪要、收藏的文章、PDF 手册散落在各种文件夹和笔记软件里。以前想找某个具体结论基本靠grep加肉眼翻效率极低。真正让我下决心动手的是有一次排查一个线上问题明明记得半年前写过一篇复盘笔记结果翻了四十分钟才找到那一刻我就知道必须搞个能对话式检索的东西了。这就是个人知识库问答机器人的由来。它的核心能力很朴素我把自己的文档丢进去然后用自然语言提问它基于我的文档内容给出答案并且告诉我答案来自哪一段。技术上这类系统的标准范式叫RAG检索增强生成配合Agent的编排能力再落地到LangChain和FAISS这两个具体工具上。它解决的问题有三个层次。第一层是找得到把散落的文档统一索引语义检索而不是关键词匹配第二层是答得准让大模型只基于检索到的原文回答减少胡编第三层是能追问通过 Agent 的多轮编排支持那第二点展开说说这种上下文相关的追问。适合谁来参考我认为有三类人最合适一是像我这样有个人资料管理刚需的技术从业者二是正在学LangChain和RAG想找个完整项目练手的开发者三是想理解Agent到底怎么下地干活、而不是停留在概念层面的人。整篇内容我会按真实搭建顺序讲包括我踩过的坑和参数取舍代码可以直接抄。2. 整体架构设计与技术选型思路2.1 为什么是 RAG 而不是微调很多人一上来就问为什么不直接拿我的文档去微调一个模型我实测过个人场景下微调是性价比最低的方案。原因很直接微调的成本高要准备训练数据、要算力、要反复调而且知识更新极其麻烦——你加一篇新笔记就得重新训一遍。更致命的是微调容易让模型记住错误细节还很难溯源你根本不知道它那句话是从哪来的。RAG 的思路完全不同。它把知识和模型解耦模型负责理解和生成知识放在外部向量库里随时增删改。你新增一篇文档只要重新索引那一段就行模型完全不用动。对于个人知识库这种高频更新、要求可溯源的场景RAG 是唯一合理的选择。这也是为什么热词里rag检索增强、rag实战一直居高不下。2.2 为什么选 LangChain FAISS 这套组合LangChain的价值在于它把 RAG 的各个环节抽象成了可替换的组件文档加载器、文本分割器、嵌入模型、向量库、检索器、LLM 调用每一环都能单独换。对新手来说它省掉了大量胶水代码对老手来说它提供了统一的接口方便做实验对比。热词里langchain入门、langchain中文教程搜索量高就是因为它的抽象层确实降低了门槛。FAISS是 Facebook 开源的向量检索库选它的理由很实在轻量、纯本地、无需额外部署服务、单机性能足够。个人知识库的规模通常在几千到几万条 chunk 之间FAISS 的IndexFlatL2或IndexIVFFlat完全扛得住检索延迟在毫秒级。相比需要起独立服务的向量数据库FAISS 就是一个 Python 库pip install就能用对个人项目太友好了。热词里faiss使用被频繁搜索也说明它是很多人的第一选择。2.3 Agent 在这里扮演什么角色如果只是提问-检索-回答一条直线其实用不上 Agent一个 Chain 就够了。但真实使用中用户的问题往往需要多步处理。比如帮我对比一下 A 方案和 B 方案的优缺点这需要先分别检索 A 和 B再让模型做对比。再比如我上次提到的那个配置参数是多少需要先理解上次指什么再定向检索。Agent 的核心是让模型自己决定下一步做什么是直接检索还是先改写问题还是需要多轮检索后综合。热词里agent架构、agent框架与编排、agent记忆都是围绕这个能力展开的。我的设计里Agent 负责编排检索工具和回答工具同时维护对话记忆让多轮追问成为可能。这里要区分一下harness和agent区别harness 更像是给模型套的外壳和约束而 agent 是具备自主决策和工具调用能力的实体两者定位不同。2.4 整体数据流整个系统的数据流我拆成两条线。索引线文档加载 → 文本分割 → 向量化 → 存入 FAISS。查询线用户提问 → Agent 判断意图 → 调用检索工具 → 拿到相关 chunk → 拼进 prompt → LLM 生成答案 → 返回答案和来源。两条线通过向量库这个共享内存连接。理解这个数据流后面每一步的取舍就都有依据了。3. 核心环节拆解与实操要点3.1 文档加载格式决定加载器个人知识库的文档格式通常很杂.md、.txt、.pdf、.docx甚至网页存档。LangChain 提供了对应的 Loader比如TextLoader、PyPDFLoader、UnstructuredMarkdownLoader。我的经验是不要指望一个 Loader 通吃按扩展名分发最稳。这里有个大坑PDF 加载。扫描版 PDF 没有文字层PyPDFLoader读出来是空的必须上 OCR。而带复杂表格的 PDF文字顺序会乱检索出来的片段可能语义错位。我的处理方式是纯文本类文档优先PDF 只作为补充且加载后人工抽查几段。热词里有没有本地的rag文本拆解工具被搜说明文本拆解确实是痛点。3.2 文本分割chunk 大小是门玄学文本分割直接决定检索质量。分太大一个 chunk 里混了多个主题检索命中后噪声多分太小语义不完整模型拿到的上下文残缺。我实测下来中文技术文档用500 到 800 字符、overlap 100 到 150 字符比较均衡。overlap 的作用是防止一句话被硬生生切断保证边界处的语义连续。分割器我推荐RecursiveCharacterTextSplitter它会按段落、句子、字符逐级尝试切分尽量在自然边界断开。参数上separators要针对中文调整把\n\n、\n、。、、都放进去。这里有个细节中文没有空格默认按空格切会失效必须显式配置中文标点作为分隔符。3.3 向量化嵌入模型怎么选嵌入模型负责把文本转成向量。选择上有两条路一是调用在线嵌入 API质量高但依赖网络和额度二是本地嵌入模型比如text2vec系列或bge系列完全离线、免费、隐私安全。个人知识库涉及私人笔记我强烈建议用本地嵌入模型数据不出本机。维度方面常见的是 768 维或 1024 维。维度越高表达能力越强但存储和检索开销也越大。个人场景 768 维足够。要注意的是索引和查询必须用同一个嵌入模型换了模型就得重建整个索引否则向量空间对不上检索结果会完全错乱。这个坑我踩过一次排查了半天才发现是模型不一致。3.4 FAISS 索引Flat 还是 IVFFAISS 的索引类型选择取决于数据量。IndexFlatL2是暴力检索精度 100%几千条数据毫无压力。数据量上万后可以换IndexIVFFlat它先聚类再检索速度快很多但会损失一点精度需要调nlist聚类数和nprobe检索的聚类数。我的建议是先用 Flat等真的慢了再换。过早优化是浪费。另外 FAISS 索引默认在内存里要持久化得用save_local和load_local。注意保存的是索引文件原始文档和元数据要另外存否则检索出来只有向量没有原文等于白搭。3.5 Agent 编排工具怎么定义Agent 的能力边界由它手里的工具决定。我给它配了三个核心工具retrieve语义检索、list_sources列出知识库里的文档、answer_with_context基于上下文回答。工具的描述description非常关键模型是靠描述来判断该调哪个工具的描述写得含糊模型就会乱调。热词里agent skill教程、agent 开发 教程热度高本质都是在讲怎么把能力封装成工具。我的经验是工具粒度不要太细否则模型选择困难也不要太粗否则一个工具干太多事参数复杂。三到五个工具是比较舒服的区间。4. 完整实操流程与关键代码4.1 环境准备与依赖安装先把环境搭起来。我用的 Python 3.10依赖如下。注意版本要锁LangChain 迭代快不同版本 API 差异大不锁版本很容易跑不起来。pip install langchain0.1.0 pip install langchain-community0.0.10 pip install faiss-cpu1.7.4 pip install sentence-transformers2.2.2 pip install pypdf3.17.0 pip install unstructured0.11.0提示faiss-cpu是 CPU 版本个人使用完全够。如果你有 GPU 且数据量大可以换faiss-gpu但要注意 CUDA 版本匹配否则装完 import 就报错。4.2 文档加载与分割代码下面这段是索引线的核心。我按扩展名分发加载器然后统一分割。import os from langchain_community.document_loaders import TextLoader, PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter def load_documents(folder_path): docs [] for root, _, files in os.walk(folder_path): for f in files: path os.path.join(root, f) if f.endswith(.md) or f.endswith(.txt): loader TextLoader(path, encodingutf-8) elif f.endswith(.pdf): loader PyPDFLoader(path) else: continue docs.extend(loader.load()) return docs splitter RecursiveCharacterTextSplitter( chunk_size600, chunk_overlap120, separators[\n\n, \n, 。, , , , , ] ) docs load_documents(./my_knowledge) chunks splitter.split_documents(docs) print(f共加载 {len(docs)} 篇文档切分为 {len(chunks)} 个 chunk)这里chunk_size600和chunk_overlap120是我反复调出来的值。你可以先用这个跑再根据检索效果微调。分割完一定要打印 chunk 数量如果数量异常少多半是加载器没读到内容。4.3 向量化与 FAISS 索引构建嵌入模型我用bge-small-zh中文效果好、体积小、速度快。构建索引时把 chunk 的原文和元数据一起存进去。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) print(索引已保存)normalize_embeddingsTrue很重要它把向量归一化配合余弦相似度检索更准。bge 系列官方也建议开启归一化。索引保存后下次启动直接load_local加载不用重新算省时间。4.4 检索器与 Agent 组装检索器设置k4即每次取最相关的 4 个 chunk。k 太小可能漏掉关键信息太大则 prompt 塞太多噪声还会挤占上下文窗口。4 是个平衡点。from langchain.agents import Tool, AgentExecutor, create_react_agent from langchain_community.llms import Ollama retriever vectorstore.as_retriever(search_kwargs{k: 4}) def retrieve_tool(query: str) - str: docs retriever.get_relevant_documents(query) return \n\n.join([d.page_content for d in docs]) tools [ Tool( nameretrieve, funcretrieve_tool, description当需要查找个人知识库中的具体信息时使用输入是检索问题 ) ] llm Ollama(modelqwen2:7b) agent create_react_agent(llm, tools, prompt) executor AgentExecutor(agentagent, toolstools, verboseTrue)LLM 我用本地 Ollama 跑qwen2:7b完全离线。热词里ollama 简易本地 rag 知识库被搜说明本地化是很多人的诉求。7B 模型在消费级显卡或大内存机器上能跑效果对个人问答够用。4.5 参数选择背后的计算为什么k4假设每个 chunk 平均 600 字符4 个就是 2400 字符加上系统提示和问题总 prompt 约 3000 字符折合 token 大约 2000 出头。7B 模型的上下文窗口通常 8K留足余量给生成。如果 k 设成 10prompt 就逼近 6000 字符生成空间被压缩还容易触发截断。为什么chunk_overlap120经验值是 chunk_size 的 15% 到 25%。600 的 20% 就是 120。这个比例能保证跨 chunk 的句子在边界处至少有一份完整副本检索时不会因为切断而丢语义。5. 常见问题与排查技巧实录5.1 检索结果答非所问这是最高频的问题。排查顺序我总结成一张表按可能性从高到低。现象可能原因排查方法解决检索到的片段和问题无关嵌入模型不一致检查索引和查询是否同一模型统一模型重建索引检索到相关但答案错chunk 太大混入噪声打印 chunk 内容看是否串主题减小 chunk_size完全检索不到文档没加载成功打印 chunk 数量检查加载器和编码中文检索效果差分隔符没配中文标点看分割结果补全中文 separators我遇到过一次检索不到最后发现是TextLoader没指定encodingutf-8中文全变乱码向量自然对不上。这种问题不看原始 chunk 根本发现不了。5.2 回答里出现知识库没有的内容这是模型幻觉。RAG 的核心价值就是抑制幻觉但如果 prompt 没约束好模型还是会自由发挥。我的做法是在系统提示里写死一句只允许基于提供的上下文回答如果上下文中没有相关信息直接回答知识库中没有找到相关内容。这句话看似简单效果立竿见影。另外检索为空时不要硬让模型答直接返回未找到从源头掐断幻觉。热词里rag瓶颈讨论很多幻觉控制就是核心瓶颈之一。5.3 多轮追问丢失上下文用户问那第二点呢如果 Agent 没有记忆它根本不知道第二点指什么。解决方式是给 Agent 加对话记忆把历史问答拼进 prompt。LangChain 的ConversationBufferMemory可以直接用。但要注意记忆不能无限增长否则 prompt 越来越长最后爆窗口。我一般只保留最近 5 轮。5.4 索引更新后检索不到新内容新增文档后必须重新构建索引或者用add_documents增量添加。很多人以为把文件丢进文件夹就完事了其实索引是静态的不会自动感知文件变化。我的做法是写个脚本每次更新文档后手动跑一遍重建简单可靠。5.5 性能与并发个人使用基本不涉及高并发但如果想做成多人共享的服务就要考虑ai agent 怎么扛并发这个问题。FAISS 检索本身很快瓶颈在 LLM 生成。我的建议是检索和生成分离检索可以并发生成用队列串行避免显存被打爆。如果只是自己用完全不用操心这些。6. 我踩过的坑和几条实在建议第一个坑是盲目追求大模型。我一开始用 13B 甚至更大的模型结果机器跑不动响应慢到没法用。换成 7B 后配合好的检索效果反而更稳。个人场景模型够用就行检索质量比模型大小更重要。第二个坑是忽略元数据。一开始我只存了文本检索出来不知道来自哪个文件。后来给每个 chunk 加上source元数据回答时能标注来源体验立刻上了一个档次。可溯源是 RAG 相对微调的核心优势一定要用起来。第三个坑是prompt 写得太随意。Agent 的工具描述、系统提示这些看起来是软的东西实际对结果影响巨大。我花在调 prompt 上的时间比调代码还多。工具描述要精确到什么时候用、输入是什么系统提示要明确能做什么、不能做什么。关于rag知识库能存储图片嘛这个问题我的答案是纯文本 RAG 存不了图片语义但可以存图片的描述文本或 OCR 结果。如果要做多模态需要专门的图文嵌入模型那是另一个复杂度了。个人知识库先从文本做起别一上来就贪多。最后说下ontology rag和kg知识库这类进阶方向。它们用知识图谱补充向量检索能处理更复杂的推理关系但搭建成本高很多。我的建议是先用基础 RAG 跑通等真的遇到向量检索解决不了的关系推理问题再考虑上图谱。别为了技术而技术。这套东西我从动手到跑顺大概花了一个周末现在每天在用。它不是什么高大上的系统但确实把我从翻文件夹里解放出来了。如果你也有类似的资料管理痛点照着上面的步骤走一遍基本能跑起来。
返回列表