SAG 结构化增强生成:用 Agent 查询规划突破 RAG 检索瓶颈
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 数据库,都通过统一的接口暴露给 Agent,Agent 不需要关心底层是 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 每一步输出一个"动作"(调用哪个工具、传什么参数),执行后拿到"观察结果",再决定下一步动作,直到得出最终答案。这个模式就是经典的 ReAct(Reasoning + 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_database,SQL 是SELECT product_id, name FROM products ORDER BY price DESC LIMIT 1。返回{product_id: 1024, name: "旗舰款X"}。
第二步:现在要查这个产品的用户评价,评价是文本,存在向量库里。调用search_vector_store,query 是"质量问题 缺陷 故障",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 层:把查询工具注册给 Agent
Agent 层的核心工作是定义工具并注册。前面讲了工具定义的三要素,这里给一个具体的注册示例。
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], where=where, n_results=5) return results["documents"][0] agent = Agent( llm=llm, tools=[ Tool(name="query_sql", func=query_sql, description="执行只读SQL查询..."), Tool(name="search_vector", func=search_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 的能力边界往外推一大截;磨不好,再先进的框架也救不了。