这一篇是Agentic RAG实战系列的第14篇,按系列进度算是第三个完整落地案例。前两个案例分别处理了纯文本资料库问答和多文档综述生成,这次我换了一个更复杂的场景:把结构化数据库查询和文档检索塞进同一个Agent循环里,让系统既能查订单数据,又能读供应链文档,最后自己组织一份带证据链的诊断摘要。
先说明一下为什么值得单独写一篇。我之前在不少场合表达过一个观点:经典RAG适合回答"知识型问题",但不适合回答"任务型问题"。你让一个传统RAG系统做"分析华东区最近30天退货最多的三个SKU,再结合供应商A的质量标准评估风险",它第一步就卡住了。它不会查数据库,不知道去哪调接口,更不会把SQL查询结果和文档段落组合成完整答案。如果硬把数据库内容灌进向量索引,维护成本会高到崩溃,而且实时数据永远跟不上。Agentic RAG在这种场景下才是正解,这也是我把它单独拎出来做三期实战的原因。
这篇东西适合谁看呢?如果你已经做过基础RAG,现在想进入Agentic RAG,或者你正面临"文档检索+数据库查询+多步推理"混合需求,那这篇应该能帮你少走不少弯路。我不会只给架构图,会把架构里的每个模块拆开讲,包括代码级别的实现、踩坑记录和上线前的评估思路。
1. 为什么第三个案例选"文档+数据库"混合查询
1.1 传统RAG在结构化数据面前的局限
先回顾一下经典RAG的套路:用户提问,系统把问题向量化,去向量库里找相似片段,把片段拼进Prompt交给LLM生成答案。这个链路在"知识密度高、答案就在文档里"的场景很有效,比如产品手册问答、论文阅读助手。但一旦问题里带上了数据属性,就开始露馅。
举个例子。"华东区最近30天退货最多的三个SKU是哪些?"这个问题本身没有语义信息可检索。向量库里存的如果是文档,文档里不会写"华东区、最近30天、退货最多"这种变化频繁的事实性数据;即使有,也是过期的。你让RAG回答,它会用文档里某段话"蒙"一个答案出来,最常见的结果是:模型生成了一堆看似合理但实际上没有任何数据支撑的SKU编号。
我见过不少团队试图用同步索引解决这个问题:每天把数据库导出为文档,灌进向量库。结果就是索引构建任务越跑越重,回答延迟越来越高,数据时效性还是差一截。退一步说,就算索引能保证及时更新,"退货率计算""TOP 3排序""同比环比"这些计算逻辑也没法靠语义检索实现,必须有代码去执行。
1.2 本案例的场景定义
这期案例我选了一个虚构但很典型的业务场景:一个供应链管理助手。它要能回答两类问题,而且要能组合回答。
第一类是文档类检索,知识相对静态:比如"供应商A的质量标准里对包装破损率的要求是多少"。
第二类是数据类查询,数据实时变动:比如"最近30天华东区的退货订单有多少"。
最关键的是第三类组合问题:"上个月华东区退货最多的三个SKU,对照供应商A的质量标准,评估这几个SKU的包装风险是否超标。"这类问题靠任何单一工具都答不了,必须分几步走:先去订单库查出退货数据,再去文档库找到质量标准,最后把两边结果交给模型推理判断。
说实话,这类"先说结论、再列证据"的输出,在内部知识助手里接受度非常高。用户已经受够了那种给一段泛泛而谈的AI答案,他们真正要的是:结论有数据支撑,判断有文档依据,每一步都能追溯。
1.3 为什么叫Agent而不叫Chain
很多人在这个点上理解有偏差。LangChain的Chain是把一系列固定步骤串起来,A步骤做完必做B步骤,中间没有分支。但"组合查询"场景最大的特点是路径不确定:有些问题只需要查文档,有些问题只需要查数据库,有些问题需要先查数据库再查文档,还有的问题可能需要来回好几次。
举个例子,用户问"供应链文档里提到的质量标准适用于华东区哪些SKU?"机器需要先看到文档内容,才能决定下一步是去数据库查SKU还是继续读文档。这是一个动态规划过程。Agentic RAG的核心就是给系统装了一个"规划器",由LLM根据阶段性结果决定下一步动作,而不是靠工程师预先写死流程。
这也是我建议团队在起手时不要过度设计的原因。很多人一上来就搞复杂多智能体框架,实际上大多数业务场景只需要一个Agent循环加几个工具。这期案例里我一个Agent都没拆,就是一个主循环控制四个工具,跑下来效果已经很稳定。
2. 四层循环:Planner、Router、Tool、Verifier的职责与协作
2.1 模块职责划分
整个系统虽然不是物理上的多智能体,但逻辑上我把它分成了四个模块,每个模块只干一件事。这个划分方法我建议你直接抄,因为它对后续维护特别友好。
Planner负责拆解目标。用户输入一个复杂问题,Planner把它变成若干子任务,并且保持子任务之间的依赖关系。比如"查退货数据"和"查质量标准"是并列的,但"评估风险"必须等前两个子任务都完成才能执行。
Router负责选工具。每个子任务进来,Router判断该走哪个工具:向量检索、SQL查询、文档摘要还是直接回答。这块我在后面第3章会详细讲,它可以直接用LLM函数调用实现,也可以用轻量规则实现。
Tool层就是实际执行动作的地方。这里我注册了四个工具:搜索文档、执行SQL查询、读取指定文档段落、汇总生成报告。工具返回的内容不只是结果字符串,还带着结构化元数据,方便后面验证和追踪。
Verifier负责检查结果。这也是最容易被人忽略的一层。LLM规划出来的步骤不一定对,工具返回的数据也可能不符合预期,Verifier的作用就是用规则或二次LLM调用确认:数据完整性是否达标、结论是否有证据支撑、有没有幻觉风险。
2.2 循环的运转逻辑
整个系统的执行流程是一个标准循环:
首先用户输入进入Planner,Planner输出一组候选步骤。然后进入Router,Router为每个步骤分配工具,按顺序或并行执行。工具结果返回后,系统会先做一次轻量检查:这一步的结果是否满足下一步需要的字段。如果满足,继续推进;如果不满足,把错误信息反馈给Planner重新规划。循环直到所有子任务完成,最后调用一次汇总工具生成最终答案。
这个流程看起来和普通工作流差不多,但区别在于每一步的"下一步"不是写死的。比如Router发现数据库查询结果为空,它可以选择调大时间窗口重新查,也可以选择直接告诉用户没有数据,而不是机械地走到文档检索。
2.3 核心数据结构:Agent State
工程实现上,我强烈建议用一个贯穿全局的State对象来管理上下文。这个State不只是聊天记录,而是要包含:原始问题、已经执行的步骤列表、每步的工具返回结果、当前待执行的步骤队列、以及最后的答案草稿。
我踩过一个坑:一开始把上下文丢在Python字典里,各种散落变量,Debug时根本不知道系统"想"到了哪一步。后来改成显式State对象,每一步执行时都更新State并写入日志,排查问题就轻松多了。你可以把它理解成手术室里的麻醉记录单,每一步用了什么药、剂量多少都留痕,出了问题能回溯。
这个State对象同时还是后续Trace日志的基础,每轮Agent循环产生一条记录,存进JSONL文件,评估的时候直接重放。
3. 路由决策的工程实现:向量检索与SQL工具怎么选
3.1 两种路由方案对比
Router是整个系统里最容易过度设计的部分。我刚开始做的时候,想用LLM做复杂路由决策,给模型写了很长的工具说明文档,结果一次调用就要消耗不少token,偶尔还会选错工具。后来我把实验数据拉出来看,发现大部分问题的路由用规则就能解掉:问题里含"退货、订单、金额、数量、SKU"这类词,优先走SQL;问题里含"标准、文档、条款、要求"这类词,优先走文档检索;两掺的问题才需要LLM决策。
所以我在生产系统里用的是"规则+LLM兜底"的混合路由。先过一个关键词匹配器,匹配置信度超过阈值就直接路由;匹配不到或者出现冲突信号,再交给LLM做函数调用决策。实测效果是:大概85%的问题被规则正确路由,剩下15%交给LLM,整体路由准确率到了96%以上,成本还比全LLM路由低一半。
你可能会问,为什么不全用规则?因为有大量问法是带否定、带模糊指代的。比如"除了A供应商之外的其他供应商",关键词匹配很容易把"供应商"路由到文档查询,实际这个问题的意图是查数据库。这种语义层面的理解绕不开LLM。
3.2 工具定义与function calling的JSON Schema
我用的工具注册方式是最标准的OpenAI function calling格式,每个工具定义包含name、description、parameters三个字段。这里有一个很关键的工程细节:description一定要写清楚工具能干什么、不能干什么、什么情况下应该选它。
举个例子,SQL查询工具的description我写成这样:
"query_database(tool)用于执行SQL查询,只支持只读操作,可以查询orders、products、suppliers三张表。当用户的问题涉及具体数字、统计、排行或时间范围时,优先选择此工具。不要用它查询文档内容。"
这句话看着简单,但实际上是在帮LLM做路由,给它提供决策依据。我见过很多团队在工具描述里写得很随意,比如"查询数据库",结果模型根本不知道什么时候该调用它,导致路由效果崩盘。
3.3 函数调用循环的代码骨架
下面这段代码是路由和工具调用循环的核心骨架。它不是我纸上谈兵写出来的,是从我项目里简化出来的,你可以直接拿去做底子。
import json from typing import Callable TOOL_REGISTRY = { "search_documents": search_documents, # 向量检索 "query_database": query_database, # SQL只读查询 "read_document_section": read_doc_section, # 按ID读指定文档段落 "finalize_report": finalize_report, # 汇总生成最终回答 } TOOL_SCHEMAS = [ { "type": "function", "function": { "name": "search_documents", "description": "在知识库中检索最相关的文档片段,适用于质量标准、合同条款、操作规范等内容。", "parameters": { "type": "object", "properties": { "query": {"type": "string", "description": "搜索关键词"} }, "required": ["query"] } } }, # 其余工具的schema结构类似,不再重复列出 ] def call_llm_with_tools(messages, tools): # 伪代码:实际替换成你的LLM客户端调用 response = llm.chat(messages=messages, tools=tools) return response def run_agent(question): state = { "question": question, "history": [], "pending_steps": [], "tool_results": {}, "final_answer": None } # 第一次调用:让LLM决定初始工具 messages = [{"role": "user", "content": question}] for _ in range(MAX_STEPS): # 设置最大步数,防止死循环 response = call_llm_with_tools(messages, TOOL_SCHEMAS) if not response.tool_calls: # 没有工具调用,说明Agent认为可以收尾 state["final_answer"] = response.content break for tool_call in response.tool_calls: fn_name = tool_call.function.name fn_args = json.loads(tool_call.function.arguments) if fn_name not in TOOL_REGISTRY: messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps({"error": f"Unknown tool {fn_name}"}) }) continue # 执行工具 result = TOOL_REGISTRY[fn_name](**fn_args) state["tool_results"][tool_call.id] = result messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result) }) state["history"] = messages return state这段代码有几个细节要专门注意。第一,MAX_STEPS必须设置上限,我用的值是8,防止Agent在复杂问题上无限循环烧钱。第二,工具调用结果必须转成JSON字符串塞回messages里,这个格式如果出错会导致整个上下文断裂。第三,每轮循环结束后把state完整保存一份,这是后面做评估和Trace的基础。
4. 多跳拆解与证据链组装:从"退货最多的SKU"到"风险评估"
4.1 把复杂问题拆成可执行的子任务
现在回到开头那个组合问题,完整跑一遍这个系统,看看它实际是怎么工作的。
用户输入:"上个月华东区退货最多的三个SKU,对照供应商A的质量标准,评估这几个SKU的包装风险是否超标。"
Planner收到问题后,输出一个三步计划:
- 查询数据库,统计华东区上个月退货量TOP 3的SKU。
- 在知识库检索供应商A的质量标准文档,重点找包装相关条款。
- 把SKU的退货数据和标准条款一起交给LLM,生成风险评估结论。
这三步不是凭空生成的,全靠Planner对工具能力的理解。所以我前面说工具description要写清楚,如果计划器不知道有个查询数据库的工具,它大概率会把这个问题硬塞给文档搜索,然后翻车。
4.2 每个子任务的执行细节
第一步执行SQL查询。我给query_database工具内置了几个安全包装好的查询函数,不允许Agent直接写任意SQL,只允许调用预定义的get_top_sku_by_returns和get_order_stats等函数。
这个设计是为了安全,也是为了稳定。让LLM写自由SQL的风险很大:表结构稍微复杂一点,它就写错JOIN;更不用说如果线上库没有权限隔离,一次失控的查询可能拖垮数据库。预定义查询函数把LLM的自由度限制在一个安全范围内,它能选择的参数只有时间范围、业务区域、返回条数等,而不是整个SQL语法空间。
第二步是文档检索。这步相对标准:向量召回Top20片段,然后重排取Top5。但这里有一个和纯RAG不同的细节:工具返回的文档片段必须带文档ID和段落编号,因为第四步生成报告时需要引用来源,没有结构化的引用信息,报告里的证据链就是编的。
第三步就是与第四步一起完成的,因为风险评估不是一个独立的函数,而是在所有工具结果收集完后,由LLM进行的一次生成。这个生成过程需要仔细设计Prompt,要求模型必须引用工具结果里的具体数字和条款编号,不能甩出文档里没有的结论。
4.3 证据链组装的关键技巧
这个环节我踩过不少坑,总结出两条最有用的经验。
第一个经验是:工具返回内容不要做太多加工,保留原始数据的结构和关键字段。比如数据库查询返回的不应该是"华东区退货最多的SKU是A、B、C"这种LLM加工的叙述,而是结构化JSON:{rank, sku, return_count, return_rate}。因为加工过的叙述会丢失精确数值,而LLM在推理时需要的恰恰是这些数字。再传给模型的时候,保留原始JSON加一个简短的字段说明,效果最好,token消耗也最省。
第二个经验是:在生成最终报告之前,要先让LLM做一个"证据检查清单",也就是把每个结论点对应到一条工具结果引用上。这个步骤可以就用一个简单的Prompt实现,告诉模型:如果你要给出一条结论,必须同时给出支撑这条结论的工具记录ID和关键数值,否则不要写这条。这一步能显著减少Agent胡说八道的情况。
我自己试过更复杂的方案,比如用额外的验证模型去交叉检查引用,但现阶段收益不大,用Prompt约束加一层规则检查已经够用了。后面第5章我会专门说验证的事情。
5. 结果验证与幻觉抑制:Agent自信时的最后一道闸门
5.1 为什么Agent比传统RAG更容易产出幻觉
很多人以为Agent只是把RAG的检索能力增强了一下,幻觉问题应该缓解才对,但实际做下来恰恰相反:Agent化之后,幻觉风险反而升高了。
原因有两个。
第一,Agent状态下,LLM拥有工具调用的权限,它会倾向于"觉得自己已经查到了数据",然后基于一个不存在的查询结果开始推理。有一次我实测系统时,它居然在数据库工具还没返回结果的情况下就生成了结论,而且结论里捏造了一组SKU。原因是Planner把"查询数据"和"生成报告"合并成了一个步骤,Router觉得不需要额外调用工具。
第二,工具返回结果通常包含大量结构化信息,长度可能超过上下文窗口,截断后关键数值丢失,模型就开始补全缺失信息。这个和人类一样,信息不完整时大脑会自动填充,LLM也有这个倾向。
5.2 验证器的设计:规则优先,模型兜底
我最终采用的验证器是双层设计。第一层是硬性规则,检查一些绝对不能出错的点,比如:最终答案中出现的SKU编号必须存在于工具返回结果中;数据库查询的时间范围和用户要求的一致;答案中的数值必须是工具返回原始值,不能是模型自己"算"出来的。这些规则实现起来不复杂,就是字符串和JSON的比对,但能堵住80%的幻觉。
第二层是软性检查,用一次轻量LLM调用,让模型判断"最终答案的每个结论点是否都能在工具结果里找到支撑"。这一层主要抓逻辑连贯性,比如工具返回的是"退货率上升12%",而最终答案说"退货率显著上升",这个"显著"是不是过度解读?软性检查会把这种可疑表述标出来,回传给Planner要求重新组织。
这套双层验证器上线后,我在测试集上的幻觉率大约从18%降到了5%。剩下5%目前主要集中在"模型过度总结"这个维度,比如把"3个SKU的数据"总结成"多个SKU",这个暂时只能靠Prompt继续压制,规则很难穷尽。
5.3 失败模式与重试策略
验证器一旦发现问题,不能直接给用户报错,而是要走重试流程。我设计了三个重试策略。
如果某一步工具结果不满足下一步需求,比如数据库查询为空,Planner会调整查询条件,比如把时间范围从30天放宽到90天重新查询。这个叫参数重试。
如果验证器发现工具结果被截断导致关键信息缺失,系统会触发定向读取工具,只取缺失的那条文档段落,不重复整段检索。这个叫局部补全。
如果LLM在最终生成阶段被验证器判为引用不可靠,系统会把验证器的错误信息拼进Prompt里,重新调用一次生成,强制要求模型忽略自己脑中已有的内容、只基于工具结果原文回答。这个叫生成重试。
我见过很多团队在这个环节直接做"整个Agent从头跑一遍",这是最烧钱也最没用方案。问题通常出在某个节点,局部修正比全局重跑高效得多。把失败日志打出来以后,我建议按失败链路分类统计,先修占比最高的那一类,逐个击破,不要一次性想着所有场景都稳定。
6. Trace、回归集与成本控制:从Demo到生产的最后一公里
6.1 Trace日志:没有日志的系统等于盲飞
Agent项目的调试难度比普通RAG高一个量级,因为中间多了一堆工具调用跳转。没有Trace日志的情况下,你看到最终答案错了,根本不知道是路由错了、工具写错了、还是生成Prompt有问题。所以从第一天起就要把每轮循环完整记录下来。
我在每个State里落一个JSONL文件,记录内容包括:时间戳、当前步骤序号、Planner出的计划、Router选中的工具、工具入参、工具返回结果的摘要、验证器判定结果、本轮LLM生成的中间内容。生产环境里这个日志还能直接接到监控面板上做可视化。
有一个成本优化小技巧:Trace日志里不需要存完整工具返回结果,存一个截断版本加字段哈希就够了。否则日志两三天就能撑爆几个GB,排查问题时其实根本不会看完整数据。
6.2 回归测试集的设计
做Agent项目最怕的就是"修好了A,弄坏了B"。回归测试集是唯一能拦住这个问题的方案。我这次项目里组织了大概60条测试问题,分成几个类别:纯文档类、纯数据类、组合查询类、边界异常类。
其中最有价值的是边界异常类。比如"昨天退货数据是多少"这种问题——"昨天"在不同语境里可能指自然日,也可能指业务日;再比如"所有供应商的文档都对比一遍"这种可能超出工具调用次数上限的请求。这些边界问题往往是代码里最容易出Bug的地方,测试集里没有它们,你都不敢上线。
回归测试的判定我并没有完全依赖简单对比答案字符串。更实用的做法是让一个独立的评估LLM根据参考答案判断"核心结论是否一致、关键数值是否准确、引用是否完整",输出通过或不通过。我现在这套评估流程每次跑完大概需要15分钟左右,成本可控,收益非常明显。
6.3 成本与延迟的取舍
Agent系统的成本问题很多人没算过一笔细账。一个复杂的组合查询问题,可能会调用3次左右的LLM函数调用循环,加上工具执行,单次问题的token消耗可能是普通RAG的5到10倍。如果每天有上千次调用,成本压力会非常明显。
我的经验是三板斧:路由前置规则化,能走关键词匹配解决的问题就不进LLM决策;工具结果缓存,同一天内相同参数的SQL查询直接命中缓存;模型分级,Planner和Router这类决策环节用小规模强指令模型,最终报告生成环节才用满配模型。这三招合起来,能把单次问题成本压到原来的三分之一左右。
延迟这块同样有优化空间。并行执行子任务是见效最快的手段。比如前面例子里的"查退货数据"和"查质量标准"其实是可以并行跑的,只要Planner把它们标记为无依赖关系。我在工程实现里对无依赖的子任务做了一个简单的并发池处理,端到端延迟从12秒降到8秒左右。追求极致的团队还可以在这层加缓存和流式输出,但要注意流式输出会打断Agent循环里工具调用的节奏,需要额外设计。
7. 上线后最常踩的五个坑(个人经验总结)
7.1 最大坑:Agent循环不终止
我系统上线第三天就遇到一次事故,某个用户的请求触发了Agent无限循环,数据库查询和文档检索来回交替执行,整个任务跑了十几分钟,成本飙升到了正常情况的几十倍。后来加了MAX_STEPS限制才解决。
这个坑我建议所有人在做架构时就提前埋好防线,不要等出事故了再补。除了设置最大步数之外,还要加一个"重复动作检测":如果Agent连续三次调用同一个工具、且入参没变化,直接终止循环并报错,说明它已经进入死胡同,继续下去只会浪费token。
7.2 Prompt陷阱:让模型学会"不知道"
Agent的Prompt和普通RAG有一个很大的区别:它需要告诉模型什么情况下不要调用工具,什么情况下不要强行回答。我踩过的坑是给Agent的指令里只写了工具怎么调用,没有写"如果数据查不到,要如实告知用户",结果模型经常拿着一份空查询结果硬编答案。
后来我在系统Prompt里加了一句很直白的话:"如果你没有检索到足够的信息来回答问题,请明确告诉用户当前知识库和数据库中没有足够数据,不要尝试推测。"就这一句话,最终答案里的幻觉率降了不少。
7.3 工具返回格式的规范性
这个坑是代码层面的。有一段时间我让Tool层直接返回自然语言字符串,比如"查询成功,返回了3条记录,分别是XXX"。结果LLM在下一步推理时经常被这些描述性文字干扰,甚至把"查询成功"这种废话当成数据内容。改成统一返回结构化JSON后,问题立刻消失。
工具返回的数据格式要和LLM向用户的输出格式彻底解耦。工具返回的是机器可读的结构化数据,最终生成阶段才是LLM把它转成人类可读的叙述。这两个阶段一旦混淆,系统的稳定性就会急剧下降。
7.4 日志里缺了"为什么"
刚开始做Trace日志的时候,我只记录了"执行了什么",没记录"为什么执行这个"。结果模型行为异常时,我根本不知道是哪个环节的决策逻辑出了问题。加上Planner的思考过程记录之后,排查效率翻了一倍。
这一步技术上很简单,就是在LLM调用的返回参数里把reasoning字段一起存下来。很多模型的API已经默认返回这部分内容,直接用就行。
7.5 别在没评估前就调Prompt
最后一个坑是关于迭代节奏的。Agent系统涉及大量Prompt和参数,很容易陷入"今天改一下试试,好像好一点,明天再改一下"的低效循环。没有回归集的时候,这种调参行为基本等于盲调,你可能越调越偏。
我现在给自己定的规矩是:所有对系统的影响性修改,必须先在回归测试集上跑一轮,对比通过率变化,通过率不降才能上线。这条规矩一立,系统的稳定性立刻上了一个台阶。
这个内容后续想继续深挖的话,可以从两条线走。一条是做更细的领域拆解,比如加入数值推理工具,让Agent能直接做计算而不仅仅是查静态数据;另一条是做更复杂的多Agent协作,比如把文档检索、数据库查询、报告生成拆成三个独立Agent,各配一个工作记忆,处理更长流程的任务。无论如何,先把单Agent循环做好做稳,后面走哪条线都会顺畅很多。