ARTICLE DETAIL

资讯详情

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

从传统RAG到Agent智能知识库:混合检索、记忆管理与工程落地实践

从传统RAG到Agent智能知识库:混合检索、记忆管理与工程落地实践 从第一篇搭 Agent 骨架到第二篇给 Agent 接上工具调用这一篇我来讲点更实用的东西把知识库从“搜索框”升级成“会自己思考的 Agent”。先说一个让我很郁闷的场景。之前帮团队做过一个传统 RAG 知识库原理很简单——把文档切块、向量化用户问一句系统把最相似的几个片段拉出来拼接成答案。看起来没问题对吧但实际用起来就翻车用户问“报销单忘贴了怎么办”系统检索出来的片段是“报销单粘贴处不得涂改”因为向量相似度够高但压根没回答忘贴怎么补救。问题出在哪检索只解决了“哪里可能有答案”没解决“这个答案是否真正覆盖用户的问题”。而 Agent 增强版要解决的正是这一层缺失的推理与判断。这篇是我 Agent 实践系列的第三篇我会把增强版智能知识库的完整思路拆开讲清楚整体架构怎么设计、检索层怎么做混合召回、Agent 怎么编排多轮决策、记忆和引用怎么管理、并发和工程化有哪些坑。适合两类人看一类是已经被传统 RAG 搞到怀疑人生的开发者另一类是准备从零搭 Agent 知识库但不确定架构怎么选的产品或技术负责人这篇文章都能帮你把“增强”两个字落到实处。1. 内容整体设计与思路拆解1.1 从“搜索框”到“Agent 决策器”的转变传统的智能知识库本质还是一个搜索引擎的变形query 进top-k 片段出。它最大的问题不是检索技术差而是语义理解和答案组织之间缺一个“决策层”。向量检索告诉你哪些文本跟问题像但它不告诉你这些文本够不够、准不准、要不要再问一轮、要不要换一种检索方式。增强版智能知识库的核心思路是把 RAG 从“单轮检索”升级为“Agent 工作流”。我用一个生活化类比帮助你理解传统 RAG 像你去图书馆问管理员“有没有讲某件事的书”管理员甩给你三本书名就走人Agent 知识库像你雇了一个研究助理他会先判断你问的是什么类型的问题去资料室翻一遍发现某一本更全面再核对另一本的交叉信源最后写一页带出处的摘要给你如果信息不够他还会反问你一句“你指的是哪个场景”。在技术层面这个“研究助理”就是 Agent。它需要具备几项能力判定问题意图决定走哪条检索路径调用多个工具向量检索、关键词检索、数据库查询时决定调用顺序和次数对检索结果做二次筛选与重组不轻信单一片段保留多轮对话中的关键信息避免用户重复描述答案必须带可追溯的引用来源这些能力叠加在一起才配得上“增强版”三个字。1.2 增强版知识库的三个核心模块我设计系统时把整体拆成三个模块检索层、编排层、记忆层。三者各司其职但通过一个统一的 Agent 内核驱动。检索层负责“找材料”但不是直接一条向量检索打到死而是提供多种召回通道包括语义向量检索、关键词精确匹配BM25、以及结构化数据的数据库查询。我自己的经验是纯向量检索对专业名词、人名、编号这类信息非常不敏感BM25 恰好能补上这一环。后面我会详细讲混合检索的调参细节。编排层是最关键的一层它接收用户 query结合记忆层的上下文状态决定当前动作是“检索、追问、总结还是结束”。这一层解决的是“LLM 怎么决定下一步做什么”的问题。我用的是 ReAct 模式即推理穿插动作每一轮循环里模型观察当前状态思考下一步然后执行动作再根据动作结果决定继续还是给终答。记忆层则承担两个职责短期记忆保存当前会话里用户刚提供过的关键信息比如“我说的报销是指差旅报销不是采购报销”长期记忆则沉淀跨会话的用户偏好比如“用户是财务部员工回答要偏向财务制度视角”。记忆不是简单把聊天记录堆进 prompt 就能解决的后面会讲记忆压缩和分层的实用方案。1.3 为什么我放弃了固定流水线转而采用 Agent 架构可能有人会问这些事情我用 if-else 写一个固定流水线不也能做吗比如先向量检索如果置信度低再换 BM25最后拼答案。我试过这种方案维护两个星期就撑不住了。固定流水线的核心问题是“规则写不完”。有一次用户问“上次说的那个预算表模板在哪个页面”这个 query 里有“上次”这个指代词单纯看当前语句不可能知道是指哪次。流水线问候语接住就断而 Agent 可以实现递归查找先从记忆中索引“上次”这个指代对应的文档名再拿文档名去检索。同样的场景固定流程需要针对每一种指代、歧义、缺信息写一堆特判而 Agent 模式让模型在上下文中自闭环地推理代码量反而更少、泛化性更强。另一个原因是为了扩展性。固定流水线加一种新数据源就要改主流程代码侵入性很高。Agent 架构下我只需要给 Agent 新增一个工具并写清楚工具的输入输出描述编排逻辑完全不用动模型自己会学会什么时候用这个工具。所以如果你已经确定只有“问一个问题召回固定文档生成答案”这一个简单场景固定流水线够用但只要是开放式问答、多轮对话、多知识库路由Agent 架构是更省心的选择。2. 核心细节解析与实操要点2.1 文档切分与向量化chunk size 不是玄学是数学文档切分是知识库效果的地基我见过太多项目在上面踩坑。切得太粗比如一段 3000 字塞进一个向量检索召回后把无关信息掺进答案幻觉率直线上升切得太细比如一句话一个 chunk语义不完整向量表达的信息量不够召回率又下降。我的实操要领是“按语义块切而不是按字数硬切”。对 Markdown 类文档我按标题层级#、##、###切分对 PDF按段落边界切分段落过长时再利用句号、分号做二次拆分。目标是把每个 chunk 控制在 300600 字之间并且带上一个“标题链路前缀”比如[财务管理 报销制度 报销单粘贴规范] 报销单粘贴处不得涂改……这个标题链路很关键。向量检索时它帮助模型定位语义上下文生成答案时它又自然成为引用的出处。如果你只是粗暴地按字数把一个大文档切成无数块每一块之间互相没有层级关系召回结果就是一堆“孤岛文本”答案的拼接感会非常重。切分之后做向量化时我建议用 bge-m3 这类中英文混合向量模型embedding 维度 768对比 OpenAI 的 text-embedding-3-small维度 1536召回效果接近但资源开销小一半以上。批量向量化时我习惯把 batch size 调到 32超出后显存占用容易吃紧速度反而下降。2.2 混合检索与重排让“相关”更精确增强版知识库跟朴素 RAG 拉开差距的第二个关键点是混合检索。什么叫混合就是同时跑出来条路再做决策融合。向量检索的优点是语义泛化能力强“忘贴报销单怎么处理”和“报销单粘贴要求”虽然字面不重叠但向量空间距离近。缺点是它对精确字符不敏感用户问“预算表在第三章还是第五章”向量检索很难区分章节数字差异。这时 BM25 关键词检索的优势就体现出来了它按词频和逆文档频率打分字面匹配精度高。我当前的生产配置是“向量检索召回 50 条BM25 召回 30 条合并后进 RRFReciprocal Rank Fusion重排取 Top 10 作为最终上下文”。RRF 的公式不复杂每个文档在某个排序列表中的得分是 1 / (k rank)k 是一个平滑常数一般取 60。这么做的思路其实很朴素——两个列表都靠前的文档综合相关性往往更高。这个方案的优点是不用调权重向量分和 BM25 分直接量纲不同硬加权反而不稳定。重排是另一个提升点用小模型比如 bge-reranker-base对合并后的 Top 20 逐条跟用户 query 算相关性重新打分后取 Top 5。重排这一步单次要跑几百毫秒到一秒但对答案精度的提升非常直观。如果预算有限至少保留 RRF省掉重排也能有约 10% 的相关性提升。2.3 Agent 路由与多知识库设计当知识库从单个变成多个新问题就来了用户问“上次月度运营数据是多少”Agent 应该去哪个库查我的方案是维护一个“知识库注册表”类似路由表。每个知识库注册时填写名称、简介、适用问题示例。Agent 在收到 query 之后不是直接检索而是先走一步路由判断模型根据注册表描述决定使用哪个工具。举个例子我系统里有“财务制度库”、“研发文档库”、“客户案例库”三个知识库。用户问“研发团队的报销标准是什么”Agent 路由会先判定“研发文档库”和“财务制度库”都相关于是并行调用两个工具的检索接口。而用户只问“报销标准是什么”则只路由到“财务制度库”。这个路由动作本质上是模型的一次函数调用你只需要把一个知识库封装成一个工具函数它的 system prompt 里配备好注册表信息。这里要特别注意一个坑不要把知识库工具的参数只写一个“query”。我当时加了一个参数叫“filters”专门用于传递结构化筛选条件比如 “filters: {department: “研发部”}”。有了这个参数Agent 就能把问题里抽取到的限定条件结构化检索端的过滤就能做得更精细matching 的准确性完全不一样。后续优化时如果你想让 Agent 反问澄清而不是猜错库只需在路由工具描述里加一句“如果无法明确该用哪个库调用 clarify 工具向用户询问”模型就能学会。2.4 Agent 记忆机制短期记忆与长期记忆的分层管理增强版知识库如果没有记忆多轮对话就会非常“傻”。用户第一句说“报销单忘贴发票了怎么办”你回答完流程第二句问“那电子发票呢”如果模型把上一轮语境丢了它可能不知道“那”指代的什么。我在项目里把记忆做了两层。短期记忆用的是滑动窗口加摘要最近 4 轮完整对话直接存入 prompt超过 4 轮的部分用一个小模型异步生成摘要像“用户询问了报销单忘贴发票的处理流程随后追问电子发票是否适用”然后把摘要放进系统提示里。这个设计让 Agent 既不丢最近细节又不至于把上下文撑爆。长期记忆我用的是“用户画像键值库”格式为{ user_id: 10001, preferences: {部门: 财务部}, facts: [用户使用报销系统版本为 v2.4] }每次对话结束后一个单独的“记忆提取 Agent”跑一遍这轮对话抽取可长期沉淀的事实更新到键值库里。下次该用户再提问系统先把用户画像注入上下文。这个方法在一两个月内就能让回答的个性化程度明显上升。3. 实操过程与核心环节实现3.1 环境准备与依赖安装开始之前先说我用的环境Python 3.10 以上LangChain 0.2 或直接裸写 Agent 逻辑都行我当前生产环境是用 LangGraph 做编排的。向量库我用 Milvus 的简化版Milvus Lite开发生产部署换成了 qdrant因为 qdrant 支持 payload 过滤做得更灵活。用到的核心依赖如下pip install langgraph langchain langchain-openai langchain-community qdrant-client bge-reranker sentence-transformers bm25s这里提醒一句bm25s 比 rank_bm25 快很多尤其是在文档量超过 10 万条时差距能到一个数量级。你如果只是 demo用 rank_bm25 图个简单也行但生产就别考虑了。3.2 构建知识库索引两步走第一步是文档切分。我写了一个 MarkdownHeaderSplitter按标题层级切然后设置一个最大块长度from langchain.text_splitter import MarkdownHeaderTextSplitter splitter MarkdownHeaderTextSplitter( headers_to_split_on[(#, H1), (##, H2), (###, H3)] ) sections splitter.split_text(md_text) final_chunks [] for section in sections: prefix .join(section.metadata.values()) content f[{prefix}] {section.page_content} if len(content) 600: # 超长时按句号二次切分这里省略细节 final_chunks.extend(split_long_text(content, max_len600)) else: final_chunks.append(content)第二步是向量化并写入向量库。我建议在写入时就把 metadata 塞全比如 source来源文档名、page页码、title_chain标题链路因为重排阶段和引用生成阶段都要用这些字段from qdrant_client import QdrantClient from qdrant_client.models import PointStruct, VectorParams, Distance client QdrantClient(path./qdrant_data) client.create_collection( collection_nameknowledge_base, vectors_configVectorParams(size768, distanceDistance.COSINE), ) points [] for idx, chunk in enumerate(final_chunks): embedding embed_model.encode(chunk, normalize_embeddingsTrue) points.append(PointStruct( ididx, vectorembedding.tolist(), payload{text: chunk, source: source_name, page: page_num, title_chain: prefix} )) client.upsert(collection_nameknowledge_base, pointspoints)索引构建这一步没有太多技巧核心就是把 metadata 写完整。后面如果发现答案常常丢失出处基本都是这一步 payload 没塞好。3.3 定义知识库检索工具供 Agent 调用接下来是 Agent 的“手”——把检索能力封装成工具。我定义了三个工具向量检索、BM25 检索、数据库查询。每个工具函数都有 name 和 descriptiondescription 写得越具体模型越能正确使用。比如向量检索工具def vector_search(query: str, filters: dict {}, top_k: int 10) - list: 基于语义向量检索知识库片段。适合模糊查询、自然语言问题。 Args: query: 问题文本 filters: 过滤条件如 {department: 研发部} top_k: 返回数量 qvec embed_model.encode(query, normalize_embeddingsTrue) hits client.query_points( collection_nameknowledge_base, queryqvec.tolist(), query_filterbuild_filter(filters), limittop_k, ).points return [{text: p.payload[text], source: p.payload[source], score: p.score} for p in hits]BM25 工具我直接加载预构建的索引检索后把结果按同样格式返回为的是让下游重排阶段能统一处理。这里有一个非常重要的细节工具返回结果里不要只回文本一定要带上源信息source、page。因为最终生成回答时Agent 需要根据这些信息给用户展示“出自哪份文档”没有源信息的引用就是耍流氓。3.4 编排主循环状态机 ReAct 循环我用 LangGraph 搭状态机。核心状态结构定义如下class AgentState(TypedDict): messages: list # 对话历史 query: str # 当前问题 retrieved_context: list # 检索到的候选片段 final_answer: str # 最终回答可选 followup_question: str # 追问内容可选 need_more_info: bool # 是否还需要检索主循环是 ReAct 模式的 LangGraph 图。节点包括路由与意图识别节点、检索工具调用节点、结果评估节点、回答生成节点。路由节点让 LLM 输出结构化 JSON选择调用哪个工具。结果评估节点判断当前检索到的材料是否足够回答问题不足则决定是继续检索还是向用户追问。回答生成节点把材料整理成最终答案。伪代码的关键部分如下def route_node(state: AgentState) - AgentState: prompt f判断这个问题该用哪个工具{state[query]} 可用工具vector_search, bm25_search, db_query 如果信息不足无法判断请调用 clarify 工具。 输出 JSON: {{tool: tool_name, reason: ...}} decision llm.complete(prompt, return_jsonTrue) state[selected_tool] decision[tool] return state def action_node(state: AgentState) - AgentState: if state[selected_tool] vector_search: ctx vector_search(state[query], filtersextract_filters(state[query]), top_k10) state[retrieved_context].extend(ctx) # 类似处理 bm25_search return state def evaluate_node(state: AgentState) - AgentState: prompt f以下是检索到的候选片段是否足够回答用户问题如果不足是信息不相关还是缺少关键细节 {state[retrieved_context]} verdict llm.complete(prompt, return_jsonTrue) if verdict[enough] False and verdict[need_clarify] True: state[need_more_info] True state[followup_question] verdict[question] return state把“需要追问”和“需要继续检索”作为两条边接到返回的节点LangGraph 就能实现循环直到模型确认信息足够再进入回答节点。3.5 最终回答生成与引用落地回答生成这一步决定了用户体感的“高级感”。我的做法是把检索到的 Top 5 片段按来源分组然后用编号引用式回答。Prompt 模板是这样你是知识库助手基于以下资料回答问题回答要结构化标注每条信息的来源编号 [1][2]。 资料 [1] 来源财务制度手册p12“报销单粘贴处不得涂改……” [2] 来源报销操作指南p4“若发票丢失需提交遗失声明……”关键要求是模型必须使用资料原文作为事实依据不能自行发挥并且每个输出段落必须挂对应的引用编号。为了强制模型遵守格式我在 prompt 里加了“如果资料中没有相关内容直接回答‘资料中没有覆盖该问题’禁止编造”。实测下来加这个约束可以将幻觉发生率降一半左右。虽然偶尔模型还是会给出一条不太相关的来源但配合重排和过滤总体可控。4. 常见问题与排查技巧实录4.1 检索总能搜到文档但答案仍然答不到点子上这个问题最隐蔽。表面看是生成模型的问题实际是重排阶段的缺陷。具体表现为Top 5 片段里混入了 2 条相似但不含关键信息的片段模型被带偏了。我的排查思路是“逐层看”。先把重排后的 Top 5 片段全部打印出来人工判断这些片段是否真的回答了用户 query如果 5 条里只有 2 条是精准的那就是重排不够准。此时可以加大重排前召回候选量比如 Top 20 重排取 5或者调整 RRF 的 k 值让排序靠前的文档权重更突出。我通常把 k 从 60 调到 30效果更激进。另一个有用的手段是“正则约束”当用户的问题涉及具体编号、日期、型号时手动在 query 后面追加这些实体强制 BM25 检索时做精确匹配。比如“查一下 CM-1003 产品参数”BM25 检索时把 CM-1003 作为一个整体短语强制匹配可显著提升命中率。4.2 多轮对话里指代消解失败上一轮的“它”被当成新问题这是 Agent 知识库最容易翻车的地方。用户上一轮提到“报销单忘贴发票”下一轮问“那电子发票呢”系统如果直接拿“那电子发票呢”去检索什么也搜不到。我用的解决方案是“查询改写”节点放在路由节点之前。用一个轻量模型把包含指代词的消息改写为独立完整 query。例如用户历史消息报销单忘贴发票了怎么办 当前消息那电子发票呢 改写后报销单忘贴发票电子发票可以作为凭证吗改写后再走检索流程。这个节点增加了一次 LLM 调用大概多花 300 毫秒但解决了一类非常影响体验的问题。如果你用的底层模型支持历史消息拼接也可以尝试让 routing 节点带着历史做判断但实测下来单独分一个改写节点更稳定因为路由节点能看到完整上下文后决策更清晰。4.3 Agent 在循环里出不来反复检索浪费 token我遇到过 Agent 在“不够信息→再检索→还是不够→再检索”的死循环。最大原因是“检索结果的评估 prompt”写得不够严格。模型找不到绝对答案就一直想“再找找”。两个对策。第一给评估节点加一个 max_retrieval_rounds 计数当前检索轮数达到 2 次后强制让 Agent 进入“针对缺失信息向用户提问”的节点而不是继续检索——现实情况里继续检索的边际收益非常低。第二在评估 prompt 里写得再死一点当候选片段包含以下情况之一即可认为“已足够” 1. 直接回答了用户问题 2. 提供了可以指导下一步操作的明确信息 3. 有 2 条以上来自不同来源的交叉佐证这样模型的判断会更快收敛而不是陷入“再确认一下”的过滤。4.4 并发一上来系统就垮检索接口变成了性能瓶颈Agent 式知识库对并发的压力比传统 RAG 大因为多了路由节点、评估节点、改写节点后端模型调用量翻了 3 倍以上。如果只是 demo并发 10 个用户可能就把 LLM API 打满。生产环境我的做法是把在线服务拆成两层一层是同步 API 接入层负责接收请求、调 Agent 编排。另一层是异步任务池把路由节点和评估节点的 LLM 调用丢到一个独立的高吞吐模型端点比如按 TPM 配额更充足的专用 channel避免跟用户真实回答生成的调用抢资源。向量检索本身到不是瓶颈qdrant 单机并发跑几百 QPS 没问题瓶颈主要在 LLM API 的限流上。策略是给 Agent 的每一步调用做“超时 重试”超时设 15 秒重试 2 次接熔断否则用户会直接看到 504。另外缓存值得认真做。对完全相同的 query我在 Redis 里做了 5 分钟的 TTL 缓存KV 结构是 query_hash 到 final_answer。对相同意图但措辞不同的 query则没做语义缓存因为误命中率不可控容易答非所问。5. 增强版知识库的工程落地细节5.1 从 demo 到生产的三个必改项如果是做 demo上面搭建的思路已经够用。但上了生产环境有几个问题一定要处理。第一是 Prompt 版本管理。Agent 的每一步路由、评估、改写、回答都是一个独立的 prompt线上改 prompt 时的回归风险比改代码还高。我现在把所有 prompt 模板存成单独的文件纳入 Git 管理每次修改都走 MR 流程并且标注 prompt 版本号。甚至在日志层面把每一步的 prompt 版本号打出来排查问题时能直接定位“是这个 prompt 版本导致的问题”。第二是检索结果的“日志化”。传统 RAG 只需要记录“用户问了什么、答了什么”Agent 版需要记录完整的推理轨迹路由决策、调用工具、检索片段、评估结论、最终回答。这些日志是排查问题的宝藏也是后续做评测集的最佳素材。我现在每条请求都会生成一份 JSON trace存到日志系统里线上疑难问题基本都是靠翻 trace 解决的。第三是知识库更新策略。文档更新后不能只对增量文档建索引。我看过一个事故旧文档里的一个错误数据没有被删除新旧两版数据互相矛盾Agent 把两个都检索出来给出了前后打架的答案。后来改成“版本化发布”同一个 source 文档每次发布生成新版本 ID向量库里的旧版本通过 payload filter 标记为 archived检索时强制排除 archived 文档。这个机制虽然简单但避免了一大类数据混乱。5.2 评测不是事后工作是 Agent 开发的伴随工作开发 Agent 知识库最怕“凭感觉调参”。我建议从第一天就开始积累评测集。把知识库里最典型的 200 个问题、每个问题配一个理想答案和检索期望片段做成离线评测的话每次改配置后直接跑一遍看准确率变化。评测集不需要复杂。一条评测数据可以是{ query: 报销单忘贴发票怎么办, expected_sources: [报销操作指南_p4, 财务制度手册_p12], ideal_answer: 先提交遗失说明再补开发票作为附件流程和注意事项…… }评测时跑三个指标答案相关性人工打分的抽测、引用准确率模型给的来源是否在 expected_sources 范围内、端到端响应时长。这三个指标放在一起看比单独看哪个 LLM 的“答得好不好”要客观很多。我自己坚持每两周跑一次评测对比本轮改动是否让指标上升还是下降效果立竿见影。5.3 成本控制Agent 化之后 token 消耗涨了 3 倍值吗坦白说Agent 化的知识库比传统 RAG 贵。传统 RAG 一次问答大约 10002000 token不含检索Agent 版因为有路由、评估、改写平均一次问答会达到 40006000 token。但值不值要看场景对于“随便搜索一下就能满足”的场景Agent 确实是浪费但对于“回答错了就要出问题”的企业内部场景多花的 token 换来了更高的准确率与更可控的答案质量。成本控制我给三条建议。一是轻量模型承担中间步骤路由、改写、评估用 mini 系列模型足够最终回答用大模型。二是尽最大可能做缓存同类问题重复率在企业内部非常高。三是给检索结果评估节点加“早停机制”如果第一轮检索的结果已经足够直接进入回答节点不再做第二轮实测能省 20%30% 的调用量。写在最后的几点体会增强版智能知识库做到现在我最大的感受是Agent 不会变魔术它只是把“检索—判断—回答”这件事做得更结构化了。它的价值不在于模型多聪明而在于你给模型搭了多少个可靠的台阶——检索准不准、记忆乱不乱、引用实不实每一样都直接决定用户体验。如果你最近也在做类似的东西我建议你先别急着上多复杂的编排框架把一个最简单的 Agent 循环路由→检索→评估→回答跑通再把混合检索、记忆、引用一个模块一个模块地加进去。每加一个模块就跑一遍评测集看效果稳扎稳打比什么都重要。最后分享一个小技巧把你日常工作中反复被问到的十个问题设成“黄金问题集”每次改完系统用它们做冒烟测试比你翻一堆文档都管用。
返回列表