把大语言模型接到自己的私有知识库上,这件事听起来简单,做起来全是坑。大语言模型足够强大,但在被迫“猜答案”时,它们会以令人瞠目的自信编造事实——这就是大家常说的幻觉。RAG的使命,正是用你私有知识库里可验证的数据去“接地”模型输出,从根本上消除这种不靠谱。过去几年,RAG已经从一个实验性技巧,变成了严肃AI系统的基础模块——无论是智能体框架还是开发者副驾,背后都离不开它。
一、高层架构
RAG由两条独立但紧密协作的流水线构成。
1. 摄取管线(Ingestion Pipeline)
从知识库里抽取出文本,再把它转换成可检索的格式。这是整个系统里最复杂也最关键的一环——这一步没做好,后续全部免谈。难就难在:技术选项太多,你必须根据手里的数据类型,在大量配置之间反复权衡。
2. 检索 + 生成(Retrieval + Generation)
数据准备好之后,接下来要做两件事:用户提问时,先从库里捞出相关片段;再把这些片段以尽可能好用的方式喂给LLM,作为生成回答的上下文。
两条流水线打通之后,最终效果是什么?用户向基于LLM的应用提问时,回答不再是凭空生成,而是先被知识库里的真实信息“增强”了一遍。
你的数据,决定你的系统
RAG社区里堆了无数技巧,但这不意味着你全都得用上。世上也不存在对所有数据类型都万能的“完美设计”。真正驱动架构决策的,永远是你的数据本身,以及你希望它怎么被消费。动工之前,先问清楚这几个关键问题:
- 数据是什么格式?(PDF、HTML、JSON、纯文本……)
- 结构性强不强?(有无章节、层级、表格、法律引用等)
- 有没有需要额外索引的元数据?
- 是否包含非文本内容?(图片、视频、音频)
- 数据规模有多大?
- 用户最终会怎样去检索它?
二、采用哪种检索方式?
这是整个系统里最重要的决策,后续几乎一切设计都由它派生而来。
关键词检索(稀疏向量)
搜索引擎用了多年的老办法。把查询中的词抽取出来,跟知识库里每篇文档的词做匹配。匹配靠稀疏向量完成——基于词表给每个词建索引。匹配方式可以从简单的词频,进化到更成熟的BM25。BM25利用整个语料库的逆文档频率给词项赋权,那些在大量文档里频繁出现的词会被降权,从而给出更靠谱的匹配结果。大多数全文数据库(比如Lucene、ElasticSearch)底层用的都是BM25的变体。
嵌入相似度(稠密向量)
关键词检索有个死xue:如果查询里用的词恰好不在知识库里,它就彻底失效。嵌入(Embedding)通过把文本转换成数值向量来解决这个问题。嵌入由专门的机器学习模型生成,能捕捉词的语义含义。之所以叫“稠密向量”,是因为模型对每段文本输出的向量维度都一样。语义相近的词,在向量空间里彼此挨得更近。检索时用余弦相似度来找语义相近的文本,所以这类检索也常被称为语义检索。向量数据库天然适合存数值向量,原生支持余弦相似度查询;关系型数据库(比如PostgreSQL、SQL Server)也开始原生支持向量了。
混合检索
稠密向量和稀疏向量可以合并起来用,往往能拿到最好的效果。不过,开箱即用就支持混合检索的数据库不多,通常需要自己动手做一套复杂的融合逻辑来合并不同检索结果。
决策理由:采用混合检索
本案例选择混合检索,因为它在Kubernetes文档上最可能表现最好。权重分配如下:
- 嵌入(语义)占70%——预期表现最佳;
- 关键词(BM25)占30%——用来支持按特定术语或代码的精确检索。
这是RAG实践中很常见的配比。
数据库选型:Qdrant
选数据库时,通常要考虑几点:支持的检索功能类型、预算(开源还是商业)、隐私需求(数据是否需要自托管)、流行度(社区支持力度)、SDK覆盖度,以及基于实际负载的性能表现。
支持混合检索的数据库有几个,但BM25比较棘手,因为它需要维护跨所有文本的词频索引。所以,一个稳妥的做法是选一个能替你搞定这件事的向量库。
Qdrant可以同时存储稠密向量和稀疏向量,并原生支持混合查询,结果用倒数排名融合(RRF)合并——这是RAG的标准做法。Qdrant开源、能在Docker里跑、托管非常省事,而且对.NET生态支持极好。当然,其他优秀选项也值得关注:Wea viate、Pinecone、Milvus。
三、摄取管线
既然决定用混合检索,摄取管线就需要同时生成并存储稠密向量嵌入和稀疏向量。
Kubernetes源文档篇幅很长,这在RAG知识库里并不罕见。长文本对检索策略尤其不友好——嵌入试图提炼文本的语义含义,但一篇长文往往覆盖很多不同主题。嵌入在语义内聚得好的小块文本上效果最佳。而且,我们也很难把整篇长文档直接塞进LLM的上下文里。理想做法是根据用户查询,只取出最相关的一部分。RAG系统通过在生成嵌入和存储文档之前进行切块来解决这个矛盾。
一套标准摄取管线通常包括这几个关键步骤:
- 抽取
- 预处理
- 切块
- 生成元数据
- 生成稠密与稀疏向量
- 存储
1. 抽取
第一步是从源文档里把文本抽出来。任何能用于检索或丰富回答的元数据(比如文档名、页码、摘要)也应该一并捞出来。
如果文档结构清晰、被拆成了章节,最好按章节抽取文本。这对接下来的切块大有好处——能保证一个块不会横跨多个章节。章节按页存储,方便后续拼接还原,也便于理解某个块归属于哪些页。实践中,通常会把子章节扁平化成一条“章节路径”,并指定结构层级要嵌套多深。
因为文档布局和结构千差万别,通常需要准备多种抽取策略。示例仓库提供了几种:
- SimplePdfExtractor:把整个PDF的所有文本当作单个章节抽出。
- BookmarkPdfExtractor:搜索PDF书签,用来识别章节和嵌套章节。
- FormatBasedPdfExtractor:分析文本格式来推断章节,假设标题字号更大或字体更粗,越深层的章节字号越轻。
Kubernetes文档的标题格式非常统一,因此FormatBasedExtractor用起来很顺手。下图展示了一个基于标题格式抽取出的章节与子章节示例。
2. 切块
把文档拆成章节或子章节后,切块工作已经完成了一半。接下来,需要把过长的章节进一步切成更小的块,以便生成高质量的嵌入。
块长度
- 一般来说,200–300个词元是一个不错的起点。但务必拿自己的数据测一测不同长度下的召回率。
- 对于复杂的政策类或法律类文档,块长度可以拉高到600词元。
- 要注意嵌入模型的限制:很多开源模型有512词元的硬上限。
块切分点
- 千万别用严格的固定块大小,那会把句子拦腰切断。
- 应该优先在段落分隔处切块,至少也要在句末切。
- 更高级的做法是,用嵌入模型或LLM在语义明显不同的文本处切块。
块重叠
- 无论切块策略多精妙,总会有边界切得不理想的时候。
- 在块之间制造10%–20%的重叠,通常能有效提升召回率,而且不会明显损伤精度或延迟。
针对Kubernetes文档里一些篇幅较大的章节,实践下来发现以下配置效果不错:
- 最大块长度:400词元
- 块重叠:50词元
3. 生成元数据
给每个块附加额外元数据,主要有两个目的:
- 丰富LLM生成的内容,比如加入引用来源和页码。
- 在检索时实现自定义过滤或定向检索。
具体存哪些元数据,取决于你的数据本身。常见的例子包括:
- 源文档名
- 页码
- 章节与章节路径
- 块在章节内的索引和块总数
- 引用ID、条款号、规则/错误/产品代码
- 由LLM生成的块内容摘要
4. 生成稠密向量(语义嵌入)
嵌入由选定的嵌入模型基于块文本生成。如果有助于提升语义相关性,也可以把源文档名或章节路径拼进嵌入文本。
模型选择很多,不同模型对不同数据类型表现各异。更强的模型通常产出维度更高的向量,但需要更多算力和GPU资源。这里选用BAAI/bge-small-en-v1.5,因为它足够小、能在CPU上较快运行,产出384维向量。通过一个Python FastAPI应用来托管Hugging Face模型。
5. 生成稀疏向量(BM25)
稀疏向量只记录块中间出现词项的词频。存的时候不保存词项文本,而是给每个词项分配一个整数ID。有两种分配方式:一是用一份词表做映射,二是对词项直接哈希得到整数。简单哈希更常见一些,省去了维护词表的麻烦。像xxHash这样的算法极快,而且几乎不会发生哈希冲突。
摄取调度
摄取管线怎么跑、多久跑一次,完全取决于你的数据和实际场景:
- 手动:数据极少变化时,手动运行就行。
- 定时任务:文档变化较频繁时,用cron定时任务,每日运行是常见做法。
- 实时:如果有文档管理层且文档频繁变动,可以用异步事件驱动流程,源文档一变化就触发管线。
四、检索 + 生成
大部分复杂决策和开发工作都在摄取阶段解决了。到了这一步,只需要一个根据用户提示来检索块的流程。
检索
选择Qdrant,核心原因就是它能开箱即用地同时执行BM25关键词检索和嵌入余弦相似度检索。混合检索时,建议给不同策略分配权重,RAG实践中常用:
- 稠密向量(嵌入)70%
- 稀疏向量(关键词/BM25)30%
嵌入能捕捉语义,通常表现最好,所以一般最依赖它。但理想的权重取决于你的数据,以及关键词精准查询有多重要。
检索前,需要先把用户查询走一遍摄取管线里的部分步骤:
- 预处理
- 生成稠密与稀疏向量
- 检索
检索时应该返回多个块,因为可能有多个块匹配同一个查询——这通常被称为“Top k”结果。Top k取5–10通常够用,具体取决于数据和切块策略。块越小,通常需要的Top k就越大。
这套做法不错,但并非万无一失。混合检索也不完美,尤其是当用户查询很短的时候。可以在检索之后叠加一些技术来进一步提升召回率和精度。
相邻块
我们是按章节抽取并在章节内切块的,这意味着检索结果的相邻块极有可能也相关(即使它们没有被检索直接命中)。一个推荐的后续步骤是:用相邻块去丰富检索结果。一般来说,取检索块前后1–2个索引内的块,效果就很不错。这正是为什么要把文档名、块索引、块总数、章节路径存为元数据——有了它们,才能高效查询相邻块。如果向量库支持索引(Qdrant支持),在这些字段上建索引能明显提升查询效率。
重排序
检索时经常会带出完全不相关的块,关键词检索尤其容易这样。重排序可以按相关性对结果排序,并过滤掉那些无关的块。一个很实用的技巧:检索时返回Top k的2–3倍结果,再用重排序器筛到最相关的Top k。
举个例子(假设Top k = 5):
- 混合检索返回Top k × 2 = 10个块
- 加入相邻块 → 18个块
- 重排序器按相关性排序,返回Top k → 5个块
这种方式能显著提升检索精度。
重排序器是一种专门用来给一对文本打分、输出相关性分数的模型。把每个检索结果连同用户查询一起喂给它就行。还可以用相关性分数设定一个最低阈值,而不仅仅是取Top k。
交叉编码器:专为相关性打分训练的机器学习模型,处理文本比LLM快得多。质量更高的重排序器在生产环境中通常仍需要GPU。这里选用免费开源的BAAI/bge-reranker-base,表现不错且在CPU上够快,同样通过Python FastAPI托管Hugging Face模型。
LLM重排序:目前云上托管的专用重排序模型选择不多,另一种方式是直接用LLM配合定制的系统消息来做重排序。建议用轻量LLM(比如GPT-4.1 Mini),避免显著拖慢RAG系统的响应速度。
生成
检索搞定之后,剩下的就是把检索到的块送进LLM做最终总结。
上下文格式:如果LLM支持,把块作为工具消息注入是最佳方式。在系统消息中,写入LLM应该如何解释检索结果,以及找不到匹配信息时该如何回应的规则和护栏。
角色的划分要清晰:
- 系统消息:规则、行为、护栏
- 工具消息:检索到的上下文(各类块)
- 用户消息:用户提出的问题
工具消息中要包含你希望LLM引用的任何元数据,连同块内容一起。
编排
一种更易于管理的方式是用编排器(比如Semantic Kernel或LangChain)来生成工具调用和输出。使用编排器还有两个额外的好处:发往RAG系统的查询先由LLM解释,可以在需要时被修正或优化;知识库可以作为更大的AI智能体或副驾的一部分来使用。用编排器时,你只需要关心系统消息和用户消息,编排器会负责在需要时调用RAG检索,并把数据作为工具消息(通常是JSON格式)加入上下文。
LLM模型选择
- 如果系统只有RAG这一个功能,便宜的LLM就够用了——它只需要对检索到的文档做基本的总结。
- 如果用编排器,则需要支持工具调用的模型。可以通过Ollama使用Llama 3.2 3B(一个小语言模型),以便在CPU上运行;当然,更大的模型会可靠得多。
五、托管
示例中将Semantic Kernel RAG助手打包成了CLI控制台应用,简单场景下够用。如果要面向更广泛的用户,最好在上面加一层小型的Web UI(比如React或Angular)。
每个服务(重排序器、嵌入器等)作为独立进程运行。独立托管的好处是,每个服务可以独立扩缩,并为各服务使用不同的基础设施或资源配置。例如,重排序器可能很吃资源,需要扩容到大量副本;而LLM很可能需要在GPU基础设施上运行。
容器很适合这类场景——把各服务隔离打包成镜像,在不同环境中都能可靠运行。推荐用容器编排器来快速扩缩,Kubernetes是理想选择。
自托管 vs 云服务
示例方案全部基于开源技术,你可以完全自托管,甚至直接在CPU上本地运行。不过,除非你的公司有重大的数据安全风险且不接受云服务,否则一般不太建议纯自托管。需要注意的是,大多数云服务(比如Azure OpenAI)面向企业用途且完全无状态——它们不会存储你的提示或回答中的任何数据。
混合方案也是可行的:把轻量的模型自托管。嵌入和重排序模型通常不像LLM那么吃算力。理想情况下,生成用的LLM需要GPU才能可靠运行,在这方面,云服务通常比自建基础设施成本低得多。
市面上有很多通用的开箱即用RAG方案,但自建架构能让一切贴合你的特定数据需求,往往能换来更优的召回率和精度。即使你最终决定用外部RAG服务,这里讨论的很多技术(不同切块策略、重排序等)也同样能派上用场。
完整代码见原作者GitHub:matt-bentley/LLM-RAG-Architecture。
关键参数速查
| 决策点 | 推荐值 / 选择 |
|---|---|
| 检索方式 | 混合检索(Hybrid Search) |
| 混合权重 | 稠密70% / 稀疏30% |
| 结果融合 | 倒数排名融合(RRF) |
| 向量数据库 | Qdrant(备选:Wea viate / Pinecone / Milvus) |
| 块长度 | 一般200–300词元;复杂文档可到600;K8s案例用400 |
| 块重叠 | 10–20%;K8s案例用50词元 |
| 嵌入模型 | BAAI/bge-small-en-v1.5(384维,CPU友好) |
| 稀疏向量词项ID | 偏好哈希(如xxHash),免维护词表 |
| Top k | 5–10(块越小,k越大) |
| 重排序召回 | 检索Top k的2–3倍,再筛到Top k |
| 重排序模型 | BAAI/bge-reranker-base(交叉编码器)或轻量LLM(如GPT-4.1 Mini) |
| 相邻块扩展 | 前后1–2个索引 |
| 编排器 | Semantic Kernel / LangChain |
| 生成LLM | 纯RAG用便宜模型;需工具调用用Llama 3.2 3B等 |
| 部署 | 各服务独立容器化,用Kubernetes编排 |
