☰
企业级RAG+Agent编排:从多表查询到多轮对话的落地实践
2026/10/19 14:04:15 网站建设 项目流程

业务同学在群里问了一个看起来很普通的问题:"上个月华东区的退货率为什么比华北区高?帮我查查原因,顺便翻一下上月退货处理报告。"

这个问题的麻烦在于,它同时牵扯了三件事:先要在订单表和退货表之间做关联计算,再要比对两个区域的数据差异,最后还得去翻一份PDF格式的退货处理报告做归因。传统对话机器人只会查接口,纯大模型只会凭空编一个答案,真正的企业级系统需要把RAG(检索增强生成)和Agent编排结合起来,让模型自己决定什么时候查数据库、什么时候翻文档、怎么把多轮上下文的指代关系搞清楚。

这篇内容就是把我在这类落地项目里踩过的坑、趟过的路完整拆开讲,从多表查询、多文档检索、多轮对话到Agent编排与上线评测,不做概念堆砌,只讲能直接拿去用的方案和教训。

1. 先拆需求:企业级问答为什么不能只靠大模型

1.1 企业数据的三种形态,决定了三种技术路线

我在某零售企业的落地项目里接手的第一天就发现,团队对"RAG"的理解太乐观了。大家以为把PDF丢进向量库、接上大模型就能回答所有问题,结果一测,连"上个月退货率是多少"这种查询都答不对,因为退货率这个指标根本不在文档里,而是在数据库里。

企业数据大体上分三种形态,技术路线完全不同:

数据形态典型来源技术路线主要难点
结构化数据订单表、退货表、库存表、客户表Text-to-SQL,让大模型生成SQL去执行schema理解、多表join、权限控制
半结构化数据Excel报表、CSV导出、内部周报先入库,或做模板化解析格式不稳定、字段不统一
非结构化数据PDF合同、制度文档、质检报告切片+向量检索,即传统RAG切片质量、召回率、表格提取

很多团队犯的第一个错误,就是用一套RAG管道去处理全部数据。实际落地没多久就会意识到,结构化数据必须走"查询生成"的路线,非结构化数据才走"向量检索"的路线,两者在Agent里是两个独立的工具,最后由编排层决定调用哪个。

1.2 一个业务问题,在Agent视角下是四个子任务

把"上个月华东区的退货率为什么比华北区高"这个问题拆开,你会发现它不是一个检索任务,而是一个任务序列。

第一个子任务是参数抽取。需要从问题里抽出时间范围"上个月"、区域"华东区""华北区"、指标"退货率",还要理解退货率这两个字在业务里的定义是"退货数量/销售数量"还是"退货金额/销售金额",定义不同SQL就完全不同。

第二个子任务是数据路由。系统要判断这个问题该查数据库还是该查文档。这里指标计算和差异对比显然是数据库的活,但"为什么高"的归因分析需要去翻文档和质检记录。

第三个子任务是查询生成。大模型需要根据表结构和业务语义生成一条能在线上库执行的SQL,把订单表和退货表按订单号关联起来,再按区域分组算比率。

第四个子任务是文档检索。拿到"华东区退货率偏高"的数据结果后,Agent需要去检索退货处理报告、质检记录这些文档,把数据差异和文档里的原因描述拼成完整答案。

我建议所有人在动手写代码之前,先拿二十个真实业务问题做一遍这个拆解练习。拆完之后你会发现,大多数问题都不是单一工具能解决的,Agent编排不是花架子,而是真实需求。

1.3 先划清边界:哪些场景不该交给Agent

不是所有问题都应该让Agent回答。我见过最惨的一个项目,是让AI直接对接财务报表口径的取数,结果模型把"净利润"和"营业利润"搞混了,财务部门直接叫停了整个项目。

落地前必须和业务方一起定一份"能答/不能答"清单。涉及财务关账数据、法务合同条款解释、员工薪酬这类高风险场景,要么不做,要么必须在回答里附带SQL原文和文档引用来源,方便人工复核。

还有一类场景虽然能答,但要限制频率:比如每天跑一次的固定报表查询,写上缓存,不要每次都用大模型重新生成SQL,既省成本又稳定。RAG和Agent不是炫技,企业级系统的第一原则永远是稳定和可审计。

2. 多表查询的路由与改写:让Agent学会查数据库

2.1 schema感知:没有业务注释的大模型SQL质量很差

刚开始做Text-to-SQL的时候,我以为把数据库的表结构DDL喂给大模型就够了。实测下来,在只有字段名和类型的情况下,大模型生成的SQL连字段含义都经常搞错,更别提业务口径。

最典型的一次,退货表里有个字段叫is_return,直接看名字是"是否退货",但业务实际定义是"1代表退货,0代表正常",模型经常把它当成布尔类型传true。还有一张表叫refund_status,取值有PENDING、APPROVED、REJECTED,没有注释的情况下模型会瞎猜枚举含义。

解决方案是维护一份"语义增强字典",把表、字段、枚举值都解释清楚后拼进prompt:

{ "table": "return_records", "description": "退货记录表,每行代表一笔退货申请", "fields": [ {"name": "id", "description": "退货单号,主键"}, {"name": "order_id", "description": "关联订单表orders.id的外键"}, {"name": "return_qty", "description": "退货数量,单位:件"}, {"name": "return_amount", "description": "退货金额,单位:元"}, {"name": "region", "description": "区域,枚举值:east_china华东 / north_china华北 / south_china华南"}, {"name": "return_reason", "description": "退货原因,枚举值:quality_issue质量问题 / size_mismatch尺码不合适 / no_reason七天无理由"} ] }

这层字典做得越细,SQL生成效果越好。我建议这个字典由业务数据分析师和开发一起维护,因为很多口径只存在于老员工的脑子里,不写进去,大模型永远不知道。

2.2 多表join与更安全的查询生成

多表查询最容易翻车的不是语法,而是表关系。模型不知道return_records.order_id指向orders.id,就可能生成orders.order_id = return_records.order_id这种关联条件,或者干脆做笛卡尔积,数据量大一点的表直接跑死。

所以在schema增强里,我还会放一张关系图描述:

orders(id, order_no, customer_id, region, order_date, total_amount, status) return_records(id, order_id, return_qty, return_amount, return_date, region) 关系: - return_records.order_id = orders.id,一对一或一对多 - orders.customer_id = customers.id join时优先使用orders.id作为关联主键

实测下来,明确写出"用什么字段关联、是几对几关系"之后,SQL的join准确率明显提升,乱关联的情况少了很多。

另外还会在生成SQL的prompt里加一条硬约束:所有联表查询必须先写出join条件和预估值,不允许出现无join条件的多表select。这个约束不能靠模型自觉,要在代码层面做静态检查,发现from后面有多个表但没有join条件,直接判定为非法SQL,触发重新生成。

2.3 把自然语言里的"反话"翻译成SQL条件

业务提问很少像SQL一样直白,他们最爱说"不含华东""除了促销品""大概上个月十号左右"。这些说法如果不做改写,模型生成的SQL经常漏条件。

我做了一张常用改写映射表,喂给大模型当示例:

用户说法SQL语义示例
"不含/不包括X"NOT IN / <>"不含华东区" →region != 'east_china'
"除了促销品"NOT IN(子查询)product_id NOT IN (SELECT product_id FROM promotions)
"全部/所有产品"不加where过滤"所有产品的退货率" → 不要加product筛选
"大概/左右"使用范围或聚合近似"退货率大概多少" → 保留两位小数即可
"上个月"按当前日期回推自然月order_date >= date_trunc('month', CURRENT_DATE - INTERVAL '1 month')等

一个很容易踩的坑是时间表达。用户的"上个月"是自然月,不是"最近30天",很多模型会把order_date >= CURRENT_DATE - 30当成"上个月",这样算出来的退货率口径完全不对。我在prompt里会明确写:涉及月份一律用date_trunc('month', ...)处理,禁止用天数差替代。

还有单位归一化。用户说"退货率是百分之几",数据库里存的可能是0.05这种比例值,生成的SQL就得乘100。这种口径问题,我会直接通过schema说明写进字段的description里。

2.4 权限隔离:行级权限与数据脱敏

企业级系统绕不开权限。不是所有业务同学都能看所有区域的数据,华东的运营不应该查到华南的退货明细。

我的做法是在查询执行层强制加行级过滤。数据库连接使用只读账号,每次会话开始前,把当前用户的可见范围写入会话变量,然后所有生成的SQL都强制附带这一层过滤条件:

-- 假设当前用户只允许查看east_china和north_china的数据 SET app.visible_regions = 'east_china,north_china'; -- 执行前检查SQL,提示模型统一加上 AND region IN (...)

这个检查不依赖大模型自觉,而是在SQL执行前做一次解析,如果发现select的表涉及区域字段但没有带上会话变量里的可见范围,就拒绝执行并触发重新生成。

返回结果也做了脱敏。手机号、身份证、银行卡这些字段在schema描述里标记为sensitive: true,Agent拿到结果后会用掩码处理,业务侧只能看到138****1234这种形式。

2.5 用黄金SQL集做底线评测

Text-to-SQL这个环节不能靠"感觉差不多"来验收。我建了一套黄金SQL集,从真实工单选了50个高频业务问题,每个问题标注标准SQL、期望返回字段和正确结果。

评测分三层看。第一层是SQL可执行率,生成出来能不能在数据库里跑通,跑不通的直接算失败。第二层是结果一致性,把生成SQL执行出来的结果和标准SQL的结果做对比,看行数和关键数值是否一致。第三层是结构合理性,检查是否有多余的全表扫描、是否缺少应有的过滤条件。

我们当时给自己定的底线是:可执行率不低于95%,结果一致性不低于80%。低于这个数不要谈上线,先在schema增强和prompt示例上继续调。实测加了一轮few-shot示例之后,结果一致性从65%涨到82%,效果非常明显。

3. 多文档检索的切片与融合:让Agent学会翻文档

3.1 切片策略:固定长度是最稳的起点,但不是最优

多文档RAG第一个坑就是切片。团队初期图省事,把所有PDF按固定500字符切,结果文档里的表格被拦腰截断,语义全丢了。后来我用的是"固定长度+结构感知"结合的策略。

基础做法是:正文用500 token左右切片,切片之间保留50 token重叠;遇到有标题层级的地方优先按章节切,不让一个章节的内容被切开。表格是最大的问题,直接把表格文本化送到向量库里,召回效果很差,我一般先把表格转成markdown格式保留行列结构,再作为独立切片单独索引。

页眉页脚、版本声明这类噪声要在预处理阶段去掉,否则检索时会频繁命中一些无意义片段。有一个供应商合同文档,页脚里公司名出现了几百次,第一次检索时前五条命中的全是页脚,加了一步去重和规则过滤才解决。

3.2 混合检索:向量不是唯一答案

纯向量检索在专业文档上表现不稳定。合同里"违约金"和"违约责任"是两个不同表述但语义相近,这是向量的强项;但"合同编号HC-2023-001"这种精确字符串,向量检索几乎找不到,BM25却一找一个准。

所以生产环境我用的都是混合检索:BM25和向量召回各取top 20,做分数归一化后按权重合并,再取top 5送进重排模型。权重我一般设置向量0.6、BM25 0.4,具体调参可以在验证集上用hit@5指标来回测。

重排模型这一层不能省。便宜的方案是用 bge-reranker-base,效果更好的用 cross-encoder,实测重排之后命中率能再涨5到10个百分点。代价是耗时增加,但如果并发压力不大,这笔成本值得花。

3.3 多文档场景的文档路由

当知识库里有几百份文档时,直接把所有文档切片混在一个集合里检索,效果会迅速退化,因为语义相近的内容会互相干扰。比如退货政策、客户投诉处理流程、质检标准三份文档,都聊"退货"但实际内容完全不一样。

我在多文档场景里加了一道"文档路由":先为每份文档建立摘要和目录信息,第一次检索目标锁定在"应该查哪个文档",而不是直接在几十万个切片里大海捞针。用户问"退货处理时限是几天",先匹配到《退货管理制度》这份文档,再在这个文档的切片子集里检索。

这个思路跟人找资料很像:先知道去哪个文件夹找,再打开具体文件精读。实现上也不复杂,文档的标题、摘要、一级目录单独建一个小索引,路由判断可以用大模型做一次分类,也可以用embedding相似度先捞候选文档。

3.4 检索不到时需要的是拒绝,不是编造

RAG最危险的时刻不是检索不到,而是检索到了无关内容但模型硬要答。我在上线前专门测了一批"知识库之外"的问题,比如问某个根本没发布过的新政策,模型在最开始会试图编一个看起来合理的答案。

后来的处理方式是:检索结果的相似度分数要过一个阈值,低于阈值时不进入生成环节,Agent直接回答"知识库里没有找到相关内容,建议联系XXX部门确认",并附上当前检索到的近似文档标题,方便用户判断是不是换个问法。

这个"拒绝"机制极其重要。企业场景里,给出一个错误的制度解释,比不给答案严重得多。敢说不知道,是系统成熟的表现。

4. 多轮对话的上下文管理:让Agent记住刚才聊了什么

4.1 会话状态的三层存储

多轮问答比单轮难在状态管理。我通常把会话状态拆成三层:短期层用Redis存最近十轮对话原文,中期层把每轮问答做摘要后向量化存进记忆库,长期层存用户偏好,比如"这个用户常看华东区数据""他用万元作为金额单位"。

短期层直接服务当轮问答,中期层用来做指代消解和个性化回答,长期层是跨会话的。比如用户第二周回来说"还是看华东的退货率",系统能认出他上次关注的区域。

注意一点,记忆是双刃剑。长期记忆一旦错了,用户说"华东"其实是想看"华北",Agent会被旧记忆带偏。所以我们只把明确的、经过用户确认过的信息写入长期记忆,其他信息最多放中期。

4.2 Query改写与指代消解

多轮问答最核心的一步是Query改写。用户说"那上个月呢?",单独拿出来没有检索价值,必须结合前文补全成"上个月华东区退货率是多少"。

我用的是一段改写prompt:

你是对话系统的查询改写器,根据对话历史和当前用户问题,生成一条完整的独立查询。 要求: 1. 补全所有指代词:"它"、"这个"、"那边"等指代对象必须还原 2. 补全缺失的时间、区域、指标 3. 保持原问题的业务含义不变 4. 如果当前问题不需要改写,直接输出原问题 对话历史: 用户:上个月华东区的退货率是多少? 助手:上个月华东区退货率是3.2%,比前月上升了0.5个百分点。 当前问题:那华北区呢? 输出:

这里有一个容易翻车的细节:改写要发生在检索之前。如果先拿"那华北区呢?"去检索,再拿改写后的query去检索,会浪费一次计算且可能污染上下文。正确的顺序永远是先改写当前问题,再用改写后的完整问题去做路由、生成SQL或检索。

4.3 Token控制:不是把全部历史塞给模型

最初我们图省事,把最近十轮对话全部拼进prompt,结果token消耗大不说,模型注意力被分散,还容易被早期错误信息干扰。

现在我的策略是:最近三轮原文完整保留,更早的对话只保留摘要。摘要由模型在每轮结束后异步生成,包含问题、答非所问的情况、用户的偏好信息。这样传入上下文基座的是"最近对话+历史摘要"的组合,既保住多轮关联性,又控制长度。

另外,当用户明确说"不是问那个,重新来"之类的转移信号时,Agent要能清空短期上下文,不让之前的指代关系继续污染后续问题。这个能力Scott不如做不到,但企业场景里必须做。

4.4 多轮问答的典型失败与修法

我调试时发现,多轮问答最大的失败点是"修正"场景。用户先问"华东区退货率",Agent答了,用户又说"我要的是金额不是数量",如果Agent不理解这是修正,就会把"金额"当成一个新问题去检索,答非所问。

修法是显式识别修正意图。在改写prompt里加一条规则:如果当前问题包含"我说的是""不是""改成"这类词,优先处理修正关系,把用户要修正的对象和原始问题的同类结构匹配。另外,当Agent发现自己上一轮的SQL口径有误时,要主动道歉并重新执行查询,而不是辩解。

5. Agent编排与全流程串联:从单次问答到复杂任务

5.1 工具定义与路由判断

Agent层我把能力收敛成四个工具:query_database查结构化数据、search_documents查文档、calculate做简单计算和单位换算、escalate_to_human转人工。每个工具都有清晰描述,给大模型做函数调用时用。

路由判断是第一步。一个问题是查数据库还是查文档,可以用一条指令让模型选:

根据用户问题,判断应该调用哪个工具: - 涉及具体数值、统计、对比、趋势问题:query_database - 涉及制度、流程、合同条款、报告解读问题:search_documents - 两者都要:"先查数据,再检索文档归因" - 超出能力范围:escalate_to_human

实测这条路由方法在大多数场景够用。但如果并发量大,我建议接一个轻量意图分类模型先做好工具选择,只有分类置信度低的时候才交给大模型判断,成本更低、响应更稳定。

5.2 复杂任务的拆解与中间结果传递

复杂问题需要多个工具配合。比如"分析华东区退货率为什么比华北区高,并列出主要原因",Agent的工作流是:先用query_database拿到两个区域的退货率数据,再用search_documents检索退货处理报告和质量报告,最后把数据表格和文档要点拼成分析结论。

中间结果怎么传很关键。我使用的是结构化传递:SQL工具返回结果为JSON(包含数字和表格摘要),检索工具返回文档引用片段,写答案时模型只基于这些结构化结果生成,不允许自己脑补外部知识。

我们还在这一步做了trace日志,每一轮问答都记录:用户问题、改写后query、路由结果、工具调用链、检索到的文档ID、最终答案。这个日志既是评测的数据来源,也是出问题之后排查的救命稻草。

5.3 反思机制:让Agent从SQL错误中自愈

SQL生成不可能一次成功。线上最常见的错误是字段不存在、函数用错、语法不兼容。我加了反思重试机制:SQL执行报错时,把错误信息原样返回给模型,让它根据错误修正SQL,最多重试两次。

这个机制的实现很简单,但效果很好。最开始错误信息只是"执行失败",模型无从下手;把数据库返回的具体报错如column "return_time" does not exist反馈回去后,模型就能自己改成正确的字段名。

还要注意一种特殊情况:重试两次仍然失败时,不要第三次重试,直接走"转人工"流程,告诉用户"这个问题我暂时无法查询,已记录反馈"。无限重试不仅浪费token,还会让用户等待时间过长。

5.4 人工复核与安全阀

Agent权限上,数据库连接账户始终是只读的,物理上就不允许执行任何insert、update、delete。查询结果高风险时,比如涉及财务数字、法律条款、客户隐私,系统会强制附加"该回答由AI生成,建议人工复核"的提示,并在前端提供一键转人工入口。

另外所有Agent的调用记录都会写入审计日志,包含用户身份、完整对话、工具调用参数、返回结果。这部分不是可选项,是安全底线。

6. 上线前的评测与性能优化:交给业务前的最后一公里

6.1 评测数据集怎么建

我会从两部分收集评测问题:一是线上IM工单里真实用户问过的问题,二是业务方整理的高频知识问答。目标是50到100条,必须覆盖单表查询、多表join、多文档检索、多轮指代、纠错修正这几个类别。

每条记录标注:标准答案、答案来源(哪张表哪个字段,或哪份文档哪一页)、期望的工具链路。比如一条评测里可以写"期望调用query_database,且SQL必须包含region和return_qty字段"。

这个评测集不仅要用来做上线验收,还要在每次换模型、调prompt之后回归一遍。没有评测集的项目,改一次prompt全靠肉眼感受,根本不可持续。

6.2 三个核心指标:准确率、召回率、多轮成功率

端到端准确率是业务最关心的,我建议用"关键事实匹配"来评:不要求模型逐字一致,但答案里的关键数字、区域、时间必须和标准答案一致。比如"退货率3.2%"这个数字错了一点点,就算错。

检索召回率(hit@5)单独评:标准答案所指向的文档/表格,是否出现在检索结果的前5条里。这个指标反映的是检索质量,不掺生成环节的影响,能帮你定位到底是"没检索到"还是"检索到了但生成不好"。

多轮成功率评的是:在第N轮用户给出指代或修正时,系统是否理解对了并给出正确回答。这个指标一开始肯定难看,我们当时只有55%,逐步优化到70%多,再多就要靠产品话术引导用户把问题问清楚。

6.3 性能与成本优化

性能优化先看瓶颈在哪。如果慢在向量检索,上索引量化(如HNSW的M参数、efSearch)能显著提速;如果慢在大模型生成,优先控制prompt长度、减少重试次数;如果慢在重排模型,可以只对top 10重排取top 3,不必对所有候选重排。

成本优化上最重要的一招是缓存。相同或高度相似的query,在30分钟窗口内命中缓存直接返回结果。当时我们用embedding相似度判重,相同问题重复率达到20%以上,缓存节省的成本相当可观。纯计算型问题缓存价值更高,因为SQL结果本来就是确定性的。

向量库选型上,数据量在百万级以下,用开源版本完全够,配置简单、部署轻;亿级以上再考虑分布式方案。不要一开始就上最重的架构,企业项目最怕过度设计。

6.4 实际效果与剩余风险

在这个项目后期跑稳定后,单表查询的SQL生成准确率能到91%,多表join降到78%,文档检索hit@5约85%,多轮指代成功率70%,端到端人工综合评分4.5分(满5分)。这个成绩可以交给业务试用,但离"全自动没人管"还差得远。

剩余的坑也很清楚:复杂多跳推理仍然不稳,用户问题里超过两层逻辑就容易答偏;涉及多个数据源的交叉校验容易出错;权限和数据质量问题会让SQL生成再难一点。这些风险控制的方式就是:高风险场景保留人工复核入口,系统明确告知能力边界,不承诺100%准确。

最后说几点我自己在多个项目里反复验证过的体会。第一,先跑通单轮,再做多轮;先做好单表,再碰多表,顺序别反。很多问题其实是单表都没做好就急着上Agent编排,结果崩在基础层。第二,schema增强和黄金评测集是投入产出比最高的两件事,比换任何模型都见效快。第三,上线不是终点,每天抽20条线上日志回头检查,把错误样本回流到评测集,一路迭代,准确率才会稳中有升。

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

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

立即咨询