从RAG到Agentic RAG:让知识库从问答机变成办事员
2026/9/20 3:49:09 网站建设 项目流程

很多团队第一次把RAG知识库跑通上线的时候,都会有一种接近真实的幻觉:系统能从文档里引经据典,感觉自己已经建成“AI助手”了。但实际用下来你会发现,它更像一个“带原文引用的搜索引擎”。用户真正想问的往往是“这件事能不能办、怎么办、帮我办”,不是“你给我读一段文档”。这个差别的本质,决定了RAG知识库要不要继续往Agent方向走。

我去年帮一家做售后服务的客户升级知识库系统,感触特别深:朴素RAG只能完成“信息检索型”问答,比如“退货政策是什么”;而真实业务场景是“根据某个订单的实际状态判断能否退货、生成退换方案、并同步到客服工作台”——这里面有查询、有判断、有动作。论文里的RAG做不到,工程上也做不到。后来我把架构改成基于FastAPI、LangChain、LangGraph、pgvector的Agentic RAG,并把业务动作封装成Skill给Agent调用,系统才算从“问答机”变成了“办事员”。

所以这篇文章不抠概念,直接讲清楚三件事:为什么要从RAG走向Agent和Skill;技术选型与执行链路怎么搭;落地过程中那些文档不写的坑怎么处理。核心问题不在技术,而在你设定的用户预期。如果你做的是知识库问答,那就老老实实做检索;如果你做的是“助手”,就必须把知识变成决策和动作的基础。

1. 题眼在“助手”两个字:朴素RAG到Agentic RAG差在哪

1.1 从“回答正确”到“任务完成”的跨越

朴素RAG的标准流程大家都知道:文档加载、切块、向量化、存库、召回、重排、让LLM基于上下文生成回答。这套链路解决的是“信息获取”问题,做得好不好,核心指标是答案是否忠于知识库、是否有引用依据。

但“助手”这个词的标准完全不同。助手要处理的是“任务”,任务分三步:理解意图、收集证据、执行动作。举个售后客服的例子。

用户说:“我上周买的耳机坏了,订单号是SU20240115,帮我申请换货。”

朴素RAG的做法是:把这句话当检索query,去知识库找“换货政策”,然后生成一段“根据政策,7天内可以换货”的文字。它不会也不敢去查订单系统,不会更新工单,不会告诉用户“你的订单符合换货条件,我已经提交了申请,物流取件码将在明天10点前发送”。

真正的问题在于,知识库里的静态文档无法覆盖动态业务数据。订单状态、库存数量、审批流程、用户身份,这些信息永远不在切好的文档chunk里。你要么提前把它们整理成结构化数据塞进知识库,要么让系统具备实时调取外部系统的能力。后者就是Agent天然要承担的角色。

1.2 朴素RAG的三道坎

把这三道坎拆开,你就能理解为什么要在RAG外面套Agent。

第一道坎是“信息边界狭窄”。知识库只是存量知识的快照,查不到订单、查不到审批流、查不到实时库存。第二道坎是“流程断裂”。回答完问题,后续动作没人接,用户需要自己拿着政策去点按钮、填表单。第三道坎是“状态缺失”。多轮对话里,“这个订单”指代哪个?“按前面说的来”到底是什么意思?朴素RAG没有记忆结构,更谈不上跨轮推理。

Agentic RAG的贡献不是把RAG扔掉,而是把RAG压缩成一个能力:当Agent需要场景知识时,它去查知识库;当Agent需要业务事实时,它去调Skill;当外界信息既不在知识库也调不到时,它明确告诉你“我办不到,原因是……”。

1.3 为什么还需要“Skill”这个概念

那Agent为什么不直接写一堆工具函数?这里有个工程化问题:Agent面对的LLM输出是非确定性的,如果工具参数定义得模糊,它就会瞎猜参数、瞎调接口。Skill在这里承担的角色,是把“知识依据”和“业务动作”绑定在一起。

打个比方,知识库是“操作手册”,Skill是“按钮”。Agent看懂操作手册后按按钮,用户不用自己研究流程。这个拆法,比一上来就让Agent直接生成SQL查业务库要安全得多,也容易审计得多。

2. FastAPI + LangChain + LangGraph + pgvector:这套组合的选型逻辑

2.1 每个组件负责哪个环节

先说结论:这套组合不是为了“新而新”,也不是某种被困绑定的技术全家桶。它刚好把Agentic RAG里最麻烦的工程问题拆开了。

  • FastAPI:负责对外暴露HTTP接口。Agent任务的平均耗时普遍比单一RAG高,可能20到40秒,FastAPI的异步特性不会阻塞其他请求,配合流式输出能很大程度缓解用户等待感。
  • LangChain:负责与模型、工具、文档加载器的粘合。LangChain的Chain抽象我其实用得不多,但它的Tool抽象、DocumentLoader生态、以及各种模型的统一调用方式,确实能省掉不少重复代码。
  • LangGraph:接管工作流。这是和朴素RAG最核心的区别。你不再用代码if-else控制“要不要检索、要不要调工具”,而是定义一张状态图,让LLM在图上的节点之间做决策。
  • pgvector:处理知识存储。它不是独立向量库,而是PostgreSQL的插件。业务数据、向量、Agent运行日志可以在同一个库里,非常方便事务管理。

2.2 为什么先选pgvector而不是独立向量库

我当时确实犹豫过:数据量到百万级了,是不是该上Milvus?但做完评估后,我仍然建议先用pgvector。

原因很简单:企业知识库不只是纯文本。它天然包含订单表、用户表、权限表、工单表。如果用独立向量库,业务数据和向量数据分离,你就得自己管理同步、解决两个库之间“join不了”的问题。pgvector的性能边界很清晰,百万级向量、HNSW索引、单机内存足够,它的响应延迟通常在几十毫秒到一两百毫秒之间,这完全能满足RAG场景。

大多数企业知识库根本到不了千万级。真到了,再迁移到专用向量库也不迟,反正LangChain的VectorStore接口是统一的。用pgvector最大的价值是:你在还没成为AI公司之前,不用为了AI专门增加一个昂贵的数据库依赖。

2.3 一套完整的调用链路长什么样

把组件拼起来之后,线上调用的主链路是这样走的:

  1. 用户在对话端输入问题。
  2. FastAPI Gateway接收请求,解析会话上下文。
  3. LangGraph状态图冷启动,进入意图识别节点。
  4. Agent决定:这个问题需要知识库支持,还是需要调用业务Skill,还是两者都要。
  5. 如果走RAG分支,LangChain调用pgvector检索,召回候选文档,经过重排后把最相关的几段内容放进上下文。
  6. 如果走Skill分支,Agent从Skill列表中选择合适的工具,补充参数,调用工具节点。
  7. 生成最终答复,写回运行日志和会话状态。

很多人只把RAG视为“输入文档、输出答案”的离线流程,但Agentic RAG里,RAG更像是一个“按需触发的证据收集器”。Agent先判断“我需要什么”,再决定“去哪里拿”,最后才生成语言。这个顺序一旦理清楚,选型就顺了。

3. 业务Skill的正确落地姿势:把回答变成可执行的业务动作

3.1 Skill不是prompt,是标准接口

我见过很多团队的“Skill”写法和写prompt没两样:给模型一段“当用户要求退款时,你要调用退款接口,参数从对话中提取”的文字。

这种搞法在Demo里能跑,到线上就会露馅。因为没有任何参数校验,模型一旦把parameter名猜错,或者传了不该传的值,轻则接口报错,重则产生脏数据。

真正的Skill要具备三类信息:入参说明、出参结构、触发条件。最好还带一个失败兜底策略。以售后场景为例,我把Skill封装成这样:

from langchain_core.tools import tool from pydantic import BaseModel, Field class AfterSaleInput(BaseModel): order_id: str = Field(description="订单号,用户提供或从会话上下文提取,不能为空") apply_reason: str = Field(description="用户填写的售后原因") policy_evidence: str = Field(description="从知识库召回的售后服务政策原文,用于工单留痕") @tool(args_schema=AfterSaleInput) def create_after_sale_order(order_id: str, apply_reason: str, policy_evidence: str): """创建售后处理工单。仅当用户明确要求办理退货、换货、维修,且知识库政策确认该场景可受理时调用。""" # 在这里调售后系统OpenAPI return {"ticket_id": "...", "status": "submitted", "estimated_time": "24h"}

关键细节在description里。你要写清楚“什么时候不用”,这比“什么时候用”更让模型省心。比如售后创建工单,如果用户只是问政策不要求办理,就不该调用;如果订单不存在,应该返回“查不到订单”。这些描述会影响LLM的工具选择准确率。

3.2 Skill的分类管理

我把业务Skill分成三类:查询型、动作型、复合型。

查询型Skill是无副作用的,比如查订单状态、查库存、查物流。动作型Skill会写数据和推进流程,比如创建工单、发审批、调整额度,这类Skill必须加操作确认。复合型Skill是几个原子Skill的组合,一般放在LangGraph里编排,不在工具层处理。

在代码组织上,我建议把每个Skill单独建目录。目录里放工具函数、参数Schema、单元测试、调用示例。这样每个Skill就能独立评审、独立发布、独立回滚。Agent接入新能力时,只需要在Skill registry里注册一下。

from langchain_core.tools import tool SKILL_REGISTRY = { "query_order": query_order, "create_after_sale_order": create_after_sale_order, "check_inventory": check_inventory, "send_coupon": send_coupon, }

选型上一开始就可以用LangChain的@tool装饰器。如果团队微服务用的是多语言,也可以用FastAPI把每个Skill包成独立的HTTP服务,Agent通过统一网关调用。只要入参出参是JSON Schema,模型侧其实不关心你背后是什么。

3.3 失败的降级设计

Skill调用失败是最容易被忽略的环节。LLM调工具失败之后,如果没有任何fallback,它就会自己编一个结果,这是灾难。我的做法是给所有Skill调用包一个统一错误处理:

  • 超时:返回“系统繁忙,请稍后重试”,不落地任何状态。
  • 校验失败:把具体缺失参数返回给Agent,让Agent追问用户。
  • 业务失败:比如订单不存在、订单已关闭,Skill返回结构化错误码,Agent根据错误码生成对应回答。

这一步一定要在开发期就闭环。否则上线后一定会出现“系统说工单创建成功了,但后台根本没数据”的情况。到时候你没法复盘,因为日志里只有一句模型生成的话,工具调用记录被覆盖了。

4. LangGraph显式编排:把“要不要查、要不要动”变成Agent决策

4.1 状态图先于代码

我在搭建Agentic RAG的时候,第一件事不是写代码,而是画一张状态流转图。不是说不能用mermaid这类工具,而是你要想清楚节点有哪些,边有哪些分支。

我的LangGraph状态图大概是这样:意图识别节点、知识库检索节点、Skill执行节点、回复生成节点、转人工节点,外加一个人工审批旁路。

重点在于条件边怎么设计。很多团队把“是否检索”写成固定规则,比如“所有问题都先检索”。这产生了一个问题:用户问“今天天气怎么样”,系统也去知识库里捞一遍,浪费算力还答不对。正确做法是让LLM在意图识别节点输出两个标志:need_retrievalneed_tool。然后图根据这两个标志走不同分支。

from typing import TypedDict from langgraph.graph import StateGraph, END from langchain_core.messages import BaseMessage class AgentState(TypedDict): messages: list[BaseMessage] need_retrieval: bool need_tool: bool selected_tool: str tool_input: dict retrieved_docs: list[str] answer: str

定义好状态之后,图结构就跟着状态走了:

def intent_node(state: AgentState) -> dict: # 通过LLM判断:识别意图、决定是否需要检索或调用工具 # 返回 {"need_retrieval": True, "need_tool": False} 等标志 ... def retrieval_node(state: AgentState) -> dict: # 调用pgvector检索,筛选最相关文档 ... def tool_node(state: AgentState) -> dict: # 从SKILL_REGISTRY中取对应Skill并执行 ... def generate_node(state: AgentState) -> dict: # 基于检索结果或工具结果生成最终回复 ... graph = StateGraph(AgentState) graph.add_node("intent", intent_node) graph.add_node("retrieval", retrieval_node) graph.add_node("tool", tool_node) graph.add_node("generate", generate_node) def route_to_retrieval(state: AgentState): if state["need_retrieval"] and not state["need_tool"]: return "retrieval" if state["need_tool"] and not state["need_retrieval"]: return "tool" return "retrieval" # 两个都需要时,先检索再走工具 graph.add_conditional_edges("intent", route_to_retrieval, ...) graph.add_edge("retrieval", "generate") graph.add_edge("tool", "generate") graph.add_edge("generate", END)

有人会说,这不就是if-else吗?区别在两点:第一,if条件不是程序员手写死的,是LLM实时判断的;第二,LangGraph允许你在任意节点之间增加循环和人工确认,这要比在代码里维护复杂状态机简单得多。人工审批这类需求,纯函数式编排很难写,图结构天然支持“挂起、等待、恢复”。

4.2 循环和人工审批节点

业务Skill里有不少“动作型”操作会涉及钱和流程,不能一上来就自动执行。我给这类Skill加了一个approval节点:Agent调用前先生成待审批内容,推给主管确认,主管同意了图才继续往下走。

LangGraph实现这个方案有标准模式:节点执行到一半,返回一个特殊状态,表示“等待人工”,系统把节点状态持久化到数据库;用户端回复“已提交审批,待确认”。人工在审批后触发恢复接口,图从断点继续跑。

这块是纯RAG完全无法触碰的领域。纯RAG根本没有“执行”和“等待”的概念,它只负责文本。而有了LangGraph,知识库问答才真正具备“业务流程引擎”的雏形。

4.3 检索质量仍然是整条链的底座

最后强调一点:节点编排再好,RAG的召回质量不过关,整个Agent给出的答案就是空中楼阁。我在此处用的还是经典的“切块+embedding+向量召回”,然后加一个重排模型。重排这一步不能省。第一次建工程时我只用向量召回的top 5直接喂给LLM,结果答案经常“打了擦边球”。后来加了一个重排模型,把候选从50条缩小到5条,准确率提升非常明显。

pgvector侧有一些要注意的参数。HNSW索引满意后,要结合文档规模调mef_searchef_search太小召回不足,太大延迟上去了。线上数据量在十万级时,我通常会把ef_search设为128,这个值在延迟和召回率之间比较平衡。切块长度一般设400到800个字符并带100到200个字符的重叠,太碎会让语义不完整,太长又会让embedding向量不聚焦。

5. 踩坑实录:Agentic RAG上线前我踩过的几个真坑

5.1 切块太碎,召回结果像被打碎的图片

第一次做知识库切块,我图省事按固定500字符切,没考虑段落标题。结果一个完整的“售后流程五步”被切成三块,向量检索时,第一块讲“准备材料”,第二块讲“提交申请”,第三块讲“退款到账时间”。用户问“申请退货要准备什么”,系统回答时只拿到了第一块,给出的材料清单缺了两项。

这个问题的排查链路是这样的:先看召回命中文档的原文片段,发现内容上下不连贯,再看切块的分隔符,发现切在段落中间。后来我改成了按Markdown标题切块,再对过长的段落做二次切分,同时在每个chunk里额外存了section_titlesource_page两个metadata字段。检索时不仅能定位到相关内容,还能给用户标明出处。

5.2 Agent无脑循环调用工具

这是Agent化之后最让人头疼的坑。我的Agent在测试时出现过连续调用同一个Skill四次的情况:第一次查询失败,返回“订单不存在”,模型不死心,又换参数查了一次;第二次还是失败;第三次模型直接编了一个订单编号,工具调用又失败。最后回复“该订单可能存在系统延迟,请稍后再试”。

根因是工具返回的错误码没有进入模型的判断闭环。模型看到的是一个莫名其妙的结果,不知道这代表“业务上不允许重试”,导致它不断幻想新参数重试。

修复方式是在Skill返回结构里加了两个字段:retryablesuggestionretryable=false表示这个错重试也没用,suggestion则告诉模型该对用户说什么。同时给整个Agent加了最大工具调用次数限制,超过3次就强制进入“转人工”节点。这样至少不会出现浪费几十秒算力却毫无产出的情况。

5.3 召回命中“相关文档”,但它不能作为行动依据

这个坑更隐蔽。用户问“我这种情况能退款吗”,知识库召回了一段“不支持7天无理由退款的商品清单”,但用户问的是耳机,耳机并不在清单里。这时候Agent容易犯两类错:一类是认为清单里没有耳机,所以直接回答“可以退款”,忽略了还要看其他条件;另一类是看到“不支持”标题就回答“不能退款”,完全没有看正文。

我最后给出的方案是两段式生成:第一段让LLM只做事实提取,判断召回文档与问题是否真正相关、是否冲突;第二段才让LLM基于事实判断金额、期限和路径。中间加了一个“文档相关性校验”节点,凡是相关度低于阈值的就明确禁止作为工具调用的依据。

5.4 FastAPI长任务会把前端活活等死

Agentic RAG的一次完整调用可能要跑20到40秒。如果FastAPI接口是同步的,连接会被占用,网关超时,前端等不到结果,用户看到的就是一片空白。这个问题的解法我试过两种:第一种是FastAPI接口改异步,用asyncio.to_thread跑LLM调用,配合SSE流式输出,至少能让用户看到token逐步生成的过程;第二种是任务提交模式,接口先返回task_id,客户端轮询拉结果,适合后台离线处理。

如果面向实际客服场景,我推荐第二种,因为客服工作台不一定有强交互的流式界面,任务轮询更稳定。但面向普通用户聊天窗的话,SSE更友好。

5.5 评估只能看到“答得干不漂亮”,看不到动作对不对

我评估过一阵子基于LLM的自动打分,发现翻车很严重:模型觉得你回答得有理有据,实际上工具调用参数全是乱编的。后来我在评估里加了三层指标:

第一层,工具调用正确率:选中的Skill是否真的解决了问题,参数是否合法,返回是否落地成功。这一层用自动化脚本就能测。第二层,业务链路完成率:一个复杂问题是否走完了查询、判断、创建工单、回复提醒的完整链路。第三层,回复质量:只抽检部分样本,由有经验的员工打分,不再全量自动化。

这套分层评估上线之后,我才真正看得清系统在什么地方弱。前面两层没过关,后面谈“回复好”没意义。

6. 上线后的效果验证与迭代思路:用什么指标判断“助手”够格

6.1 离线测试集要覆盖“非标准提问”

业务方一开始给的知识库问答测试集大多是标准提问,比如直接写“退货政策是什么”。但用户真实表达千奇百怪:“我耳机坏了想退”“那个能不能换新的”“刚买的就出问题,我不想要了”。如果你测试集里没有这些变体,系统召回再好也会在意图识别上卡住。

我的做法是把过去半年的客服对话记录全部脱敏,挑出1000条高频真实问题,人工打标成“知识问答”“业务办理”“闲聊寒暄”“需人工服务”四类,再抽一部分做回归测试。每次模型或Skill更新,先跑这1000条,看各分类的准确率变化。这样做的好处是,你升级一个Skill时,不用肉眼去点几十遍。

6.2 线上日志是迭代的富矿

一定要把每次调用的完整链路日志记下来:用户输入、意图识别结果、检索候选、召回分数、重排结果、Skill名、入参、出参、错误码、最终回复、用户反馈。我用pgvector存文本,同时也在PostgreSQL里建了运行日志表,按会话ID关联。

查问题的时候,你才会知道哪些问法总是走错分支,哪些Skill触发率极低,哪些回复在“强行道歉”。这些日志的价值比任何监控面板都大。没有留存日志的Agent系统,进步曲线就是一条横线。

6.3 迭代顺序:先修链路稳定性,再提升“聪明度”

上线初期我犯过急躁,总想优化prompt让回答看起来更聪明。后来发现问题不在prompt,而在链路。Agent偶尔会把并不支持的场景“脑补”成可以办理,工单表里出现垃圾数据。这时候首要任务是加强Skill参数校验、收紧工具调用条件、增加人工确认节点,而不是去调温度参数。

它们可以并行:第一周做链路的硬约束,第二周处理召回的边界案例,第三周再回头调回复模板。一个“笨但稳定”的助手,比一个“聪明但乱来”的助手更值得上线。

6.4 别忘了给用户一个“退出通道”

最后一点,语音很轻但很重要:所有Agent回复里都要预留“转人工”的出口。模型的能力是有限度的,超出Skill覆盖范围时,强行生成答案不如直接说“目前这个问题需要人工业务专席处理,我来帮你转接”。

我在实际系统里观察到,加了转人工作为兜底后,用户投诉比例反而下降了。因为用户真正愤怒的不是助手不能解决,而是助手明明解决不了还一直绕圈子。把这条兜底逻辑写进LangGraph的最终判断节点,比训练模型一千遍“不知道怎么回答就要承认”要可靠得多。

这套从RAG到Agentic RAG的改造,简单说就是把“会说话的知识库”变成“会办事的助手”。技术选型上,FastAPI、LangChain、LangGraph、pgvector的组合足够承担大部分企业场景;业务价值上,Skill让知识库和真实业务流程连了起来;工程难点上,链路稳定性、工具调用正确率、日志评估体系,每一步都要比“跑通Demo”做得更细。你踩过坑再多,只要评估闭环在,系统就会持续像一个真正的助手那样成长。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询