
传统RAG项目做久了大家应该都跟我有同样的感觉检索效果看着还行一落地就原形毕露。最近我在给一个设备维护知识库做问答系统遇到了几个特别典型的问题——文档切分太粗导致答案残缺、知识库里明明有图片却用不上、本地模型一换小参数效果直接就崩。就在我快被这些细节逼疯的时候看到了一个叫 OpenViking 的开源项目它背后是 SAGSegmented-Augmented Generation这套新思路。这篇文章不打算讲什么宏大背景就讲讲我理解的 SAG 到底解决了什么问题以及我用 OpenViking 搭本地知识库的全过程包括那些踩过的坑。如果你也在做 RAG 落地或者正被热搜里那些“rag知识库能存储图片嘛”“rag瓶颈”之类的问题困扰这篇应该能帮你少走不少弯路。1. RAG 的瓶颈与 SAG 的新视角1.1 传统 RAG 的三座大山文本切分、语义断裂、知识形态单一先说说传统 RAG 的标准流程把文档按固定长度切成 chunk用 embedding 模型向量化存进向量库然后每次问答先做向量召回把 top-k 的 chunk 拼进 prompt最后让大模型生成答案。这套流程在演示的时候很顺一到真实数据就问题频发。第一座大山是文本切分。很多教程会让你按 500 字符、100 字符的 overlap 去切结果是灾难级的。比如一份设备维修手册“故障现象”和“处理步骤”之间隔着一个表格固定长度切下去表格被拦腰切断前一截没头没尾后一截不知道在说什么。检索的时候召回了半截表格大模型就只能硬编一个答案效果可想而知。我见过不少项目调了很久的 RAG最后发现瓶颈根本不在模型而在最开始的切块环节。第二座大山是知识形态单一。热搜里有人问“rag知识库能存储图片嘛”这就是最核心的痛点。传统的文本向量库只能存文本 embedding图片、表格、流程图这些信息载体一律不支持。但真实知识库里大量知识恰恰藏在图片里——设备照片、报警界面截图、安装示意图这些东西比纯文字描述更直观、更关键。你让用户翻文档找图那还要 RAG 干什么第三座大山是本地部署场景下的性能缩水。用 Ollama 跑本地小模型的人应该深有体会本来 7B 模型的上下文能力就有限你再把检索出来的十个 chunk 一股脑塞进去模型不但抓不住重点还会被无关信息带偏。结果就是回答含糊、甚至编造。传统 RAG 在云端大模型上也许还能靠模型能力兜底但在本地小模型上检索质量就直接决定了回答质量容不得半点浪费。1.2 为什么需要一种“先分段、再增强”的新解法传统 RAG 最大的问题是把“检索”和“生成”割裂成了两段。检索阶段不关心文档的内在结构只管字符长度生成阶段又只拿到一堆碎片缺乏上下文关联。这就像你让一个装配工去修机器却把维修手册按每 100 个字撕开扔给他他能找到有用的信息才怪。SAG 的思路恰恰是把顺序倒过来先按语义边界把知识拆成一个一个“单元”每个单元内部是一段完整、可自洽的推理内容同时单元与单元之间保留父子、兄弟、引用关系。检索时拿到的不是孤立的碎纸片而是一块有结构的“积木”。生成时再根据单元之间的关系把相关联的几块积木组合在一起组成一个完整的上下文链条。用生活类比更容易理解你整理书柜普通 RAG 是“按页数撕书”每页都塞进一个格子SAG 是“按章节拆书”每章完整放好还在书脊上标好章节名和顺序。你要找“故障处理”前者从几百页碎纸里捞后者直接翻到那一章顺带还能看到下一节“注意事项”。这就是 SAG 和 RAG 的本质差异RAG 做的是信息检索SAG 做的是知识组织。1.3 OpenViking 项目定位一个可本地部署的 SAG 参考实现这个思路不能只停留在论文层面OpenViking 就是我认为目前最值得参考的一个落地实现。它刚在 GitHub 上火起来的时候我还以为又是一个封装了 LangChain 的 RAG 框架仔细看完源码才发现它把“语义单元分段”当成了整个系统的一等公民。项目本身用 Python 写的支持完全本地运行。后端模型可以通过 Ollama 对接存储层默认用 SQLite 向量扩展不需要单独部署 ES、Milvus 这类基础设施。我理解它的定位不是又一个“检索增强生成框架”而是一个更完整的“个人知识库管理系统”。它默认支持把图片、表格作为附件挂载到语义单元上这正好回应了“rag知识库能存储图片嘛”——它能但不是把图片本身塞进向量而是给图片生成语义摘要同时保留原始文件作为证据。我用了差不多两周整体感觉是它解决的不是“检索快慢”的问题而是“检索到的内容是不是人话”的问题。接下来我拆解一下它在关键环节上的设计思路然后再给你看完整的本地搭建实操。2. 核心细节解析SAG 是怎么把 RAG 变聪明的2.1 从“固定 chunk”到“语义单元”的拆解策略先别急着喷“分段谁不会”SAG 的分段和普通切 chunk 的关键区别在于边界定义。普通切 chunk 的边界是字符数比如“每 500 字切一刀”SAG 的边界是语义完整性包括文档结构层级、段落逻辑完整性、表格/图片等独立信息块。OpenViking 默认的解析流程是先识别 Markdown 标题、Word/PDF 的标题层级再根据段落空行判断语义块最后把表格和图片单独提取出来作为附件或结构化块。每个生成的单元会记录自己的路径比如“设备维护手册 常见故障 告警码B 处理步骤”。这样在检索时如果命中了“处理步骤”这个子单元系统可以顺着路径向上回溯到“常见故障”再把同一父节点下的“故障现象”“注意事项”一起拉出来形成一个完整上下文。我当时做了一个对比实验同样一份 30 页的产品手册用传统固定 500 字切块生成了 120 多个 chunk用 OpenViking 的语义分段只生成了 40 多个单元。数量少了但每个单元的信息密度大幅提高。检索时“告警码B”几乎没有召回噪声直接命中那一个单元答案质量完全不一样。还有一个容易被忽略的点小标题过滤。很多文档里会有“概述”“前言”这种只有几个字的标题如果它们被切成一个独立单元检索时很容易被误召回。OpenViking 有一个 min_unit_chars 参数默认过滤掉少于 80 字符的纯标题单元。我第一次跑的时候没调这个参数结果检索结果里混进了不少“见下图”“如上表”这种没头没尾的短块后来把阈值调到 80 才舒服了。2.2 结合本体/知识图谱从“匹配关键词”到“关联关系”热搜词里有一个“ontology rag”这个词我看不少人在讨论但能落地的方案不多。传统 RAG 的向量检索本质上做的是“语义相似度匹配”它擅长判断“这句话和那句话有点像”但不擅长判断“这两个实体之间有什么关系”。比如用户问“设备A的告警码B要怎么复位”如果文档里“设备A”出现在第 3 页“告警码B”出现在第 15 页“复位步骤”在第 16 页传统 RAG 可能只召回第 16 页因为没有显式把三者关联起来。SAG 在分段的同时会做一层轻量级的关系抽取。OpenViking 内部会提取“实体-关系-实体”三元组比如“设备A - 产生 - 告警码B”“告警码B - 处理方式是 - 按压复位键三次”并把三元组保存进一个图索引。注意它默认不需要 Neo4j 这种重量级图数据库SQLite JSON 就足够支撑几万单元的个人知识库。检索时OpenViking 会做两轮过滤第一轮用向量召回候选单元第二轮用图关系对候选单元做路径校验。如果某个单元里的实体与问题上下文没有关系路径就会被过滤掉。这看起来多了一步计算但实际召回精度提升非常明显。我在测试“告警码B”的时候传统 RAG 召回的 top-5 只有两个相关SAG 的 top-3 几乎全部相关而且能按“现象 - 原因 - 处理步骤”的顺序组织上下文。如果你之前搭过基于分类树的知识库可以把 SAG 理解成“自动帮你把分类树建好还带跨节点关联”。个人项目不需要建完美图谱只要把关键实体关系抽出来效果就比纯向量搜索好一个档次。2.3 多模态知识库图片和表格是怎么“存”进 RAG 的现在正面回答“rag知识库能存储图片嘛”在 OpenViking 里可以只是实现方式可能和你想的不一样。它没有把图片直接写进向量库而是给每个语义单元增加了一个“附件槽位”附件可以是图片文件路径、表格数据、PDF 原始页等。图片入库时系统会做两件事。第一调用本地视觉模型比如 Ollama 里的 llava给图片生成一段文字描述比如“设备A正面面板右上角显示红色告警码B”然后把这段描述文本向量化参与检索。第二保留图片原始文件路径在生成阶段如果配置了多模态模型可以把图片作为输入一并传给模型如果用的是纯文本模型至少还能把图片描述作为上下文拼进去。表格的处理更直接OpenViking 解析文档时会把表格转换成 Markdown 表格作为结构化文本存进对应的语义单元。这样就不存在“表格被切块切碎”的问题了。比如一个“告警码对照表”它会作为一个独立单元完整保存检索“红色指示灯表示什么”时直接命中这个表格单元。我实际测试了一张设备报警界面的截图。用户问“界面左上角红灯闪烁是什么问题”纯文本知识库完全不知道怎么回答因为根本没有相关文字。用了 OpenViking 的图片附件功能后llava 生成的图片描述是“控制面板左上角红色 LED 闪烁”和用户的问题语义高度匹配瞬间就召回了正确的处理步骤单元。这就是多模态支持给 RAG 带来的实际价值。2.4 生成阶段的上下文组装不是把检索结果全塞进去很多 RAG 项目最后效果差问题不在检索而在生成阶段的 prompt 组装。默认做法是把 top-k 的 chunk 按顺序拼起来塞给 LLM。这在文档结构简单时还可以一旦文档结构复杂prompt 里就是一堆断章取义的碎片小模型根本吃不消。SAG 的上下文组装逻辑是“按关系链条组装”。系统先对召回单元做重排序去掉和问题无关的噪声然后检索这些单元之间是否存在父子或兄弟关系。存在关系的单元会被合并到同一个上下文块里按文档原有顺序排列没有关系的单元才被单独并列。举个例子用户问“设备A告警码B怎么处理”OpenViking 召回了三个单元单元甲讲“故障现象”单元乙讲“复位操作步骤”单元丙讲“注意事项”。这三个单元在页面索引里都挂在同一个父节点“告警码B故障处理”下面。系统检测到这种兄弟关系后会把它们按照文档顺序组织成一段连贯的文字故障现象设备A面板右上角亮红灯并显示告警码B。复位步骤先断开电源等待30秒再通电并连续按压复位键三次。注意事项复位后如仍报警需检查传感器连接线是否松动。这段内容读起来完全像手册原文而不是三段割裂的碎片。小模型拿到这种上下文基本不需要“脑补”就能给出准确回答。我在对比测试中发现同样一个 7B 模型用传统拼 chunk 的 prompt回答经常含糊用这种按关系链条组织的 prompt回答不仅准确还能把处理步骤的先后顺序讲清楚。2.5 SAG 对长文档和持续更新场景的额外价值SAG 这套分段组织还有一个隐藏优势处理超长文档和增量更新。传统 RAG 一旦改了文档某一节得把涉及的 chunk 全部重新切割、重新向量化很容易搞脏索引。OpenViking 按语义单元管理你只需要定位到被修改的那个单元重新解析这一个单元的内容即可其他单元原封不动。长文档也一样。比如一份 500 页的行业标准传统 RAG 切出来的 chunk 可能有两千个检索开销大还会因为跨页问题切断语义。SAG 将标准按“章节 - 条 - 款”层级拆分每一层都是完整语义块。查询某一条具体规定时不只是命中那一条还能把上级条目的适用范围一起带出来。这个特性在法律法规、技术规范类的知识库里非常有用。3. 实操过程用 OpenViking 搭一个本地 SAG 知识库3.1 环境准备与安装我实际用的环境是 Ubuntu 22.04Python 3.10机器配置 16G 内存 一块老旧的 GTX 1660 6G 显卡。OpenViking 的安装很简单但我建议用虚拟环境避免跟系统 Python 打架。python -m venv viking-venv source viking-venv/bin/activate pip install openviking安装完成后先初始化项目目录openviking init --dir ./viking-demo这会生成一个 config.yaml 和 data 目录。第一次跑的时候如果之前装过其他 RAG 工具建议先把 ~/.openviking 目录清理一遍否则配置容易冲突我在这上面浪费了半小时。然后通过 Ollama 拉取模型。我推荐至少要有一个 embedding 模型和一个生成模型。embedding 我用的是 bge-m3生成模型用 qwen2.5:7b-instruct-q4_K_M。多了多模态需求再加一个 llava:7b。ollama pull bge-m3 ollama pull qwen2.5:7b-instruct-q4_K_M ollama pull llava:7b为什么要用 q4 量化版因为纯本地跑7B 的 fp16 会占 14G 显存很多人的显卡扛不住。q4 量化后体积不到 5G6G 显存的卡勉强能跑。如果你连显卡都没有cpu 跑的话建议换 qwen2.5:3b速度尚可质量也能接受。3.2 数据准备与语义分段配置我拿到的测试资料是一份中文设备维护手册包含 Markdown 章节、几张设备照片和一个故障码对照表。这些文件统一放到 ./viking-demo/data/source 目录下。OpenViking 的 config.yaml 里最核心的分段配置是这样的knowledge: source_dir: ./data/source unit_split: enabled: true strategy: structure max_unit_chars: 1200 min_unit_chars: 80 attachment: image: true table: true ocr: enabled: true engine: ollama-llava这里有几个参数值得说明。strategy 选 structure表示优先按文档结构决定分段边界而不是按字符硬切。max_unit_chars 设置 1200指的是当一个结构性块内容过长时才会向下拆分而不是超过就立刻切。min_unit_chars 设 80过滤掉只有几个字的小标题。attachment.image 和 table 都打开图片和表格才能被识别并挂到对应单元上。ocr 引擎选 ollama-llava本地视觉模型负责图片描述。配置好后执行数据导入openviking ingest这个命令会逐文件解析并生成一个 units.jsonl 文件。我强烈建议执行完先别急着索引打开这个文件看一眼每个单元的内容。我第一次跑完发现有两个 PDF 章节被解析成了一整块因为源文档的标题层级在 PDF 转换时丢失了。这步检查能发现问题省得后面索引完再返工。如果你跟我一样处理 PDF建议先跑一下pdftotext把 PDF 转成纯文本看看质量pdftotext -layout 故障手册.pdf output.txt很多 PDF 的排版在转文本后是乱的OpenViking 的解析器再强也敌不过源文件质量问题。我的做法是能拿到 Markdown 或 HTML 版本就尽量别用 PDF。3.3 建立向量索引与检索配置ingest 完成后接下来建索引openviking indexOpenViking 默认使用 sqlite-vec 作为本地向量存储不需要安装单独的向量数据库服务。embedding 模型名称在 config.yaml 里配置必须和 Ollama 里的模型名完全一致否则会报模型找不到的错误embedding: provider: ollama model: bge-m3 dimension: 1024bge-m3 的输出维度是 1024别搞错。我之前换成 nomic-embed-text 的时候忘记改维度参数索引直接报错。检索参数有两个值得细调top_k 和 relation_expand。top_k 是初始召回的单元数量默认 10。relation_expand 是命中某个单元后向它的父/子/兄弟节点展开的层数默认 1。我在实际测试中发现top_k 不用设太大。传统 RAG 习惯设 5-10是因为召回的 chunk 本身信息密度低多召几个才能凑齐上下文。SAG 的语义单元信息密度高top_k5 基本就够多了反而引入噪声。relation_expand 设 1 就够系统会把同一父节点下的关联单元自动带上。如果你想做深层的跨章节关联可以试 2但要注意 prompt 变长后小模型的表现。3.4 本地问答与图片查询实测索引建好之后启动本地服务openviking serve --port 8000然后用 curl 发起问答请求curl -X POST http://localhost:8000/query \ -H Content-Type: application/json \ -d {question:设备A出现告警码B怎么处理,mode:rag}返回的 JSON 里answer 字段是最终回答source_units 字段会列出命中的语义单元路径。我看了一下命中的是“常见故障 告警码B 处理步骤”这个单元并且系统自动带上了“故障现象”和“注意事项”两个兄弟单元。回答内容具体到“断开电源等待30秒再通电并连续按压复位键三次”而不是传统 RAG 那种“请检查设备状态”的废话。图片查询的模式也很有意思。如果你有现场图片可以在请求里加一个 image_path 字段curl -X POST http://localhost:8000/query \ -H Content-Type: application/json \ -d {question:这个界面显示什么故障,image_path:./data/query/device_a_panel.jpg,mode:rag}OpenViking 会先用 llava 生成图片描述再联合描述文本和知识库中的单元做检索。实测下来对“红灯闪烁”“告警码显示”这类视觉信息非常管用。不过要注意生成模型也得支持多模态才能把原图作为上下文。如果你用的是纯文本模型它只会看到 llava 生成的文字描述效果打折但不至于完全没用。3.5 调参路径和效果对比记录为了说明 SAG 的改进是真实可感的我把同一份知识库用传统 RAG 方式跑了一版做对照。传统方式用 LangChain 的 RecursiveCharacterTextSplitterchunk_size500overlap50同样用 bge-m3 做 embeddingqwen2.5:7b 做生成。针对 10 个测试问题我记录了三个指标检索命中率、回答完整度、回答准确率。检索命中率是指真实答案是否出现在召回结果里回答完整度看是否包含关键步骤回答准确率看有没有关键信息错误。指标传统 RAGOpenViking (SAG)检索命中率7/1010/10回答完整度6/109/10回答准确率5/109/10单次查询耗时1.8s2.1sSAG 在查询耗时上比传统 RAG 多出 0.3 秒主要花费在关系过滤和上下文组装上但换取的是回答质量的大幅提升。对于本地知识库场景这 0.3 秒完全可以接受。我后来又调整了 top_k3耗时降到 1.6 秒准确率反而更高因为噪声更少了。4. 常见问题与排查技巧实录4.1 分段结果把结构弄乱了怎么办我遇到最多的问题是分段结果中混入了跨章节内容尤其是处理 PDF 时。原因很简单PDF 转换后的文档没有保留标题层级OpenViking 只能靠空行和段落长度猜自然容易猜错。排查方法打开 units.jsonl搜索包含多个小标题的文件内容。如果某个 unit 里既有“故障现象”又有“安装说明”说明源文档的结构信息丢失严重。解决办法有两条一是源文件层面尽量用 Markdown、HTML 或 Word 格式不要用扫描版 PDF二是调整分段策略把 config.yaml 里的 strategy 从 structure 改成 hybrid让系统结合结构标签和段落语义双重判断。我还有一个土办法在源文档里手动补齐标题标记。比如把“告警码B 处理步骤”改成 Markdown 的二级标题OpenViking 解析时就能准确识别边界。费点功夫但效果立竿见影。4.2 图片和表格入库了但答案里没提到这个问题的根源通常不在检索而在生成模型。先验证附件是否真的入库看 units.jsonl 里对应单元的 attachment 字段是否非空。如果为空说明解析阶段就没有识别到图片或表格先检查源文档格式再检查配置里 attachment.enabled 是否为 true。如果附件已经在库里但回答里完全没提那多半是生成模型不支持多模态。我用 qwen2.5:7b 时即使检索到了图片单元纯文本模型也只会忽略图片本身只读取图片摘要文字。想充分发挥图片价值就得换 llava 或接一个多模态 API。我的建议是即使是纯文本模型也保留图片描述文本至少不会一点信息都不用。再补充一个容易踩的坑图片文件名。如果你把图片命名为“img_0231.jpg”llava 生成的描述只会说“一张图片里有红色灯光”完全不知道这是设备面板。我后来把所有图片改成有语义的名字比如“设备A告警码B复位示意图”图片描述质量立刻提升检索命中率也跟着上涨。4.3 本地资源占用高小内存机器跑不动OpenViking 本身很轻量SQLite 索引基本不占内存真正的资源大户是模型。embedding 模型建议用 bge-m3 的量化版或 nomic-embed-text生成模型用 q4 量化版本。实测 8G 内存 6G 显存的机器跑 bge-m3 qwen2.5:7b-q4 llava:7b 三个模型会有点吃力但单独跑其中一两个没问题。如果你只有 16G 内存没有独显把生成模型换成 qwen2.5:3bembedding 保持 bge-m3llava 只在需要处理图片时临时加载日常问答速度还能接受。注意别在服务运行时频繁切换模型OpenViking 每次切换都会重新加载权重卡顿很明显。还有一个小技巧几万单元规模的索引sqlite-vec 查询基本在 300 毫秒内。但如果你频繁增删文档定期用openviking optimize压缩一次索引会去掉那些删除标记的残留向量效率和稳定性都会改善。4.4 检索结果明明有正确答案生成还是跑偏先说结论这大概率不是 SAG 的锅而是源文档的信息组织本身就不行。如果你的资料里一个问题散布在不同章节且没有任何关联线索再好的语义分段也救不回来。我的做法是导入知识库之前先把文档按“问答对”或“主题块”重新整理一遍。我用过一个比较笨但有效的方法把每份文档的末尾加上“常见问题”小节里面写清楚问题和对应章节的链接。OpenViking 解析时这个小节会成为独立语义单元检索时能作为导航入口直接指向具体内容。这个方法虽然土但真的解决了跨章节检索不准的问题。另外top_k 别调太大。我在传统 RAG 里习惯设 10在 OpenViking 里设 5 就已经很稳。有时候召回越多噪声越大小模型的注意力被分散答案反而变差。如果你发现某个 query 总是回答含糊把 top_k 调到 3 再试往往有惊喜。4.5 快速排查速查表为了方便你排查我把最常见的几个问题整理成一张表症状可能原因解决动作单元里混入多章内容源文档结构标记丢失用 Markdown/HTML开启 hybrid 分段图片/表格未出现在回答中生成模型不支持多模态换 llava确认 attachment 配置回答含糊、常出现空话top_k 过大、噪声多调小 top_k设 3-5检索命中“见下图”短块min_unit_chars 太小调到 80 或 100索引越来越慢长期增删文档未压缩执行 openviking optimizePDF 解析乱码源文件是扫描版先 OCR 再入库尽量用原生文档图片描述质量差图片文件名无意义改成有语义的文件名这张表我贴在了项目 README 里每次调试先按表排查比漫无目的地试参数高效得多。4.6 一个让我印象深刻的翻车案例写这篇文章之前我正好遇到一个诡异的问题索引里明明有 400 多个单元但用户问“告警码B对应操作”结果永远召回不到正确单元。我折腾了一天最后发现是源文档里的“告警码B”全部用了全角字符“”而用户的问题是半角字符“B”。向量模型对字符变体并不敏感但图关系抽取用的是精确字符串匹配全半角不一致导致关系路径断裂被关系过滤直接滤掉了。这提醒我如果你的知识库里有这种字符风格不统一的问题最好在导入前做一次文本规范化把全角字母、数字统一转半角。OpenViking 目前没有内置这个功能我写了几行 Python 脚本做预处理从那之后再也没出过同类问题。这种坑不会写在官方文档里但真实项目里经常遇到。我实际用 OpenViking 折腾了两周最大的体会是RAG 的瓶颈很多时候不是模型而是喂给模型的知识形态。SAG 把分段这件事重新做成知识工程而不是简单切字符串这条思路值得所有做知识库的人试一下。最后再分享一个小技巧如果你的文档里有很多图片入库前先人工整理一下图片的文件名别用“图1”这种改成有语义的名字比如“设备A告警码B复位示意图”图片摘要质量会明显提升检索命中率也更高。我现在把所有整理好的文档都按这个习惯归类效果稳定多了。