首页 / 资讯中心 / 文章详情

SAG 结构化增强生成:用 Agent 查询规划突破 RAG 检索瓶颈

SAG 结构化增强生成:用 Agent 查询规划突破 RAG 检索瓶颈 ★ FEATURED ARTICLE
1. 从 RAG 的检索瓶颈说起为什么需要 SAG 这套新思路做 RAG 应用的人大概率都经历过这样一个阶段一开始觉得检索增强生成简直是万能药把文档切一切、向量化、塞进向量库用户提问时召回 Top-K 片段丢给大模型答案就出来了。Demo 阶段确实惊艳但一旦进入真实业务场景问题就集中爆发了。最典型的三个症状第一召回不准。用户问上季度华东区的退货率是多少向量检索可能给你召回一堆讲退货政策的段落但真正包含数字的那张表格却没被捞出来。第二多跳问题直接崩。用户问哪个供应商的账期最长他们的联系人是谁这需要先查供应商表找到账期最长的那个再拿它的 ID 去查联系人表向量检索一次性召回根本做不到这种链式推理。第三结构化数据完全使不上劲。企业里大量知识躺在 SQL 数据库里向量库对表格、数值、聚合查询几乎无能为力。这就是 RAG 的核心瓶颈它把检索这件事过度简化成了语义相似度匹配。而真实的问答需求往往需要的是精确查找、条件过滤、聚合计算、多步推理的组合。SAG 这套思路本质上就是冲着这个瓶颈去的。SAG 这个词可以理解为Structured Augmented Generation结构化增强生成也有说法叫SQL-Augmented Generation。不管哪种展开核心思想是一致的在向量检索之外引入结构化查询能力让 Agent 能够根据问题类型自主选择走语义检索还是走结构化查询甚至两者协同。而 OpenViking 在这个体系里扮演的角色是一个把结构化查询、向量检索、Agent 调度整合到一起的开源框架层。我先把结论摆在这SAG 不是要取代 RAG而是给 RAG 补上它天生缺失的那条腿。向量检索擅长模糊的、语义的、非结构化的场景结构化查询擅长精确的、条件的、可聚合的场景。真实业务里这两类需求是混在一起的所以正确的做法是让 Agent 来判断该用哪条路。1.1 向量检索到底在哪些问题上失灵我们具体拆一下向量检索的失效场景这样你才能判断自己的项目要不要上 SAG。场景一数值和聚合查询。用户问今年销售额超过 100 万的客户有几个向量库里存的是文本片段它没法做COUNT(*) WHERE amount 1000000这种操作。你可能会说那我把统计结果也写进文档不就行了问题是数据是动态的今天统计完明天就过期了。场景二精确匹配和枚举。用户问订单号 SO-2024-00871 的状态向量检索会召回一堆订单状态说明的文档但真正那条订单记录它定位不到。因为订单号这种高熵字符串在语义空间里没有区分度。场景三多跳关联。前面举的供应商例子就是典型。这类问题需要 JOIN、需要中间结果传递向量检索的一次性召回范式天然不支持。场景四时间范围和条件组合。上个月、华东区、VIP 客户、退款这四个条件同时满足的记录向量检索只能靠语义近似精度完全没法保证。把这四类场景列出来你会发现它们有个共同点都需要确定性的检索能力。而向量检索提供的是概率性的相似度排序。这不是向量检索做得不好而是它本来就不是干这个的。1.2 SAG 的核心主张让检索路由化SAG 的思路其实很朴素既然问题分类型那检索就该分路由。具体来说一个 SAG 系统在收到用户问题后会先做一次意图分类和查询规划判断这个问题属于哪一类纯语义问题这个产品的设计理念是什么→ 走向量检索纯结构化问题有多少订单未发货→ 走 SQL 查询混合问题评价最差的三个产品的退货政策是什么→ 先 SQL 拿到产品 ID再向量检索对应政策文档多跳问题账期最长的供应商的联系人邮箱→ 拆成多个子查询链式执行这个路由 规划的过程通常由 Agent 来承担。Agent 拿到问题后不是直接检索而是先思考我该怎么查然后调用相应的工具向量检索工具、SQL 执行工具拿到中间结果后再决定下一步。这就是为什么 SAG 和 Agent 是天然绑定的。没有 Agent 的调度能力SAG 就退化成了手动写 if-else 判断走哪条路那还不如老老实实做 RAG。1.3 OpenViking 在 SAG 体系里的定位OpenViking 这个名字从字面和社区讨论来看是一个面向 Agent 的开源框架/工具集它的价值在于把 SAG 需要的几个关键能力封装好了统一的检索抽象层不管是向量库还是 SQL 数据库都通过统一的接口暴露给 AgentAgent 不需要关心底层是 Chroma 还是 PostgreSQL。查询规划与工具调用内置了让 Agent 做多步查询规划的能力支持把复杂问题拆成子查询链。结构化与非结构化数据的桥接能把 SQL 查询结果转成自然语言上下文也能把向量检索结果和结构化数据做关联。我个人的判断是OpenViking 这类框架出现的背景是大家发现纯 RAG 框架比如早期的 LangChain 检索链在处理企业级复杂问答时力不从心需要往Agent 编排 多源检索的方向演进。它和 LangChain4j 的 Easy RAG、Ollama 本地知识库方案是不同层次的工具——后两者更偏快速搭一个能用的 RAG而 OpenViking 这类更偏搭一个能处理复杂查询的检索 Agent。2. 拆解 SAG 的查询规划机制Agent 是怎么决定查哪里的理解了 SAG 要解决什么问题接下来最关键的问题是Agent 到底是怎么做查询规划的这部分如果搞不清楚你搭出来的系统就是个黑盒出了问题根本没法调。2.1 意图识别三种查询路径的判定逻辑查询规划的第一步是意图识别。在实际实现里这一步通常有两种做法基于规则的分类和基于 LLM 的分类。基于规则的做法是写一堆关键词和模式匹配比如问题里出现多少几个统计就走 SQL出现为什么怎么样介绍就走向量检索。这种做法快、便宜、可控但覆盖不全遇到复杂表述就歇菜。基于 LLM 的做法是把问题丢给模型让它输出一个分类标签或者一个查询计划。这种做法灵活但每次都要调模型成本和延迟都上去了。我的经验是两者结合先用规则做一层快速筛选命中明确规则的直接走对应路径规则拿不准的再交给 LLM 做精细判断。这样既保证了常见问题的响应速度又保留了处理复杂问题的能力。具体到三种路径的判定我总结了一个实用的判断表问题特征推荐路径判断依据包含数值、聚合词多少、总计、平均SQL 查询需要精确计算包含精确标识符订单号、ID、编码SQL 查询需要精确匹配包含条件组合时间地区类别SQL 查询需要条件过滤开放式、描述性、概念性向量检索语义相似度有效需要跨表关联多跳 SQL需要 JOIN 或链式查询结构化筛选 语义描述混合路径先 SQL 后向量这张表不是死的实际项目里你需要根据自己的数据特点调整。比如如果你的文档里全是产品说明那产品这个词就不该作为走 SQL 的信号。2.2 查询分解把复杂问题拆成可执行的子查询意图识别之后如果是复杂问题就需要做查询分解。这是 SAG 里技术含量最高的部分。举个具体例子。用户问上个月投诉最多的三个产品的退货率分别是多少这个问题拆解下来是先查上个月每个产品的投诉数量按数量降序取前三 → SQL 聚合查询拿到这三个产品的 ID分别查这三个产品的总销量和退货量算退货率 → SQL 聚合查询把结果组织成自然语言回答这里的关键是子查询之间的依赖关系第 3 步依赖第 2 步的输出。Agent 需要能够维护这个依赖链把上一步的结果作为下一步的输入。在实现上这通常通过Agent 的循环执行机制来实现Agent 每一步输出一个动作调用哪个工具、传什么参数执行后拿到观察结果再决定下一步动作直到得出最终答案。这个模式就是经典的 ReActReasoning Acting循环。我踩过的一个坑是子查询的中间结果格式没统一。比如第一步 SQL 返回的是[{product_id: 1, count: 50}]第二步需要的是product_id列表[1, 2, 3]如果 Agent 不会做这个转换链条就断了。解决办法是在工具定义里明确输入输出格式并且在 Prompt 里给出转换示例。2.3 工具定义给 Agent 一套清晰的武器库Agent 能不能正确规划很大程度上取决于你给它的工具定义得好不好。工具定义包括三部分名称、描述、参数 schema。名称要见名知意比如query_sql_database、search_vector_store、get_table_schema。描述要写清楚这个工具干什么、什么时候用、有什么限制。参数 schema 要明确每个参数的类型和含义。我见过很多项目工具描述写得太简略比如就写个查询数据库结果 Agent 根本不知道该传什么参数或者在不该用的时候用了。好的工具描述应该像这样工具名query_sql_database 描述对业务数据库执行只读 SQL 查询。适用于需要精确统计、条件过滤、 聚合计算的场景。不适用于开放式语义问题。查询前建议先用 get_table_schema 确认表结构。 参数 - sql (string, 必填): 要执行的 SELECT 语句只允许 SELECT - database (string, 可选): 数据库名默认 main 返回查询结果的行列表每行是一个字典这种描述Agent 一看就知道什么时候该用、怎么用。工具描述的质量直接决定了 Agent 规划的质量这一点怎么强调都不过分。2.4 一个完整的查询规划实例我把上面这些串起来走一个完整例子。用户问我们最贵的那个产品的用户评价里有没有提到质量问题Agent 的思考过程第一步这个问题需要先找到最贵的产品这是结构化查询。调用query_sql_databaseSQL 是SELECT product_id, name FROM products ORDER BY price DESC LIMIT 1。返回{product_id: 1024, name: 旗舰款X}。第二步现在要查这个产品的用户评价评价是文本存在向量库里。调用search_vector_storequery 是质量问题 缺陷 故障filter 是product_id 1024。返回若干条评价片段。第三步把评价片段和产品信息组织成上下文交给 LLM 生成最终回答。这个例子里Agent 自动完成了先结构化查询定位实体再向量检索查文本的混合路径。这就是 SAG 相比纯 RAG 的核心优势——它能根据问题的内在结构动态组合不同的检索能力。3. 用 OpenViking 搭一套 SAG 系统的实操路径理论讲完了接下来是动手部分。我用 OpenViking 的思路带你搭一套最小可用的 SAG 系统。这里要说明的是OpenViking 作为一个较新的框架具体 API 可能随版本变化所以我讲的是架构层面的搭建路径具体代码你可以根据实际版本调整。3.1 环境准备与依赖选型先说选型。一套 SAG 系统需要这几块向量库Chroma 适合本地开发Qdrant 适合生产Milvus 适合大规模。个人项目我推荐先用 Chroma零配置起步。关系数据库SQLite 够用PostgreSQL 更稳。如果你的业务数据本来就在 MySQL 或 SQL Server 里直接连过去就行。LLM本地可以用 Ollama 跑云端可以接各家 API。做 Agent 规划建议用能力强的模型因为规划质量直接决定效果。Agent 框架OpenViking 本身或者 LangChain、LlamaIndex 的 Agent 模块。安装上Python 环境建议 3.10 以上因为很多 Agent 框架的新特性依赖较新的语法。依赖管理用 venv 或 conda 都行我个人习惯 venv轻量。python -m venv sag_env source sag_env/bin/activate # Windows 用 sag_env\Scripts\activate pip install openviking chromadb sqlalchemy ollama这里有个坑要提醒向量库和 SQL 库的版本兼容性。有些向量库对 numpy 版本有要求而 SQL 驱动可能又依赖另一个版本装的时候容易冲突。建议先装向量库再装数据库驱动最后装 Agent 框架出问题好定位。3.2 数据层结构化与非结构化的双轨存储SAG 的数据层是双轨的结构化数据进 SQL非结构化数据进向量库。结构化数据就是那些有明确字段的比如订单表、产品表、客户表。这部分直接建表导入就行。关键是要设计好表结构和索引因为 Agent 生成的 SQL 性能全靠这个。常用的查询字段一定要建索引否则 Agent 一个聚合查询就能把数据库拖垮。非结构化数据就是文档、评价、说明这些。这部分要切分、向量化、入库。切分策略很关键我一般按语义段落切每段 300-500 字重叠 50 字。切太碎会丢上下文切太大召回不准。这里有个双轨关联的技巧在向量库的 metadata 里存上对应的结构化 ID。比如产品评价的向量metadata 里存product_id。这样 Agent 做混合查询时可以用 SQL 拿到的 ID 去过滤向量检索实现两边的桥接。# 向量入库时带上结构化 ID collection.add( documents[review_text], metadatas[{product_id: 1024, review_date: 2024-05-01}], ids[review_id] )这个 metadata 设计是 SAG 能不能做混合查询的关键。没有它向量库和 SQL 库就是两个孤岛。3.3 Agent 层把查询工具注册给 AgentAgent 层的核心工作是定义工具并注册。前面讲了工具定义的三要素这里给一个具体的注册示例。from openviking import Agent, Tool def query_sql(sql: str) - list: 执行只读 SQL 查询 # 实际实现里要加 SQL 注入防护 with engine.connect() as conn: result conn.execute(text(sql)) return [dict(row._mapping) for row in result] def search_vector(query: str, product_id: int None) - list: 向量检索可选按 product_id 过滤 where {product_id: product_id} if product_id else None results collection.query(query_texts[query], wherewhere, n_results5) return results[documents][0] agent Agent( llmllm, tools[ Tool(namequery_sql, funcquery_sql, description执行只读SQL查询...), Tool(namesearch_vector, funcsearch_vector, description向量检索...), ] )SQL 注入防护这块必须单独说。Agent 生成的 SQL 是动态的如果直接拼接用户输入风险极大。防护措施有三层第一只允许 SELECT用正则或 SQL 解析器拦截其他语句第二用参数化查询不要字符串拼接第三数据库账号只给只读权限。这三层缺一不可。3.4 调度层ReAct 循环与结果整合调度层就是 Agent 的大脑负责循环执行思考-行动-观察。OpenViking 这类框架一般内置了这个循环你只需要配置好最大迭代次数和终止条件。最大迭代次数很重要设太小复杂问题跑不完设太大可能陷入死循环。我一般设 8-10 次。终止条件是 Agent 输出最终答案或者达到最大次数。结果整合这一步容易被忽视。Agent 拿到多个子查询的结果后需要把它们组织成连贯的上下文交给 LLM。这里要注意结果的去重和排序尤其是向量检索返回的片段可能有重复。我一般会按相关性分数排序然后取 Top-N。还有一个细节中间结果的格式转换。SQL 返回的是结构化行向量检索返回的是文本片段这两者要统一成 LLM 能理解的格式。我通常转成这样的结构[结构化查询结果] 产品ID: 1024, 名称: 旗舰款X, 价格: 8999 [相关文档片段] 片段1相关度0.89: 用户反馈该产品... 片段2相关度0.76: 有用户提到...这种格式清晰LLM 容易理解也方便调试。3.5 实测三类问题的效果对比我拿三类问题做了对比测试结果很能说明问题。第一类纯结构化问题。上个月销售额最高的产品是哪个纯 RAG 基本答不对因为它召回的是产品介绍文档没有销售数据。SAG 走 SQL 路径一次查询搞定。第二类纯语义问题。这个产品的设计理念是什么纯 RAG 和 SAG 效果差不多因为这类问题本来就是向量检索的强项。SAG 在这里没有优势但也没劣势。第三类混合问题。评价最差的产品它的售后政策是什么纯 RAG 直接崩因为它没法先做评价最差这个排序。SAG 先 SQL 排序拿到产品 ID再向量检索售后政策答得又准又全。这个对比说明SAG 的价值在混合问题和结构化问题上纯语义问题用不用 SAG 都行。所以你要评估自己的业务如果大部分问题是纯语义的那 RAG 就够了上 SAG 是过度设计。4. 落地 SAG 时最容易踩的五个坑前面讲的都是应该怎么做这一节讲实际做的时候会怎么翻车。这些都是我在真实项目里踩过的或者看别人踩过的。4.1 坑一Agent 生成的 SQL 跑不动或跑错这是最高频的问题。Agent 生成的 SQL 有两类毛病语法错误和语义错误。语法错误好办执行报错后把错误信息回传给 Agent让它重写。语义错误就麻烦了SQL 能跑但查出来的不是用户想要的。比如用户问最近的订单Agent 可能理解成按创建时间倒序但用户其实想要最近一周的订单。解决办法有两个一是在 Prompt 里给出表结构和字段说明让 Agent 知道有哪些字段可用二是给几个 Few-shot 示例展示常见问题的正确 SQL 写法。我实测下来加了 Few-shot 之后SQL 正确率能提升一大截。还有一个技巧让 Agent 先输出查询计划再输出 SQL。这样你能在它写 SQL 之前就发现理解偏差及时纠正。4.2 坑二向量检索和 SQL 结果对不上混合查询时经常出现 SQL 拿到的 ID 在向量库里查不到对应数据的情况。原因通常是数据同步延迟或者ID 类型不一致。数据同步延迟好理解SQL 库更新了向量库还没更新。解决办法是建立同步机制数据变更时触发向量库更新。ID 类型不一致更隐蔽。SQL 里product_id是整数1024向量库 metadata 里存成了字符串1024过滤时匹配不上。这种问题排查起来很费劲因为两边单独看都没错。建议在入库时就统一 ID 类型全用字符串或者全用整数别混。4.3 坑三Agent 陷入无限循环ReAct 循环如果没有好的终止条件Agent 可能反复调用同一个工具或者在不同工具之间来回横跳。我遇到过一次Agent 连续调了 15 次 SQL每次都是微调一下 SQL 再查就是不给最终答案。解决办法设置最大迭代次数到次数强制终止并返回当前最好结果。另外在 Prompt 里明确告诉 Agent如果已经拿到足够信息就直接回答减少它过度探索的倾向。还有一个隐蔽的原因工具返回的结果太长Agent 处理不过来就反复重试。这时候要限制工具返回的条数比如 SQL 最多返回 50 行向量检索最多返回 5 条。4.4 坑四Prompt 里的表结构信息过期Agent 生成 SQL 依赖 Prompt 里的表结构描述。如果数据库加了字段或改了表名而 Prompt 没更新Agent 就会生成错误的 SQL。这个坑的根源是表结构信息硬编码在 Prompt 里。更好的做法是动态获取表结构提供一个get_table_schema工具Agent 需要时自己查。这样表结构变了Agent 下次查就是最新的。如果非要在 Prompt 里放表结构那就要建立变更同步机制数据库 schema 一变就更新 Prompt。但说实话动态获取更省心。4.5 坑五把 SAG 当成万能药最后一个坑是心态问题。有些人觉得上了 SAG 就能解决所有问答问题结果发现效果还不如精心调优的 RAG。SAG 的适用场景是结构化查询和混合查询占比高的业务。如果你的业务 90% 的问题都是这个文档讲了什么这类纯语义问题那 SAG 带来的复杂度多一套 SQL 库、多一层 Agent 调度、多一堆 Prompt 要维护完全不划算。我的建议是先用 RAG 跑起来统计一下有多少问题答不好其中有多少是结构化或混合类型的。如果这个比例超过 30%再考虑上 SAG。别一上来就追求架构先进能解决问题的架构才是好架构。5. SAG 与现有 RAG 生态的衔接不是推倒重来很多人一听 SAG以为要把现有的 RAG 系统全部推翻重建。完全没必要。SAG 是增量式的你可以在现有 RAG 基础上逐步加能力。5.1 从纯 RAG 平滑演进到 SAG 的路径演进路径我建议分三步走。第一步加一个 SQL 查询工具。保留现有的向量检索链路只是额外给 Agent 注册一个 SQL 工具。这样纯语义问题走原路径结构化问题走新路径。改动小风险低。第二步加查询路由。在检索前加一层意图判断决定走哪条路。这一步开始引入 Agent 调度但可以先做简单的规则路由不急着上 LLM 规划。第三步加多跳查询。等前两步跑稳了再引入查询分解和多步执行。这一步复杂度最高建议放在最后。这个渐进路径的好处是每一步都能独立验证效果出问题也好回滚。我见过一些团队一上来就搞全套 SAG结果各个环节的问题混在一起根本没法定位。5.2 和 LangChain4j Easy RAG、Ollama 本地知识库的关系社区里经常有人问SAG 和 LangChain4j 的 Easy RAG、Ollama 本地知识库是什么关系。我的理解是层次不同。LangChain4j Easy RAG 和 Ollama 本地知识库方案解决的是怎么快速搭一个能用的 RAG它们把文档加载、切分、向量化、检索这条链路封装好了你几行代码就能跑起来。它们的目标是降低 RAG 的入门门槛。SAG 和 OpenViking 解决的是怎么让 RAG 处理复杂查询它们在检索之上加了查询规划和多源调度。它们的目标是提升 RAG 的能力上限。所以这两者不冲突甚至可以组合用 Ollama 做本地 LLM 和向量化用 LangChain4j 做基础 RAG 链路再在上面加 SAG 的查询规划层。我实际项目里就是这么搭的各取所长。5.3 什么规模的业务适合上 SAG最后说说适用规模。我的经验判断个人项目、Demo纯 RAG 足够别折腾 SAG。中小业务、文档问答为主RAG 简单规则路由够用。企业级、结构化数据占比高值得上 SAG收益明显。复杂分析场景、多跳查询多必须上 SAG纯 RAG 根本做不了。判断的核心指标是结构化查询需求的占比。这个占比越高SAG 的价值越大。你可以先统计一周的真实用户问题人工分类一下看看结构化、语义、混合各占多少再决定要不要投入。我个人在实际操作中的体会是SAG 这套东西的技术门槛不在框架本身而在数据层的设计和 Prompt 的调优。框架帮你把调度循环封装好了但表结构怎么设计、工具描述怎么写、Few-shot 示例怎么给这些才是决定效果的关键。这些活儿没有捷径只能根据你的业务数据一点点磨。磨好了SAG 确实能把 RAG 的能力边界往外推一大截磨不好再先进的框架也救不了。
阅读完成 · 觉得有帮助?
咨询建站