RAG+AI Agent实战:手把手搭建私人大模型知识库

发布时间:2026/9/6 4:37:48
RAG+AI Agent实战:手把手搭建私人大模型知识库 我电脑里堆了三年多的工作笔记、项目文档和技术摘录加起来差不多两千多篇散落在本地文件夹、Notion、还有个早就忘了密码的旧博客里。平时想找点东西全靠印象印象一旦模糊整个知识库就等于不存在。以前我试过给文档打标签、建目录、手动整理坚持不到两周就放弃了——整理知识本身就是巨大的时间黑洞。后来开始折腾 AI Agent 和 RAG才真正觉得这事有解。RAG 负责把散落的知识变成可检索、可引用的外挂记忆AI Agent 负责在检索结果之上做规划、调工具、跟人对话两者配合起来一个能听懂人话、又记得住全量资料的专属知识库就搭起来了。这篇文章我想把从原理到落地整套思路拆开讲清楚包括文档切分、向量化、检索重排、Agent 工具调用这些核心环节也顺手把踩过的坑和评估方法一并分享出来。写给那些手上资料很多、想搭个人知识库但不知道从哪下手的开发者也写给已经搭过一版但发现效果不如预期、想搞清楚哪里出了问题的人。1. 先想明白RAG 和 AI Agent 到底在解决哪两个不同的问题很多人一上来就搜RAG怎么搭AI Agent 框架哪个好结果被一堆名词绕晕。我觉得动手之前最该先搞清楚的是这两样东西各自的定位。它们不是同一个层面的概念更像是记忆和行动的关系。1.1 大模型的本性记不住也不会动手先看看大模型本身有什么毛病。第一它的知识是有截止日期的训练完之后世界里发生了什么都不再更新。第二即便训练数据里包含某些内容它也可能记错、记混尤其在面对你私有的、小众的专业资料时它根本没学过只能硬编编出来的东西看着挺像回事实际错得离谱。更麻烦的是大模型默认是一次性对话选手。你关掉对话窗口再打开它什么都不记得。上下文窗口再长也就是一次对话里能装更多内容窗口一关照样清零。这就决定了想让大模型精准回答某个特定领域的问题必须每次把相关材料喂给它或者找到一种机制让它自己临时去查。1.2 RAG 负责外挂记忆Agent 负责自主行动RAGRetrieval-Augmented Generation检索增强生成解决的是怎么把外部知识送进生成过程的问题。它的思路很直白你不需要让模型记住所有东西只需要在回答问题前从一个外部知识库里检索出最相关的片段然后把这些片段和问题一起交给模型让模型基于这些材料作答。整个过程可以拆成两步索引端负责把文档切碎、向量化、存进向量库检索端拿到用户问题后做同样的向量化到库里找最相近的内容再拿给大模型。AI Agent 解决的则是另一个问题怎么让模型不止会说还会做。它引入了一个运行循环——模型根据任务目标制定计划决定调用哪个工具拿到工具的返回结果后判断是否需要继续直到任务完成。这个计划-行动-观察的能力正是 Agent 和普通对话机器人最大的区别。放在知识库场景里两者的分工非常清晰RAG 是信息供给系统保证回答有据可依Agent 是决策调度系统决定什么时候去检索、检索结果怎么用、要不要再查一次、要不要调用别的工具。1.3 为什么把几千页直接塞进上下文是个坏主意有一种偷懒的做法被提过很多次既然上下文窗口都到 128K 甚至 200K 了干脆把所有文档都塞进去不就行了我试过行不通。首先是成本问题。长上下文的 token 费用随长度线性上涨你问一次问题烧掉几十万 token日常使用根本扛不住。其次是效果问题模型对中间位置信息的关注度明显低于开头和结尾这就是所谓的lost in the middle文档一长关键信息夹在中间很容易被模型忽略。而且几千页文档一次性塞入模型注意力被分散回答反而更容易跑偏。RAG 的核心价值恰恰在于减负——不把所有内容交给模型只把和当前问题最相关的几段送进去。这样既控制成本又能让模型聚焦在最相关的信息上。2. RAG 管线拆解从原始文档到可检索的向量库理解了定位下面进入最关键的实操环节。RAG 的质量不是由某一个环节决定的而是整条链路的综合结果。很多人搭完发现回答效果差首先怀疑模型不行其实多半是索引端就出了问题——文档没处理好后面检索再厉害也是白搭。2.1 文档解析格式差异是第一个坑我之前处理自己的笔记库时遇到的第一个问题就是文件格式极其混乱有 Markdown、PDF、Word、TXT还有从网页复制下来的 HTML 片段。不同格式的解析难度完全不一样Markdown 和 TXT 最省心读进来就是文本PDF 最恶心尤其是扫描版没有文本层必须先 OCR。这里有个很多人忽视的点解析时要把内容主体和页面噪声区分开。PDF 的页眉页脚、Word 里的批注、网页里的导航栏这些都会被当作正文存进知识库检索时就会带来大量噪声。比如你问项目的截止日期是什么结果检索到的内容是某页页脚的公司地址回答自然就偏了。我现在的处理方式是按格式走不同的解析器解析完统一清洗一遍去掉多余空白、页码、页眉再按文档原有结构提取标题和层级信息。这些结构化信息后面切分时非常重要。2.2 切分策略分块大小决定了召回上限切分Chunking是整个 RAG 里最容易被低估的一步。块太大一个块里包含多个主题检索时指向性差模型容易读到一堆不相关内容块太小语义不完整一条独立的信息被拦腰截断召回之后模型看不懂。通用做法是固定大小加重叠窗口比如每块 500 个 token块与块之间重叠 50 个 token。这样处理简单但对结构型文档不太友好——一个二级标题下的内容可能被切到两个块里语义就断了。对技术文档和笔记我更推荐结构化切分先按 Markdown 标题层级拆成章节再对特别长的章节做二次切分。这样每个块天然拥有相对完整的语义边界。举个例子一份 API 文档里create_order接口说明就应该是一个独立的块而不是和delete_order挤在一起。切分参数没有标准答案取决于你的文档类型和问题粒度。我自己在笔记库上用的是块大小 800 字符中文场景按字符算比按 token 方便重叠 100 字符。这个数值是在对比了多组实验后确定的后面第 5 部分会说怎么评估。2.3 向量化与存储Embedding 模型怎么选、向量库怎么挑切好的块要变成向量才能被检索。这里涉及两个选择Embedding 模型和向量数据库。Embedding 模型的作用是把一段文本映射成一个高维向量语义相近的文本向量距离也近。选型时主要看三件事中文效果、向量维度、是否支持长文本。中文场景下开源的 bge 系列效果不错我自己用过 bge-m3支持包括中文在内的多语言最长可以处理 8192 个 token输出向量维度 1024在个人知识库场景里性能足够。如果机器性能有限可以选更小一号的 bge-small-zh速度更快但语义能力会弱一些。向量数据库负责存储和检索这些向量。选择标准其实不复杂数据量、查询性能、运维成本、是否需要混合检索。个人知识库规模通常在几万到几十万个 chunk这个量级用轻量级方案就够了。我整理了一个简表可以按需参考方案适合场景特点Chroma本地小规模快速验证零配置轻量直接可用FAISS百万级以下向量检索Meta 出品性能好但要自己管索引持久化Qdrant需要过滤向量混合查询支持 payload 过滤API 友好Milvus大规模、分布式、需要高可用功能全但部署和维护成本高pgvector已有 PostgreSQL 存储体系避免引入额外组件SQL 统一管理我自己做个人知识库用的 pgvector理由是笔记元数据来源、日期、标签都放在 PostgreSQL 里向量和结构化数据放一起查询时可以直接用 SQL 做标签过滤加向量排序少一套系统少一堆麻烦。2.4 检索增强向量召回之后一定要做重排向量召回有个先天问题它只能按语义相似度取 Top-K这个 Top-K 里往往混着不少表面相似、实际不相关的结果。直接把这些结果塞给大模型轻则浪费上下文重则被错误信息带偏。解决办法是加重排Rerank环节。召回阶段先用向量检索拿到比如 50 条候选再用一个专门的重排模型对候选做精细打分取前 5 条进入最终生成。重排模型会综合语义匹配、上下文关系做更精细的判断准确率比单纯向量相似度高不少。开源的 bge-reranker-base 在中文场景下表现不错虽然推理比向量检索慢但只对少量候选做打分时延完全可接受。到这里RAG 的索引端和检索端核心链路就算完整了。很多教程到这里就结束但实际做知识库会发现光有这个链路用户体验仍然一般——因为你只能一问一答地被动触发检索没法让系统主动处理复杂问题。这就是 Agent 要上场的地方。3. Agent 编排让知识库从能查到变成能干活我把 RAG 链路做完之后第一版体验是问什么答什么但只能回答简单事实型问题。一旦问题涉及多步推理或者需要在查知识库之外再做点别的事整个系统就卡住了。直到我把 Agent 逻辑加进去知识库才算真正活起来。3.1 Agent 的运行逻辑规划、调用、观察、再规划Agent 的核心是一个循环。模型先分析用户意图判断这个问题需不需要检索需要调用哪个工具。然后执行工具观察返回结果根据结果决定下一步动作——是继续调用别的工具还是直接生成最终回答。这个模式被叫做 ReActReasoning and Acting是当前大多数 Agent 框架的基础。理解这个循环的关键是Agent 的智能不在某一次调用里而是体现在多次决策中。比如用户问最近三个月我们讨论过哪些知识库方案如果只做一次检索很可能漏掉时间信息Agent 会先检索知识库方案相关文档然后从结果中筛选带时间戳的条目再根据筛选结果决定是否补查一次。3.2 工具调用与知识库查询的协作方式知识库在 Agent 架构里通常被封装成一个工具。技术实现的通用标准是 Function Calling做法是把知识库检索函数描述成结构化 JSON SchemaAgent 收到问题后模型判断是否需要调用这个函数如果需要就输出函数名和参数代码侧负责执行把结果返回给模型。一个典型的知识库检索工具描述大概是这样的{ name: search_knowledge_base, description: 在个人知识库中检索与问题相关的文档片段, parameters: { type: object, properties: { query: { type: string, description: 用户问题或需要检索的关键词 }, top_k: { type: integer, description: 返回的片段数量默认5 } }, required: [query] } }这个描述会随系统提示词一起发给模型。注意 description 里要写清楚工具的能力边界如何时用何时不用。我踩过的坑是描述写得不够明确模型遇到一些问题时会乱调工具比如问你好也去检索一遍知识库白白增加时延。后来在描述里加了仅在用户问题涉及知识库内容时必须调用寒暄、翻译、通用知识问题不要调用行为才正常。3.3 一个最小可用的 ReAct 循环附代码很多框架能帮你隐藏这一层但我建议至少手写一次 ReAct 循环才能理解 Agent 内部发生了什么。下面是一个极简实现没有用任何 Agent 框架只依赖大模型的对话补全和函数调用能力import json from openai import OpenAI client OpenAI(base_urlhttp://localhost:11434/v1, api_keyollama) TOOLS [search_knowledge_base_schema] # 上面那个 schema def search_knowledge_base(query: str, top_k: int 5): # 执行向量检索返回格式化的片段 results vector_search(query, top_k) return format_results(results) messages [ {role: system, content: 你是个人知识库助手请基于检索到的内容回答并标注来源。}, {role: user, content: user_question} ] # 最多循环 5 轮防止死循环 for _ in range(5): resp client.chat.completions.create( modelqwen2.5-32b, messagesmessages, toolsTOOLS, tool_choiceauto, ) msg resp.choices[0].message messages.append(msg) if msg.tool_calls: for call in msg.tool_calls: args json.loads(call.function.arguments) if call.function.name search_knowledge_base: result search_knowledge_base(**args) messages.append({ role: tool, tool_call_id: call.id, content: result }) else: # 没有工具调用说明可以直接回答 return msg.content这段代码没有处理很多边界情况但足够让你看到 Agent 循环的本质模型决定要不要调工具工具结果回填进对话历史模型基于结果继续推理。实际生产环境建议直接使用成熟的 Agent 框架或平台来管理这些细节循环上限、异常处理、并发控制都要考虑进去。3.4 多轮对话里的状态管理知识库还有一个常见痛点用户问完第一个问题后会继续追问那这个方案的成本是多少后来效果怎么样。这些省略了主语的追问单看当前这轮根本不知道该检索什么。解决思路是把历史对话摘要注入系统提示词。做法是每轮对话结束后让大模型把核心上下文压缩成一两句摘要下一轮检索时把问题和摘要一起作为查询词。这样既保留了上下文信息又不会让检索query被太多无关内容污染。我实践下来更倾向另一种做法把最近两轮完整对话直接拼进检索 query。代价是检索精度可能下降但实现简单。如果追求精确可以在两者之间做 AB 测试看哪种在你的数据上表现更好。4. 从零搭建一套个人知识库开源组件怎么拼原理讲完该说怎么落地了。这一部分我分享一套可以照着搭的方案全部基于开源组件数据完全本地。整个架构只有三个核心部分文档索引管线、检索服务、Agent 对话层。4.1 选型思路框架、向量库、模型的分工先说我最终用的组合再解释为什么这么选文档索引LlamaIndex unstructured 解析库向量库pgvector本机 PostgreSQLEmbeddingbge-m3重排bge-reranker-baseLLM本地部署的 Qwen2.5-32BAgent 编排自己写的轻量循环 Function Calling为什么不直接上 LangChain 或者 Dify原因是个人知识库规模不大LangChain 的抽象层和依赖太多出了问题不好排查Dify 功能全但也有学习成本。LlamaIndex 在文档索引这个环节做得非常细切分、节点关系、元数据处理都很顺手我只需要自己写检索和 Agent 层。本地大模型的选择上32B 量化模型在个人电脑上能跑速度慢一点但能接受。如果机器只有 16G 显存可以降级到 7B14B 的模型回答质量会差一些但结构不变。Embedding 和重排模型都很小CPU 跑也不成问题。4.2 索引端的实际代码与配置整个索引管线用 Python 写核心是三个函数加载文档、切分、向量化入库。from llama_index.core import VectorStoreIndex, Document, Settings from llama_index.core.node_parser import MarkdownNodeParser from llama_index.vector_stores.postgres import PGVectorStore from llama_index.embeddings.huggingface import HuggingFaceEmbedding import psycopg2 # 配置全局模型 Settings.embed_model HuggingFaceEmbedding(model_nameBAAI/bge-m3) Settings.llm None # 索引阶段不需要 LLM # 解析并切分 doc Document(textload_markdown(deep_work_notes.md)) parser MarkdownNodeParser() nodes parser.get_nodes_from_documents([doc]) # 每个节点补上来源元数据 for i, node in enumerate(nodes): node.metadata[source] deep_work_notes.md node.metadata[chunk_id] i # 连接 pgvector conn_str postgresql://user:passlocalhost:5432/kb vector_store PGVectorStore.from_params( databasekb, hostlocalhost, passwordpass, port5432, table_namenote_chunks, embed_dim1024 # bge-m3 是 1024 维 ) # 入库 index VectorStoreIndex(nodes, vector_storevector_store)这段代码里有几个容易出错的地方。第一个是 embed_dim 必须和模型输出维度一致bge-m3 是 1024如果用了 bge-small-zh512 维不改配置建表时就会报错。第二个是 MarkdownNodeParser 依赖文档里的标题层级如果源文档结构混乱比如没有二级标题切分结果会和预期差别很大这种情况建议换成递归字符切分器。4.3 检索服务与生成链路的代码组织检索服务要封装成独立接口方便 Agent 侧调用。这里我加了一个关键步骤先过向量召回再过重排最后才把结果交给大模型。from llama_index.core.retrievers import VectorIndexRetriever from llama_index.core.postprocessor import SentenceTransformerRerank from llama_index.core.query_engine import RetrieverQueryEngine retriever VectorIndexRetriever( indexindex, similarity_top_k50 ) reranker SentenceTransformerRerank( modelBAAI/bge-reranker-base, top_n5 ) query_engine RetrieverQueryEngine( retrieverretriever, node_postprocessors[reranker] ) # 单独提供检索逻辑Agent 调用时不用走生成 def search_knowledge_base(query: str, top_k: int 5): nodes retriever.retrieve(query) reranked reranker.postprocess_nodes(nodes, query_strquery) return format_nodes(reranked[:top_k])注意这里的策略召回 50 条重排后只留 5 条。召回阶段尽量放宽宁可多召回一些候选把精确筛选交给重排环节——向量召回是快而糙重排是慢而精两者结合才能兼顾效果和速度。生成阶段我直接把检索结果拼进提示词并强制要求模型标注引用来源。这样回答里就带上编号脚注用户能追溯每条信息出自哪篇文档。4.4 如果你想少写代码Dify / AnythingLLM 这类平台不是所有人都需要从零写代码。如果目标是快速给团队或自己搭一个知识库问答系统Dify 和 AnythingLLM 这类平台是更省事的选择。AnythingLLM 的优势是本地部署极其简单桌面版装完就能用支持把本地文件夹直接作为知识库源内置了切分和向量化流程适合非技术用户。Dify 更强在编排能力它把工作流、Agent、知识库串成可视化的流水线支持在知识库里设置检索策略、召回模式、重排模型也能接外部工具。网上经常有人遇到Dify 升级后无法保存知识库报 Internal Server Error这类问题我身边也有人碰到过。常见原因有两个一是升级过程中数据库迁移没执行成功导致知识库相关表结构不匹配二是向量数据库连接配置在升级后失效需要重新设置。遇到这种情况建议先备份数据再查看日志确认具体报错位置不要盲目回滚版本。5. 知识库好不好不能只靠感觉很多人搭完知识库试了几个问题觉得答得还行就收工了。但感觉还行很容易骗人因为你会不自觉地测试自己知道答案的问题而真实使用场景里检索是否精准、回答是否忠实都需要有评测依据。5.1 检索质量的三个指标召回率、命中率和 MRR检索环节推荐看三个指标Hit Rate命中率针对一批测试问题看正确答案对应文档是否出现在召回结果里。我们通常用 Recall5 来表示前 5 条召回中包含正确答案的比例。MRRMean Reciprocal Rank衡量正确答案在召回结果中的排名位置。比如正确答案排在第 1 位这个问题的得分就是 1/11.0排在第 3 位得分就是 1/3≈0.33。MRR 越高说明正确答案越靠前。假正例率召回结果中与问题不相关的片段占比。这个指标容易忽略但它直接影响最终回答质量。我整理了一批测试问题时用的办法是从自己的笔记里挑 50 个信息点每个信息点构造一个问题并记录答案所在的原文片段。这样就能自动评估 Hit Rate 和 MRR。5.2 回答质量怎么评忠实度与相关性检索指标只能反映有没有找到对的材料不能反映最终回答得好不好。回答质量需要两个维度忠实度Faithfulness回答里的每一条陈述是否都能在提供的检索材料里找到依据。这是最重要的指标它决定 AI 有没有胡编。最常见的评测方式是人工逐条核对也可以用大模型来做自动评判——把回答和参考材料一起发给一个更强的模型让它逐句判断是否有依据。相关性Relevancy回答是否对应用户的问题。有些回答很忠实但答非所问这通常是检索 query 理解出了问题或是 Agent 的推理方向偏了。实际操作中我的评测流程是先跑 30 个测试问题的完整问答对每个回答做忠实度和相关性打分1-5 分再把分数低的 case 挖出来反推问题是出在切分、检索、重排还是生成环节。这套流程看起来土但非常有效。5.3 我做测评时遇到的典型翻车现场举一个真实的例子。我在笔记库里存了几篇关于分布式事务方案选型的文章测试时问Seata 的 AT 模式适合什么场景。前 5 条召回结果里有两条是TCC 模式对比的段落只有一条和 AT 模式直接相关。重排之后AT 模式那篇排到了第 2但回答里仍然把 TCC 的适用场景混进来了。排查下来问题出在切分上那篇对比文章里有大段文字同时讨论 AT 和 TCC切分后一个块里混合了两个主题向量检索时这个块和Seata 场景的相似度很高排在前面导致模型看到的信息里混杂了错误主题。解决方案是把这类对比型长文档改为按段落语义切分让每个块只包含一个技术点的完整描述。改完之后同一测试的准确率明显提升。这个案例说明一个道理RAG 的问题经常嵌套在多层因素里检索不准不一定是向量库不行可能是上游切分埋了雷。所以排查时一定要从数据层一层层往上查而不是一上来就换模型、换框架。6. 一些只有踩过坑才会记住的经验整个项目从零到能日常使用我前前后后折腾了两三周。最后这部分分享几个最值得记住的实践经验都是文档里不会写、但真实影响使用体验的细节。元数据一定要从第一天就维护好。我第一次建知识库时偷懒没有给每个块存来源文档和章节路径。后来发现回答里出现信息错误时想溯源根本找不到原始位置只能重新索引。现在每个块至少保存 source、title、heading_path、timestamp 四个字段。不要觉得这是小事知识库越大元数据越值钱。检索的 query 不是原封不动的用户问题。用户经常用口语提问比如那个啥协议后来改了没直接把这种问题拿去检索效果很差。我的做法是在检索前加一步 query 改写让大模型把通俗问题改写成适合检索的关键词组合。这步对检索效果提升非常明显但要注意时延增加实测大概多 100~200ms能接受。本地模型和 API 模型的差距主要在生成环节不在检索环节。检索质量由切分、Embedding、重排决定这部分本地方案已经做得很好。真正有差距的是把检索结果组织成答案的能力——小模型经常遗漏材料里的关键细节或者没有按引用格式输出。如果预算有限优先换一个好一点的生成模型比换 Embedding 模型收益更大。关于 Agent 循环一定要设上限。我看到过不少新手写的 Agent 没有迭代次数限制一个简单问题把工具调了十几轮既浪费钱又拖慢响应。我的经验是个人知识库场景最多 3~5 轮迭代就够了超过这个次数说明 Agent 在兜圈子直接让它基于现有信息回答并说明不确定性比死循环强得多。最后一条是个心态问题不要追求一次做完。知识库是一个持续迭代的系统我到现在还在隔一段时间调整切分参数、补充测试问题。与其花大量时间追求完美架构不如先跑通一版让它在真实使用中暴露问题再针对性地改。很多问题你不实际用是永远发现不了的。这套 AI Agent 加 RAG 的组合目前已经成为我日常工作里最常用的工具。每天往里面丢几篇新笔记过段时间回看发现以前翻了半天找不到的资料现在一句话就能拿到出处那种体验真的很难用几句话形容。希望这篇文章能把你在搭建路上的一些弯给绕过去。