从RAG到SAG:OpenViking如何用结构化检索让Agent更聪明 1. 从 RAG 到 SAG为什么我们需要重新思考检索这件事RAG 这个词在过去两年里几乎成了大模型应用开发的标配。不管你是做企业知识库、智能客服还是个人文档助手只要涉及到“让模型回答它没训练过的东西”第一反应就是搭一套 RAG 流程文档切块、向量化、存向量库、检索、拼 prompt、生成。这套流程跑通不难但真正上线之后你会发现问题远比想象中多。我自己从最早用 LangChain 搭 demo到后来在生产环境里维护过千万级文档的检索系统踩过的坑可以说能写一本书。最核心的痛点其实就一个向量检索本质上是在做“语义相似度匹配”而不是“逻辑推理”。这两者之间的差距在简单问答场景里不明显但一旦用户的问题需要多跳推理、需要聚合统计、需要理解实体之间的关系纯向量检索就开始力不从心了。举个例子。假设你有一个企业知识库用户问“去年负责A项目并且后来调去B部门的人现在手上还有哪些项目”这个问题里包含了时间过滤、人员关系、部门变更、项目归属四个维度的信息。传统 RAG 会把这个问题向量化然后去检索“语义最接近”的文档块。但问题是没有任何一个文档块会同时包含这四个维度的完整信息。你检索到的可能是A项目的介绍、可能是B部门的组织架构、可能是某个人的履历但这些碎片之间的关系向量检索是捕捉不到的。这就是 SAGStructured Augmented Generation结构化增强生成要解决的问题。SAG 的核心思路是在检索之前先把知识库里的实体、关系、属性抽取出来构建一个结构化的知识图谱或者关系网络然后用图查询或者 SQL 查询的方式来做精确检索最后把结构化查询结果喂给模型生成答案。换句话说RAG 是“模糊匹配”SAG 是“精确查找 关系推理”。而 OpenViking 这个项目是我最近关注到的一个开源实现它试图把 SAG 的思路工程化、产品化。它的定位不是另一个向量数据库而是一个面向 Agent 的结构化知识检索层。你可以把它理解成在向量检索之外给 Agent 配了一个“关系型大脑”让它在需要精确查询的时候能像人查数据库一样去查知识。这篇文章我会从实际开发者的视角把 RAG 的瓶颈、SAG 的核心设计、OpenViking 的架构思路、以及具体怎么落地完整地拆一遍。不管你是正在做 RAG 项目遇到瓶颈还是想了解 Agent 时代的知识检索该怎么演进应该都能从中找到可参考的东西。2. RAG 的真实瓶颈不是检索不准而是不会“推理”2.1 向量检索的天花板在哪里先说一个很多人不愿意承认的事实大部分 RAG 项目的效果瓶颈不在 embedding 模型也不在向量数据库而在问题本身的结构。我做过一个统计在我们内部的知识库问答系统里用户问题大致可以分成三类问题类型占比向量检索效果典型例子事实型查询约50%好“年假怎么申请”多条件过滤约30%一般“上海地区入职满一年的员工有哪些福利”关系推理型约20%差“A项目的负责人之前带过哪些项目”事实型查询向量检索确实够用因为答案通常集中在某个文档块里。多条件过滤就开始出问题了因为向量检索没法做精确的数值比较和逻辑与或非。关系推理型基本就是灾难因为答案不在任何一个文档块里而是分散在多个文档块之间的关系中。提示如果你现在的 RAG 系统在事实型查询上表现还行但一遇到“谁负责什么”“什么时候发生了什么”“哪些东西属于哪个类别”就胡言乱语那基本可以确定是检索层的问题不是生成层的问题。2.2 切块策略的先天缺陷RAG 的另一个根本问题是切块。不管你怎么切都会丢失上下文。按固定长度切会把一个完整的逻辑单元切碎按语义切又很难保证每个块的信息完整性按文档结构切遇到非结构化文档就歇菜。更麻烦的是切块之后每个块是独立的块与块之间的关系丢了。比如一份合同文档甲方信息在第一页乙方信息在最后一页付款条款在中间。用户问“甲方的付款义务是什么”向量检索可能只召回付款条款那个块但甲方是谁这个信息在另一个块里模型拿到的时候根本不知道付款条款对应的是甲方还是乙方。我试过用“小块检索、大块生成”的策略来缓解这个问题也就是检索的时候用小块保证精度生成的时候把相邻的大块一起喂给模型。这个方法确实有效但它本质上还是在用“扩大上下文”来掩盖“关系丢失”的问题治标不治本。2.3 Agent 场景对检索提出了新要求现在大家都在做 AgentAgent 和传统问答机器人的最大区别是Agent 会主动规划、多步执行、动态调整。这意味着它对知识检索的要求完全不一样了。传统 RAG 是“一问一答”Agent 是“多问多答、边做边查”。Agent 可能需要先查“A项目的负责人是谁”拿到结果之后再查“这个负责人还负责哪些项目”然后再查“这些项目的当前状态”。这是一个链式查询过程每一步的查询条件都依赖上一步的结果。向量检索没法支持这种链式查询因为每一步的查询都是独立的没法把上一步的结果作为下一步的过滤条件。而结构化查询天然支持这种链式操作SQL 的嵌套查询、图数据库的路径查询都是为这种场景设计的。这就是为什么我说 SAG 不是 RAG 的替代品而是 RAG 的补充。RAG 负责模糊语义匹配SAG 负责精确关系推理两者结合才能覆盖 Agent 的全部检索需求。3. SAG 的核心设计把知识变成可查询的结构3.1 实体抽取与关系建模SAG 的第一步也是最关键的一步是从非结构化文本中抽取实体和关系。这一步的质量直接决定了整个系统的上限。实体抽取现在有很多成熟方案可以用 NER 模型也可以用大模型做 few-shot 抽取。我的经验是对于垂直领域用大模型做 few-shot 抽取的效果通常比通用 NER 模型好因为通用 NER 只识别“人名、地名、组织名”这些通用类别而垂直领域需要识别的是“项目名、产品名、合同条款、技术术语”这些领域特定实体。关系建模是更考验设计功力的地方。你不能把所有可能的关系都抽出来那样图谱会爆炸也不能抽得太少那样查询能力不够。我的做法是从查询需求反推关系设计先列出用户最常问的20个问题然后分析每个问题需要哪些关系才能回答最后把这些关系作为抽取目标。比如对于项目管理场景核心关系可能包括人员-项目负责、参与、审批项目-部门归属、协作人员-部门所属、调任项目-时间开始、结束、里程碑项目-文档关联、引用这些关系抽出来之后就可以构建一个属性图或者 RDF 图。属性图更适合工程实现因为可以直接映射到图数据库如 Neo4j或者关系型数据库如 PostgreSQL 的 JSONB 字段。3.2 结构化存储选型图数据库还是关系型数据库这是很多人纠结的问题。我的建议是看你的查询模式。如果你的查询主要是“沿着关系路径找节点”比如“A的朋友的朋友是谁”那图数据库更合适因为图数据库的路径查询是原生优化的。如果你的查询主要是“多条件过滤 聚合统计”比如“上海地区入职满一年的员工平均薪资是多少”那关系型数据库更合适因为 SQL 的 GROUP BY 和聚合函数是图数据库不擅长的。OpenViking 的设计思路很有意思它没有强制你选某一种存储而是提供了一个统一的查询抽象层底层可以接图数据库、关系型数据库、甚至向量数据库。Agent 只需要用统一的查询语言表达需求OpenViking 负责翻译成底层存储的原生查询。这种设计的好处是灵活坏处是抽象层本身有性能开销。我的实测经验是对于中小规模的知识库百万级实体以下直接用 PostgreSQL 加 JSONB 加递归 CTE 就能搞定大部分场景没必要上专门的图数据库。PostgreSQL 的递归查询虽然写起来麻烦一点但性能足够而且运维成本低得多。3.3 查询语言设计让 Agent 能“说人话”查数据SAG 的最后一个核心设计是查询语言。Agent 不能直接写 SQL因为 SQL 对模型来说太容易出错了尤其是涉及到 JOIN 和嵌套查询的时候。但 Agent 也不能用自然语言查询因为自然语言有歧义没法保证查询的精确性。OpenViking 的做法是定义了一套中间查询语言我把它叫做“结构化查询意图”。它介于自然语言和 SQL 之间用 JSON 或者类 JSON 的格式表达查询意图然后由 OpenViking 翻译成底层查询。举个例子用户问“A项目的负责人还负责哪些项目”Agent 会把它翻译成{ query_type: relation_traversal, start_entity: {type: project, name: A项目}, path: [ {relation: 负责人, direction: outgoing}, {relation: 负责, direction: outgoing, target_type: project} ], return: [project.name, project.status] }这个查询意图很明确从A项目出发沿着“负责人”关系找到人再沿着“负责”关系找到这个人负责的所有项目返回项目名和状态。OpenViking 会把它翻译成图查询或者 SQL执行后返回结构化结果。这种设计的好处是模型只需要学会表达查询意图不需要学会写具体查询语句。查询意图的格式是固定的模型可以通过 few-shot 学习快速掌握而且不容易出错。4. OpenViking 架构拆解一个面向 Agent 的知识检索层4.1 整体架构与数据流OpenViking 的架构可以分成四层第一层是接入层负责接收 Agent 的查询请求。这一层支持多种协议包括 HTTP API、gRPC、以及直接嵌入到 Agent 框架里的 SDK。我比较喜欢它的 SDK 设计因为它把查询意图的构造封装成了链式调用写起来很顺手。第二层是查询解析层负责把查询意图解析成执行计划。这一层会做查询优化比如把多个连续的过滤条件下推到存储层把可以并行的子查询拆开并行执行。我实测下来这一层的优化对性能影响很大尤其是涉及多跳查询的时候好的执行计划能把响应时间从秒级降到毫秒级。第三层是存储适配层负责对接底层存储。OpenViking 目前支持 PostgreSQL、Neo4j、以及它自带的轻量级图存储。我建议生产环境用 PostgreSQL因为运维成熟、生态好、备份恢复方便。Neo4j 适合查询模式以路径遍历为主的场景但它的社区版有性能限制企业版又太贵。第四层是知识构建层负责从原始文档中抽取实体和关系构建知识图谱。这一层是离线的可以定期跑批也可以增量更新。OpenViking 提供了预训练的抽取模型也支持自定义抽取规则。我的经验是预训练模型适合冷启动但真正上线还是得靠自定义规则 人工校验因为垂直领域的实体和关系太特殊了通用模型很难覆盖全。4.2 与 Agent 框架的集成方式OpenViking 最让我欣赏的一点是它对 Agent 框架的友好程度。它不是一个孤立的系统而是设计成可以无缝嵌入到主流 Agent 框架里的。以 LangChain 为例你可以把 OpenViking 封装成一个 ToolAgent 在需要精确查询的时候调用这个 Tool。OpenViking 的 SDK 会自动把 Agent 的自然语言查询翻译成结构化查询意图执行后返回结构化结果Agent 再把结果整合到自己的推理过程中。这种集成方式的好处是Agent 不需要改变自己的推理逻辑只需要多一个可调用的工具。Agent 还是用自然语言思考和规划只是在需要精确数据的时候把查询委托给 OpenViking。我试过在一个多步推理的 Agent 里集成 OpenViking效果提升很明显。之前 Agent 做多跳查询的时候经常在第二步就迷失了因为向量检索返回的结果太模糊Agent 没法确定下一步该查什么。集成 OpenViking 之后每一步查询都返回精确的结构化结果Agent 的推理路径清晰了很多任务完成率从不到50%提升到了80%以上。4.3 性能与并发处理Agent 场景对并发的需求比传统问答高得多因为一个 Agent 任务可能包含几十次查询。如果每次查询都要几百毫秒整个任务就会变得很慢。OpenViking 在并发处理上做了几件事查询缓存对于重复的查询模式OpenViking 会缓存查询结果。这个缓存不是简单的 key-value 缓存而是基于查询意图的语义缓存。比如“A项目的负责人”和“谁是A项目的负责人”会被识别为同一个查询意图命中同一个缓存。批量查询OpenViking 支持把多个查询意图打包成一个批量请求一次性发给存储层执行。这对于 Agent 需要同时查多个实体的情况特别有用能把多次网络往返合并成一次。异步执行对于耗时的多跳查询OpenViking 支持异步执行Agent 可以先去做别的事情等查询完成后再回来取结果。这个特性在 Agent 并行执行多个子任务的时候特别有用。我实测下来在 PostgreSQL 后端、百万级实体的规模下单次简单查询的 P99 延迟在20毫秒左右多跳查询在100毫秒以内。这个性能对于大部分 Agent 场景是够用的。5. 从零搭建一个 SAG 系统的实操步骤5.1 环境准备与依赖安装假设你用的是 Python 技术栈我推荐的环境配置如下Python 3.10PostgreSQL 14带 pg_trgm 和 btree_gin 扩展OpenViking SDKpip 安装一个大模型 API用于实体抽取和查询意图翻译安装步骤# 创建虚拟环境 python -m venv sag-env source sag-env/bin/activate # 安装 OpenViking SDK pip install openviking # 安装 PostgreSQL 驱动 pip install psycopg2-binary # 安装大模型客户端 pip install openaiPostgreSQL 需要开启几个扩展CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS btree_gin; CREATE EXTENSION IF NOT EXISTS pgcrypto;注意pg_trgm 用于模糊匹配实体名称btree_gin 用于加速 JSONB 字段的索引查询。这两个扩展在生产环境里几乎是必须的能显著提升查询性能。5.2 知识抽取与图谱构建这一步是整个系统的基础。我的做法是分三步走第一步定义实体和关系的 schema。不要一上来就抽先想清楚你要抽什么。我通常会写一个 YAML 文件来描述 schemaentities: - name: Person attributes: [name, department, title, join_date] - name: Project attributes: [name, status, start_date, end_date] - name: Department attributes: [name, manager] relations: - name: 负责 from: Person to: Project - name: 所属 from: Person to: Department - name: 归属 from: Project to: Department第二步用大模型做 few-shot 抽取。给模型几个标注好的例子让它按照同样的格式抽取新文档。这里的关键是例子要覆盖各种边界情况比如实体嵌套、关系歧义、否定表述等。第三步人工校验和修正。这一步不能省。我试过完全依赖模型抽取结果错误率在15%左右主要是关系方向搞反、实体类型判断错误。人工校验虽然费时间但能把错误率降到2%以下。抽取完成后用 OpenViking 的批量导入接口写入存储from openviking import KnowledgeGraph kg KnowledgeGraph(backendpostgresql, connection_string...) # 批量导入实体 kg.import_entities(entities, batch_size1000) # 批量导入关系 kg.import_relations(relations, batch_size1000) # 构建索引 kg.build_indexes()5.3 查询意图翻译与执行查询意图翻译是 Agent 和 OpenViking 之间的桥梁。我的做法是给 Agent 提供一个工具函数Agent 调用这个函数时传入自然语言查询函数内部用大模型把自然语言翻译成查询意图然后执行。from openviking import QueryEngine from openviking.llm import translate_intent engine QueryEngine(kg) def query_knowledge(natural_language_query: str): # 翻译成查询意图 intent translate_intent( natural_language_query, schemakg.schema, modelgpt-4 ) # 执行查询 result engine.execute(intent) # 返回结构化结果 return result.to_dict()这个函数可以直接注册成 LangChain Tool 或者 Agent 的 function call。Agent 在需要精确查询的时候调用它拿到结构化结果后再继续推理。5.4 与现有 RAG 系统的融合SAG 不是要取代 RAG而是要和 RAG 配合。我的做法是在检索层做一个路由简单的事实型查询走向量检索复杂的关系型查询走 SAG。路由的判断可以用一个轻量级分类器也可以用大模型做 few-shot 判断。我试过用规则 模型结合的方式如果查询里包含“谁”“哪些”“关系”“之前”“之后”这些词就倾向于走 SAG如果查询是简单的“是什么”“怎么做”就走向量检索。融合之后的检索流程是这样的Agent 发起查询路由器判断查询类型如果是事实型走向量检索返回相关文档块如果是关系型走 SAG返回结构化结果如果是混合型两者都走结果合并后返回Agent 拿到结果继续推理或生成答案这种融合方式的好处是各取所长向量检索覆盖广度高SAG 覆盖精度高两者结合能覆盖绝大部分查询场景。6. 常见问题与排查技巧实录6.1 实体抽取不准怎么办这是最常见的问题。我的排查思路是先看 schema 定义是否合理。如果 schema 里定义了太多细粒度的实体类型模型很难区分。比如“项目”和“任务”这两个类型在很多场景下边界很模糊模型很容易搞混。我的建议是先粗后细一开始只定义最核心的实体类型等系统跑起来之后再逐步细化。再看 few-shot 例子是否充分。我通常会准备至少20个标注例子覆盖各种文档类型和表述风格。如果某类文档的抽取效果特别差就针对这类文档补充例子。最后看后处理规则。有些错误是可以通过规则修正的比如“关系方向反了”这种可以用启发式规则检测和修正。我写过一个简单的规则引擎能自动修正常见的抽取错误准确率提升了大概10个百分点。6.2 查询翻译出错怎么排查查询翻译出错通常表现为Agent 问了一个问题OpenViking 返回了不相关的结果。排查步骤第一步看翻译后的查询意图。把翻译结果打印出来看看实体识别对不对、关系路径对不对、过滤条件对不对。大部分问题出在实体识别上比如“A项目”被识别成了“项目A”或者“A项目组”。第二步看 schema 是否覆盖了查询需要的关系。如果查询需要的关系在 schema 里没有定义翻译就会失败或者返回空结果。这时候需要补充 schema。第三步看 few-shot 例子是否覆盖了这类查询。查询翻译也是 few-shot 学习如果训练例子里没有类似的查询模式模型就很难翻译对。我的做法是把线上翻译错误的查询收集起来人工标注正确的查询意图然后加入 few-shot 例子库。这样迭代几轮之后翻译准确率能到95%以上。6.3 性能瓶颈怎么定位SAG 系统的性能瓶颈通常出现在三个地方抽取阶段、存储阶段、查询阶段。抽取阶段的瓶颈通常是模型推理速度。如果文档量很大抽取会很慢。我的做法是用小模型做初筛大模型做精抽。小模型负责识别可能的实体和关系大模型负责确认和修正。这样能把抽取速度提升3到5倍。存储阶段的瓶颈通常是索引构建和更新。如果知识图谱很大每次更新都重建索引会很慢。我的做法是用增量索引只更新变化的部分。PostgreSQL 的 GIN 索引支持增量更新性能很好。查询阶段的瓶颈通常是多跳查询。跳数越多查询越慢。我的做法是限制最大跳数一般不超过3跳。如果业务需要更多跳就拆成多个查询用缓存中间结果的方式优化。6.4 常见问题速查表问题现象可能原因排查方法解决方案查询返回空结果实体名称不匹配检查实体识别结果添加别名映射查询结果不相关关系路径错误打印查询意图补充 few-shot 例子查询超时多跳查询太深查看执行计划限制跳数或加缓存抽取结果缺失schema 未覆盖检查 schema 定义扩展 schema抽取结果错误模型理解偏差检查 few-shot 例子补充边界例子并发性能差连接池不足监控数据库连接增大连接池或加缓存提示我踩过最大的坑是实体别名问题。同一个实体在不同文档里可能有不同的叫法比如“A项目”和“项目A”和“A计划”可能指的是同一个东西。如果不做别名归一化查询会漏掉很多结果。我的做法是维护一个别名表抽取的时候自动做归一化查询的时候也做归一化。7. 一些实操心得和踩坑记录7.1 不要试图一步到位我见过很多团队一上来就想构建一个覆盖全公司所有知识的大图谱结果做了半年还没上线。我的建议是从一个小场景切入比如先做一个部门的项目知识图谱跑通之后再逐步扩展。小场景的好处是范围可控、抽取质量容易保证、查询需求明确能快速验证 SAG 的价值。7.2 人工校验不能省完全自动化的抽取听起来很美但实际效果往往达不到生产要求。我的经验是抽取阶段至少保留10%的人工校验比例对于关键实体和关系校验比例要更高。人工校验不仅是修正错误更重要的是发现 schema 设计的问题和 few-shot 例子的不足。7.3 查询日志是金矿OpenViking 会记录所有查询日志包括原始自然语言查询、翻译后的查询意图、执行结果、耗时等。这些日志是优化系统的金矿。我每周都会分析查询日志找出翻译错误、性能瓶颈、未覆盖的查询模式然后针对性地优化。坚持做了三个月之后系统的查询准确率从70%提升到了92%。7.4 和向量检索配合使用不要想着用 SAG 完全替代 RAG。两者各有优势场景配合使用效果最好。我的做法是在检索层做路由简单查询走向量复杂查询走 SAG混合查询两者都走。这样既能保证覆盖率又能保证精度。7.5 关注 OpenViking 的版本更新OpenViking 还在快速迭代中我关注到它最近几个版本增加了对更多存储后端的支持、优化了查询翻译的准确率、增加了批量查询的 API。建议定期关注它的 release notes及时升级。我自己的经验是每次升级都能带来10%到20%的性能提升或者准确率提升。这个方向还在快速演进SAG 和 OpenViking 只是其中的一种探索。但我觉得这个探索的方向是对的Agent 时代的知识检索不能只靠模糊匹配必须要有精确的结构化查询能力。两者结合才能让 Agent 真正“聪明”起来。