Appearance
RAG知识框架
先回顾一下前面我们学过的 RAG 基础知识,一个基础的 RAG 分为两个阶段:
数据索引阶段:核心就是将数据存储到向量数据库
数据检索阶段:核心就是将用户的问题转成向量,然后从向量数据库里面找出语义相同的内容,将其作为上下文给 llm 生成答案。

那有这些知识就够了么?
不够,光掌握这些知识,你是没办法回答这些面试题的:
- 如何设计一个 RAG 的 SDK?
- chunking 如何设计?
- 怎么评估 RAG 系统效果?
- rerank 为什么重要?
- Recall@K 是什么?
- MRR 是什么?
- ....
这一套课程,就是让你对整个 RAG 彻底通关的。当你学习完这套课程后,你能够对整个 RAG 有一个系统性的理解,而一个东西,当你对它有系统性理解之后,面试题无论怎么变,你都能从容应对。
如何搭建 RAG 的知识框架呢?这里我们可以对 RAG 知识进行分类,而这个分类是有讲究的,分类的视角不同,分出来的类别就不同的。这就好比针对编程语言做分类,按照强弱类型可以分出一个类别,按照编译解释可以分出来一个类别。
RAG 相关的知识,可以按照下面两个不同视角来做分类:
- RAG pipeline 视角
- RAG 工程视角
1. RAG pipeline 视角
我们先来看 RAG pipeline 视角下的分类。所谓 RAG pipeline,就是整个 RAG 的工作流程,其实和我们之前所学的 RAG 知识大差不差的。这里我将其分为 5 个阶段:
- 数据索引阶段
- 检索前处理
- 检索操作
- 检索后处理
- 答案生成
1-1. 数据索引阶段
所谓数据索引,就是将文档存储到向量数据库的过程,回忆一下我们前面所讲的知识:
- 加载文档
- 切分成 chunks
- 转化为向量嵌入
- 存入向量数据库

这里面最重要的在于切分 chunks,这很大程度影响了后续 RAG 的检索质量,前面我们讲过,切割的时候可以:
语义结构维度:强调的是语义完整性,防止模型拿到“断句、不完整”的上下文。可以按照句子的粒度进行切割,将每一段文本按句号、问号、叹号等 标点符号 分割。
原文
ChatGPT 是由 OpenAI 开发的大语言模型。它基于 Transformer 架构,具有强大的语言理解和生成能力。切割后
ChatGPT 是由 OpenAI 开发的大语言模型。它基于 Transformer 架构,具有强大的语言理解和生成能力。实现策略维度:满足向量模型有最大词元限制,比如 OpenAI embedding 最大约 8192 词元数。
- 固定长度字符切分:每 N 字符为一段,适合规则性较强的文档
- 词元切分:每 N 个词元切一段,兼容模型的词元数限制
上面这两个策略可以组合着来使用。
另外,在切割这一块儿,你还需要理解下面的这些技术名词:
- Simple RAG
- Semantic Chunking
- Recursive Chunking
- Contextual Chunk Headers
- Document Augmentation
- Knowledge Graph
- Hierarchical Indexing
- 元数据抽取
- 去重与清洗
- 增量更新
这些技术名词各自的意思,我会放到具体介绍 pipeline 的时候来解释。
另外一个重点,就是关于向量数据库,这是自 AI 火起来后的一种新型数据库类型,在完成文本切分与 embedding 之后,数据通常会被存储到向量数据库或支持向量检索能力的存储系统中。
这些系统提供高效的相似度搜索(如 ANN / KNN),从而支持基于语义的检索能力。常见向量数据库如下:
| 向量库 | 特点 |
|---|---|
| Supabase | PostgreSQL + pgvector 扩展 |
| Weaviate | 云服务 + 本地部署均可 |
| Pinecone | 高性能、托管服务、易接入 |
| Milvus | 面向海量数据的高性能搜索 |
| Chroma | 轻量级、本地优先、开发友好 |
| MemoryVectorStore | 纯 JS 内存向量库(测试用) |
1-2. 检索前处理
英语称之为 Pre-Retrieval。
所谓检索前处理,就是完成诸如:
- 查询转换
- 查询扩充
- 检索路由
等处理工作。
查询转换(Query Rewriting)
将用户的原始问题“重写”为一个更清晰、更标准、更检索友好的表达方式。例如:
| 原始问题 | 转换后查询 |
|---|---|
| “这个 App 支不支持退款?” | “App 退款政策是什么?” |
| “我买的耳机怎么一直没声音?” | “耳机无声音的常见原因与解决方法” |
查询扩充(Query Expansion)
在不改变用户意图的前提下,添加相关词语或同义表达,让检索系统能够匹配到更多语义相关的文档。例如用户输入:
项目合同扩充后的 Query 变成了:
["项目合同", "合作协议", "法律文件", "合同模板"]这类操作能显著提高召回率,避免“字不对字”导致遗漏。
检索路由(Retrieval Routing)
在准备外挂语料库(知识库)的时候,往往会有多个语料库。
针对不同类型的问题,选择不同的知识库或不同的检索方式,实现多路检索策略。例如:
| 用户问题 | 路由策略说明 |
|---|---|
| “这款产品电池容量是多少?” | 路由到【产品参数知识库】 |
| “怎么退货?” | 路由到【客服 FAQ 知识库】 |
| “能再详细说明一下工作原理吗?” | 路由到【技术文档知识库】,或使用多跳检索 |
检索前处理的目的,是为后面的检索和检索后处理做必要的准备,以便提高检索阶段 召回知识 的精确度与最终生成的质量。
这里解释一下 召回率 和 精确率 这两个名词:
召回率(Recall)
指的是所有相关内容中被成功检索出来的比例。公式为:
召回率 = 检索到的相关内容数量 / 所有实际相关内容的总数例如:实际相关文档有 10 个,系统只检索到其中 6 个,那么召回率 = 6 / 10 = 60%
精确率(Precision)
指的是检索出来的所有内容中,真正相关的占比。公式为:
精确率 = 检索到的相关内容数量 / 检索到的所有内容数量例如:实际相关文档有 10 个,系统共返回 8 个文档,其中 6 个是相关的,那么精确率 = 6 / 8 = 75%
也就是说,召回率反映的是系统的覆盖能力,而精确率衡量的是系统的准确性。
另外,下面是 检索前 处理涉及到的你必须要理解的名词:
- Query Transformation
- Query Rewrite
- Multi-query
- Query Decomposition
- HyDE
这些技术名词各自的意思,我会放到具体介绍 pipeline 的时候来解释。
1-3. 检索操作
检索阶段分为下面几个步骤:
- 将 Query 转化为向量
- 在向量数据库中进行相似度检索,有几种方式:
- 余弦相似度:关注向量方向是否一致
- 欧氏距离:衡量向量之间的绝对距离远近
- 点积:综合体现方向一致性与向量大小(长度)。
这一阶段你需要掌握的技术名词:
- Vector Search
- Keyword Search
- Fusion
- RRF
- Small-to-Big Retrieval
- Hypothetical Retrieval
- CRG
这些技术名词各自的意思,我会放到具体介绍 pipeline 的时候来解释。
1-4. 检索后处理
英语为 Post-Retrieval,顾名思义就是对检索出的相关知识块做必要的补充处理的阶段。
即使用了很优秀的向量搜索,检索回来的 Top-K chunks 仍然可能存在这些问题:
- 内容相关性差:向量相似度高但语义上与用户问题不匹配
- 信息冗余:多个 chunks 说的是同一件事,浪费词元空间
- 有害内容:包含过时、不合法、敏感内容
- 结构混乱:拼接后的上下文缺少逻辑、排序不合用户意图
因此需要对检索结果进行进一步处理,确保大模型接收到的上下文内容是「最相关 + 最高质量 + 最结构化」的。
常见的处理有:
- 重排序(Reranking)
- 过滤无关内容(Filtering)
- 合并去重(Merging)
- 精简摘要(Summarization)
- 格式优化(Structuring)
经过了这些后处理之后,能够使得最需要、最合规的知识块处于上下文的最前端,从而有助于提高大模型的输出质量。
该阶段需要掌握的技术名词:
- Re-rank(核心)
- Context Enriched Retrieval
- RSC
- Context Compression
- 文档过滤 / threshold
这些技术名词各自的意思,我会放到具体介绍 pipeline 的时候来解释。
1-5. 答案生成
答案生成,就是将检索到的内容和用户问题一起发送给 llm,然后由大模型生成答案:

构造出来的提示词大致如下:
[系统提示]:
你是一个智能客服助手,请基于以下资料回答用户的问题。
[资料内容]:
1. 本产品支持7天无理由退货。
2. 如存在质量问题,可申请退换货。
3. ...
[用户问题]:
我买的这个产品坏了还能退吗?
[你的回答]:该阶段需要掌握的技术名词:
- Prompt Template
- Self-RAG
- Grounding
- Answer with citation
- LLM-as-Judge
- 多答案一致性
- Context Compression(部分也在这里)
这些技术名词各自的意思,我会放到具体介绍 pipeline 的时候来解释。
2. RAG 工程视角
前面是从 RAG 工作流程的视角做的分类,如果从工程视角来分类,就又不一样了,可以分为:
- 数据构建系统
- 推理系统
- 评测系统
- 观测系统
2-1. 数据构建系统
该系统负责的就是将原始资料加工成“可被检索、可被推理”知识形态的整条链路:
- 文档解析
- 文本清洗
- chunk 切分
- 数据增强
- embedding
- 写入向量库 / 索引库
很多人学 RAG 时只盯着检索和生成,但工程上,RAG 的上限很大程度由数据构建系统决定。原因很简单:
- 文档切不好,检索就不准
- 元数据不全,过滤就做不了
- embedding 质量差,召回就不行
- 数据更新不及时,回答就过时
所以这个系统本质上是在解决:如何把“原始资料”加工成“机器可用的知识资产”
工程上重点关注:
- 数据接入是否稳定
- 构建速度是否可接受
- 索引是否支持增量更新
- chunk schema 是否统一
- embedding 是否可替换
- 数据是否能回溯
2-2. 推理系统
推理系统就是大家最熟悉的 RAG 主链路,它是真正直接面对用户请求的在线系统,负责处理用户问题,并返回答案。
典型链路是用户提出问题,然后:
- query 预处理
- 检索
- 重排 / 过滤 / 上下文组装
- LLM 生成
- 返回答案
也就是说,这个系统基本覆盖了 RAG pipeline 的后 4 个阶段:检索前处理、检索、检索后处理 以及 答案生成。
工程上会重点关注:
- 延迟
- 吞吐
- 检索准确率
- 生成忠实度
- 可扩展性
- 容错能力
因为这些点能够决定用户体验的核心:
- 能不能答对
- 答得快不快
- 有没有依据
- 会不会胡编
2-3. 评测系统
评测系统不是给用户用的,而是给研发团队用的。它的核心任务是:量化 RAG 系统效果。
它通常会准备一组测试集,然后:
批量运行 RAG
收集检索结果和生成结果
计算指标
输出评测报告
对比不同版本的输出结果,比如:
同一套测试集对比 baseline 和优化版
对比是否加 rerank
对比不同 chunking 策略
对比不同 embedding 模型
这套系统是跳出了 pipeline 之外的一套系统,RAG 最大的问题之一就是:你感觉“好像还行”,但没有量化依据。有了这套评测系统,你就能够很好的回答这些问题:
- 是检索变好了,还是 prompt 凑巧有效
- rerank 到底提升了多少
- 新模型值不值得换
- 线上事故到底是偶发还是系统性退化
因此这个系统本质上解决的是:如何用数据证明 RAG 系统到底好不好。
常见的评测内容有:
检索评测
Recall@K
Precision@K
MRR
NDCG
生成评测
BLEU
ROUGE
BERTScore
Faithfulness
Answer Relevance
端到端评测
LLM-as-Judge
人工打分
A/B 测试
这里主要涉及到了一些指标,例如 MRR、BLEU 等,这些指标在后面具体介绍评测系统的时候再介绍。
2-4. 观测系统
观测系统负责在真实线上环境中持续采集运行数据,例如:
- 用户的query
- 检索到的文档
- 最终答案
- 响应耗时
- token 消耗
- 错误日志
- 用户反馈
- 点赞 / 点踩 / 追问行为
它关注的是 真实请求发生了什么,而不是 离线测试集上的得分是多少。因为离线评测再完整,也覆盖不了真实世界的所有情况。线上会遇到:
- 奇怪表达
- 多轮上下文污染
- 热点问题
- 错别字
- 脏数据
- 突发流量
- 第三方模型超时
它和评测系统的区别
这个你可以直接记成一句话:
- 评测系统:有没有做好
- 观测系统:有没有出问题
换句话说,评测系统关注“离线质量”,观测系统关注“线上运行状态”。通过观测系统能让你看清 RAG 在真实环境中的运行情况。
观测系统主要看什么
性能类
Average Latency
P95 / P99
QPS
错误率
超时率
业务类
用户满意度
追问率
引用点击率
反馈分布
调试类
每一步耗时
query rewrite 后内容
召回候选数
rerank 前后变化
prompt 与返回结果
3. 什么是RAG SDK?
前面介绍了不同视角下,对 RAG 知识做的不同分类。至此,你现在应该搭建起了整个 RAG 的知识框架,即便某个知识点不是很清楚,但是整体框架是有的,这就为下一步的学习打好了基础。
那么接下来我们要实现的 RAG,具体是一种什么样的形式呢?
考虑到 RAG 可能会接入到不同的系统里面,因此应该单独抽离出来,做成一个 RAG SDK。那说到 RAG SDK,有同学会有疑问,什么是 RAG SDK 啊?这个词感觉有一些陌生,实际上,你主要是对 SDK 这个词比较陌生。
3-1. 理解SDK
SDK 英语全称是 software development kit,翻译成中文是“软件开发工具包”,本质上是一套开发工具集,为开发者提供创建软件应用所需的工具、库和示例代码。
也就是说,SDK 等价于一套“别人可以直接拿来用的能力封装”。这个词最早在后端非常流行,现在逐渐扩散到了前端,平时我们开发所用到的:
- axios
- react
- vue
- firebase sdk
- 微信支付 sdk
其实都可以看作是一种 SDK。这里以 axios 为例,当我们书写:
js
axios.get('/api')我们没有做:
new XMLHttpRequest()- 处理 headers
- 处理状态码
- 处理重试
因为 axios 帮我们封装了,就是 SDK 价值的一种体现,把复杂事情封装成简单 API。
再举一个例子,这里以 RAG 为例:
js
const docs = await vectorSearch(query)
const reranked = await rerank(docs)
const answer = await generate(query, reranked)开发者需要自己拼接 RAG 流程。但是倘若封装成 RAG SDK,那么开发者大概是这么用的:
js
const rag = createRagPipeline({
retriever,
reranker,
generator
})
const result = await rag.query("AI 在医疗中的应用")开发者在接入 RAG SDK 后,只需要 rag.query() 一行代码即可。
3-2. SDK核心
SDK 不仅仅是代码的集合,更是对复杂业务逻辑的深度抽象。一般一个优秀的 SDK 需要具备这么几项:
1. 复杂性的极致封装
SDK 的本质是“减负”。
以 RAG SDK 为例,我们要将底层的:
- Embedding 向量化
- Vector DB 存储
- Rerank 精排
- Prompt 工程
- LLM 调度
- ....
等一系列繁琐流程,通过高层抽象封装。用户不需要关心数据是如何流转的,他们只需要调用一个简洁的 rag.query(),即可获得最终结果。
2. 面向接口编程的稳定性
接口(Interface)是 SDK 与用户之间的契约,也是最核心的资产。
js
interface Retriever {}
interface Reranker {}
interface Generator {}通过定义标准的 Retriever、Reranker 和 Generator 接口,我们将底层逻辑与用户业务逻辑解耦。用户应当依赖于这些稳定的接口协议,而非具体的类实现,从而确保在技术栈迭代时,业务代码依然稳固。
3. 高内聚、低耦合的可扩展性
优秀的架构必须允许用户“自备零件”。就拿检索器来讲,用户可以自由更换:
- retriever: qdrant
- retriever: elasticsearch
- retriever: custom
无论是想从 Qdrant 切换到 Elasticsearch,还是接入自研的检索算法,用户只需实现对应接口即可完成无缝替换。这种插件式的扩展能力,决定了 SDK 的生命周期。
3-3. RAG SDK目录结构
理解了什么是 SDK 之后,接下来我们来看一下我们要做的 RAG SDK,目录层面应该如何设计。
这里我们采用 Monorepo 的设计。
packages/
core/ # 抽象层(接口 + 数据契约)
types/
spec/ # Zod schema
interfaces/
pipeline/
errors/
runtime/ # 推理能力(RAG 主链路)
retriever/
reranker/
generator/
context/
pipeline/
__tests__/
indexing/ # 数据构建能力
loader/
chunking/
enrich/
embedding/
writer/
pipeline/
__tests__/
adapters/ # 第三方实现(唯一外部依赖点)
llm/
embedding/
vector-store/
reranker/
loader/
observability/ # 观测能力(hook / tracing)
hooks/
tracing/
metrics/
eval/ # 评测能力(可选模块)
dataset/
runner/
metrics/
judge/
__tests__/
utils/ # 公共工具(可选)
logger/
config/
helpers/- core:定义“能做什么”(接口)
- runtime:定义“怎么回答问题”(推理)
- indexing:定义“怎么构建知识”(入库)
- adapters:定义“用谁来实现”(OpenAI / 向量库)
- observability:定义“怎么监控”
- eval:定义“怎么评估”
整个 RAG SDK 的设计,涵盖了前面我们所介绍的:
- RAG pipeline
- RAG 4大系统
另外,这里回答 3 个问题:
- 为什么采用 Monorepo 结构?
- adapters包是什么?
- 观测系统和评测系统需要放到 RAG SDK 里面么?
1. 为什么采用 Monorepo 结构?
要理解为什么采用 monorepo 结构,得先从为什么要拆包说起。
一说到拆包,有的同学会有一个误解,认为像 工具库 和 组件库 这种可独立运行、可发布的代码才需要拆包。但实际上不是这样的,拆包的真正目的不是“运行”,而是要控制复杂度和明确边界。
也就是说,只要你某一个功能块具备下面几个特点:
- 被多个地方使用
- 逻辑开始变得复杂
- 需要独立演进
那么就推荐拆成一个单独的包。
理解了拆包的原则后,接下来我们来看一下包的组织信息,目前就两种:
- multirepo 多仓库结构
- monorepo 单仓库结构
那到底选哪一个?关键在于:这些包之间的关系是什么样的。
由于我们做的是一个完整的 RAG SDK,它天然包含多个子模块,例如:
- core:接口与数据契约
- runtime:推理链路
- indexing:数据构建
- eval:评测
- observability:观测
这些模块并不是“松散独立”的,而是具有非常强的关联关系:
- runtime 和 indexing 都依赖 core 中定义的类型和 schema
- eval 需要直接调用 runtime 做评测
- observability 需要贯穿整个 pipeline
- 一个字段或结构的修改,往往会影响多个模块
换句话说,这些包的特点是:
- 强依赖(Strong Coupling)
- 高频联动修改(High Change Frequency)
- 共享类型与数据结构(Shared Schema)
在这种情况下,如果采用 Multi-repo,会带来明显的问题:
- 每次修改 core,都需要发布新版本,其他仓库需要升级依赖才能使用最新代码
- 多个仓库之间容易出现版本不一致
- 本地调试成本很高(跨仓库联调困难)
而 Monorepo 正好解决这些问题:
- 所有包在同一仓库中,基于源码直接依赖
- 修改一个模块,其他模块可以立即感知变化
- 类型和 schema 可以天然共享
- 支持跨模块的统一构建、测试和调试
因此,对于像 RAG SDK 这种:多模块 + 强协作 + 高频演进的工程,monorepo 是更合适的选择。
NOTE
Multi-repo 并不是“必须发版”,其实还有几种绕法:
- npm link(本地链接)
- git dependency,例如
"rag-core": "github:xxx/rag-core" - 私有 registry / 内网源
但这些都有问题:
- 不稳定
- 团队协作混乱
- 版本不可控
- CI/CD 麻烦
因此现实是如果团队采用 Multirepo 开发,几乎等于必须发版,然后其他仓库需要升级依赖才能使用最新代码。
2. adapters包是什么?
要讲清楚为什么需要 adapters 包,首先我们需要提前了解一件事儿,那就是我们开发这个 RAG SDK 会依赖 langchain.
为什么会依赖 langchain 呢?
因为 langchain 已经帮我买做了:
- LLM 抽象(OpenAI / Anthropic / etc)
- Embedding 抽象
- VectorStore 抽象
- Tool / Agent(可选)
这些如果要我们自己写一遍成本很高,而且还可能不稳定。
但是,我们不能直接绑定 langchain,例如假设我们这么写:
js
import { ChatOpenAI } from "langchain"
function generate() {
return new ChatOpenAI(...)
}那么相当于这个 RAG SDK 和 langchain 绑死了,这所带来的问题有:
- 不能直接用 OpenAI SDK
- 不能用其他模型,只能用 langchain 支持的那些模型
- LangChain API 发生变化,SDK 会被拖着走
- 也无法支持诸如 LlamaIndex 等框架
因此回忆一下设计模式里面的开闭原则,正确的方式是:langchain 是一种“实现”,而不是“接口”。
也就是说,我们应该这么设计:
js
// core/interfaces/generator.ts
export interface Generator {
generate(input: {
query: string
context: string[]
}): Promise<string>
}然后用 langchain 做一个适配器:
js
// adapters/langchain/generator.ts
import { ChatOpenAI } from "langchain"
export class LangChainGenerator implements Generator {
private model = new ChatOpenAI()
async generate({ query, context }) {
return this.model.invoke(...)
}
}这样回头开发者在使用这个 SDK 的时候,能够替换:
js
const rag = createRagPipeline({
generator: new LangChainGenerator()
})或者
js
generator: new CustomGenerator()这种设计模式就叫做适配器模式(Adapter Pattern)。理解了这个点,你自然也就知道为什么会有 adapters 这个包了。
3. 观测系统和评测系统需要放到 RAG SDK 里面么?
这可能是你还会存在的一个疑问:前面说了 SDK 应该简化使用,那为什么还要放 observability、eval 这些工程能力?
实际上,SDK 的职责虽说是对外提供能力以及隐藏复杂性,但这里有一个容易误解的点:SDK 不只是 功能调用,还包括 可扩展能力,但必须是接口或者扩展点,而不能是具体实现。
我们以 observability 观测系统为例,如果你这样设计:
logToConsole()sendToLangfuse()sendToDatadog()
每个方法写了你特有的实现,那么所带来的问题就是:
- 强绑定某个工具
- 用户无法自定义
- SDK 变重
正确设计方式是 SDK 只提供 hook 或者扩展点,用于决定什么时候触发。至于具体要做什么,这是用开发者来决定的,例如具体是要打日志,还是要上报,还是要做分析。
下面来看一个例子:
开发者最终使用:
js
const rag = createRagPipeline({
retriever,
generator,
hooks: {
onRetrieve: (ctx) => console.log("retrieve:", ctx),
onGenerate: (ctx) => console.log("generate:", ctx)
}
})
await rag.query("什么是 RAG?")runtime:核心 pipeline
js
// packages/runtime/pipeline/ragPipeline.ts
export function createRagPipeline(options) {
const { retriever, generator, hooks } = options
return {
async query(query) {
// ====== Step 1: Retrieval ======
const docs = await retriever.search(query)
// observability hook(插入点1)
hooks?.onRetrieve?.({
query,
docs
})
// ====== Step 2: Generation ======
const answer = await generator.generate({
query,
context: docs
})
// observability hook(插入点2)
hooks?.onGenerate?.({
query,
docs,
answer
})
return answer
}
}
}observability:提供“类型 + 规范”
js
// packages/observability/hooks/types.ts
export interface RagHooks {
onRetrieve?: (ctx: {
query: string
docs: any[]
}) => void
onGenerate?: (ctx: {
query: string
docs: any[]
answer: string
}) => void
onError?: (err: Error) => void
}因此,无论是 observability 还是 eval,在 SDK 中都是以接口的形式出现,而非具体实现。
markdown
提示词收集
@项目初始化需求文档 请你仔细阅读该项目初始化需求文档,把整个 rag sdk项目的架子搭建起来。
我现在使用的是vscode,而非cursor,.因此项目根目录下的.cursorrules应该是没用的,我看到你在该文件下写了一些内容,请你把这些内容做一个迁移,目标是保证能在vscode下使用的
接下来我需要你做一件事情【为每个package补src入口与导出约定】,如果涉及到文件,那么文件内容
仅仅是占位(sub)【一行代码就够了】,目前不需要书写任何的实际业务代码。
目前我们已经把项目初始化好了,我需要你落地一版文档出来,方便后面A做交接工作。另外已存在的文
档,如果存在过时的内容,请更新这些内容。-EOF-