如果你最近在折腾 AI Agent 或者知识库类应用,应该能明显感觉到一个变化:大家不再追求做一个包打天下的巨型 RAG 系统,而是把检索、重排、问答这些能力拆成一个个 Skill,按需加载、随取随用。我这次用开源的 deepseek-v4-flash-vision 做了一套 Skill RAG,把完整的多模态知识库问答流程封装成可插拔的技能包,实测下来不仅接入成本低,还解决了我长期头疼的问题——RAG 能力的复用与编排。这篇文章从架构选型到落地实现,再到调优召回质量时踩过的坑,完整记录下来,希望给正在折腾 RAG 的朋友一个可参考的样本。
1. 从“独立服务”到“可插拔技能”:我为什么要做 Skill RAG
1.1 传统 RAG 服务让人别扭的几个地方
在聊 Skill RAG 之前,先说说我之前是怎么做 RAG 的。最常见的套路是把检索能力封装成一个独立的 HTTP 服务,文档切块、向量化、建索引,然后对外暴露一个/query接口。这套方案在 demo 阶段没问题,但真要接入 Agent 或者多个业务方时,痛点就冒出来了。
第一个痛点是“场景感知”完全缺失。知识库场景千差万别:技术文档需要精确匹配函数名和报错信息,合同审查需要定位条款号和日期,客服问答则更看重语义相似度。传统 RAG 服务的接口是固定的,不管谁来问,都是同一套向量检索逻辑跑到底。你要想针对不同场景调整检索策略,只能改代码重新部署,很笨重。
第二个痛点是图文内容被切割。很多知识库文档不是纯文本,里面有表格、流程图、截图。传统管线通常只抽文本,图片里的信息要么丢掉要么变成乱码。等到用户真的问“这张架构图里的链路是怎样的”的时候,系统就傻了。
第三个痛点是“检索”和“推理”没有打通。传统 RAG 是流水线式的:检索一堆片段,拼接到提示词里,让模型生成答案。整个过程是一次性的,模型不能根据中间结果决定“我还需要再查一份材料”,也不能调用外部工具核对数字。这就是为什么后来大家开始讨论 agentic RAG。这些痛点单靠优化模型或者调参数解决不了,本质问题是 RAG 的能力被硬编码成了一根不可组合的管道。
1.2 Skill 机制给 RAG 带来了什么变化
Skill 这个概念是从 Claude Code、Codex 这类编程智能体带火的。一个 Skill 包本质上就是一个“带说明书的工作目录”:里面有一份 SKILL.md 描述文件,写清楚这个技能解决什么问题、在什么场景触发、怎么调用,然后把脚本、提示词模板、参考数据放在同一个目录下。模型在运行时读取这份说明书,自己决定要不要加载这个技能、什么时候调用。
类比一下:传统 RAG 就像一把瑞士军刀,所有功能焊死在一把刀上,想加个开瓶器得重新造一把。Skill RAG 则是把开瓶器、剪刀、螺丝刀分开放在工具箱里,需要哪个拿哪个,甚至可以组合使用。
把 RAG 流程拆成 Skill 之后,有几个明显的好处。首先是按需加载,不需要常驻一个检索服务,降低了运维成本。其次是策略隔离,不同的检索策略、提示词模板、解析逻辑互不干扰。更重要的是自然语言触发,Agent 读到用户问题后,会自动匹配 SKILL.md 里的描述,决定启用哪个技能,而不是靠硬编码路由。
我这次的项目把整个 RAG 流程拆成了三个 Skill:文档解析 Skill、混合检索 Skill、答案生成 Skill。deepseek-v4-flash-vision 在这三个 Skill 里都有参与,分别承担视觉解析、查询理解、答案合成的工作。整个结构搭起来之后,我再回头改任何一个环节,都只需要替换对应 Skill 包,不用动其他模块。
2. 整体架构与选型思路:模型、向量库与检索方案的取舍
2.1 为什么选 deepseek-v4-flash-vision 作为核心模型
选型的时候,我其实对比了好几款开源模型,最后把 deepseek-v4-flash-vision 定为核心模型,主要看中三点。
第一是 flash 版本定位轻量快速。知识库问答是要给业务人员用的,一个查询如果等十几秒,体验就废了。flash 版本在保证效果的前提下把推理时延压了下来,本地部署后单次查询的解析和生成可以控制在几秒以内。我试过用更大规模的模型做同样的事,质量确实更高,但响应时间不可接受。
第二是视觉能力。这个版本带 vision,可以直接吃图片、PDF 版面、表格、图表。这三类内容恰好是知识库文档里最难处理的。以前我要在管线里串 OCR、表格解析、图像理解好几个模型,现在一个视觉模型能覆盖大部分工作。
第三是开源可私有化部署。知识库类项目的文档往往涉及内部资料,不能随便传到外部 API。开源模型可以完全跑在内网,数据不出边界,这一点在很多企业场景里是硬性要求。
它在 Skill RAG 链路里主要出现在三个位置:文档解析阶段做版面分析、OCR、表格转 Markdown;查询理解阶段做意图识别和 query 改写;答案生成阶段把检索到的片段整理成最终回答。这三个位置恰好是一问一答链路里最需要“智能”的地方。
2.2 向量库和 Embedding 模型的选型逻辑
向量库的选择,我按“先轻后重”的原则来。项目初期文档量在几十万条以内,直接用 LanceDB 这种嵌入式向量库,零运维,进程内读取,部署非常简单。等到数据量真的大到需要分布式了,再迁移到 Milvus 或 Elasticsearch,反正检索逻辑层已经做了抽象,换底座不影响上层。
Embedding 模型方面,中文知识库场景我优先试了 BGE 系列和 M3E,英文资料多的时候会换 multilingual-e5。这里想提醒一句:向量库本身对检索质量的影响远没有 Embedding 模型和分块策略那么大。很多人上来就纠结 Chroma 还是 Milvus,其实跑个标准测试集对比一下,往往发现差距在毫厘之间,真正决定天花板的是你用什么模型把文本变成向量。
| Embedding 模型 | 中文语义理解 | 检索速度 | 部署资源 |
|---|---|---|---|
| BGE-large-zh | 好 | 中 | 较高 |
| M3E-base | 中上 | 快 | 低 |
| multilingual-e5-base | 中上 | 快 | 低 |
对于大多数知识库场景,M3E-base 已经够用,BGE-large 在检索精度上有优势但资源消耗也上去。我的建议是先跑最小验证,再决定要不要上大模型。
2.3 检索方案:我为什么坚持多路召回而不是单一路径
早期版本我图省事,只用了向量检索。上线后很快就发现问题:用户问“请帮我找 ID 为 ERR-2048 的报错记录”,向量检索经常返回一堆语义相近但 ID 完全对不上的内容。这类精确匹配需求,语义检索天然不擅长。
后来我把检索改成了“向量检索 + 关键词检索”多路召回,再用 Rerank 统一排序。向量召回负责语义匹配,BM25 关键词召回负责精确匹配,两条路的结果做加权融合。这一步改动之后,召回质量提升非常明显。
这里给一个我这边样例库上的实测数据,仅供参考:只用向量召回,Top5 精确命中率大概 78%;加上关键词多路召回后到 84%;再叠加 Rerank 之后能到 92%。这个趋势在不同数据集上应该是一致的,只是具体数值会有差异。
融合方式我一开始用的是加权求和,向量分数权重 0.7、关键词分数权重 0.3,简单但有效。后来换成了 RRF(Reciprocal Rank Fusion)的无参融合,稳定性更好,不用反复调权重。
3. 核心代码实现:Skill 结构、检索链路与生成提示词
3.1 Skill 包的基础结构
先看一个 Skill 包长什么样。我按社区里常见的 Skill 格式组织,核心是一份 SKILL.md 加上若干脚本:
skill-rag/ ├── SKILL.md ├── requirements.txt ├── src/ │ ├── parse_document.py │ ├── retrieve.py │ ├── rerank.py │ └── generate.py ├── scripts/ │ ├── build_index.py │ └── query.py └── assets/ └── prompts/ ├── query_rewrite.md └── answer.mdSKILL.md 是所有 Skill 的灵魂。模型靠它来判断“这个技能能不能解决用户当前的问题、应该怎么用”。写这份文件的时候,不能只罗列功能,要把触发场景、输入输出格式、注意事项都写清楚。
--- name: skill-rag-knowledge-base description: 当用户需要从内部知识库检索文档、查找数据、回答基于文档内容的问题时使用。 适用于多模态PDF、表格、图表混排的企业知识库场景。 --- ## 使用步骤 1. 接收用户问题后,先调用 scripts/query.py 传入 query 参数。 2. 脚本会执行混合检索,返回带来源编号的片段列表。 3. 根据片段列表生成最终答案,必须引用来源编号。这段说明看着简单,实际是整套流程的“路由大脑”。你给模型的信息越清晰,它越不会在不该用知识库的时候乱调,或者在该用的时候拒绝调用。
3.2 混合检索的实现
检索模块我单独抽了一个文件。核心逻辑是先并行跑向量召回和 BM25 召回,再按 RRF 方式融合。下面是一个简化实现,省略了向量库的连接细节:
# src/retrieve.py import jieba from rank_bm25 import BM25Okapi class HybridRetriever: def __init__(self, embed_model, vector_store, chunks): self.embed_model = embed_model self.vector_store = vector_store self.chunks = chunks tokenized = [self._tokenize(c) for c in chunks] self.bm25 = BM25Okapi(tokenized) def _tokenize(self, text): # 中文务必分词后再进 BM25,否则关键词召回基本等于空转 return [w for w in jieba.lcut(text) if w.strip()] def retrieve(self, query, top_k=15): q_vec = self.embed_model.encode(query) vector_hits = self.vector_store.search(q_vec, top_k=top_k) bm25_hits = self.bm25.get_top_n( self._tokenize(query), self.chunks, n=top_k ) return self._merge(vector_hits, bm25_hits, top_k) def _merge(self, vector_hits, bm25_hits, top_k): # 简化版 RRF:对排名取倒数作为融合分 scores = {} for rank, (idx, _score) in enumerate(vector_hits): scores[idx] = scores.get(idx, 0) + 1 / (60 + rank) for rank, chunk in enumerate(bm25_hits): idx = chunk["id"] scores[idx] = scores.get(idx, 0) + 1 / (60 + rank) ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) return ranked[:top_k]两个要注意的细节。第一,中文文本进 BM25 之前一定要分词,我用的是 jieba,否则关键词召回基本等于空转。第二,Rerank 之前不要着急把 top_k 压太小,混合检索阶段建议先召回 15 到 20 条,让 Rerank 有足够的选择空间。
3.3 查询理解:让 flash-vision 先看懂问题
这个版本的核心卖点是视觉能力,所以查询理解模块我交给 deepseek-v4-flash-vision 来做。用户提问时,系统先调用视觉模型做两件事:判断问题是否需要视觉信息(比如“看这张图的第三个模块”),然后把用户问题改写成适合检索的形式。
# src/generate.py 片段 import json from openai import OpenAI client = OpenAI( base_url="http://localhost:8000/v1", api_key="local-key" ) def analyze_query(user_query, image=None): content = [ {"type": "text", "text": ( "判断问题是否需要检索知识库,以及是否需要视觉信息。" "输出JSON:{\"need_rag\": true/false, \"need_vision\": true/false, " "\"rewritten_query\": \"改写后的查询\"}" )}, {"type": "user", "text": user_query} ] if image is not None: content.append({"type": "image_url", "image_url": {"url": image}}) resp = client.chat.completions.create( model="deepseek-v4-flash-vision", messages=[{"role": "user", "content": content}], temperature=0 ) return json.loads(resp.choices[0].message.content)这里想强调一点,query 改写不是越复杂越好。我最初设计的改写逻辑会引入很多领域词汇,结果检索效果反而变差,因为改写后的句子距离原文的措辞越来越远。后来加了约束:改写后的查询必须在语义上和原问题保持接近,否则退回原问题。
3.4 Rerank 与答案生成
召回和 Rerank 是两回事。向量召回是拿 query 和 chunk 向量做内积,Rerank 则是直接用交叉编码器把 query 和每个候选片段放在一起打分,精度高但速度慢,所以只对 top15 做重排是合理的。
# src/rerank.py def rerank(query, candidates, cross_encoder): pairs = [[query, c["text"]] for c in candidates] scores = cross_encoder.predict(pairs) sorted_items = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True) return [item for item, _ in sorted_items[:5]]答案生成阶段,提示词模板我长期迭代后固定成一套相对稳定的结构:角色定位、任务目标、检索片段、来源约束、输出格式。“来源约束”是最重要的一条,它要求模型在回答时把依据定位到具体片段编号,从机制上减少幻觉。
你是知识库问答助手。请仅根据【检索片段】回答用户问题。 规则: 1. 禁止编造片段中不存在的事实。 2. 每个关键结论后标注来源编号,格式如[来源:3]。 3. 如果片段不足以回答,明确说“知识库中没有找到相关信息”。 4. 如果用户问题需要看图或表格,优先引用视觉解析结果。 【检索片段】 {retrieved_chunks} 【用户问题】 {question}这段提示词写下来没啥花哨,但防幻觉效果非常直接。我见过很多 RAG 项目在调模型、调向量库里花了大量精力,提示词却写得敷衍,其实答案质量的下限恰恰是由提示词和检索片段的配合决定的。
4. 多模态文档场景实战:图文混排与表格解析
4.1 一个让传统 RAG 束手无策的文档场景
我拿一个实际场景来演示 Skill RAG 的价值。企业知识库里有一批 PDF,内容包含:季度销售数据柱状图、组织结构图、合同关键条款表格、扫描版有公章的页面。如果用传统文本管线,流程大概是解析 PDF、抽文本、切块、向量化。结果是什么?表格可能乱成一团,图片和图表直接被跳过,扫描件依赖的 OCR 一旦识别不准就全盘崩掉。
用户提问“2024年Q3的毛利率是多少”或者“看这张组织架构图,产品部挂在哪个中心下面”,传统 RAG 基本答不上来。问题不是模型不够强,而是索引里根本没有这些视觉信息。
skill 化的视觉解析能力就是为这个痛点设计的。
4.2 表格解析:让 flash-vision 把结构还回来
deepseek-v4-flash-vision 处理 PDF 表格,我的做法不是让它直接转文字,而是让它输出结构化的 Markdown 表格,同时记录每个单元格的坐标位置。后面接一道校验逻辑,把表格里的数字和原文 OCR 结果做一次交叉比对,数字对不上的单元格标记出来,搜索时降低权重。
请把图片中的表格完整转成Markdown格式: - 保留所有行列关系,不要合并单元格。 - 数字原样输出,不要四舍五入。 - 表头和单位单独标出。 - 如果某单元格内容被截断,用行号标注“需人工复核”。这里有个经验:视觉模型转换表格,偶尔会在数字上出错,尤其是长数字和带小数的数字。所以我在流程里增加了“数字校验”关卡。模型输出表格后,程序会把表格里的所有纯数字提取出来,和独立 OCR 结果做比对,不一致的地方直接标记。听起来简单,但这一关能把表格解析的准确率明显拉高。
4.3 图表问答:把图片变成“会说话”的索引
图表是这个项目里最能体现视觉模型价值的地方。以前做知识库,柱状图、折线图、流程图里的信息是完全不可检索的。现在我用 flash-vision 把每个图表转成两段内容:一段是图表的文字摘要(比如“2024年Q3销售额环比增长12%,毛利率为34%”),一段是结构化的数据表。摘要进向量索引,数据表进结构化字段。
用户问“哪个季度毛利率最高”,向量检索直接命中图表摘要,答案自然就出来了。如果不做这一步,这个问题对传统 RAG 来说就是无解。
我用一批真实业务 PDF 做了个对比测试,给大家一个直观感受:
| 文档类型 | 传统文本RAG | 加上视觉解析 |
|---|---|---|
| 纯文字技术文档 | 可用 | 可用,无显著差异 |
| 含表格的文档 | 表格乱序、命中率低 | 表格结构可检索 |
| 含图表/截图的文档 | 完全不可检索 | 摘要进索引后可直接回答 |
| 扫描件 | 依赖OCR质量,不稳定 | 视觉模型直接理解版面 |
这个表不是想说明视觉模型是银弹,而是想说:只有当视觉信息真正进入索引,RAG 对多模态文档才算拥有了完整的感知。
4.4 视觉检索:让“看这张图”不再是一句空话
再往前一步,我在项目里实验性地给部分图片建了视觉索引。不是用 CLIP 那种图像向量,而是用多模态 Embedding 直接把图片向量化,存进向量库。当用户问“有没有一张描述系统架构的图”这种问题时,query 会走图像向量检索,找到最匹配的图片。
这个功能目前还比较初级,主要受限于多模态 Embedding 模型的成熟度。但方向是对的:RAG 不应该只检索文本,还要能检索图片、表格,甚至音视频。你在知识库里埋下了什么样的索引,模型才能回答什么样的问题。
5. 我在调优召回质量时踩过的四个坑
5.1 分块策略:不要一上来玩花活
第一版的时候我用了不少花哨的分块方法,什么按句子嵌入聚类、按语义窗口自适应切分,效果反而一塌糊涂。后来退回到最朴素的方案——Markdown/HTML 文档按标题层级结构分块,普通文本按固定 200 到 300 字切分,重叠 50 字。这个简单方案在几乎所有数据集上表现都稳定。
这里的关键不是算法本身,而是“块必须是一个语义完整的最小单元”。按句号切分看起来优雅,但经常把一段完整的观点切碎;固定长度切片又容易切断代码块或表格。花哨的分块算法适合作为优化阶段的手段,起步阶段越朴素越好。我见过太多同学一上来就调分块参数,调了几天数据还是乱的,其实问题出在文档解析阶段。
5.2 Embedding 和 Rerank 要配合,不是二选一
有个常见误解:我用了很好的 Embedding 模型,是不是就不需要 Rerank 了?我实际测下来,Embedding 负责“召回范围”下限,Rerank 负责“排序精度”上限,两者解决的问题不同,不能互相替代。
在混合检索场景下,Rerank 的作用更明显。因为多路召回会把大量候选混在一起,向量分数和关键词分数根本不在一个量纲上,直接排序必然混乱。Rerank 用交叉编码器重新打分后,两类候选才被放在同一尺度上比较。
一个很实用的建议:Rerank 模型不要选太大。我一开始用的一个超大的 Rerank 模型,效果确实好,但延迟从 200 毫秒涨到 2 秒,体验完全不能接受。后来换了一个轻量级版本,损失一点精度,延迟控制在 500 毫秒内,整体可用性大幅提升。
5.3 视觉模型也有幻觉,尤其是数字
很多人以为多模态模型处理图像就绝对可靠,实测下来完全不是。deepseek-v4-flash-vision 在理解版面结构、图表意图方面表现很好,但在具体数字上偶尔会一本正经地编造。比如读取一个表格,它能答对 90% 的行,但中间某一行的数字可能就错位了。
解决办法是在流程里加交叉校验。表格数据出来后,用一个独立的小型 OCR 模型对表格区域做字符识别,两者做布尔比对,不一致的标记为低置信度。答案生成时,如果模型引用了低置信度片段,提示词里会要求它加一句“该数据可能需要人工核对”,减少误导。这个机制很土,但非常有效。
5.4 高质量片段太多了也是一种麻烦
RAG 系统做到后面,检索质量上去之后反而出现一个新问题:每次召回的高质量片段太多,全部塞进提示词会导致上下文爆炸,而且关键信息被淹没在大量相关但不重要的内容里。
我现在的策略是三步走:先 Rerank 只保留 top5,再用一个小模型对每个片段做一句话摘要,最后按问题相关性对摘要再排一次序。实际效果比单纯加大 top_k 好很多。这条经验可能有点反直觉——你花了大把力气提升召回率,最后却要主动限制进上下文的片段数量。但大模型生成质量对上下文里的噪声非常敏感,宁可少而精,不要多而杂。
6. 开源发布过程中的一些实践
6.1 开源许可证怎么选
项目做完后,我打算开源,第一个遇到的就是许可证选择。License 选择是最容易被新手卡住的第一关,你到 Gitee 的社区问答里逛一圈,“开源许可证选什么”这类问题永远是热点。我的建议:
- 如果希望被尽可能多的人拿去用,选 MIT,最宽松,几乎没有义务。
- 如果项目涉及专利且你想保护贡献者,选 Apache 2.0。
- 如果希望任何衍生代码都必须开源回馈社区,选 GPL。
我这个项目主体用了 MIT,因为 Skill 机制的价值在于生态,限制越少越容易被集成。模型部分则遵循 deepseek-v4-flash-vision 原始开源的许可条款,代码和模型分开授权。这条经验不一定适合所有人,但建议在做决定前认真想清楚:你开源这个项目,到底是想看它被广泛使用,还是想强制别人回馈。
6.2 开源文档贡献比写代码更花时间
这次开源让我体会最深的一点是:写好文档,比写代码更费时间。项目不仅写了 README,还整理了一份完整的部署指南、一份常见问题列表、一份基准测试结果。为什么这么做?因为开源的真正门槛不是代码质量,而是别人能不能快速跑起来。
给想开源的朋友几个建议。第一,README 开头放一张完整的架构图或者 demo 动图,让读者 10 秒内看懂项目在做什么。第二,提供 Docker 一键启动,能把部署时间压到五分钟以内。第三,把你测试用的样例数据也放进去,这是帮助别人复现你效果的最好方式。第四,不要只看 GitHub Star,要关注有没有人真的 clone 下来跑通了,那才是项目被使用的信号。
6.3 下一步:从 Skill RAG 走向 Agentic RAG
这个项目目前还在稳定迭代,我的下一步计划是把它往 Agentic RAG 方向演进。所谓 Agentic RAG,就是让 RAG 不再是一次性问答管道,而是让 Agent 根据用户的复杂问题,自主决定检索什么、检索几轮、是否需要对比多份文档。Skill 机制天然适合这种演进,因为每个 Skill 本质上就是 Agent 可以调用的一个工具。
另外也在考虑和 Dify 这类低代码平台集成。近期社区里关于 Dify 落地知识库的实践分享特别多,集成方式其实很标准——通过普通 API 把这套 Skill 作为外部工具暴露出来,低代码平台只需要填写接口地址就能调用。这样可以大幅降低非技术用户使用 RAG 的门槛,也是开源项目扩大影响面的一条实际路径。
最后再分享一个很实际的感受。我做完这套 Skill RAG 后最大的收获,不是某个指标从 80% 涨到 92%,而是意识到模型本身迭代太快了,但围绕模型搭出来的“可插拔能力层”才是真正能沉淀的资产。两个月前用的 deepseek-v4-flash-vision,可能很快就会被更强的开源模型替代,但只要 Skill 的结构还在,换模型就是改几行配置的事。这也是我为什么坚持把能力拆成技能包而不是做成一个单体 RAG 服务的原因。如果你也在折腾知识库,我建议你先想清楚一个问题:你做的是一棵焊死的树,还是一个能长出新的枝干的系统?