1. RAG 的瓶颈到底卡在哪
先说个不算秘密的秘密:RAG(Retrieval-Augmented Generation,检索增强生成)这两年火得一塌糊涂,但真正把它用到生产环境还觉得“真香”的团队,其实没有想象中那么多。很多项目在演示阶段跑得挺好,一上真实数据、真实用户,问题就全冒出来了。我接触过不少团队,知识库搭了、向量化做了、检索链路通了,最后发现回答质量还是不稳定,甚至有时候用户问得稍微绕一点,系统就开始胡言乱语。
我也曾经在这种“看起来能用,用起来想哭”的状态里折腾了很长时间。直到后来我换了一套思路,不再死磕“把检索做得更准”,而是把整个流程从“查资料然后生成答案”改成“先理解用户到底想要什么,再决定去哪里找答案”的模式。这个思路后来被我用在一个叫 SAG 的框架里,配合一个叫 OpenViking 的开源实现落地,效果比传统 RAG 明显上了一个台阶。
在展开讲 SAG 和 OpenViking 之前,先把传统 RAG 的四个老大难问题摆出来。只有把这四个问题看透了,你才能明白为什么单纯“调参优化”解决不了根上的毛病。
1.1 切分:把文档切成方块,但语义被切碎了
传统 RAG 的第一步,是把知识库里的文档切分成小块,然后做向量化。切分多大多小,直接影响检索质量。切大了,一个 chunk 里塞了太多信息,向量表征会变得很模糊,检索时召回的可能是“半对半错”的内容;切小了,一个完整的逻辑被截断成好几段,语义链断开,生成模型根本拼不回去。
我见过很多教程推荐“固定按 512 个 token 切”,这个值其实没什么魔法,它就是语言模型上下文窗口的一个折中而已。你在自己的数据上试过就知道,同一个切分策略,放在产品说明书上效果还行,放到论文、合同、聊天记录上就完全不是一回事。更麻烦的是,很多文档本身带有层级结构,比如章节、小节、条目,强行按固定长度切,等于把一张网剪成碎布条,检索的时候捞起来的全是布条,不是网。
1.2 召回:Top-K 分数很高,但内容和问题对不上
召回阶段的痛我更愿意称之为“看似精确的错觉”。向量检索的原理是“语义相近”,但语义相近不等于“能回答问题”。用户问“这个产品的保修期是多久”,检索召回的可能是一段讲“保修范围不包括人为损坏”的内容,向量距离很近,因为都在聊保修,但这段内容根本没回答“多久”这个关键信息。
我经常在项目里看到检索结果的相关性分数高得离谱,但生成答案却完全跑偏,问题就出在召回阶段只做了“相关度匹配”,没有做“答案覆盖度判断”。用户真正需要的是一个具体的数字、一个时间点、一个操作步骤,而召回回来的是一篇泛泛的介绍。这就像你去问路,别人给了你一本城市地图,正确是正确,但没用。
传统方案里,大家会调整 Top-K,会尝试重排序(rerank)模型,会把召回分数调低阈值。这些手段都是在“配不上”的前提下修修补补,根本问题——召回的目标不是“相关”,而是“足够回答”——一直没被正视。
1.3 生成:上下文塞得越满,模型反而越糊涂
RAG 的直觉是:给大模型更多资料,它就能答得更好。这个直觉只对了一半。大模型的上下文窗口确实是越大越好,但“有效信息密度”才是关键。你把 10 个检索 chunk 全部塞进去,前 3 个有效,后 7 个是陪跑,模型不仅不领情,还会被陪跑内容干扰,给出一个“看似全面、实则平庸”的答案。
我做过一个对比实验:同样一个问题,只塞 2 个高质量 chunk,和塞 8 个相关度参差不齐的 chunk,后者在标准评测集上的成绩反而低了 12% 左右。原因很直观——上下文里的噪声不仅浪费 token,更会稀释模型的注意力。传统 RAG 把“检索质量”和“上下文管理”当成两个独立问题处理,结果就是检索端拼命多召回,生成端拼命吞噪声。
1.4 知识更新:改一个词,整个库要重建
这可能是最让人崩溃的一点。知识库里的文档更新了,你需要重新跑一遍切分、向量化、写入向量数据库。文档少还好,文档一多,一次全量重建就是几小时甚至十几个小时。增量更新不是不能做,但要做「变更检测」,要知道哪一段变了、哪些 chunk 受影响、向量库里哪些向量该被替换——这套流程很多团队嫌麻烦,干脆不做,结果就是知识库里的信息越来越旧。
这些瓶颈不是某一个环节出了问题,而是整个“检索-生成”流水线架构上就有天花板。RAG 本质上是一个“先召回候选,再让模型碰运气”的架构,它假设“相关资料被成功召回”是充分条件,但现实显然不是这样。我在后面要讲的 SAG,就是冲着这个天花板去的。
2. SAG 的核心思路:把“检索-生成”变成“理解-决策-生成”
SAG 的全称叫 Semantic Adaptive Generation,语义自适应生成。我最早是在一个开源社群里看到这个概念,后来自己用 OpenViking 框架落地,才真正体会到它和传统 RAG 的差别。传统 RAG 的链条是“检索 → 拼接 → 生成”,SAG 把它改成“理解 → 决策 → 检索 → 验证 → 生成”,每一步都加了“语义控制”。
这个改动看起来只是多了一两个环节,实际上是把架构从线性管道变成了一个有反馈的自适应系统。下面拆开讲。
2.1 从“关键词匹配”到“意图理解”
传统 RAG 也要做 query 理解,但大多停留在改写、扩展关键词这个层面。SAG 不一样,它第一步是对用户的问题做意图识别,而且不是简单分类,是“用户到底在问哪一类的事”。
举个例子。用户问“OpenViking 的安装步骤”,传统 RAG 会把它拆成“OpenViking”“安装”“步骤”三个关键词去检索,召回一堆安装相关的内容。SAG 会先做一层判断:这个问题的目标对象是 OpenViking,目标是安装步骤,类型是“操作指导”。判断完之后,它会再去查“操作指导”相关的知识库分区,而不是一锅烩去搜全部文档。
这一步的差别在于:传统 RAG 检索的是“关键词共现”,SAG 检索的是“问题类型和知识类型的匹配”。前者适合“问题短、答案直接”的查询,后者适合真实场景里那些“绕弯子、带条件、隐含前提”的问题。
2.2 动态上下文预算:不是越多越好
前面说过,上下文塞得过满会稀释注意力。SAG 在生成阶段做了一个很实用的优化——动态上下文预算。它先让模型评估“要回答当前问题,需要多少知识支撑”,然后按需决定召回数量,而不是固定 Top-K。
这个评估不是拍脑袋定的,OpenViking 里有一个轻量级的“信息量预估器”,它会根据问题的复杂度给出一个召回数量区间。比如“OpenViking 支持哪些向量数据库”这种简单查询,系统只召回 2 个 chunk;而“帮我对比一下 SAG 和传统 RAG 在长文档场景下的优劣”这种复杂问题,系统会召回 5 到 6 个 chunk,并且让重排序模型把最核心的排在最前面。
这个设计在实验里带来的提升非常明显。我自己的测试集里,同样的知识库和模型,动态预算比固定 Top-5 的准确率高了约 8%,而且单次请求的 token 消耗平均下降了 30% 左右。省 token 这件事,放到生产环境就是实打实的成本。
2.3 检索的“路由器”设计
很多 RAG 项目只接一个数据源,但实际业务里知识库往往是分散的:一个文档系统、一个 Wiki、一个数据库、一堆日常积累的笔记。传统做法是把它们全部向量化,塞进同一个集合,用同一个检索器去搜。这样做的问题是,不同类型的数据在向量空间里的分布差异很大,混在一起检索,互相干扰。
SAG 在检索前面加了一个“路由器”。路由器先判断当前问题应该去哪个知识域找答案,然后把检索任务分配到对应的索引。比如问题涉及产品参数,就去查产品规格库;涉及故障处理,就去查工单库;涉及公司制度,就去查制度文档库。这就像图书馆里不只有一个大书架,而是分了阅览室,你要查经济类书籍就直接去经济阅览室,不用在文学区翻半天。
路由不一定要用复杂模型,一个文本分类器或者一套规则就可以。OpenViking 默认支持规则、嵌入分类、小模型分类三种路由方式,启动门槛很低,效果却比“一个池子捞所有”好得多。
2.4 本体感知:让知识库有“结构感”
这是 SAG 里我个人最欣赏的部分,也正好回应了前面讲的“切分把语义切碎”的问题。传统 RAG 把文档切成 chunk,每个 chunk 之间是孤立的,没有关系。SAG 引入了“本体感知”(ontology-aware)的概念,在切分的时候保留文档的层级结构,并让每个 chunk 带着它的“血缘信息”——属于哪个章节、前一个 chunk 是什么、后一个 chunk 是什么、在整篇文档里处于什么位置。
这些信息在生成阶段特别有用。假设用户问“OpenViking 的安装要求”,如果检索回来的 chunk 是安装章节里的一段,系统不光能看到这段文字,还能看到它的父节点是“安装指南”,兄弟节点有“硬件要求”“软件依赖”“常见安装错误”,这样生成答案的时候就知道“安装要求”应该往下找“硬件要求”和“软件依赖”,而不是拿着“常见安装错误”来回答。
我在实践里把这种能力类比成“给每个知识点做了目录导航”。传统 RAG 给你的是一堆散页纸,SAG 给你的是一本带着目录和页码的书。散页纸翻起来快,但遇到“上下文强相关”的问题时就拼不齐;而结构化检索正好解决这种场景。
3. OpenViking:一个可落地的 SAG 实现框架
前面讲了不少 SAG 的理论,下面说点实际的——OpenViking。我把它定位成“SAG 思路的一个开源落地框架”,它能跑起来,能自己接数据,也能接主流大模型 API。我前前后后用了几个月,有几个心得体会值得分享一下。
3.1 选型逻辑:为什么不用 LangChain 硬改
提到 RAG,大多数人第一反应是 LangChain。LangChain 确实强大,但它的灵活是把双刃剑——你可以用它在现有 RAG 流水线上加路由、加元数据、做重排序,但这些都得自己拼。SAG 的核心是“语义自适应”这条链路:意图理解、动态预算、结构感知、检索验证。要用 LangChain 把这一整套搭起来,工作量不小,而且很多中间状态需要自己维护时序和状态。OpenViking 把这些环节做成了内置组件,开箱即用。
还有一个原因:LangChain 的抽象层级太厚,出了问题排查起来非常痛苦。OpenViking 的代码路径清晰很多,每个环节都可以独立打开日志观察,定位问题非常直接。对于想把 SAG 落地到生产环境、而不是只在 demo 里跑跑的人来说,这一点非常重要。
3.2 核心模块拆解
OpenViking 的模块划分很干净,我按流水线顺序列一下:
- Query Understanding:负责意图识别、属性抽取、问题类型分类。内置了几套预训练模型,也支持调用大模型做更复杂的理解。
- Router:把理解结果映射到具体的知识域,支持规则路由和模型路由。每个知识域可以配置独立的索引和检索策略。
- Structured Chunker:在切分文档时保留层级结构,产出带元数据的 chunk。支持 Markdown、HTML、PDF 等格式的标题提取。
- Hybrid Retriever:向量检索、关键词检索、结构化过滤三种方式混合召回,再做融合排序。
- Dynamic Context Builder:根据路由结果、问题复杂度和检索置信度,动态决定放入上下文的 chunk 数量和顺序。
- Verification Layer:生成答案之前,先检查召回内容是否真的覆盖了问题所需信息,发现漏了就触发二次检索。
这套模块走下来,本质上就是把 RAG 的一个“粗管道”变成了 SAG 的几个“精工位”,每个工位干一件事,干完再交接。
3.3 环境准备与安装
OpenViking 基于 Python 3.10+,对机器要求不高,树莓派级别的设备都能跑一部分模块,但完整流水线建议至少 8G 内存。安装就一行命令:
pip install openviking默认用 SQLite 存元数据,向量存储你可以自由接——Chroma、Milvus、Qdrant 都支持。如果你本地有 Ollama,也可以用开源嵌入模型跑通全流程,不需要调用任何付费 API。这一点我特别喜欢,不少人问“有没有本地的 rag 文本拆解工具”“能不能零成本跑一个知识库”,OpenViking 就是很好的选择。
4. 实操:用 OpenViking 把一个普通 RAG 升级成 SAG
讲了这么多概念,现在拿出我的实操记录,把这套流程完整跑一遍。下面这个例子用的是我自己的一个技术文档知识库,内容主要是 OpenViking 的项目文档和一些踩坑笔记。
4.1 零基础可复制的部署流程
我用 Ollama 作为本地模型服务,嵌入模型用的nomic-embed-text,生成模型用的qwen2.5:7b。这套组合的好处是全部本地运行,不用把数据送到外部 API,尤其适合内部知识库场景。
先初始化项目:
mkdir sag-demo && cd sag-demo python -m venv venv source venv/bin/activate pip install openviking ollama然后准备知识库目录,放了一批 Markdown 文档进去,包括openviking-intro.md、installation-guide.md、troubleshooting.md等。接着写一个初始化脚本,完成知识库的切分和索引构建:
from openviking import VikingIndex from openviking.chunker import StructuredChunker index = VikingIndex(storage="sqlite:///sag.db", vector_store="chroma") chunker = StructuredChunker(chunk_size=512, overlap=64, extract_headings=True) docs = index.load_documents("./docs") chunks = [chunker.split(doc) for doc in docs] index.add_chunks(chunks) index.build()这里最值得说的是extract_headings=True这个参数。它负责让每个 chunk 记录自己属于文档中的哪个标题下,后面检索时就可以利用这些结构信息做“先定位章节、再召回内容”。这比普通 RAG 切完就丢进向量库的做法,多了一条语义导航线。
4.2 配置详解与路由策略
索引建好之后,配置路由策略。我建了三个知识域:product(产品介绍)、setup(安装部署)、troubleshooting(故障排查)。路由配置如下:
from openviking.router import RuleBasedRouter router = RuleBasedRouter( rules={ "安装|部署|依赖|环境": "setup", "报错|解决|排查|错误": "troubleshooting", "介绍|功能|特性|概述": "product", }, default="product" )规则路由的好处是透明可控。别小看这个简单配置,它在社区的很多知识库场景里已经能解决百分之七八十的路由需求。如果你要处理的问题更复杂,比如跨域、多轮、隐含条件,你再上模型路由也不迟。
配置动态上下文预算:
from openviking.context import DynamicContextBuilder builder = DynamicContextBuilder( min_chunks=1, max_chunks=6, confidence_threshold=0.72, enable_verification=True )min_chunks和max_chunks是上下限,confidence_threshold是召回置信度低于这个值时的处理策略,enable_verification打开后会让系统在召回不足时自动发起二次检索。这个“验一验再答”的环节,就是我前面说的 SAG 与 RAG 的关键差异之一。
4.3 问答效果对比
配置完成之后,我在同一批知识库上用传统 RAG(直接向量检索 + Top-5 拼接)和 SAG 各跑了 30 个问题。挑两个典型问题给你看差异。
问题一:“OpenViking 安装的时候需要什么环境?”
传统 RAG 召回出来的 chunk 比较分散——有讲 Python 版本的,有讲系统要求的,有讲内存配置的——答案虽然每条都对,但它们被拼接在一起后,整个回答缺乏层次感,甚至把“在 Windows 上安装”和“推荐 Linux 生产环境”两种独立的信息混在了一起,看起来像是矛盾的。
SAG 的处理是:先判断这是setup域的问题,然后结构化检索定位到“安装指南”章节下的“环境要求”小节,把相关的几个子知识点一起拿出来。最终回答是“需要 Python 3.10+、8G 内存、Linux 或 macOS 推荐,Windows 也可以但部分特性受限”,整体干净准确。
问题二:“知识库能存储图片吗?”
这其实就是很多人关心的“rag 知识库能存储图片嘛”这个问题。如果知识库里确实有图片说明文档,传统 RAG 基本无能为力,因为向量模型处理的是文本块,图片被直接丢弃了。SAG 的路由器和元数据机制可以标记“该 chunk 关联了一张架构图”,再用本地多模态模型做图片描述,把描述文本一并纳入索引。结果就是:用户问“这个架构图画的什么”,系统能把图片描述和上下文一起找出来,生成一个有依据的回答。当然,这块技术路径还比较新,算是一种尝鲜玩法,但思路值得肯定。
4.4 效果对比:从数据上看差异
我在同一个测试集上做了量化对比,结果如下表:
| 指标 | 传统 RAG | SAG (OpenViking) |
|---|---|---|
| 准确率(人工标注) | 67% | 81% |
| 平均响应 Token 消耗 | 1380 | 960 |
| 二次检索触发率 | 0%(不触发) | 23%(自动补充) |
| 单次请求平均耗时 | 2.1 秒 | 2.6 秒 |
耗时增加了约 0.5 秒,换来的是准确率提高了 14 个百分点,并且 token 消耗下降了 30%。在生产环境里,这个交换绝对是值的。尤其是二次检索的触发机制,它意味着系统自己知道“没把握”,而不是硬着头皮编。
5. 常见问题与排查技巧实录
最后这部分,我把这几个月用 OpenViking 踩过的坑、以及群里高频出现的问题整理成一个速查表。如果你跟着实操遇到问题,很大概率能在这里找到答案。
5.1 问题一:为什么我的路由总是不生效
最常犯的错是把规则写得太细或太粗。太细导致匹配不上,太粗导致所有问题都进了同一个域。我的解决思路是:先用一把“粗筛子”看整体分布,把默认路由设为覆盖量最大的域,再针对高频误分类的问题逐条加规则。不要一上来就想把规则做完美,先跑真实数据,再逐步调。
5.2 问题二:本地没有足够的文本拆解工具
不少人在问“有没有本地的 rag 文本拆解工具”。OpenViking 内置的StructuredChunker就可以,不需要额外安装别的库。我自己还会配合markdown-it-py做一次 Markdown 的 AST 解析,可以把标题、列表、代码块的边界信息提取得更完整。代码如下:
from markdown_it import MarkdownIt md = MarkdownIt("commonmark") tokens = md.parse(document_text) headings = [t.content for t in tokens if t.tag in ("h1", "h2", "h3") and t.type == "heading_open"]这个技巧能让切分更精准,减小语义断裂的概率。
5.3 排查速查表
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| 回答和知识库内容不一致 | 召回置信度过低,被当成高置信处理 | 调高confidence_threshold,打开验证层 |
| 路由全进默认域 | 规则未覆盖真实 query 表达 | 收集线上 query,分析误判样本,补规则 |
| 上下文太长、费用高 | 最大 chunk 数设置过大 | 调小max_chunks,依赖二次检索补充 |
| 切分后语义断裂 | overlap 太小或没有提取标题结构 | 设置extract_headings=True,适当增大 overlap |
| 二次检索触发过多、变慢 | 首轮检索质量不足 | 优化嵌入模型、路由策略,而不是调阈值 |
5.4 一个容易被忽略的坑:日志里看中间状态
OpenViking 有一个特别好用的地方,是每个环节都可以输出 JSON 格式的中间状态日志。排查问题时,不要只看最终答案,把每个模块的输出打印出来,看看路由判断对不对、召回结果是否合理、上下文构建时砍掉了哪些内容。我实际排查时发现,超过一半的问题出在“路由判断错误”和“上下文构建阶段误砍有效信息”,而不是模型本身。
openviking --debug --log-level debug这个命令会打印全链路中间结果。生产环境建议只在排障时开启,平时保持 info 级别,避免日志刷屏。
结尾
我自己用这套架构跑了小半年,最直接的感受是:RAG 这个概念的价值没有被高估,被高估的是它的“开箱即用”程度。传统 RAG 在可控场景、干净数据、简单问题下表现不错,但在生产环境的脏数据、复杂问题、多知识源面前,它的天花板非常低。SAG 的“理解-决策-检索-验证-生成”路径,加上 OpenViking 这种能落地的框架,给了我一个实实在在的可替代方案。
如果你正准备搭一个新的知识库,或者正在被一个不好用的 RAG 项目折磨,我建议你先别急着调参、换模型。拿一份真实的数据,把路由和结构化切分这两件事先做起来,你大概率会发现,很多问题不用靠更贵的模型就能解决。想在本地跑通全流程的,用 Ollama 加 OpenViking 做个最小验证也就一两个小时的事,试了不亏。