ARTICLE DETAIL

资讯详情

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

从零搭建个人知识库问答机器人:RAG与Agent实战指南

从零搭建个人知识库问答机器人:RAG与Agent实战指南 1. 为什么我要从零搭一个个人知识库问答机器人我平时有大量碎片化的信息需要管理技术文档、会议纪要、读书笔记、随手记的灵感、收藏的文章。这些东西散落在各种地方用的时候找不到找到了又忘了当时为什么记下来。传统的文件夹分类和全文搜索解决不了语义层面的问题——我记得“那个讲缓存穿透的解决方案”但搜“缓存”出来几百条结果翻半天也找不到想要的那一条。这就是我动手做个人知识库问答机器人的直接原因。它本质上是一个基于RAG检索增强生成架构的Agent应用我把自己的文档丢进去它自动切分、向量化、建索引我问问题时它先从知识库里检索最相关的片段再让大模型基于这些片段生成回答。整个过程不需要我把文档上传到任何第三方平台数据完全留在本地。这个项目适合什么人参考如果你有下面这些情况这篇内容应该能帮到你手头积累了大量 Markdown、PDF、TXT 文档想用自然语言快速检索对Agent和RAG感兴趣但看了一堆概念文章还是不知道怎么落地想搭一个完全本地运行、不依赖外部服务的问答系统已经在用 Obsidian、Logseq 之类的工具做笔记想给笔记加一个“会说话”的入口我踩过的坑包括文档切分粒度选错导致检索不准、向量模型选型时忽略了中文支持、检索结果排序策略过于单一、大模型幻觉严重时不知道怎么抑制。这些在后面都会展开讲。2. 整体架构设计与技术选型思路2.1 为什么是 RAG 而不是微调一开始我考虑过微调一个模型来“记住”我的知识库内容。试了一轮之后放弃了原因很实际成本问题微调需要标注数据、需要 GPU 资源、每次知识库更新都要重新训练个人项目根本扛不住这个迭代频率幻觉问题微调后的模型仍然会编造内容而且你很难追溯它到底是从哪条知识里“学”来的更新问题我今天新写了一篇笔记微调方案要重新跑训练RAG 方案只需要把新文档追加到索引里RAG 的核心优势在于知识外挂模型本身不变知识库作为外部检索源。更新知识库就是更新索引跟模型无关。对于个人知识库这种高频更新、内容私有的场景RAG 是更合理的选择。2.2 Agent 在其中的角色单纯的 RAG 是“检索-拼接-生成”的线性流程。加上Agent之后系统有了自主决策能力判断用户问题是否需要检索知识库还是直接回答决定检索几次、用什么查询词检索对检索结果做相关性判断不相关就换个策略重试多轮对话中维护上下文知道“它”指的是什么我用的是ReAct 模式的 AgentReasoning推理和 Acting行动交替进行。Agent 先思考“我需要什么信息”然后调用检索工具观察结果再决定下一步。这个循环让问答准确率比单次 RAG 高出一截。2.3 技术栈选型与理由组件选型理由大模型本地部署 7B 级别模型数据不出本地响应速度可接受向量模型BGE-small-zh中文效果好体积小CPU 可跑向量数据库Chroma轻量、零配置、Python 原生编排框架LangChain生态成熟Agent 和 RAG 组件齐全文档加载Unstructured 自写解析器覆盖 Markdown、PDF、TXT前端Gradio快速搭交互界面不用写前端代码选 Chroma 而不是 Milvus 或 Pinecone是因为个人知识库的向量规模通常在几万到几十万条Chroma 完全够用而且它支持本地持久化重启不丢数据。选 BGE-small-zh 而不是 OpenAI 的 embedding 接口一是数据隐私二是中文语义匹配确实更准三是免费。提示如果你手头有 GPU大模型可以换成 14B 或 32B 级别回答质量会明显提升。但如果只有 CPU7B 量化版本是性价比最高的选择。3. 核心细节解析与实操要点3.1 文档切分粒度决定检索质量文档切分是 RAG 系统里最容易被忽视、但影响最大的环节。我一开始按固定 500 字符切分结果检索出来的片段经常是“半句话”模型拿到这种上下文也生成不出好答案。后来改成按语义切分Markdown 按标题层级切PDF 按段落切代码文件按函数切。每个片段控制在 200-500 字之间相邻片段之间保留 50 字的重叠区域避免关键信息被切断。from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size400, chunk_overlap50, separators[\n## , \n### , \n\n, \n, 。, ] )separators的顺序很重要优先按二级标题切其次三级标题再次空行最后才按句号和空格。这样切出来的片段天然带有语义完整性。3.2 向量化与索引构建向量化就是把文本片段转成一串数字向量语义相近的文本在向量空间里距离更近。我用 BGE-small-zh 模型输出 512 维向量。from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-small-zh-v1.5) embeddings model.encode(chunks, normalize_embeddingsTrue)normalize_embeddingsTrue很关键它把向量归一化到单位长度这样计算余弦相似度时只需要做点积速度快很多。建索引时我用了 Chroma 的add_documents方法同时把原文、来源文件路径、片段序号作为 metadata 存进去。这样检索出来之后能追溯到原始文档方便核对。3.3 检索策略不只是向量相似度纯向量检索有个问题它擅长语义匹配但对关键词精确匹配不敏感。比如我问“BGE-small-zh 的向量维度是多少”向量检索可能返回一堆讲 embedding 的片段但没有一条精确提到“512”。我的解决方案是混合检索向量检索 BM25 关键词检索两路结果合并后做重排序Rerank。BM25 用 rank_bm25 库实现重排序用一个小的 cross-encoder 模型。from rank_bm25 import BM25Okapi tokenized_corpus [list(chunk) for chunk in chunks] bm25 BM25Okapi(tokenized_corpus) bm25_scores bm25.get_scores(list(query))两路检索各取 Top 10合并去重后取 Top 5 送给大模型。实测下来混合检索比纯向量检索的命中率高 20% 左右。3.4 Agent 的推理循环设计Agent 的核心是一个 while 循环把用户问题和对话历史拼成 prompt让大模型输出下一步动作检索 / 直接回答 / 结束如果是检索调用检索工具把结果追加到上下文重复直到模型输出最终答案或达到最大轮数我设了最大 3 轮检索防止无限循环。每轮检索的查询词由模型自己生成它可能会把“那个缓存穿透的方案”改写成“缓存穿透 解决方案 布隆过滤器 空对象”。注意Agent 的 prompt 里必须明确告诉模型“如果检索结果不相关不要强行编造答案直接说不知道”。这一条能大幅降低幻觉率。4. 完整实操流程与关键环节实现4.1 环境准备与依赖安装我是在 macOS 上开发的Linux 同样适用。Python 版本 3.10 以上。python -m venv venv source venv/bin/activate pip install langchain langchain-community chromadb sentence-transformers rank-bm25 gradio pypdf unstructured大模型我用 Ollama 跑本地模型先安装 Ollama然后拉一个模型ollama pull qwen2:7bQwen2 7B 的中文能力在开源模型里属于第一梯队而且量化后显存占用约 5GBCPU 推理也能跑。4.2 知识库目录结构设计我建议把知识库根目录设成这样的结构knowledge_base/ ├── tech/ # 技术文档 ├── notes/ # 读书笔记 ├── meetings/ # 会议纪要 └── misc/ # 其他每个子目录放对应的 Markdown 或 TXT 文件。加载时递归遍历整个目录按文件扩展名选择解析器。这样做的好处是 metadata 里可以记录分类信息检索时可以按分类过滤。4.3 索引构建脚本import os from langchain_community.document_loaders import TextLoader, PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from sentence_transformers import SentenceTransformer import chromadb def load_documents(root_dir): docs [] for dirpath, _, filenames in os.walk(root_dir): for fname in filenames: fpath os.path.join(dirpath, fname) if fname.endswith(.md) or fname.endswith(.txt): loader TextLoader(fpath, encodingutf-8) elif fname.endswith(.pdf): loader PyPDFLoader(fpath) else: continue docs.extend(loader.load()) return docs def build_index(docs, persist_dir./chroma_db): splitter RecursiveCharacterTextSplitter( chunk_size400, chunk_overlap50, separators[\n## , \n### , \n\n, \n, 。, ] ) chunks splitter.split_documents(docs) model SentenceTransformer(BAAI/bge-small-zh-v1.5) texts [c.page_content for c in chunks] embeddings model.encode(texts, normalize_embeddingsTrue, show_progress_barTrue) client chromadb.PersistentClient(pathpersist_dir) collection client.get_or_create_collection(knowledge) collection.add( ids[fchunk_{i} for i in range(len(chunks))], embeddingsembeddings.tolist(), documentstexts, metadatas[{source: c.metadata.get(source, ), index: i} for i, c in enumerate(chunks)] ) return collection, chunks这个脚本跑一次大概几分钟取决于文档数量。之后每次新增文档只需要对新文档做增量索引不用全量重建。4.4 检索与问答主流程def retrieve(query, collection, model, top_k5): q_emb model.encode([query], normalize_embeddingsTrue) results collection.query(query_embeddingsq_emb.tolist(), n_resultstop_k) return results[documents][0], results[metadatas][0] def ask(query, collection, model, llm): docs, metas retrieve(query, collection, model) context \n\n.join([f[来源: {m[source]}]\n{d} for d, m in zip(docs, metas)]) prompt f基于以下知识库内容回答问题。如果内容不足以回答直接说知识库中没有相关信息。 知识库内容 {context} 问题{query} 回答 response llm.invoke(prompt) return response, metas这个是最简版本。加上 Agent 之后ask函数会被包在一个循环里模型可以多次调用retrieve并调整查询词。4.5 Gradio 界面搭建import gradio as gr def chat_interface(query, history): answer, sources ask(query, collection, model, llm) source_text \n.join([f- {s[source]} for s in sources]) return f{answer}\n\n参考来源\n{source_text} demo gr.ChatInterface(fnchat_interface, title个人知识库问答机器人) demo.launch()跑起来之后浏览器打开http://localhost:7860就能用了。界面很朴素但功能完整。5. 常见问题与排查技巧实录5.1 检索不准怎么办这是最高频的问题。排查顺序如下现象可能原因解决方法检索结果完全不相关向量模型不适合中文换 BGE-small-zh 或 text2vec-base-chinese检索结果相关但缺关键信息切分粒度太粗减小 chunk_size增加 overlap关键词匹配不到纯向量检索的短板加入 BM25 混合检索相似问题检索结果差异大查询词太短让 Agent 改写查询词扩展同义词我遇到过一次特别典型的情况问“怎么配置向量数据库的持久化路径”检索出来的全是讲数据库连接的片段。后来发现是切分时把“持久化”和“路径”切到了两个片段里。把chunk_overlap从 20 调到 50 之后解决了。5.2 大模型幻觉严重即使检索到了正确内容模型也可能“自由发挥”。三个抑制手段prompt 里明确要求“只基于给定内容回答不要添加外部知识”检索结果里带上来源标注让模型引用来源如果检索结果相似度低于阈值直接返回“未找到相关信息”不调模型我设的相似度阈值是 0.6余弦相似度低于这个值的结果直接丢弃。实测下来这个阈值能过滤掉大部分不相关片段同时不会误杀真正相关的内容。5.3 响应速度慢本地 7B 模型在 CPU 上生成速度大约是 5-10 token/秒一个 200 字的回答要等 20-40 秒。优化手段用 GGUF 量化格式Q4_K_M 级别在质量和速度之间平衡最好限制回答长度max_tokens300足够日常问答检索和生成分离检索结果先展示给用户生成过程流式输出我后来加了一个“仅检索”模式只返回相关片段不生成回答速度瞬间降到 1 秒以内。赶时间的时候直接用这个模式。5.4 知识库更新后检索不到新内容Chroma 的持久化集合在追加新文档后需要确认索引确实更新了。我踩过的坑是脚本跑完了但 collection 没重新加载查询用的还是旧的内存对象。解决方法是在每次查询前重新get_collection或者干脆重启服务。另一个坑是文档编码问题。有些从网页复制的文本带有不可见字符向量化之后语义偏移。我加了一个预处理步骤用正则清理掉\u200b、\ufeff这类零宽字符。5.5 多轮对话上下文丢失Agent 在多轮对话中需要知道“它”“那个”指代什么。我的做法是把最近 3 轮对话历史拼进 prompt同时让 Agent 在检索前先做指代消解。比如用户先问“RAG 的瓶颈是什么”再问“怎么解决它”Agent 会把“它”解析成“RAG 的瓶颈”然后用“RAG 瓶颈 解决方法”去检索。这个指代消解步骤是单独一次模型调用会增加一点延迟但对多轮对话的准确率提升很明显。6. 一些实操心得和后续扩展方向这个项目我从动手到跑通大概花了一个周末但后续调优花了更长时间。最大的体会是RAG 系统的效果上限取决于检索质量而不是模型大小。我试过把 7B 换成 14B回答流畅度有提升但准确率提升有限反而把切分策略和混合检索做好之后7B 模型的回答准确率提升了一大截。另一个心得是不要追求一步到位。我一开始想把所有格式的文档都支持PDF、Word、HTML、EPUB 全上结果解析器各种报错光调试解析就花了一天。后来砍到只支持 Markdown 和 TXT先把主流程跑通再逐步加格式。这个顺序很重要。后续我打算加的功能一是图片处理把知识库里的图片用多模态模型生成描述文本一起建索引二是知识图谱融合把实体关系抽出来跟向量检索互补三是定时增量索引用 cron 每天自动扫描新增文件。如果你也在搭类似的东西我的建议是先把最小闭环跑通一个目录、一种格式、一个模型、一个界面。跑通之后再逐步替换和增强。上来就搞复杂架构大概率会在某个环节卡住然后放弃。
返回列表