我见过不少RAG项目,demo阶段惊艳全场,一接真实业务就垮掉。垮的地方通常不在检索算法,而在代码结构——数据接入、分块、向量化、存储、生成,整个链路焊死在一起,换任何一块都得动全身。
今天想聊的不是怎么把准确率调高两个点,而是一个更前置的问题:如何把一个RAG项目做成可插拔的知识基础设施。可插拔的意义在于数据源、分块器、Embedding模型、向量库、检索策略、生成模型都能独立替换,不互相绑架;知识基础设施的意义在于它要像楼宇供水系统一样,多个业务方拧开龙头就有水可用,而不是各挖各的井。这适合正准备把RAG推上生产的团队,也适合搭个人知识库搭到一半、已经痛恨"换个模型就要改代码"的开发者。下面按边界设计、最小骨架、踩坑复盘、多业务落地四层展开,全程以Python为例,希望能帮你绕开我踩过的那些坑。
1. 先回答一个问题:你要做RAG功能,还是RAG设施
1.1 绝大多数RAG项目活不过第二个真实场景
一个很典型的轨迹是这样的:第一个场景通常很纯粹,拿几个PDF,选一个向量库,接一个模型,演示效果惊艳,团队信心爆棚。然后第二个场景来了,业务方说文档每周更新,要能自动同步;第三个场景说,出于合规考虑,必须换成开源模型部署在内网;第四个场景说,新项目的文档要和老项目共用一套检索,但权限严格隔离。
到了这个阶段你会发现,当初那份代码里,PDF解析、分块大小、Embedding调用、向量库collection名称、提示词模板全都紧紧耦合在一起。改一个分块策略,要重新灌一遍库;换一个向量库,所有查询代码重写;换一个模型,提示词和解析逻辑全部返工。这个过程我已经见过太多回,几乎每家公司做RAG都会经历一轮。问题不出在模型选型,而出在架构姿态——你写的其实是一个一次性脚本,只是碰巧叫了一个"RAG项目"的名字。
1.2 "可插拔"不是形容词,而是三个硬指标
把RAG当设施建设,需要给自己设三个硬指标,缺一个都谈不上可插拔的知识基础设施。
第一,可替换。六个核心环节(数据接入、分块、向量化、存储、检索、生成)里的任意一个组件,都能在不改业务代码的前提下换掉。换Embedding模型不是重写检索逻辑,而是改一段配置;换向量库不是改查询代码,而是替换一个实现类。第二,可观测。任意一个用户问题,你都能追溯它走了哪条检索链路、召回到哪些chunk、最终生成了哪些引用、每段耗时花在哪里。第三,可治理。知识有版本、有归属、有更新机制,出问题能回滚,废弃数据能清掉,而不是数据越积越乱、谁都不敢动。
说到底,这三个指标约束的是设计姿态,不是某个具体技术。满足它们,你的RAG才有资格被多个业务方依赖。否则它就只是一个跑在别人服务器里的黑盒,跟"知识基础设施"四个字没有任何关系。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 边界设计:六段链路各自该管什么,不该管什么
2.1 从数据接入到答案生成,责任要划清
可插拔的第一步是把整个链路切开,切到每个环节都能独立存活。结合我自己的实践和社区里大量RAG项目的迭代方向,我建议至少拆成六段:
| 环节 | 职责 | 典型实现 |
|---|---|---|
| Data Connector | 从各种源头拉出原始文档 | 本地文件、Web站点、RSS、数据库、云盘 |
| Chunker | 把长文档切成适合检索的片段 | 固定长度、递归字符分割、基于语义的切块 |
| Embedder | 把文本变成向量 | Ollama上的bge-m3、OpenAI接口、本地推理的m3e |
| VectorStore | 存储向量并支持相似度查询 | Milvus、PGVector、Qdrant、Chroma |
| Retriever | 根据查询召回相关片段并排序 | 纯向量检索、向量+BM25混合、重排模型 |
| Generator | 把上下文组装成最终回答 | 开源模型、商用API、带引用格式的输出 |
这个划分不是随意切的,它符合一个基本规律:每个环节的变化频率和变化原因都不一样。数据源头说加就加,分块策略要跟着文档类型调,Embedding模型可能因为成本或效果而切换,向量库往往由公司基础设施部门统一定,检索策略要针对业务场景优化,生成模型更是隔几个月就出一版新的。变化频率不同,就必须用有效的边界隔离,否则任何一处的变化都会连带毁掉其它环节。
2.2 接口归接口,实现归实现:依赖倒置的价值
边界画好后,代码上要做的事情其实只有一件:让每个上层模块只依赖接口,不依赖具体实现。这就是典型的依赖倒置原则,用大白话说,就是"插座只认插头的形状,不认插头是哪个牌子"。
在Python里,我用Protocol而不是抽象类来定义接口,因为Python是鸭子类型语言,只要一个类实现了对应的方法签名,它就可以被当作该接口的实现,不需要继承任何东西。举个例子,一个最简单的数据连接器协议长这样:
python复制from typing import Protocol, Iterable
from dataclasses import dataclass, field
@dataclass
class Document:
content: str
metadata: dict = field(default_factory=dict)
@dataclass
class Chunk:
content: str
metadata: dict
doc_id: str
class DataConnector(Protocol):
def load(self) -> Iterable[Document]: ...
这里的关键点是DataConnector只约定"给我一个能产出Document集合的load方法",至于你读的是本地文件、爬的网页还是拉的数据库,它一概不关心。后续所有组件都按同样的思路定义协议,核心业务层里只出现这些协议类型,具体的FileConnector、WebConnector被放在注册中心里,等待装配。
2.3 一个配置写清楚整个知识库
边界和接口都定好之后,下一步就是用什么方式把这些实现装起来。我不建议在代码里手动new一堆对象然后拼装,那个做法的可维护性比硬编码好不到哪里去。更合理的做法是配置驱动:每个知识库对应一份YAML配置,所有组件的类型和参数都写在配置里,程序启动时读配置,通过注册中心把对象图构建出来。
yaml复制knowledge_base:
name: "blog_archive"
connector:
type: "file"
params:
path: "./docs/blog"
chunker:
type: "fixed"
params:
chunk_size: 800
overlap: 150
embedder:
type: "ollama"
params:
model: "bge-m3"
