ARTICLE DETAIL

资讯详情

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

从零搭建个人知识库问答机器人:Agent+RAG实战与调优

从零搭建个人知识库问答机器人:Agent+RAG实战与调优 1. 为什么我要从零搭一个个人知识库问答机器人我手头攒了大概七八年的技术笔记、项目复盘、会议纪要和一些零散的读书摘录总量不算夸张几千篇 Markdown 加一堆 PDF。以前靠全文搜索凑合问题是搜出来的东西得自己一篇篇点开看真正想找当时那个接口超时最后是怎么绕过去的这种带上下文的问题搜索基本帮不上忙。大模型出来后我第一反应就是把笔记喂给它但很快撞上两个墙一是上下文塞不下二是它经常一本正经地编出我笔记里根本没有的结论。这就是我要做个人知识库问答机器人的直接动机。它本质上是一个Agent RAG的最小可用实践把私有文档切片、向量化、存进本地向量库用户提问时先做向量检索召回相关片段再交给大模型基于召回内容生成回答并且让模型自己决定要不要检索检索几次召回的够不够。整套东西跑在本地数据不出机器成本可控适合任何有私有资料、又想用自然语言直接问的人。这篇文章不是概念科普我会把选型理由、切片参数、检索策略、Agent 循环设计、踩过的坑全部摊开讲。你如果是刚接触RAG和Agent的新手照着能跑通如果你已经搭过基础版 RAG这里关于检索质量调优和Agent 工具编排的部分应该对你有用。我默认读者会一点 Python但不需要你懂向量数据库或大模型原理遇到概念我会用生活化的方式解释。先说清楚这个项目的能力边界免得你期待错位它擅长从我的资料里找答案并组织语言不擅长推理出资料里没有的结论。它是个检索增强的问答助手不是万能专家。把这条记在心里后面很多设计取舍你就能理解我为什么这么选。2. 整体架构一个最小可用的 Agent RAG 闭环2.1 数据流从文档到答案的完整链路整个系统我拆成两条链路离线索引链路和在线问答链路。离线链路负责把原始文档变成可检索的向量在线链路负责把用户问题变成答案。两条链路通过向量库这个中间仓库解耦这样我新增文档时不用动问答逻辑改问答策略时也不用重新索引。离线链路是这样的原始文档Markdown、PDF、TXT先经过加载器统一转成纯文本再经过切片器切成小块每块用嵌入模型转成一个高维向量最后连同原文和元数据一起写进向量库。这里每一步都有坑比如 PDF 解析出来的换行会把一句话劈成两半切片切太碎会丢失上下文这些后面细讲。在线链路是 Agent 的主场用户提问进来Agent 先判断这个问题需不需要查知识库比如你好就不需要需要的话调用检索工具拿到若干候选片段后评估相关性不够就换个查询词再检一次够了就把片段拼进提示词交给大模型生成答案最后附上引用来源。这个判断—检索—评估—再检索—生成的循环就是 Agent 区别于普通 RAG 的核心。提示很多人把 Agent 理解成会调用工具的模型这只说对一半。真正的 Agent 关键在于它能根据中间结果决定下一步动作。普通 RAG 是固定流程检索一次就生成Agent 是检索、看结果、不满意再检索、满意才生成。这个差别在复杂问题上非常明显。2.2 为什么用 Agent 而不是一条直线 RAG我一开始也是写的直线 RAG问题进来检索 top-k拼提示词生成。跑了两周发现三类问题它处理不了。第一类是问题太模糊比如上次那个性能问题怎么解决的直接拿这句话去检索向量相似度很低因为原文里根本不会出现上次那个这种词。第二类是需要多跳比如我对比过的那两个方案最后选了哪个为什么答案分散在两篇笔记里一次检索只能召回一篇。第三类是问题本身不需要知识库比如帮我把这段话润色一下直线 RAG 也会傻乎乎去检索浪费算力还可能引入无关内容干扰。Agent 化之后这三类问题分别有了对策模糊问题让 Agent 先改写查询再检索多跳问题让 Agent多次检索并累积证据无关问题让 Agent 直接跳过检索。代价是延迟变高、token 消耗变大所以我在提示词里明确告诉模型能一次检索解决就别检第二次在效果和成本之间找平衡。2.3 技术选型清单与取舍理由选型我遵循一个原则本地优先、依赖最少、可替换。个人项目最怕的是被某个云服务绑死哪天涨价或改接口就得重写。下面是我实际用的组合以及为什么这么选。环节我的选择备选选它的理由文档加载LangChain 的 loader 系列自己写解析格式覆盖全PDF/Word/Markdown 都有现成的文本切片RecursiveCharacterTextSplitter固定长度切按语义分隔符递归切尽量不切断句子嵌入模型本地 bge-small-zh云端 embedding API中文效果好本地跑不花钱隐私安全向量库ChromaFAISS / Milvus轻量、自带持久化、API 简单个人量级够用大模型本地部署的 7B 级模型云端大模型 API数据不出机器成本为零7B 做问答够用Agent 框架LangChain Agent手写循环工具调用、循环控制现成省得造轮子这里重点说嵌入模型和向量库。嵌入模型我选本地的小模型而不是云端 API核心原因是隐私——我的笔记里有项目细节和内部信息不想发到别人服务器。bge-small-zh 这个量级的模型在中文语义相似度上表现不错显存占用也小普通显卡甚至 CPU 都能跑。向量库选 Chroma 是因为它把存向量和存元数据合在一起还支持按元数据过滤比如我只想搜某个项目目录下的笔记加个 where 条件就行FAISS 做这个要自己额外维护映射麻烦。大模型这块我要多说一句。很多人一上来就想用最大的模型觉得效果一定最好。实测下来在 RAG 场景里检索质量对最终答案的影响远大于模型大小。你检索召回的都是垃圾再大的模型也只能基于垃圾编。反过来召回精准的话7B 模型组织语言完全够用。所以我的精力主要花在切片和检索调优上模型够用就行。3. 文档处理切片策略决定了检索质量的上限3.1 加载阶段最容易忽略的编码与格式问题加载看起来最简单其实坑最多。我第一批文档导进去检索效果差得离谱排查半天发现是 PDF 解析的问题。很多 PDF 是双栏排版解析器按从左到右的顺序读会把左栏第一行和右栏第一行拼在一起语义完全乱套。还有的 PDF 里表格被解析成一堆错位的数字检索到这种片段模型根本没法用。Markdown 相对干净但也有个隐蔽问题代码块和正文混在一起切。我的笔记里经常有大段代码如果切片时把代码和解释文字切到不同块里检索到代码块时模型不知道这段代码是干嘛的检索到解释时又看不到代码。我的处理办法是在切片前先做一次预处理把代码块用特殊标记包起来切片时尽量让代码和它上下的解释文字留在同一块。编码问题也遇到过。有些老文件是 GBK 编码直接按 UTF-8 读会乱码检索时向量全是噪声。我的做法是加载时统一做编码探测读进来先转成 UTF-8 再往下走。这一步花不了多少代码但能省掉后面大量莫名其妙的检索失败。注意加载阶段一定要抽样检查解析结果。我的习惯是随机抽 10 篇文档把解析出来的纯文本打印出来肉眼扫一遍。如果这一步就有明显错乱后面所有调优都是白费力气。垃圾进垃圾出这句话在 RAG 里是铁律。3.2 切片大小与重叠的实测参数切片是 RAG 里最需要调参的环节没有之一。切太大一个块里混了好几个主题检索时向量被平均掉相关性下降切太小一个完整的论述被切碎模型拿到半句话没法回答。我前后试了四五组参数最后定在chunk_size500、chunk_overlap80按中文字符算。为什么是 500我的笔记大多是段落式论述一个自然段大概 200 到 400 字500 字能覆盖一个完整段落加上下文衔接。为什么重叠 80因为切片是按分隔符递归切的难免在句子中间断开80 字的重叠能保证被切断的句子在相邻两块里都出现检索到任意一块都能看到完整语义。重叠太小比如 20起不到衔接作用太大比如 200会让相邻块高度重复检索时召回一堆几乎一样的片段浪费上下文窗口。这里有个反直觉的点切片大小不是越小越精确。我一度把 chunk_size 调到 200想着这样每块主题更单一检索应该更准。结果召回率反而下降了因为很多问题的答案需要一段完整的论述200 字装不下模型拿到的是残缺信息。后来我明白切片大小要和你希望模型一次看到多少上下文匹配而不是单纯追求主题纯度。chunk_sizechunk_overlap实测检索命中率问题20040偏低语义不完整答案被切断50080最高平衡点段落完整且主题集中800150中等单块混入多主题相关性被稀释1000200偏低块太大检索精度下降明显3.3 元数据设计让检索能按来源过滤光有向量还不够我给每个切片都挂了元数据来源文件名、所属目录、文档类型、创建时间、切片序号。这些字段平时不显眼但关键时刻能救命。举两个实际场景。第一个场景是限定检索范围。我有个工作目录和个人目录问工作相关问题时我不想召回个人笔记。有了目录元数据检索时加个过滤条件where{category: work}就行不用把整个库都搜一遍。第二个场景是结果溯源。模型给出答案后我要知道它是从哪篇笔记的哪一段得出的元数据里的文件名和切片序号让我能直接定位回原文方便核对。元数据还有个隐藏价值调试检索问题。当检索结果不理想时我会看召回的片段都来自哪些文件。如果全来自同一篇说明这篇文档在向量空间里太强势可能是它内容重复度高或者切片有问题如果来自一堆不相关的文件说明查询向量和文档向量没对齐可能是嵌入模型不适合我的领域。没有元数据这些排查根本无从下手。4. 向量检索召回不准时我按这个顺序排查4.1 嵌入模型选型与中文语义对齐嵌入模型的作用是把文本转成向量语义相近的文本向量距离近。选型时我踩过一个坑一开始随手用了个英文为主的模型结果中文检索效果惨不忍睹。原因是这个模型的中文语料训练不足它眼里的接口超时和请求失败向量距离很远但在我眼里这俩就是一回事。换成中文优化的 bge-small-zh 后检索质量肉眼可见地提升。选嵌入模型我的经验是优先选在你的语言和领域上有针对性训练的。中文场景就选中文模型代码多的场景可以看看有没有代码语料训练的版本。模型大小方面small 级别百兆左右在个人知识库这个量级完全够用base 或 large 会带来明显的推理延迟收益却不一定成正比。还有个细节是查询和文档要用同一个模型编码。这个听起来是废话但我见过有人文档用 A 模型编码、查询用 B 模型编码然后纳闷为什么检索全是噪声。不同模型的向量空间根本不通用混用等于随机检索。我在代码里把嵌入模型封装成一个单例加载和查询都走它从源头杜绝这个问题。4.2 相似度阈值与 top-k 的配合调法检索时有两个关键参数top-k召回几个片段和相似度阈值低于多少分就丢弃。这俩要配合调单看一个没意义。top-k 设太大会召回一堆不相关的片段把模型的上下文窗口塞满噪声设太小可能漏掉真正相关的片段。阈值设太高可能一个都召不回设太低等于没过滤。我的做法是先定阈值再定 top-k。具体来说先跑一批测试问题看相关片段的相似度分数大概落在什么区间把阈值定在这个区间的下沿。然后 top-k 设成阈值以上的片段通常有几个的两倍左右留点余量。我最后用的是阈值 0.35、top-k 5。这个组合下大部分问题能召回 2 到 4 个相关片段偶尔一个都没有时 Agent 会触发查询改写重试。这里要提醒一点相似度分数的绝对值没有统一标准不同嵌入模型、不同距离度量余弦、欧氏、点积算出来的分数范围都不一样。别照搬别人的阈值一定要在自己的数据上实测。我见过有人直接抄了个 0.8 的阈值结果他的模型分数普遍在 0.5 左右导致永远召不回任何东西还以为是代码写错了。4.3 检索效果差时的五步排查链路检索不准是 RAG 最常见的病我总结了一套排查顺序从最可能的原因开始查避免瞎调参。第一步看召回片段本身。把检索到的原文打印出来肉眼判断如果我是模型看到这些能回答吗。如果片段本身就答非所问问题在检索如果片段相关但模型答错问题在生成。这一步能快速定位问题在哪一环。第二步检查切片质量。如果召回片段是半句话或者混了多个主题回去调切片参数。我遇到过一次检索总是不准最后发现是某篇文档没有分段整篇被切成一个巨大的块向量被平均得毫无特征。第三步换查询词试试。把用户原问题手动改写成更接近文档表述的说法再检索一次。如果改写后能召回说明是查询和文档的表述差异问题需要在 Agent 里加查询改写。第四步看是不是嵌入模型不匹配。如果无论怎么改查询词都召不回可能是嵌入模型不适合你的领域。这时候可以试试换模型或者在你的数据上做一点微调。第五步检查元数据过滤。如果加了过滤条件后召回为空可能是过滤条件写错了或者元数据字段值不一致比如有的写work有的写Work。提示排查时一定要一次只改一个变量。我早期图省事一次把切片大小、阈值、top-k 全调了结果效果变好了也不知道是哪个起的作用下次遇到问题还是不会调。老老实实单变量测试虽然慢但能积累出对自己数据的直觉。5. Agent 循环设计让模型自己决定检索几次5.1 工具定义与提示词里的决策规则Agent 的能力来自它能调用工具而工具怎么定义、提示词怎么写直接决定它会不会用、用得对不对。我给它定义了三个工具search_knowledge_base检索知识库、rewrite_query改写查询、list_sources列出可用文档来源。工具描述我写得非常具体明确告诉模型每个工具什么时候用、参数是什么格式。提示词里我写了几条硬规则第一先判断问题是否需要查知识库闲聊和通用常识问题直接回答不要检索第二检索后先评估召回内容是否足够回答问题不够就改写查询再检一次但最多重试两次避免死循环第三回答必须基于召回内容召回里没有的信息不要编宁可说知识库里没有找到相关内容。这几条规则看着简单但每一条都是踩坑换来的。比如最多重试两次这条是因为我一开始没限制模型遇到召不回的问题会一直改写一直检token 烧得飞快还出不来结果。再比如必须基于召回内容是因为早期模型经常把召回片段和它自己的先验知识混在一起答案里一半是笔记内容一半是它编的很难分辨。5.2 多轮检索的触发条件与终止判断多轮检索是 Agent 的精髓但也是最容易失控的地方。我的触发条件是当召回片段的最高相似度低于阈值或者模型判断召回内容不足以回答问题时触发查询改写并重新检索。改写不是随便改我让模型基于原问题和已召回内容生成一个更可能命中文档表述的新查询。终止判断同样重要。除了最多两次这个硬限制我还加了个软条件如果第二次检索的召回内容和第一次高度重叠就停止。因为这说明知识库里确实没有相关信息再检也是重复劳动。这个判断我让模型自己做提示词里写如果新召回的片段和之前的基本重复说明知识库没有更多信息直接基于现有内容回答或说明找不到。实测下来大部分问题一次检索就够大概两成问题需要两次需要三次以上的极少。这个分布是合理的如果发现你的系统经常触发多轮可能是切片或嵌入模型有问题检索质量太差导致模型总是不满意。5.3 上下文拼装与引用来源的呈现召回片段怎么拼进提示词也有讲究。我的格式是每个片段前面加个编号和来源标记比如[片段1 | 来源: 性能优化笔记.md]然后正文。这样模型引用时能对应上用户也能看到答案的依据。片段之间用分隔线隔开避免模型把不同片段的边界搞混。引用来源的呈现我做了两层答案正文里用编号标注比如根据片段 2 的记录……答案末尾列出所有引用片段的来源文件。这样用户既能快速看到答案依据又能点回原文深挖。我试过不加引用结果模型偶尔会张冠李戴把 A 文档的内容说成 B 文档的加了引用后这种情况基本消失因为模型知道要为自己的引用负责。上下文长度也要控制。召回 5 个片段、每个 500 字加上提示词和对话历史很容易超过模型的上下文窗口。我的做法是按相似度排序优先放高分片段总长度接近窗口上限就截断。宁可少放几个片段也不要因为超长导致模型报错或者把前面的内容挤掉。6. 实测中暴露的问题与我的修复方案6.1 模型幻觉召回为空时它还在编这是我最头疼的问题。当知识库里确实没有相关内容时模型不是老实说找不到而是基于自己的先验知识编一个看起来很像的答案。比如我问我笔记里关于 XX 库的配置是怎么写的知识库里没有它却编出一段配置代码格式还挺像那么回事。修复方案分两层。第一层是提示词强化我明确写如果召回内容为空或不相关必须回答知识库中没有找到相关信息禁止使用你自己的知识补充。第二层是代码兜底在调用模型前先判断召回是否为空为空时直接返回固定话术根本不把问题交给模型。双保险之后幻觉基本被压住了。这里有个经验不要指望提示词能 100% 约束模型。提示词是软约束模型心情不好或者说采样随机性上来就可能不遵守。关键路径上一定要有代码层面的硬兜底把模型当成一个大概率听话但偶尔会飘的组件来设计。6.2 检索到重复内容导致答案啰嗦有段时间我发现答案特别啰嗦同一个意思翻来覆去说。排查发现是召回的片段里有大量重复内容——因为我的切片有重叠相邻块本来就有一部分重复如果一个问题同时命中了相邻的两块模型就会看到两段几乎一样的内容然后它会把两段都复述一遍。解决办法是召回后做去重。我用了简单的文本相似度去重如果两个片段的重叠比例超过某个阈值就只保留相似度分数高的那个。更彻底的办法是调小 chunk_overlap但重叠太小又会影响语义完整性所以我选择在检索后去重而不是牺牲切片质量。去重之后答案明显简洁了。6.3 长文档被切碎后语义断裂我有一篇很长的架构设计文档几万字讲的是一个系统的完整设计。切片后每个块只有 500 字单独看任何一块都只是局部细节检索到某一块时模型看不到整体背景回答容易片面。这个问题在长文档上特别明显。我的处理是给长文档的每个切片都加上文档级的摘要作为前缀。具体做法是先用模型给整篇文档生成一段 200 字左右的摘要然后每个切片在入库时都把这段摘要拼在前面。这样即使只召回一个局部切片模型也能从摘要里知道这段内容属于哪个大背景。代价是每个切片的向量会被摘要内容影响但实测下来利大于弊尤其是对长文档。6.4 并发提问时的资源争抢我有时候会同时问几个问题或者一边索引新文档一边提问这时候就出现资源争抢。本地模型推理是吃显存的两个请求同时进来要么排队要么爆显存。向量库写入和查询并发时也可能出现锁等待。我的方案是加一个简单的请求队列同一时刻只处理一个推理请求其他的排队等待。索引任务和问答任务分开跑索引时暂停问答或者用独立的进程。个人使用场景下并发不高一个队列足够。如果你要做多人用的版本那就得考虑模型服务的并发能力可能要用支持批处理的推理框架或者上多实例加负载均衡这是另一个量级的事了。7. 几个让我少走弯路的实操心得第一个心得先把检索调好再考虑换大模型。我见过太多人一上来就纠结用哪个模型结果检索召回的全是无关内容换再大的模型也救不回来。RAG 的效果上限由检索决定模型只是把召回内容组织成通顺的话。把精力花在切片、嵌入模型、检索参数上回报率远高于换模型。第二个心得建一个测试问题集。我从自己的笔记里挑了 30 个有明确答案的问题每次调整参数后都跑一遍看命中率变化。没有这个测试集调参全靠感觉今天觉得好了明天又觉得差了根本不知道是参数的问题还是运气。测试集不用大但要覆盖不同类型的问题事实型、对比型、多跳型、找不到答案型。第三个心得日志要记全。我每次问答都会记录原始问题、改写后的查询、召回的片段 ID 和分数、最终答案、耗时。出问题时翻日志能快速定位是哪一环出的问题。尤其是检索相关的调优没有日志根本没法分析。日志我建议存成结构化格式方便后续统计和筛选。第四个心得别追求一步到位。我第一版就是个能跑通的直线 RAG效果一般但能用。然后在这个基础上一点点加 Agent 循环、加查询改写、加去重、加摘要前缀。每加一个功能都单独验证效果确认有提升才保留。如果一开始就想搭个完美系统大概率会卡在某个细节上迟迟出不来可用版本。第五个心得注意 token 成本。本地模型虽然不花钱但推理慢token 多了等待时间长。云端模型按 token 计费Agent 多轮检索很容易把成本推高。我的做法是给每轮对话设个 token 预算超了就截断同时限制 Agent 的最大循环次数。效果和成本永远要平衡没有免费的午餐。最后说个我踩过的坑别在索引和查询之间用不同的文本预处理。我有次索引时对文本做了小写化和去标点查询时忘了做结果查询向量和文档向量对不齐检索全乱。预处理逻辑一定要封装成同一个函数索引和查询都调它从源头保证一致。这种低级错误一旦发生排查起来特别费劲因为代码看起来都没问题问题出在两边不一致上。这套东西我现在每天在用问笔记里的技术细节、翻旧项目的决策记录比全文搜索快太多了。它不是什么高深的技术就是把 RAG 和 Agent 的基本功做扎实切片切好、检索调准、提示词写清楚、兜底做扎实。剩下的就是不断用真实问题去磨磨到它真的能帮上你的忙。
返回列表