ARTICLE DETAIL

资讯详情

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

从RAG到Agent:增强版智能知识库的语义分块与混合检索实战

从RAG到Agent:增强版智能知识库的语义分块与混合检索实战 1. 从零拆解一个增强版智能知识库到底在解决什么问题做过RAG的人大概都有过这种体验demo跑通只要一个下午但真要把它丢进业务里给同事用问题就全冒出来了。用户问“上个月那笔订单为什么退款”基础版RAG给你捞回来三段产品说明书用户问“这个报错怎么处理”它把三年前已经废弃的接口文档排在了第一位。检索回来的东西不是不对而是“不对味”——这就是我动手做这个增强版智能知识库的直接原因。这个项目的核心目标很明确在标准RAG链路文档加载、切分、向量化、检索、生成之上补齐三块短板——语义分块让切出来的片段自带完整语义、混合检索加重排让召回结果真正贴合问题意图、Agent化编排让知识库不只是“问答”而是能根据问题类型自主决定检索策略、调用工具、多轮追问。说白了基础RAG是一个只会翻书的图书管理员增强版知识库是一个知道该翻哪本书、翻到第几页、还能顺手帮你把相关章节都标记出来的研究助理。适合谁来参考如果你已经跑通过一个最简RAG demo但被召回质量、分块断裂、多轮对话失忆这些问题卡住那这篇内容基本就是为你写的。如果你还没入门也没关系我会把每个环节的“为什么”讲清楚你照着搭一遍就能理解整套逻辑。技术栈上我选的是LangChain做编排骨架、向量数据库做语义索引、语义分块做预处理这三者是当前Agent知识库最主流也最稳的组合社区资料多、踩坑成本低。需要提前说明的是下面涉及的具体参数、分块阈值、检索权重都是我在实际语料上反复调出来的经验值不是放之四海皆准的真理。你的语料结构、问题分布、硬件条件不同数值一定要自己再跑一轮验证。我会把调参的思路和判断标准一并写出来这比直接抄一个数字有用得多。2. 整体架构设计与技术选型背后的取舍逻辑2.1 为什么是“Agent RAG”而不是纯RAG纯RAG的链路是线性的问题进来向量化检索Top-K拼进Prompt生成答案。这条链路最大的问题是它不会思考。用户问“对比A方案和B方案的成本差异”纯RAG会把A和B的文档各捞几段然后让模型自己去对比结果经常是模型把两边的数字张冠李戴。而Agent化的知识库会先判断这是一个对比类问题需要分别检索A和B再调用一个对比工具最后汇总。我把Agent引入的核心价值归纳为三点。第一是查询改写用户的口语化问题先被Agent改写成适合检索的多个子查询比如“退款为啥慢”会变成“退款处理时长”“退款到账周期”“退款延迟原因”三条并行检索。第二是工具调度遇到需要计算、需要查结构化数据、需要读图片OCR结果的问题Agent能自主决定调用哪个工具而不是硬塞给向量检索。第三是多轮记忆Agent维护对话状态第二轮追问“那B方案呢”时它知道你在延续上一个话题不会重新从零检索。2.2 向量数据库选型别一上来就上分布式向量数据库这块我踩过的坑最多。早期我图省事直接上了某分布式方案结果单机开发环境跑起来内存直接飙到8G索引构建慢得让人怀疑人生。后来我总结出一条选型原则按数据规模和并发量分档不要过度设计。数据规模推荐方案理由1万条以下内存向量库如FAISS本地索引零依赖启动快适合原型验证1万到100万条单机向量库如Chroma、Qdrant单机支持持久化元数据过滤完善运维简单100万条以上分布式向量库需要分片和副本但运维成本陡增我最终选的是单机Qdrant原因是它对元数据过滤的支持非常成熟。知识库场景里光靠向量相似度不够经常需要“只在某个部门文档里检索”“只检索最近半年的版本”这种带条件的检索Qdrant处理得很干净。另外它的payload设计灵活我可以把文档来源、更新时间、章节路径都塞进去检索时按需过滤。提示向量数据库的维度一定要和Embedding模型对齐。我见过有人用768维的模型建库后来换了个1024维的模型整个库得重建。选模型时就把维度定死别中途换。2.3 语义分块为什么固定长度切分是灾难固定长度切分比如每500字符一刀是最省事的做法也是最容易毁掉检索质量的做法。我拿一份技术文档做过对比测试固定切分下一个完整的“配置步骤”被从中间切断前半段在块A后半段在块B。用户问“怎么配置”检索命中块A但块A只有前半步模型生成的答案缺了关键的后半步用户照着做直接报错。语义分块的核心思想是按语义边界切而不是按字符数切。具体做法有好几种我采用的是“递归字符切分 语义相似度合并”的组合拳。先用递归切分器按段落、句子、标点逐级降级切分得到一个较细的片段集合然后计算相邻片段的Embedding相似度如果相似度高于阈值就合并低于阈值就断开。这样切出来的块每一块内部语义连贯块与块之间边界清晰。阈值怎么定我的经验是余弦相似度0.75到0.85之间比较合适。太低会把不相关的内容合并进来太高则切得太碎。这个值需要拿你的实际语料跑一批样本人工看一眼合并结果觉得“这几段确实该在一起”就对了。别嫌麻烦这一步的投入在后续检索质量上会加倍回报。2.4 LangChain在整条链路里的角色定位LangChain在这个项目里不是“必须”但它确实省了大量胶水代码。我用它主要做三件事文档加载器的统一接口、检索器的抽象层、Agent的编排框架。特别是检索器抽象它让我可以很方便地把向量检索、关键词检索、混合检索封装成统一接口Agent切换检索策略时不用改上层代码。不过LangChain也有它的脾气。版本迭代快有些API隔几个版本就变了网上搜到的教程可能对不上你装的版本。我的建议是锁定一个稳定版本把核心依赖写进requirements里别盲目追新。另外它的Agent执行链路有时候不够透明出问题时不好定位所以我在关键节点都加了日志把Agent的每一步决策、每次工具调用都打出来排查起来心里有数。3. 核心细节解析与实操要点3.1 文档预处理脏数据不清理后面全白搭很多人拿到文档直接往加载器里一丢就开始切分这是大忌。我处理过的语料里PDF有页眉页脚重复、Word有隐藏的修订标记、Markdown有大量无意义的空行和分隔线。这些噪声不清理会直接污染向量空间让检索结果变得莫名其妙。我的预处理流程是这样的。第一步做格式归一化把所有文档统一转成纯文本或Markdown去掉页眉页脚、页码、水印文字。第二步做噪声过滤用正则把连续空行、分隔线、乱码字符清掉。第三步做元数据抽取从文件名、文档路径、文档内标题里提取出“来源”“章节”“版本”这些信息存成结构化字段。第四步才是语义分块。元数据这块我要多强调一句。很多人只存一个“source”字段就完事了等到检索时想按时间过滤、按部门过滤就抓瞎。我建议至少存这几个字段source来源文件、section所属章节、updated_at更新时间、doc_type文档类型。这些字段在Qdrant里作为payload存储检索时可以精确过滤效果立竿见影。3.2 语义分块的参数调优实战语义分块听起来玄乎落地时其实就是几个参数在起作用。我拿一份约200页的产品手册做了组对比实验参数和结果如下。参数组合平均块长度检索命中率答案完整度固定500字符50062%一般经常缺步骤递归切分块大小80078071%较好偶有断裂语义合并阈值0.895084%好步骤完整语义合并阈值0.75120086%好但偶有冗余可以看到语义合并把命中率从62%拉到了84%以上提升非常明显。阈值0.75和0.8差别不大但0.75的块更长会带入一些冗余内容增加Token消耗。我最终选了0.8在完整度和成本之间取了个平衡。实操时有个细节要注意语义合并要设置块长度上限。如果不设上限遇到一份语义高度连贯的长文档可能合并出一个几千字符的巨块既超出Embedding模型的最大输入长度又让检索粒度变粗。我的做法是设一个1200字符的硬上限超过就强制断开保证每块都能被完整编码。3.3 混合检索向量检索不是万能的向量检索擅长语义匹配但有个致命弱点对精确关键词不敏感。用户问“ERR_4032这个错误码什么意思”向量检索可能给你捞回一堆讲权限错误的文档但就是漏掉了那个明确写着ERR_4032的条目。这时候就需要关键词检索BM25来兜底。混合检索的思路是把向量检索和BM25检索的结果融合。融合算法我用的是倒数排名融合RRF它的好处是不需要两路检索的分数在同一量纲上只看排名。公式很简单每个文档的最终得分等于它在各路检索中排名的倒数之和。排第一的贡献1/1排第二的贡献1/2以此类推。这样既保留了向量检索的语义能力又补上了关键词检索的精确性。权重方面我一般给向量检索0.6、BM25给0.4。如果你的语料里专业术语、错误码、产品型号特别多可以把BM25的权重提到0.5。这个权重不是拍脑袋定的我是拿一批标注好的问答对跑评测看哪个权重下MRR平均倒数排名最高就选哪个。3.4 重排把最相关的顶到最前面混合检索召回Top-20之后直接丢给模型生成其实有点浪费。因为这20条里真正和问题高度相关的可能只有3到5条其余的都是“沾边但不精准”的。重排模型的作用就是把这20条重新打分排序把最相关的顶到最前面只取Top-5送给生成模型。重排模型我用的是Cross-Encoder架构的方案它和双塔的Embedding模型不同是把问题和文档拼在一起过一遍模型能捕捉到更细粒度的交互信息精度明显更高。代价是速度慢所以只适合对少量候选做精排不适合全库检索。这个“粗排召回精排重排”的两阶段结构是当前工业界比较成熟的实践。注意重排模型和Embedding模型最好来自同一体系或经过兼容性验证。我试过混搭不同厂商的模型结果重排分数和检索分数对不上排序反而更乱了。4. 实操过程与核心环节实现4.1 环境搭建与依赖锁定先把环境搭起来。我用的是Python 3.10这个版本对主流库的兼容性最好。依赖管理用requirements.txt锁死版本避免不同机器上跑出不同结果。pip install langchain0.1.0 pip install langchain-community0.0.10 pip install qdrant-client1.7.0 pip install sentence-transformers2.2.2 pip install rank-bm250.2.2 pip install pypdf3.17.0这里我要提醒一句LangChain的版本一定要和langchain-community对齐否则会出现导入报错。我踩过一次坑langchain升到了最新版但community没升结果from langchain.retrievers import ...直接找不到模块排查了半天才发现是版本错配。Embedding模型我选的是中文场景下表现稳定的一个开源模型维度768。选它的理由是本地部署、无需联网、推理速度快适合知识库这种需要批量编码的场景。如果你对精度要求更高可以换成更大的模型但推理成本会上去自己权衡。4.2 文档加载与语义分块代码实现先看文档加载和预处理部分。我把加载、清洗、分块封装成一个流水线函数输入是文档目录输出是带元数据的片段列表。from langchain_community.document_loaders import PyPDFLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from sentence_transformers import SentenceTransformer import numpy as np def load_and_clean(file_path): if file_path.endswith(.pdf): loader PyPDFLoader(file_path) else: loader TextLoader(file_path, encodingutf-8) docs loader.load() # 清洗去掉多余空行和页码 for doc in docs: text doc.page_content text re.sub(r\n{3,}, \n\n, text) text re.sub(r^\s*\d\s*$, , text, flagsre.MULTILINE) doc.page_content text return docs def semantic_chunk(docs, model, threshold0.8, max_len1200): splitter RecursiveCharacterTextSplitter( chunk_size400, chunk_overlap50, separators[\n\n, \n, 。, , , , ] ) fine_chunks splitter.split_documents(docs) # 计算相邻片段相似度并合并 merged [] buffer fine_chunks[0] for i in range(1, len(fine_chunks)): emb1 model.encode(buffer.page_content) emb2 model.encode(fine_chunks[i].page_content) sim np.dot(emb1, emb2) / (np.linalg.norm(emb1) * np.linalg.norm(emb2)) if sim threshold and len(buffer.page_content) len(fine_chunks[i].page_content) max_len: buffer.page_content \n fine_chunks[i].page_content else: merged.append(buffer) buffer fine_chunks[i] merged.append(buffer) return merged这段代码的关键在semantic_chunk函数。先用递归切分器切成400字符左右的小片段然后逐个计算相邻片段的余弦相似度高于阈值就合并。max_len是硬上限防止合并出超长块。实测下来这个流程能把一份200页的手册从3000多个碎块压缩到800多个语义完整的块检索质量提升非常明显。4.3 向量入库与混合检索器构建分块完成后把片段编码入库。Qdrant的建库和写入代码如下。from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct client QdrantClient(path./qdrant_data) client.recreate_collection( collection_nameknowledge_base, vectors_configVectorParams(size768, distanceDistance.COSINE) ) points [] for idx, chunk in enumerate(merged_chunks): vector model.encode(chunk.page_content).tolist() points.append(PointStruct( ididx, vectorvector, payload{ text: chunk.page_content, source: chunk.metadata.get(source, ), section: chunk.metadata.get(section, ), updated_at: chunk.metadata.get(updated_at, ) } )) client.upsert(collection_nameknowledge_base, pointspoints)入库时把文本和元数据都塞进payload检索时可以直接取出来用不用再回查原始文档。这里有个性能细节批量upsert比逐条插入快得多我一般攒够100条提交一次写入速度能提升好几倍。混合检索器的构建稍微复杂一点需要同时维护向量检索和BM25检索两路。from rank_bm25 import BM25Okapi # 构建BM25索引 corpus [chunk.page_content for chunk in merged_chunks] tokenized [list(text) for text in corpus] # 中文按字切分英文可换分词器 bm25 BM25Okapi(tokenized) def hybrid_search(query, top_k20, vec_weight0.6, bm25_weight0.4): # 向量检索 query_vec model.encode(query).tolist() vec_results client.search( collection_nameknowledge_base, query_vectorquery_vec, limittop_k ) # BM25检索 bm25_scores bm25.get_scores(list(query)) bm25_top np.argsort(bm25_scores)[::-1][:top_k] # RRF融合 scores {} for rank, r in enumerate(vec_results): scores[r.id] scores.get(r.id, 0) vec_weight / (rank 1) for rank, idx in enumerate(bm25_top): scores[idx] scores.get(idx, 0) bm25_weight / (rank 1) sorted_ids sorted(scores, keyscores.get, reverseTrue)[:top_k] return sorted_idsRRF融合的精髓在于只看排名不看分数这样两路检索的分数尺度不一致也不影响融合结果。中文的BM25分词我用了最简单的按字切分效果已经够用。如果你的语料里英文和专业术语多建议换成jieba或专门的分词器召回会更准。4.4 Agent编排让知识库学会自己决定怎么查前面都是“检索层”的活Agent层负责的是“决策”。我用LangChain的Agent框架把混合检索封装成一个工具再配上查询改写工具和对话记忆让Agent根据问题类型自主调度。from langchain.agents import Tool, AgentExecutor, create_react_agent from langchain.memory import ConversationBufferMemory tools [ Tool( nameknowledge_search, funclambda q: hybrid_search(q), description在知识库中检索相关信息输入是检索查询语句 ), Tool( namequery_rewrite, funclambda q: rewrite_query(q), description把口语化问题改写成多个适合检索的子查询 ) ] memory ConversationBufferMemory(memory_keychat_history, return_messagesTrue) agent create_react_agent(llm, tools, prompt) executor AgentExecutor(agentagent, toolstools, memorymemory, verboseTrue)verboseTrue这个参数我强烈建议打开它会把Agent的每一步思考、每次工具调用都打印出来。调试阶段这是最有效的排查手段你能清楚看到Agent是“怎么想的”哪一步决策出了问题一目了然。等稳定了再关掉。查询改写工具的实现思路是让模型把用户问题拆成多个检索视角。比如“新员工入职要准备什么”改写成“新员工入职材料清单”“入职流程步骤”“入职当天注意事项”三条分别检索后再合并结果。这一步能显著提升复杂问题的召回覆盖率。5. 常见问题与排查技巧实录5.1 检索结果不相关怎么一步步定位这是最高频的问题。我的排查顺序是固定的先看分块再看Embedding最后看检索策略。第一步把检索命中的原始片段打印出来人工读一遍。如果片段本身就是断裂的、语义不完整的那问题出在分块环节回去调语义合并的阈值。第二步如果片段完整但和问题不相关检查Embedding模型是否适合你的语料。我遇到过用通用模型编码法律条文效果很差换成法律领域微调过的模型后立刻好转。第三步如果前两步都没问题那就是检索策略的事试试调高BM25权重或者加一层重排。下面这张表是我整理的常见症状和对应处理可以直接对照排查。症状可能原因处理方式召回内容沾边但不精准纯向量检索对关键词不敏感开启混合检索提高BM25权重答案缺步骤、不完整分块断裂调低语义合并阈值增大块长度相似问题召回结果差异大Embedding模型不稳定换模型或对查询做改写检索慢候选集太大先元数据过滤缩小范围再检索多轮对话失忆缺少对话记忆接入ConversationBufferMemory5.2 语义分块把不该合并的合在一起了语义相似度高不代表逻辑上该合并。我遇到过两个章节讲的是同一个概念的不同侧面语义相似度很高被合并成一块结果检索时用户问其中一个侧面另一侧面的内容也混进来干扰生成。解决办法是在合并逻辑里加一个章节边界判断。如果两个片段来自不同的章节元数据里的section字段不同即使相似度再高也不合并。这个规则很简单但效果立竿见影。另外可以给合并加一个“最大合并片段数”限制比如最多合并3个片段防止滚雪球式合并。5.3 Agent陷入死循环或者反复调用同一个工具Agent化之后最常见的新问题是“它想太多”。我遇到过Agent为了回答一个简单问题反复调用检索工具五六次每次都微调查询词最后超时。根因是提示词里没有给Agent设定“何时停止”的规则。我的处理方式是在系统提示词里明确写清楚检索工具最多调用3次如果3次后仍无法确定答案就直接基于已有信息生成并说明不确定性。另外给Agent设置最大迭代次数max_iterations硬性截断防止无限循环。这个参数在AgentExecutor里可以配我一般设5到8看问题复杂度。5.4 并发上来之后响应变慢单用户测试时一切正常多用户同时用就卡。瓶颈通常在两处Embedding编码和向量检索。Embedding编码是CPU密集型的并发高了会排队。我的做法是把编码服务单独拆出来用批处理的方式编码多个请求攒一批一起编码吞吐量能提升好几倍。向量检索这边Qdrant单机在百万级数据下并发几十路问题不大如果还不够就上副本。提示别在请求链路里做全量重排。重排只对Top-20做如果对全库做重排延迟会高到不可接受。粗排召回的范围要控制住。5.5 知识库更新后检索结果没变化这是增量更新没做对。很多人更新文档后只重新入库了新文档但旧的向量还在库里检索时新旧混在一起。正确做法是给每个文档一个唯一标识更新时先按标识删除旧向量再插入新向量。Qdrant支持按payload条件删除用client.delete配合过滤条件就能实现。另外BM25索引也要同步重建。BM25是基于全量语料统计的新增文档后如果不重建新词就进不了索引。我的做法是维护一个文档版本号每次更新后触发一次索引重建任务虽然有点重但保证了检索一致性。6. 几个我踩过之后才明白的经验先说Embedding模型的选择。我一开始迷信“维度越高越好”用了个1024维的大模型结果编码速度慢、存储占用大检索精度相比768维的模型并没有质的提升。后来才明白Embedding质量取决于训练语料和你的业务语料是否匹配而不是单纯看维度。选模型时先拿一批真实问题做小规模评测看召回率别只看参数。再说重排的投入产出比。重排确实能提升精度但它增加了一次模型推理延迟会上去。我的经验是如果你的召回Top-20里相关文档本来就在前5重排的收益有限如果相关文档经常排在10名开外那重排的价值就很大。所以要不要上重排先看你的粗排召回质量别盲目堆模块。最后说Agent的提示词。Agent的表现七成靠提示词。我写提示词的心得是把工具的能力边界、调用时机、停止条件都写清楚用具体的例子说明什么情况该调什么工具。含糊的提示词会让Agent反复试探既慢又不准。我一般会写一段“决策示例”把两三个典型问题的处理路径写进去Agent照着模仿效果稳定很多。这套增强版知识库我前后迭代了大概两个月从最初的基础RAG一路补到现在的Agent编排每一步都是被实际问题逼出来的。如果你也在做类似的东西我的建议是别一上来就追求大而全先把分块和检索这两块打磨好这两块决定了知识库的下限。Agent编排是锦上添花前提是检索层本身靠谱。等你把检索质量调到自己满意了再往上加Agent的决策能力整个系统的表现会稳得多。
返回列表