
RAG 这几个字母最近两年在技术社区里快被说烂了。随便搜一下 rag教程十篇里有八篇是同一套流程加载文档、切块、向量化、塞进向量库、检索、拼 prompt、让大模型回答。但真到落地的时候越来越多的人开始发现这套流程有点“纸面繁荣”。我最近被问得最多的不是“什么是 RAG”而是“为什么我的 RAG 知识库这么蠢”和“ rag知识库能存储图片嘛”。这些热搜词背后其实藏着同一个焦虑流程跑通了效果却不达标。今天想聊的 SAG 和 OpenViking就是我在这个背景下折腾出来的一套新思路。SAG 是 Semantic-Augmented Generation 的缩写中文可以叫语义增强生成OpenViking 是我最近在维护的一套偏本地化的 SAG 参考实现。这篇博客适合正在做 rag实战、想搭 rag知识库、或者已经被传统 RAG 的召回质量折磨到怀疑人生的人。我会从瓶颈分析讲到可复现的本地部署步骤全程没有云平台依赖也没有晦涩的数学推导都是可以直接抄的作业。1. RAG 热了这么久瓶颈到底卡在哪1.1 先把 RAG 的链路说清楚RAG 全称是 Retrieval-Augmented Generation也就是检索增强生成。核心思路很简单大模型的知识是训练时定死的你没法让它凭空知道你昨天更新的产品手册所以要在生成之前先从外部知识库里捞一批相关文本拼到 prompt 里再让模型基于这些文本作答。整套链路通常长这样文档加载 → 文本分块 → 向量化 → 建立索引 → 相似度检索 → 拼接上下文 → LLM 生成。这个思路本身没有问题但落地时绝大多数人其实只做了一半。做对的那一半叫“检索增强”也就是把相关文本捞出来没做的那一半叫“生成约束”也就是让模型必须遵守并严格基于这些文本来答。结果就是模型确实“看”到了资料但它想怎么用还是怎么用答完你根本不知道它哪句话来自知识库哪句话是它自己编的。这就像你让一个刚从图书馆抱回一摞书的人写报告他确实在翻书但你没法保证他引用的页码是对的。磕磕绊绊能跑起来但一遇到稍微复杂、稍微冷门的问题立刻就露馅。很多 rag框架 和 rag项目 都把精力放在“如何更快地检索”却没有回答一个更关键的问题检索回来的东西到底是不是生成真正需要的“约束材料”。1.2 真正让人头疼的四个“rag瓶颈”第一个瓶颈是相似度不等于相关度。向量检索的核心是把文本映射成高维向量然后按余弦相似度或者内积找最近邻。可是语义相近不代表事实相关。比如我问“这个产品的保修政策是什么”知识库里最相似的一句话可能是“申请保修时请提供发票复印件”两句话文字重合度不高但语义距离很近于是被捞了上来。模型看到这句话就开始给你背发票规则完全不接“保修范围”的茬。这类问题在 rag瓶颈 讨论里出现频率最高也最劝退新手。第二个瓶颈是暴力切块把结构切没了。绝大多数 rag教程 教你的分块方式都是固定长度比如按 500 个字符硬切。可现实中的知识库长什么样有标题层级、有表格、有列表、有父子条目。一个产品手册里“保修条款”这个大标题下面可能挂着三级子标题和一张免责表格暴力切块会把它们打散到不同的块里。检索时你命中了一块但真正起决定作用的另一块根本没进上下文。这类问题在内部知识库场景里特别常见因为内部文档比网页更依赖文档结构。第三个瓶颈是实体关系丢了。传统 RAG 的索引单元是“文本块”它不关心块里的张三和李四是什么关系也不关心“A 产品”和“B 产品”是替代关系还是互补关系。一旦用户问“A 和 B 哪个更适合我”模型就只能从两段孤立的文本里瞎推断。你让它“基于知识库回答”可知识库里根本没有一张“关系表”它只能靠大模型自己的先验知识补幻觉就这么来的。第四个瓶颈是多模态内容进不去。热搜词里那句“ rag知识库能存储图片嘛”特别有代表性。传统文本向量库确实不能“理解”图片你就算把图片二进制塞进去检索时也匹配不回来。可是产品手册里的架构图、流程图、对比表格往往比正文还重要。把图片扔了等于把知识库里最值钱的一部分切掉了。1.3 热搜词背后的真实需求如果把搜索词连起来看你会发现用户早就不是停留在“怎么搭一个 RAG demo”的阶段了。“ ontology rag”说明有人开始研究本体和概念体系“wiki和rag”说明有人想把半结构化内容接进来“有没有本地的rag文本拆解工具”说明大家发现分块质量比框架本身更能决定效果“ rag智能体”说明下一步大家想让知识库真正参与决策。这些需求堆在一起指向的不是某一个具体功能而是对“知识组织形式”的质疑。传统 RAG 把知识看成一张由文本块组成的平面列表而用户真正需要的知识底座是有层次、有关联、有多模态语义的立体结构。也正是这种需求把我的注意力从 RAG 拽向了 SAG。2. SAG把“语义结构”当成生成的下游约束2.1 SAG 到底想解决什么问题SAG 是我这里采用的叫法全称 Semantic-Augmented Generation也可以理解成 Structure-Augmented Generation语义增强生成。它的出发点不是“多一次检索”而是把“语义结构”作为整个链路里的一等公民。传统 RAG 的流程是“先检索再生成”检索结果是一堆文本块生成模型自由发挥。SAG 的流程则多了一个关键动作检索之后先把捞回来的内容做一次结构和语义的重组整理成类似“事实清单”或者“约束提纲”的东西再让模型基于这个重组结果作答。换句话说RAG 把资料交给了模型模型爱怎么用怎么用SAG 直接把资料整理成了一张“答题必须遵守的提纲”模型只能按提纲走。这个差异在工程上会带来三个结果。第一模型跑题和幻觉的空间被压缩了因为它没机会在提纲之外自由发挥。第二回答更容易溯源因为提纲里的每一条都带了出处。第三复杂问题可以被拆成多个子约束而不是指望模型从一坨文本里自己找线索。这三点刚好打中上一章说的那几个瓶颈。2.2 和传统 RAG 的对照拿一张表把两者拉出来对比会更直观。维度传统 RAGSAG索引单元固定大小文本块带语义标签的结构化块结构保留基本不保留保留标题层级、表格、实体关系实体关系不感知显式抽取并建轻量关系索引多模态通常只存文本图片先解析成语义描述再入库生成约束只拼接上下文重组为事实清单/约束提纲知识更新重新向量化增量更新块与关系索引工程复杂度低快速出demo中需要多一层结构处理幻觉控制弱中等偏强这张表不是想说 RAG 一无是处。在快速验证、通用问答、噪音容忍度高的场景里传统 RAG 依然是最省事的方案。但一旦推上生产要面对精确查询、实体关系、结构依赖RAG 的短板就会被放大。SAG 付出的额外成本主要是“结构处理层”换来的是更可控的输出。2.3 一个生活化的类比我用一个图书馆的类比来理解这两者的区别。传统 RAG 像什么你问管理员“有没有关于北极熊的资料”他抱来一摞可能相关的书。然后你写报告时只能在这些书里翻来翻去翻到哪算哪。运气好翻到关键页运气不好翻到一本讲南极企鹅的你还要自己判断这它到底相不相关。全文检索和向量检索做得再好也只是帮你把“整座图书馆”缩小成“一摞书”。SAG 则像什么管理员先带你查馆藏目录找到北极熊在“哺乳动物-食肉目-熊科”这个分类下的位置再给你一张索引卡上面写着关键页码、相关条目、交叉引用的范围。你写报告时案头放的不是一摞书而是一份整理好的“答题提纲”。你仍然会去翻书确认细节但核心论点已经被提纲锁住了。这个区别放到系统里就是 SAG 会先做一次“语义结构整理”把散落的文本块变成有分类、有层级、有关系的结构对象然后再交给生成模型。生成模型的自由度没有被完全剥夺但它的发挥空间被限制在了结构框架里。2.4 什么样的场景值得上 SAG不是所有场景都需要 SAG。我这几个月用下来最值得上 SAG 的是三类场景。第一类是内部制度与产品手册问答。这类内容结构强、条文化、答案通常藏在某个条款里传统 RAG 经常因为切块切错而答非所问SAG 的层级切分和关系索引能明显改善。第二类是 wiki 类知识库。wiki 本身就是半结构化内容有词条、有分类、有链接关系。把 wiki 当成普通文本库去切完全是浪费。sag 的思路是让 wiki 的链接关系直接参与索引和检索用户问“XX 和 YY 的区别”系统能沿着词条间的引用关系找到对比依据。第三类是多实体对比类问答。比如选型问题、参数对比、AB 产品差异。这类问题需要的是实体关系不是孤立文本块。SAG 里提到的 ontology 和轻量关系图就是为这类问题服务的。你在 ontology rag 搜索里看到的概念建模在 SAG 里是一个具体的工程组件而不是玄学。3. OpenViking把 SAG 落地的开源参考3.1 OpenViking 是什么和现有框架什么关系OpenViking 是我在维护的一套偏本地化的 SAG 参考实现。它不是一个从零发明的框架而是把现有生态里被验证过的组件按照 SAG 的思路重新组合了一遍。核心目标只有一个让 SAG 不是一个只有几张 PPT 的概念而是一个能本地跑、能接入 Ollama、能回答真实问题的可复现系统。如果你熟悉 LangChain4j 的 Easy RAG会发现 OpenViking 的接线方式和它很接近文档加载、向量化、检索这些基础能力都是基于成熟组件封装的。区别在于OpenViking 在“检索”和“生成”之间额外插了一层结构处理层对检索结果做语义重组生成带来源引用的事实清单。如果你只用过 LangChain4j 这样的现有 rag框架过渡到 OpenViking 不存在学习悬崖。OpenViking 的部署形态偏向本地和私有化模型层直接对接 Ollama 就行意味着你不需要把知识库发给任何外部服务。对于做 rag项目的人来说这个点很重要很多企业愿意用知识库但绝不愿意把内部手册传到云端。3.2 核心模块与“本地文本拆解工具”OpenViking 的核心模块可以拆成五块。输入适配器负责读文档支持 pdf、docx、markdown、html。这一步比想象中更重要因为很多 rag 项目的失败从文档解析阶段就开始了。PDF 里的表格会被解析成乱码docx 里的目录会被当成普通段落html 里的导航噪音会被当成正文。OpenViking 的输入适配器会尽可能保留文档结构标签为后面的语义切分做准备。语义切分器是第一个重头戏。它不按固定字符数硬切而是感知标题层级、表格边界、列表边界。比如产品手册里“保修范围”这个大标题会自动成为一个块的起点下面的子标题和表格不会被拆散。如果你正在找“有没有本地的rag文本拆解工具”这个内置模块就是答案它可以把结构完整的文档拆成带层次关系的语义块而不是一行一个 chunk。实体关系抽取器负责把块里的关键实体和关系抽出来。这里用到的是 LLM 加规则的混合方式规则保底LLM 补漏。抽取结果会进入轻量关系图索引存储“实体-关系-实体”三元组。三层索引是整个系统的检索底座分别是向量索引、关键词索引、轻量关系图索引。向量索引负责语义召回关键词索引负责精确匹配关系图索引负责沿实体关联扩展。生成阶段会把三层索引的结果合并重排。结构生成器是 SAG 的灵魂。它把检索结果重组为一份“事实清单”或“约束提纲”每一条事实都带来源块编号和关系路径然后把这个清单连同用户问题一起交给生成模型。3.3 零基础上手Ollama OpenViking 本地跑通先说环境一台能联网下载模型的电脑8GB 以上内存硬盘留出 10GB 左右。我用的是 Ubuntu 22.04Windows 和 macOS 有对应的命令只是安装方式略有差异。第一步安装 Ollama并拉取模型。推荐先用 qwen2.5:7b 作为生成模型再拉一个 embedding 模型比如 bge-m3。curl -fsSL https://ollama.com/install.sh | sh ollama pull qwen2.5:7b ollama pull bge-m3第二步克隆 OpenViking 仓库这个仓库还比较轻没有历史包袱。然后安装 Python 依赖。git clone https://github.com/your-org/openviking.git cd openviking pip install -r requirements.txt第三步准备一个最小配置。OpenViking 用一份 YAML 文件描述索引目录、模型地址和切块参数。knowledge_base: paths: - ./docs/product-manual.md - ./docs/faq.md recursive: true embedding: provider: ollama model: bge-m3 dimension: 1024 generation: provider: ollama model: qwen2.5:7b temperature: 0.2 index: max_block_chars: 1500 overlap_chars: 200第四步建立索引并启动问答。python openviking build --config config.yaml python openviking ask --config config.yaml --question 质保期内人为损坏可以免费维修吗如果一切顺利你会看到输出里有一个明确判断后面跟着来源引用块。这一步跑通就说明本地知识库已经具备 SAG 的基本能力了。3.4 图片到底能不能存进知识库回到热搜词“ rag知识库能存储图片嘛”我的回答是能但是存进去的不是图片本身而是图片的语义化描述。OpenViking 的处理流程分三步。第一步图片进入视觉解析模块用本地多模态模型生成一段结构化描述。第二步对图片做 OCR抽取出图里的文字信息尤其是表格和截图。第三步把“描述文本 OCR 文本 原图路径”一起封装成一个语义对象写进索引。这样你检索“产品架构图里包含哪几层”的时候系统能通过描述和 OCR 文本召回图片对象再在答案里给出原图路径方便用户点击查看。# OpenViking 图片入库的伪代码 def ingest_image(image_path): description vision_model.describe(image_path) # 本地多模态模型 ocr_text ocr_engine.extract(image_path) semantic_obj { type: image, description: description, ocr_text: ocr_text, source_path: image_path, chunk_id: None } index.add(semantic_obj) return semantic_obj这里有个容易踩的坑图片描述如果质量太差反而会污染索引。比如一张架构图多模态模型只描述成“一张蓝色背景的图片”这样的内容进索引后会把检索结果带偏。所以 OpenViking 要求视觉描述必须包含结构化要点比如图层关系、模块名称、关键文字如果描述里没有这些信息宁可让这张图片不参与语义召回只保留 OCR 文本。4. 实操记录用 OpenViking 搭一个本地知识问答4.1 准备数据和环境我最近拿一份真实的产品手册做测试大约 200 页里面包含章节、流程图、规格表和问答算是比较典型的企业 rag知识库 数据。另外我还准备了一个 markdown 格式的 wiki 目录用于验证半结构化内容的效果。准备数据这一步的核心目的是把知识的形态摸清楚哪些是纯文本、哪些是表格、哪些是图片、哪些是嵌套列表这会直接影响切分策略。项目目录我习惯这么组织my-sag-project/ ├── config.yaml ├── docs/ │ ├── product-manual.pdf │ └── wiki/ │ ├── intro.md │ └── faq.md ├── output/ │ ├── blocks.jsonl │ ├── entities.jsonl │ └── answer.log环境还是沿用上一章的 Ollama 方案没有额外依赖。这一步如果卡住大概率是 PDF 解析库没装全补一个 poppler-utils 就好。4.2 第一次跑通语义切分和实体抽取执行建索引命令后OpenViking 会先输出切分结果我建议第一次跑的时候打开 output/blocks.jsonl 看几行。你会发现语义切分和固定 chunk 的最大区别它切出来的每个块都有 type 字段标记自己是“章节”“表格”“列表”还是“普通段落”还有 parent_id 指向上一级标题。举个例子。产品手册里有一章叫“售后服务”下面有两个子章节“保修政策”和“维修流程”中间还夹着一张收费标准的表格。老办法按 500 字硬切会把表格切成两半一半归入保修政策一半归入维修流程。OpenViking 的语义切分器则会生成四个块一个“售后服务”父块、两个子章节块、一个完整表格块并且子块都带着父块 ID。公共号里搜一下你找不到这种切分逻辑因为它不是单纯调一个参数而是把文档结构当成真实信息去保留。切块切好了后面的检索才有意义。实体抽取这步OpenViking 会输出 entities.jsonl格式大概是这样的{entity: 质保期, type: 概念, attributes: {时长: 12个月}, source_block_id: block_0042} {entity: 人为损坏, type: 条款, relation: 不属于, target: 免费维修, source_block_id: block_0042}看到这些三元组你就知道为什么 SAG 在处理“人为损坏能不能免费修”这类问题时比传统 RAG 更稳因为它手里有一张关系表而不是一堆孤立的句子。4.3 检索参数怎么调top-k、阈值、embedding这一步是最容易被忽略也最影响效果的。我拿同一份产品手册做了几组对比实验问题和答案一致“质保期内人为损坏可以免费维修吗”配置组合top_k 结果示例效果top_k8无重排阈值 0.5召回保修政策、发票规则、维修流程、常见问题回答混乱混入发票规则干扰项top_k4无重排阈值 0.6召回保修政策、发票规则还是被发票规则带偏top_k4加重排阈值 0.6保修政策、人为损坏条款、维修收费表回答正确来源清晰实测下来top_k 不是越大越好。知识库内容比较集中时top_k 取 4 比取 8 更稳因为 8 个结果里有大量低相关块会把模型的注意力稀释掉。阈值方面0.5 太松会放进大量噪音0.7 太紧可能漏掉正确答案0.6 是我这个数据集上的甜点。重排器值得加。我用了交叉编码器它会逐对计算 query 和候选块的相关性比向量相似度准确得多。代价是延迟增加几十毫秒但对于一个内部知识库问答来说完全可接受。embedding 模型我选了 bge-m3它在中文长文档上的表现比 m3e 稳定而且支持 8192 长度的上下文对切块较大的场景更友好。4.4 一个效果对比传统 RAG vs SAG我把同一个问题分别喂给标准 RAG 链路和 OpenViking 的 SAG 链路对比效果。标准 RAG 的回答差不多是这种风格“根据产品手册本产品提供自购买之日起 12 个月的免费保修服务。在申请保修时请提供发票或收据复印件。如果检测结果显示故障属于人为损坏维修可能需要收取相应费用。”问题在于它绕了半天没有给出“到底能不能免费修”的明确结论。这是因为检索回来的块里没有一张完整的“责任判定表”模型只能自己拼逻辑最后用一句模棱两可的话圆过去。SAG 的回答则是“质保期内人为损坏不在免费维修范围内。根据‘售后服务-保修政策-免责条款’人为损坏导致的故障需由用户承担维修费用具体金额参考‘维修收费标准表’来源产品手册第 4 章 4.2 节、附录 B。”这个回答为什么更硬因为 SAG 生成时面前不是一段散文而是一份约束式事实清单约束1保修范围非人为损坏的制造缺陷 约束2人为损坏不属于免费维修范围 约束3维修费用按收费表执行来源附录B模型可以发挥的语言空间被压到很小它只能基于这三条约束组织句式撒谎的机会自然就少了。5. 常见问题与排查技巧实录5.1 问题速查表我把这段时间被问得最多的问题整理成一张表方便排查时快速定位。现象可能原因排查方向回答经常跑题检索召回质量低top_k 太大先看召回结果的相关性再调 top_k 和阈值引用了不存在的条款切块把标题和正文拆散来源映射混乱检查 blocks.jsonl 里的 parent_id 和标题归属图片问题答不上来图片只存路径没有语义描述检查视觉解析模块的输出质量模型总是重复原文温度参数过高或 prompt 缺少约束句式降温度到 0.2 以下用约束清单替换自由拼接切块后表格文字错乱PDF 解析阶段丢结构换解析器或先把 PDF 转成 markdown 再入库5.2 排查顺序遇到效果问题我的第一建议是先别调生成模型先看召回结果。很多 rag实战 选手会把问题归罪于模型不够聪明然后换更大的模型结果只是把噪音处理得更流畅该错的还是错。排查顺序依次是先看检索回来的每个块是否真的相关再看 prompt 里的约束是否清楚最后才考虑模型能力和温度参数。OpenViking 提供了 debug 模式可以直接打印检索结果和最终 prompt。我实际用下来这个 debug 输出比任何调参技巧都管用它能让你一眼看出是“没找到”还是“找到了没用上”。5.3 避坑与心得第一个避坑点别把 chunk 切太小。我见过有人把 200 字的块当成标准配置结果一句话都被劈成两截无论怎么调 top_k 都救不回来。更合理的做法是保留结构的大块比如 800 到 1500 字符让模型自己从完整段落里找答案。第二个避坑点实体抽取一定要加白名单。LLM 抽取实体的噪音很大会把“产品”的门类、数量、常见词全抽出来关系图变成一团乱麻。OpenViking 里我给抽取器配了一张领域词典识别范围限定在产品名、型号、条款名效果立刻提升。第三个避坑点图片描述要设门槛。上一章说过描述质量差的图片会污染索引这里再补充一句入库前先跑一轮批量校验把描述里没有出现任何领域关键词的图片对象打回不要让垃圾进索引。第四个避坑点生成端最好用带 JSON Schema 的约束式 prompt。当本地模型小于 7B 时让它输出自然语言很容易偏给它一个明确的 JSON 结构比如“结论字段、来源字段、依据字段”模型会老实很多。这也呼应了热词里的“ rag智能体”因为结构化的输出更容易被下游工具直接消费。把这套流程跑通之后我自己的体会是RAG 项目的天花板不在检索而在知识组织。SAG 和 OpenViking 不是一个银弹但它提供了一条值得尝试的路径。如果你也在折腾 rag知识库建议从这三个点开始动手先搞定切块再建实体关系最后给生成端加约束。做完这三件事你会发现同样的知识库回答质量完全是两个水平。