前情提要:该文章是博主自己总结+不断优化ai后给出的比较好理解的点+总结别的up知识点,一点一点搜集,总结,整合的还是挺全面的笔记,博主也是将他视为自己的RAG这块的学习笔记,也适合新入一同学习
一:为什么要引入RAG知识库
用自己的话来说,大模型本质就是不断搜寻网络上的数据,然后对用户的问题进行预测,从网络上搜寻与用户问题相近的回答,然后整合结果,返回给用户。既然本质就是搜索数据,那么就会出现以下情况:幻觉 和 知识滞后
1.幻觉
模型生成看似合理、逻辑通顺,但与客观事实完全不符、无中生有或错误拼接的内容,即 “一本正经地胡说八道”,典型特征就是他会造假我们的数据,比如我们写论文时让他查询两篇论文数据引用自哪里,他可能会返回给我们两篇根本不存在的引用,如果你不仔细看,根本不会发现,但是光看逻辑上来说,模型确实给你完成了任务。幻觉会出现以下特征 ,如:
无中生有:编造不存在的人名、事件、数据、文献引用。
事实错误:张冠李戴、篡改历史、给出错误结论。
为什么会这样?(底层 4 个原因)
1. 它不是 “记忆”,而是 “预测下一个字”
大模型的工作只有一件事:根据前面的话,预测下一个最可能出现的字。
它不是在 “回忆知识”,而是在组词造句。
只要句子通顺、合理、像人话,它就会输出。真不真实,它没有感知。
2. 它没有 “真假判断模块”
模型不知道:
- 这件事是否发生过
- 数据是否真实
- 引用是否存在
它只懂:
- 语法对不对
- 逻辑顺不顺
- 语气自不自然
真假 = 不在它的判断体系里。
3. 训练数据里本来就有噪音、冲突、错误
互联网内容本身:
- 有错别字
- 有谣言
- 有过时信息
- 有不同说法
模型学到的是混合概率,不是唯一真理。当信息冲突,它就会瞎编一个最通顺的。
4. 你逼它 “必须回答”,它就只能编
你问它一个它不懂 / 不确定的问题,它不会像人一样说:“我不知道。”
它的训练目标是:必须给出流畅、完整、看起来专业的回答。
于是它开始:
- 拼接信息
- 补全逻辑
- 编造细节
→ 这就是幻觉。
2.知识滞后
模型的知识有明确的时间截止点,无法获取该时间点之后发生的新事件、新数据、新知识。简单来说模型本质就会去网上查找数据,如果我们想查询24年中国人口总数,但是网络上只公布了23年的,模型就无法帮助我们查询
核心特征
- 时间边界:知识停留在训练数据的 “截止日期”(如 2023 年 10 月)。
- 非编造:不是故意说谎,而是确实不知道最新信息。
- 可预测:询问截止日后的事件,模型通常会承认信息不足,或基于旧数据推测。
常见例子
- 问:“2026 年 3 月 18 日的股市行情如何?” 模型无法实时回答,因为其知识未更新到此刻。
- 问:“2025 年新发布的手机有哪些?” 若模型训练截止于 2023 年,则无法提供准确信息。
3.知识错误
简而言之就是网络上的数据如果都是假数据,那么模型就会返回假数据给我们
4.知识局限
每个公司都有自己内部的业务数据啥的,这些数据是不公布在网络上的,模型就无法获取到这些信息。知识局限主要分 3 类
1. 时间局限(消息滞后)
- AI 只学到某个时间点之前的内容
- 之后发生的事、新数据、新新闻、新政策
- 它完全没见过,所以不知道
这就是你之前问的消息滞后。
2. 内容局限(没见过的信息)
- 没公开在网上的内容
- 你的私人信息、公司内部文档、私密聊天
- 小众领域、极专业的冷知识
- 刚写出来、刚发出去、还没被爬取的内容
AI 没学过 = 不知道。
3. 理解局限(推理能力不够)
有些东西它 “字面上见过”,但真不懂:
- 特别复杂的逻辑、数学证明
- 需要真实世界经验才能懂的常识
- 多层推理、多条件嵌套问题
- 抽象、隐喻、弦外之音
它能模仿着说,但不一定真理解。
二:RAG知识库的介绍
1.RAG定义
RAG(Retrieval-Augmented Generation,检索增强生成)是一种结合检索与生成能力的大模型优化技术,通过在生成答案前,从外部知识库中检索与用户问题相关的真实、最新数据,将其作为上下文补充给大模型,最终让模型基于检索到的可靠信息生成回答。它是基于以下工作流程:
1. 问题解析
用户输入问题后,系统先对问题进行语义理解(如关键词提取、意图识别),明确需要检索的核心信息方向(例:用户问 “2024 年行业政策”,核心检索方向是 “2024 年 + 目标行业 + 政策文件”)。
2. 外部检索
基于解析后的问题,系统从预设的外部知识库中快速匹配相关内容:
-
知识库类型:私有文档(企业手册、内部数据)、公开数据(最新新闻、政策文件)、实时数据(股市行情、天气信息)等;
-
检索方式:通过向量数据库(将文本转化为语义向量,实现快速相似性匹配)提升检索效率与准确性。
3. 上下文补充
系统将检索到的相关信息(如具体条款、数据、案例)整理为结构化上下文,与用户问题一起同步给大模型,相当于给模型 “提供参考资料”。
4. 生成回答
大模型不再仅依赖自身训练参数,而是基于 “问题 + 参考资料” 进行逻辑整合,生成符合要求的回答,且可追溯答案的信息来源。
简而言之:它就相当于给ai配备了一个标准知识库,ai会根据用户问题,到我们的知识库中检索到对应的数据,通过ai润色后返回给我们。因为知识库的数据是我们提前提供的,数据质量能得到保证,从而尽可能减少幻觉等问题
2.RAG工作流程
1.离线准备阶段(构建知识库)
这个阶段是 RAG系统的基础,目的是将原始文档处理并存储为易于检索的格式。
(1)文档加载(Document Loading)
从各种数据源(如PDF、Word、网页、数据库、企业内部文档等)中加载原始数据。
(2)文本分块(Text Chunking)
将加载的长文档切分成较小的、语义连贯的文本片段(chunks)。这一步至关重要,因为大语言模型有输入长度限制,且过长的文本会引入噪音。常见的分块策略包括按固定大小、按句子或段落边界、或按文档结构(如Markdown的标题) 进行分割。
(3)向量化与存储(Embedding & Storage)
使用嵌入模型(Embedding Model)将每个文本块转换为高维向量(即数字数组),这些向量能捕捉文本的语义信息。然后,将这些向量连同其对应的原始文本块一起存储到向量数据库 (Vector Database)中,为后续的快速相似度搜索做好准备。
下面来讲解一下具体内容:
1、文档加载器 (Document Loaders)
LangChain4j 提供了多种文档加载器,用于从不同来源加载文档:
| 加载器 | 功能描述 |
|---|---|
FileSystemDocumentLoader | 从文件系统加载文档,支持单文件、多文件和递归加载 |
ClassPathDocumentLoader | 从类路径加载文档 |
UrlDocumentLoader | 从 URL 加载文档 |
使用示例
// 文件系统加载器 - 单文件
Document doc = FileSystemDocumentLoader.loadDocument(Path.of("file.txt"), new TextDocumentParser());
// 递归加载目录下所有文件
List<Document> docs = FileSystemDocumentLoader.loadDocumentsRecursively(Path.of("docs/"), new TextDocumentParser());
// URL 加载器
Document doc = UrlDocumentLoader.load("https://example.com/page.html", new TextDocumentParser());
2、文档解析器 (Document Parsers)
文档解析器用于将不同格式的文档(PDF、Word、Excel 等)转换为 Document 对象:
表格
| Maven 依赖 | 功能描述 | 底层技术 |
|---|---|---|
langchain4j-document-parser-apache-pdfbox | 解析 PDF 文档 | Apache PDFBox |
langchain4j-document-parser-apache-poi | 解析 Excel 和 Word 文档 | Apache POI |
langchain4j-document-parser-apache-tika | 解析多种文档格式(通用型) | Apache Tika |
langchain4j-community-document-parser-llamaparse | 使用 LlamaParse 解析文档(社区版) | LlamaParse |
Document 对象是一个核心数据结构,用于表示从外部来源(如 PDF、Word、网页等)加载的原始文本内容及其元数据。
在 LangChain4j 中,Document 类定义如下(简化版):
public class Document {
private final String text; // 文档的完整文本内容
private final Metadata metadata; // 元数据(来源、作者、时间等)
// 构造方法
public static Document from(String text, Metadata metadata) { ... }
public static Document from(String text) { ... } // 无元数据
// Getter
public String text() { return text; }
public Metadata metadata() { return metadata; }
}
🔹 关键字段说明:
表格
| 字段 | 类型 | 说明 |
|---|---|---|
text | String | 整个文档的纯文本内容(解析后,不含格式) |
metadata | Metadata | 键值对形式的元信息,例如: • "source": "manual.pdf" • "page": "5" • "url": "https://example.com" |
使用示例
// 文件系统加载器 - 单文件
Document doc = FileSystemDocumentLoader.loadDocument(Path.of("file.txt"), new TextDocumentParser());
// 递归加载目录下所有文件
List<Document> docs = FileSystemDocumentLoader.loadDocumentsRecursively(Path.of("docs/"), new TextDocumentParser());
// URL 加载器
Document doc = UrlDocumentLoader.load("https://example.com/page.html", new TextDocumentParser());
3、文档分割器 (Document Splitters)
文档分割器用于将长文本分割成较小的片段,以适应大模型的上下文窗口限制:
| 分割器 | 功能描述 | 特点 |
|---|---|---|
DocumentSplitters.recursive() | 递归字符分割器 | 按字符递归分割,可配置块大小和重叠 |
DocumentBySentenceSplitter | 按句子分割 | 基于 Apache OpenNLP 的智能句子检测 |
DocumentByParagraphSplitter | 按段落分割 | 保留段落语义完整性 |
CharacterSplitter | 固定字符分割 | 按固定字符数分割,无视语义 |
HierarchicalDocumentSplitter | 层次化分割 | 支持多级分割策略 |
使用示例
// 递归分割器 - 最大 256 tokens,重叠 0
DocumentSplitter splitter = DocumentSplitters.recursive(256, 0);
List<TextSegment> segments = splitter.split(document);
// 按句子分割(需引入 easy-rag 或相关依赖)
Tokenizer tokenizer = new HuggingFaceTokenizer();
DocumentBySentenceSplitter sentenceSplitter = new DocumentBySentenceSplitter(100, 0, tokenizer);
List<TextSegment> segments = sentenceSplitter.split(document);
// 结合嵌入存储的完整流程
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.documentSplitter(DocumentSplitters.recursive(256, 0))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
ingestor.ingest(document);
4、文本向量化
文本向量化(Text Vectorization)是将人类可读的自然语言文本(如句子、段落)转换为计算机可计算的数值向量(一串数字)的过程。这是现代 AI(尤其是大模型和 RAG 系统)理解语义的核心技术。
一、为什么需要文本向量化?
计算机无法直接“理解”文字,但可以高效处理数字。 而人类语言的关键在于语义(意思),不是字面。 向量就是让计算机理解语义方法,将中文语义的远近关系转化为向量之间的举例,这样计算机就能检索出符合符合问题的向量,从而回答我们的问题
✅ 向量化的目标:让语义相近的文本,在向量空间中距离更近。
🌰 举例:
- “如何重置密码?”
- “怎样修改我的登录密码?”
这两句话用词不同,但意思高度相似。
理想情况下,它们的向量在空间中应该非常接近。
而:
- “如何重置密码?”
- “今天天气怎么样?”
语义无关,向量应相距很远。
二、向量长什么样?
一个文本向量通常是一个固定长度的浮点数数组,例如:
[0.82, -0.34, 1.21, ..., 0.05] # 长度可能是 384、768、1024 等
- 每个数字代表文本在某个“语义维度”上的强度(如“疑问语气”、“技术性”、“情感倾向”等)。
- 这些维度由模型在训练时自动学习,人类难以直观解释(称为“隐空间”)。
三、嵌入模型(Embedding Model)
嵌入模型是一种 AI 模型,它能把文字(词、句子、段落)转换成一串数字(向量),这串数字能代表这段文字的“意思。简单来说,他就是将我们之前切割好的文本块转化为向量的工具### 🔑 核心作用
- 把语言变成数字 → 计算机才能处理
- 保留语义信息 → 意思相近的文字,数字也“靠近”
🌰 举个例子
表格
| 文本 | 向量(简化示意) |
|---|---|
| “如何重置密码?” | [0.82, -0.34, 1.21, …] |
| “怎样修改登录密码?” | [0.80, -0.32, 1.19, …] |
| “今天天气怎么样?” | [-0.15, 0.91, -0.44, …] |
✅ 前两句意思相似 → 向量很接近
❌ 和“天气”那句意思无关 → 向量相差很远
四、向量维度的定义
向量维度 = 向量中包含的数字的个数
向量: [0.1, -0.3, 0.8, 0.2, -0.5, ...] ← 这是一个384维的向量(包含384个数字)
↑
第1维 第2维 第3维 第4维 ... 第384维
简单理解:
- 384维 = 384个数字排成一排
- 768维 = 768个数字排成一排
- 1536维 = 1536个数字排成一排
形象类比
| 类比 | 维度 | 说明 |
|---|---|---|
| 直线上的点 | 1维 | 只需要1个数字确定位置 (x) |
| 平面上的点 | 2维 | 需要2个数字 (x, y) |
| 空间中的点 | 3维 | 需要3个数字 (x, y, z) |
| 文本向量 | 384/768/1536维 | 需要几百到几千个数字表示语义 |
关键洞察:维度越高,能表达的信息越丰富,但计算成本也越高
为什么需要这么多维度?
核心:编码更多语义信息
低维度 (2D平面) 高维度 (1000D空间)
y
↑ ● "开心"
│ / \
│ / \
───┼───→ x ●"难过" ●"高兴" ●"兴奋"
\ / \ /
\ / \ /
● "情绪"
2维:只能区分"开心/难过"(左右分布)
1000维:可以区分"开心/难过/高兴/兴奋/沮丧/欣慰..."等无数细微差别
简言之:向量维度越高,他能在空间中表示的点更多。我们把一个点想象为语义切分的一个块,点多了,原本一段固定的文本就会切分为更多的块,就能表示更多的语义差异。比如一段文本最多切分为两个点,他只能表示开心或者难过。但是当这个点被切分的更细,比如10,100个,原本的开心会被切分为更细的喜悦,快乐,高兴等更细致的语义,这些语义又有更细微的差别。这也是高维度的好处,能够将一段文本的语义切分的更细致,让智能体更能把握住用户的问题,从而更精准的回答问题
下面罗列了一些常用的国内厂商的嵌入,本人项目中用的就是阿里云的v3嵌入模型
| 厂商 | 模型 | 特点 |
|---|---|---|
| 阿里云 | text-embedding-v1/v2/v3 | 中文优化,按token计费 |
| 百度 | Embedding-V1 | 文心系列,中文语义强 |
| 智谱AI | embedding-2 | GLM系列,长文本支持 |
| MiniMax | emb-001 | 多模态嵌入 |
| 讯飞 | 星火嵌入模型 | 语音文本联合嵌入 |
| 这里附上阿里云百炼平台大模型网址大模型服务平台百炼控制台 |
5、向量数据库
这一块博主比较迷,也不能很好理解,第一次接触到这个概念,项目中的RAG存储用的也是postgres或者redis来存储向量。为了更好理解,这里博主引用了kimi的解释,还是挺形象的
一、向量数据库讲解与核心架构
📌 什么是向量数据库?
**向量数据库是一种专门用于存储、索引和快速检索高维向量(如文本、图像的嵌入向量)的数据库系统。**它不是用来存表格数据(如 MySQL),而是用来存“语义数字指纹”,核心能力是:
在海量向量中,毫秒级找出“最相似”(topK)的几个。
🔑 为什么需要它?——传统数据库不行吗?
| 场景 | 传统数据库(MySQL/PostgreSQL) | 向量数据库 |
|---|---|---|
| 存用户信息(ID、姓名、邮箱) | ✅ 擅长 | ❌ 不适合 |
| 存文本向量(768 维 float 数组) | ⚠️ 能存但无法高效搜索 | ✅ 专为向量设计 |
| “找和这句话意思最像的 3 条” | ❌ 只能精确匹配或关键词(如where name=xxx) | ✅ 支持语义相似度搜索,根据语义返回最相近的结果 |
💡 关键区别:
- 传统 DB:查“等于”、“包含”
- 向量 DB:查“像”
┌─────────────────────────────────────────┐
│ 用户查询 │
│ "找与'苹果'相似的文本" │
└─────────────────┬───────────────────────┘
↓
┌─────────────────────────────────────────┐
│ ① 查询向量化 (Embedding Model) │
│ "苹果" → [0.2, -0.5, 0.8, ...] │
└─────────────────┬───────────────────────┘
↓
┌─────────────────────────────────────────┐
│ ② ANN索引检索 (Approximate Nearest Neighbor) │
│ 快速定位候选集(无需遍历全库) │
└─────────────────┬───────────────────────┘
↓
┌─────────────────────────────────────────┐
│ ③ 精确距离计算 + 排序 │
│ 计算余弦相似度,返回Top-K │
└─────────────────┬───────────────────────┘
↓
┌─────────────────────────────────────────┐
│ ④ 返回结果 + 可选元数据过滤 │
└─────────────────────────────────────────┘
向量数据库构造
┌─────────────────────────────────────────┐
│ 向量数据库架构 │
├─────────────────────────────────────────┤
│ 接入层 │ REST API / SDK / CLI │
├─────────┬─────────────────────────────┤
│ 索引层 │ ANN索引构建 (HNSW/IVF/Flat) │
├─────────┼─────────────────────────────┤
│ 存储层 │ 向量数据 + 元数据 (ID/标签等) │
├─────────┼─────────────────────────────┤
│ 计算层 │ 距离计算 (余弦/欧氏/点积) │
└─────────┴─────────────────────────────┘
二、ANN索引算法详解
向量数据库索引(Vector Index)是向量数据库实现高效相似度搜索的核心技术。它的目标是:
在海量高维向量中,快速找到与查询向量最相似的 Top-K 结果,而无需暴力计算所有距离。
由于“暴力搜索”(Brute-force)在大数据量下速度极慢(如 100 万向量需数秒),向量数据库普遍采用 近似最近邻搜索(Approximate Nearest Neighbor, ANN)算法构建索引,在精度损失极小的前提下,将检索速度提升 10~1000 倍。
┌─────────────────────────────────────────────────────────┐
│ ANN算法家族 │
├─────────────────────────────────────────────────────────┤
│ 图算法 │ HNSW, NSW, Vamana, DiskANN │
│ 树算法 │ Annoy, KD-Tree, Ball Tree │
│ 哈希算法 │ LSH (局部敏感哈希) │
│ 量化算法 │ PQ (乘积量化), SQ, OPQ │
│ 聚类算法 │ IVF, IVF-PQ, IVF-HNSW │
│ 混合算法 │ IVF-PQ + HNSW │
└─────────────────────────────────────────────────────────┘
- HNSW(Hierarchical Navigable Small World)
分层图结构示意:
Layer 2 (稀疏层): ●─────● ← 少量远距离连接,快速定位区域
/ \
Layer 1 (中间层): ●───●───●───● ← 中等密度,逐步逼近
/ \ / \ / \
Layer 0 (密集层): ●─●─●─●─●─●─●─● ← 全量数据,精确查找
查询路径:从顶层进入 → 找到最近节点 → 下沉到下一层 → 重复直到底层
| 特性 | 说明 |
|---|---|
| 时间复杂度 | O(logN) |
| 精度 | >95%(与暴力搜索相比) |
| 内存占用 | 高(需存储多层图结构) |
| 适用场景 | 内存充足,追求高召回 |
| 代表产品 | Milvus, Weaviate, pgvector |
- IVF(Inverted File Index)
聚类中心示意:
● 中心1 ● 中心2
/ | \ / | \
/ | \ / | \
● ● ● ● ● ●
簇1 簇2 簇3 簇4 簇5 簇6
查询过程:
1. 计算查询向量与所有中心的距离
2. 选择最近的n个簇(nprobe参数)
3. 只在选中簇内搜索
4. 返回Top-K结果
| 特性 | 说明 |
|---|---|
| 时间复杂度 | O(√N) ~ O(N/k),k为簇数 |
| 精度 | ~90%(依赖nprobe设置) |
| 内存占用 | 低(只需存储中心点) |
| 适用场景 | 大规模数据,内存受限 |
| 可调参数 | nlist(簇数)、nprobe(查询簇数) |
- PQ(Product Quantization)乘积量化
原始向量(128维) 分割为8个子向量(每段16维)
[0.1, -0.3, ..., 0.8] → [0.1,...] | [-0.3,...] | ... | [0.8,...]
↓ ↓ ↓
查码本1 查码本2 查码本8
↓ ↓ ↓
编码ID 编码ID 编码ID
↓ ↓ ↓
压缩表示:[12, 45, 7, 89, 23, 56, 91, 3](仅8字节)
压缩比:512维float(2048字节) → PQ编码(64字节) = 32倍压缩
| 特性 | 说明 |
|---|---|
| 压缩比 | 10x ~ 50x |
| 精度损失 | 轻微(5-10%召回下降) |
| 适用场景 | 超大规模数据,存储成本敏感 |
| 常组合 | IVF + PQ = IVF_PQ |
- LSH(Locality Sensitive Hashing)
哈希函数设计:相似向量 → 相同哈希桶
哈希表1: h1(v) = sign(w1·v) → 桶A / 桶B
哈希表2: h2(v) = sign(w2·v) → 桶C / 桶D
哈希表3: h3(v) = sign(w3·v) → 桶E / 桶F
查询时:计算查询向量的多个哈希值 → 找所有匹配桶 → 合并候选集
“让相似的向量,以高概率被哈希到同一个‘桶’(bucket)中。”这样,在检索时只需在同一个桶内做暴力搜索,大幅减少计算量。
想象你有一堆彩色小球(向量),要快速找出颜色相近的:
1. 普通哈希(如 MD5)
→ 颜色相近的球会被打散到不同桶 → ❌ 没用
1. 局部敏感哈希(LSH)
→ 设计一种“按颜色分桶”的规则
→ 红色球大概率进“红桶”,蓝色进“蓝桶”
→ 查“粉红色”时,只查“红桶”即可 ✅
🔑 **关键**:LSH 的哈希函数是对距离敏感的函数——距离越近,哈希值相同的概率越高。
| 特性 | 说明 |
|---|---|
| 时间复杂度 | O(1) ~ O(logN) |
| 精度 | 中等(依赖哈希函数设计) |
| 内存占用 | 极低 |
| 适用场景 | 超大规模,允许一定精度损失 |
| 缺点 | 高维数据效果下降(维度诅咒) |
- 算法对比总表
| 算法 | 精度 | 速度 | 内存 | 适用规模 | 代表实现 |
|---|---|---|---|---|---|
| Flat(暴力) | 100% | 极慢 | 低 | <10万 | Milvus-Flat |
| HNSW | 95%+ | 快 | 高 | <1亿 | Milvus-HNSW, pgvector |
| IVF | 90% | 中等 | 低 | 1亿+ | Milvus-IVF |
| IVF_PQ | 85% | 快 | 极低 | 10亿+ | Milvus-IVF_PQ, Faiss |
| LSH | 70% | 极快 | 极低 | 100亿+ | Annoy,随机投影 |
6.向量数据库选型
| 数据库 | 部署方式 | 核心算法 | 特点 | 适用场景 | Java支持 |
|---|---|---|---|---|---|
| Milvus | 云原生/本地 | HNSW, IVF_PQ, GPU | 分布式,十亿级规模 | 大规模生产环境 | ✅ 官方SDK |
| Pinecone | 全托管SaaS | 内部优化 | 零运维,自动扩缩容 | 快速上线,中小规模 | ✅ REST API |
| Weaviate | 云/本地 | HNSW | GraphQL接口,模块化 | 知识图谱,混合搜索 | ✅ 官方客户端 |
| Qdrant | 云/本地 | HNSW + 过滤索引 | Rust高性能,复杂过滤 | 高并发推荐系统 | ✅ 官方客户端 |
| Chroma | 嵌入式 | HNSW | 极简,pip即用 | 原型开发,本地测试 | ❌ 仅Python/JS |
| PGVector | PostgreSQL插件 | HNSW, IVFFlat | SQL原生,事务支持 | 已有PG基础设施 | ✅ JDBC + SQL |
| Redis | 内存数据库 | HNSW | 极速查询,易失性 | 缓存热数据 | ✅ Jedis/Lettuce |
| Elasticsearch | 搜索引擎 | HNSW | 混合搜索(文本+向量) | 需要关键词+语义联合 | ✅ 官方Java客户端 |
7.Java 引入向量数据库详解
这里讲解PGVector
1.引入依赖
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-pgvector</artifactId>
<version>1.0.0-beta1</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.1</version>
</dependency>
2.创建向量数据库
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.bge.small.en.v15.BgeSmallEnV15QuantizedEmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.store.embedding.EmbeddingMatch;
public class PgVectorExample {
public static void main(String[] args) {
// 1. 创建嵌入模型(384维)
EmbeddingModel embeddingModel = new BgeSmallEnV15QuantizedEmbeddingModel();
// 2. 初始化PGVector存储
EmbeddingStore<TextSegment> embeddingStore = PgVectorEmbeddingStore.builder()
.host("localhost") // PostgreSQL地址
.port(5432) // 端口
.database("vectordb") // 数据库名
.user("postgres") // 用户名
.password("password") // 密码
.table("embeddings") // 存储表名
.dimension(384) // 必须匹配模型维度!
.useIndex(true) // 启用HNSW索引
.indexType(PgVectorIndexType.HNSW) // HNSW算法
.metric(PgVectorDistanceMetric.COSINE) // 余弦相似度
.build();
// 3. 添加文档
String text = "LangChain4j 是 Java 的 LLM 应用框架";
Embedding embedding = embeddingModel.embed(text).content();
TextSegment segment = TextSegment.from(text);
String id = embeddingStore.add(embedding, segment);
System.out.println("已存入,ID: " + id);
// 4. 语义搜索
String query = "Java 如何开发大模型应用?";
Embedding queryEmbedding = embeddingModel.embed(query).content();
List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(
queryEmbedding,
5, // Top-5结果
0.7 // 相似度阈值(0-1)
);
// 5. 输出结果
for (EmbeddingMatch<TextSegment> match : matches) {
System.out.printf("相似度: %.3f | 内容: %s%n",
match.score(),
match.embedded().text()
);
}
}
}
8.这里附上向量数据库更加完整的别的博主的讲解什么是向量数据库?向量数据库概念,详细入门-CSDN博客
2.在线查询阶段(响应用户)
当用户输入一个问题时,系统会实时执行以下流程来生成回答。
1.用户查询(User Query):接收用户的自然语言问题。
2.查询向量化(Query Embedding):使用与离线阶段相同的嵌入模型,将用户的查询也转换为一个向量。
3.检索(Retrieval):在向量数据库中,通过计算查询向量与所有文档块向量的相似度(如余弦相似度),找出与用户问题最相关的Top-K个文本块。高级系统可能会使用混合检索(结合向量检索和关键词检索如BM25)和重排序(Re-ranking)技术来进一步提高检索的准确率。
4.构建提示词(Prompt Construction):将检索到的相关文本块作为上下文,与用户的原始问题一起,按照预设的模板组合成一个新的提示词(Prompt)。这个提示词的作用是“增强”大模型的知识背景。
5.生成响应(Response Generation):将构建好的提示词输入给大语言模型(LLM)。模型会基于提示词中提供的最新、最相关的上下文信息,结合其自身学到的通用知识,生成一个准确、流畅的回答。
6.回答输出(Answer Output):将生成的回答返回给用户。为了增强可信度和可追溯性,系统通常会附带引用其答案所依据的文档来源
三:RAG检索优化
先来看一下整个优化流程图片,该图片也是博主找的别人的图片,原作者是小红书AI白方
可以看到我们整个优化是分为三个阶段的,即检索前->检索中->检索后
1.检索前优化
解释一下元数据与元数据索引
(1).向量数据库存储结构
向量数据库不会孤立存储向量,而是通过“唯一标识+向量+元数据”的组合,实现“可定位、可计算、可关联业务”的目标,具体组成如下:
| 组成部分 | 作用与示例 |
|---|---|
| 1. 唯一标识(ID) | 类似传统数据库的“主键”,用于唯一标记一条向量数据,方便后续定位、更新或删除。 示例:商品ID(prod_123)、文档片段ID(doc_chunk_456)。 |
| 2. 核心向量(Vector) | 非结构化数据(文本、图片、音频等)的“数值化表达”,是相似性查询的计算基础,本质是一个固定长度的数值数组。示例:文本“猫咪”通过BERT模型转化的768维向量 [0.12, -0.34, 0.56, ..., 0.78];图片通过ResNet模型转化的2048维向量 [0.09, 0.21, -0.43, ..., 0.65] |
| 3. 辅助元数据(Metadata) | 描述向量对应原始数据的“结构化属性”,用于**缩小查询范围(过滤)**或关联业务信息,格式通常为键值对(Key-Value)。 示例:向量对应图片的“拍摄时间(2024-05-01)”“类别(宠物)”“上传用户(user_789)”;向量对应文档的“来源(企业手册)”“章节(第3章)”。 |
原文链接:blog.csdn.net/sjc212/arti…
可知:metadata元数据本质就是我们向量数据的一些附加信息,我们通过这些附加信息快速过滤其他向量,定位到我们具体想要的向量区间,然后在这个区间精查,细查
(2).元数据讲解:
可以用一个简单的类比来理解:
想象你正在整理一个装满各种文件的实体文件柜。
-
文件本身(文档) :是一份项目报告、一张照片、一封邮件。这是你要切分和存储的主要内容。
-
元数据(关于文件的数据) :是贴在文件柜每个文件夹上的标签,或者文件索引卡上的信息条目。比如:
- 文件名:
2023-10-27_项目A_季度报告_v2.docx - 创建日期:2023年10月27日
- 作者:张三
- 所属项目:项目A
- 关键词:预算、进度、风险
- 存储位置:柜子3,抽屉B,文件夹5
- 文件名:
当你需要找文件时,你不会把整个文件柜倒出来翻找,而是会先查索引卡,根据“项目A”和“季度报告”这些标签,快速定位到少数几个可能的目标,然后再取出文件查看细节。
1. 什么是元数据?
元数据,简单来说,就是“描述数据的数据”或“关于信息的信息,可以理解为数据的标签tag,是当前向量化数据的辅助信息,它本身不表示任何文本,文本内容任然由向量表示,他只是起到一个辅助和定位向量的作用
类比快递员送快递,元数据就相当于快递地址,外卖员根据快递地址可以快速定位到某个小区,然后在通知买家获取具体地址,这个是细查,最后买家成功拿到外卖,相当于找到目标向量”。
在文档处理的语境下,元数据就是用来描述、解释、定位或以其他方式让某个文档(或文档片段)更容易被理解、管理和使用的结构化信息。
2. 切分文档时,元数据是被自动加入的,还是我们自己加的?
答案是:两者都有!这是一个协同的过程。
通常情况下,我们会结合使用系统自动提取和人工自定义的方式。
A. 系统自动加入的元数据
现代的文档加载器和切分器(比如LangChain的RecursiveCharacterTextSplitter配合相应的文档加载器)非常智能,它们会自动为你抓取和保留一些基础但非常重要的元数据。这通常是默认行为,无需你额外编写代码。
常见的自动加入的元数据包括:
- 数据来源 (
source) :这是最重要的一个。加载器会自动记录文档来自哪里,是本地的文件路径,还是一个网页的URL。这对于追溯信息源头至关重要。 - 文档标题:如果文档格式支持(如PDF、HTML、Word),加载器会尝试提取文档的标题。
- 页码 (
page) :对于PDF或Word文档,当切分器把一个长文档切成很多小块时,它通常会记录这一小块内容来自于原文的哪一页。这对于引用和验证信息非常有用。 - 创建/修改日期:从文件属性中读取。
- 作者:从文件属性中读取。
- 文档类型 (
doctype) :比如是PDF、TXT还是Markdown。
举例:
当你加载一个名为 2024_产品手册.pdf 的文件并对其进行切分后,生成的每个小块(Chunk)可能会自动拥有如下元数据:
{
"source": "./data/2024_产品手册.pdf",
"page": 5,
"author": "市场部",
"last_modified": "2024-01-15"
}
切分后生成的document对象就包含了元数据metadata,我们可以查询该字段看看到底存了啥
B. 我们自己添加的元数据
系统自动提取的信息有时不够用,或者不符合你的特定业务需求。这时,你就可以在加载文档后、切分文档前,或在切分过程中,手动添加自定义的元数据。
为什么要自己加?
- 增强业务上下文:你可以添加**“所属项目”、“客户名称”、“保密级别”、“产品线”**等信息。
- 支持精细化过滤:在检索时,你可以利用这些自定义字段,只搜索某个特定项目下的文档块。
- 改进检索结果:有时文档块本身的内容可能很模糊,但元数据可以提供关键背景,帮助LLM(大语言模型)判断这个块是否真的是用户需要的。
举例:
接上面的例子,你可能想加上这些自定义元数据:
{
// ... (自动生成的元数据)
"source": "./data/2024_产品手册.pdf",
"page": 5,
// ... (你手动添加的)
"product_line": "智能家居系列",
"audience": "渠道合作伙伴",
"version": "v2.1"
}
怎么手动添加元数据?
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.Metadata;
import dev.langchain4j.data.segment.TextSegment;
// 方式一:创建文档时添加元数据
Map<String, Object> metadataMap = new HashMap<>();//metadata本质就是一个key-value的数据结构
metadataMap.put("document_id", "contract_2024_001");
metadataMap.put("version", "v2.3");
metadataMap.put("author", "张三");
metadataMap.put("last_modified", "2024-05-20");
metadataMap.put("status", "active");
Metadata metadata = Metadata.from(metadataMap);
Document document = Document.from("文档内容...", metadata);
// 方式二:动态添加元数据
document.metadata().put("department", "法务部");
document.metadata().put("page_count", 10);
// 获取元数据
String docId = document.metadata().getString("document_id");
Integer pages = document.metadata().getInteger("page_count");
举例我们项目中怎么使用的
// 处理文件上传(如果有文件)
if (files != null && !files.isEmpty()) {
for (MultipartFile file : files) {
// 解析文档内容
TikaDocumentReader documentReader = new TikaDocumentReader(file.getResource());
List<Document> documentList = tokenTextSplitter.apply(documentReader.get());
//创建知识库标签元数据,用于检索时过滤
//tag – 知识库标签(如:"grafana-mcp-tools-guide"),用于检索时过滤
documentList.forEach(doc -> doc.getMetadata().put("knowledge", tag));
vectorStore.accept(documentList);
totalDocumentCount += documentList.size();
log.info("知识库文件上传完成 - 名称: {}, 标签: {}, 文件: {}, 文档片段数: {}",
name, tag, file.getOriginalFilename(), documentList.size());
}
} else {
log.info("创建空知识库配置 - 名称: {}, 标签: {}", name, tag);
}
数据库存储情况
我们设置了该顾问advisor,他可以通过filter表达式关联rag知识库
// AiClientAdvisorVO.RagAnswer 结构
public static class RagAnswer {
private int topK = 4; // 检索返回最相关的前 K 个文档
private String filterExpression; // 过滤表达式
private Integer knowledgeStatus; // 知识库状态(0:禁用,1:启用)
}
filter表达式的json形式
{
"topK": 4,
"filter_expression": "knowledge == 'tech' || knowledge == 'docs'",
"knowledge_status": 1
}
后续我们通过给顾问创建searchRequest,指定我们的元数据过滤标签和topk,来创建我们的请求查询参数
// 创建 RAG 顾问时配置过滤条件
RagAnswerAdvisor advisor = new RagAnswerAdvisor(vectorStore, SearchRequest.builder()
.topK(ragAnswer.getTopK())
.filterExpression(ragAnswer.getFilterExpression()) // 这里指定过滤条件
.build());
3. 元数据有什么用?(为什么它如此重要?)
元数据在RAG(检索增强生成)应用中扮演着至关重要的角色,主要有三大用途:
用途一:精确检索与过滤(最重要!)
这是元数据的核心价值。当用户提问时,我们可以先利用元数据进行“预筛选”或“后过滤”。
- 场景:用户问“我们公司去年的销售额是多少?”
- 没有元数据:系统可能检索出所有包含“销售额”的文档片段,其中可能混杂了其他公司的数据,或者前年的旧数据,导致回答错误。
- 有元数据:系统可以这样检索:“找出所有文档片段,其中内容包含‘销售额’,并且元数据中的
company字段等于‘我们公司’,并且year字段大于等于‘2023’。” 这极大地提高了检索的准确性。
用途二:提供上下文与引用(可解释性)
当LLM生成了答案,它不仅可以给出答案,还可以“告诉”用户这个答案的依据是什么。
- 场景:LLM回答:“2023年我们的销售额增长了20%。”
- 利用元数据:LLM可以同时返回它所依据的文档片段及其元数据,比如“信息来源:
2023_财报.pdf,第12页”。这让用户能够核实信息,增加了结果的可信度。
用途三:辅助文档管理与处理
元数据可以帮助我们追踪文档的来源、版本,方便后续的数据清洗、更新或删除。例如,当我们发现某个 source 的文档有更新时,可以精准地找到向量数据库中对应的旧数据并替换掉。
总结
- 元数据是什么:是关于文档的标签和信息卡片,用于描述文档本身。
- 如何产生:一半自动,一半手动。系统负责提取基础信息(来源、页码),我们负责添加业务相关的关键信息(项目、类别、版本)。
- 有什么用:它让检索从“大海捞针”变成“按标签找针”,提高了RAG系统的准确性、可解释性和可管理性。没有元数据的RAG,就像没有目录和索引的图书馆,虽然能找到书,但效率极低。
(3).元数据索引
结论: 只有为元数据字段创建了索引(metadata index),向量数据库才能高效地支持基于这些字段的过滤(filtering)功能。类似于mysql的索引机制,不给字段加索引,那么根据该字段过滤还是会触发全表扫描。这里给元数据加索引就是为了元数据过滤时,能够快速查询到所有符合元数据的向量,实现快速定位向量的功能
🔍 为什么必须“创建索引”才能过滤?
虽然你把元数据(比如 {"year": 2025, "category": "finance"})随文本块一起存进了向量数据库,但默认情况下,这些字段只是“被存储”,并未被“优化查询”。
向量数据库的核心是向量相似度搜索(如余弦相似度),而元数据过滤属于结构化查询,两者机制不同:
没有索引 → 数据库只能对每个结果做“全量扫描 + 条件判断”(O(N) 时间复杂度),慢且可能被禁用。
有索引 → 数据库能快速定位满足 year >= 2024 的子集,再在这个子集上做向量检索(高效、可扩展)。
结论一句话:
元数据提供了“可以按什么条件过滤”的信息,而元数据索引决定了“能不能高效地按这些条件过滤”。
所以,在构建 RAG 或语义搜索系统时:
- 设计好元数据结构(哪些字段要用于过滤?)
- 在向量数据库中为这些字段创建索引
- 测试 filter 查询是否生效 & 高效
这样才能真正发挥“语义搜索 + 结构化过滤”的混合检索威力 🚀。
(4).Small to Big策略讲解
一、核心思想
用“小”信息精准检索,用“大”上下文保障生成质量
这句话是整个策略的精髓:
- “小”信息:指文档中关键的、细粒度的信息点(如某个概念、数据、结论等),它们可以被用来做精确匹配。
- “大”上下文:指包含这些关键信息的原始文本块或段落,内容更完整、语义更丰富,适合用于 LLM 生成高质量回答。
目标:
- 先通过精细索引快速定位到最相关的“核心信息”
- 再召回其所属的更大、更完整的上下文
- 最终用这个完整上下文生成更准确、连贯的答案
🧩 二、具体实现方式
方式一:子问题检索(Subquestion Retrieval)
利用大模型对上传的文本块自动提取出几个关键子问题,并为每个子问题建立索引。
-
预处理阶段:
-
输入一个长文档(比如一篇论文、报告)
-
使用大模型(如 GPT、Qwen)分析内容,提出若干个“子问题”
例如:- “该政策的目标是什么?”
- “2024 年的GDP增长率是多少?”
- “主要影响因素有哪些?”
-
将这些子问题和对应的原文片段一起存入向量数据库(作为索引项)
-
-
检索阶段:
- 用户提问:“2024年经济形势如何?”
- 系统将这个问题去检索所有子问题(语义相似度)
- 找到匹配的子问题(如“2024年的GDP增长率是多少?”)
- 召回与该子问题关联的原始文本块
-
生成阶段:
- 用召回的原始文本块作为输入,让大模型生成最终答案
优点:
- 检索精度高(因为子问题是高度聚焦的)
- 能避免“相关但不准确”的结果
简述:让大模型根据你的知识库文本提出几个问题,然后将子问题与知识库对应的片段保存在一起,建立索引。这样用户在提出问题时,他会先去向量化匹配语义相近的子问题,再去子问题中查询关联的知识库片段,类似于key-value形式,子问题是key,对应的知识库片段为value
方式二:文本摘要(Text Summarization + Indexing)
对每个文本块生成一个简短的摘要,并以摘要为索引进行检索。
-
预处理阶段:
- 对每个文本块使用大模型生成一段摘要(summary)
- 将摘要和原始文本块一起存储,摘要作为“检索键”
-
检索阶段:
- 用户提问时,先在摘要库中搜索最相关的摘要
- 找到后,召回对应的原始文本块
-
生成阶段:
- 用原始文本块生成最终答案
优点:
- 摘要更简洁,适合快速检索
- 减少向量维度,提高检索效率
- 避免了“碎片化”检索带来的信息断层
简述:对文本的每个文本块先让ai生成简述,让简述与对应的文本块一起向量化存储,建立索引。这里key就是简述,value为具体文本块。先根据用户的问题匹配语义相近的简述,再去每个简述中细查
三、两种方式对比
表格
| 方法 | 优势 | 缺点 |
|---|---|---|
| 子问题检索 | 精准性强,能捕捉复杂逻辑关系 | 需要额外生成子问题,计算成本稍高 |
| 文本摘要 | 实现简单,易于扩展 | 摘要可能丢失细节,不如子问题精准 |
实际应用中,两者可以结合使用:先用摘要快速筛选,再用子问题精炼结果。
四、为什么叫 “Small to Big”?
表格
| 步骤 | “小” | “大” |
|---|---|---|
| 检索阶段 | 检索的是“子问题”或“摘要” → 信息量小但精准 | 检索的是“原始文本块” → 信息量大但冗余 |
| 生成阶段 | 不直接用“小”信息生成 | 用“大”上下文生成高质量答案 |
- “小” :用于高效、精准地定位核心信息
- “大” :用于保障生成的完整性与准确性
这正是“从精细到完整”的过程,故称 Small to Big。
五、适用场景
- 文档较长、结构复杂(如年报、科研论文)
- 需要高准确率的回答(如金融、医疗、法律)
- 希望减少幻觉(hallucination)和信息遗漏
-
- *S
总结一句话:
“Small to Big” 是一种“先精后广”的检索策略:用小而准的索引(子问题/摘要)找到关键信息,再用大的原始上下文生成可靠答案,兼顾检索效率与生成质量。
检索前优化可以总结为以下几点
1.选用更好的embedding模型
2.RAG文档切分时选择合适的切分器,以及RAG文档解析时使用专业的解析器,比如PDF的文档就用专门的PDF解析器
3.创建元数据(metadata)索引,设置元数据
4.提前对rag知识库文件进行数据清洗,给ai提示词,让他修改知识库文件,目的是保证知识库检索的准确性
5.Small to Big策略
6.提示用户端,让他提出更完整,更精确的问题,便于ai快速理解用户需求
7.设置一个专门的语义识别ai,让他在用户提出问题后,自动结合上下文,理解用户需求,并且将用户问题转化为更正规化,标准化,提供一些标准化示例,目的还是避免后续ai无法理解用户真实需求,从而影响rag检索质量
2.检索中优化
1、HyDE(Hypothetical Document Embeddings)
(1)定义讲解
定义:让大模型先对用户的问题生成一些“假设答案”(hypothetical answer),然后用这个“假设答案”的向量去检索文档。
核心思想:
用户的问题可能很模糊或抽象(如:“为什么2024年经济复苏?”)
直接拿这个问题去查,容易召回不相关的内容
如果我们能先猜一个合理的答案(比如:“因为消费支出增长了65%”),再用这个答案去查,就能更精准地找到支持它的原文
用户问题 → 大模型 → 生成“假设答案” → 转成 embedding → 向量检索 → 召回真实文档
举个例子:
用户问:
“2024年中国经济复苏的主要原因是什么?”
步骤1:生成假设答案(由大模型生成)
“主要原因是最终消费支出对经济增长的贡献率达到65%,拉动了整体复苏。”
步骤2:将这个假设答案转成 embedding
把这句话变成向量(embedding)
步骤3:用这个向量去检索
在知识库中找和这个“假设答案”最相似的文本块
步骤4:召回结果
找到一段原文:“根据国家统计局数据,2024年消费支出贡献率高达65%……” ✅ 这样就找到了真正相关的证据!
💡 为什么叫“双向奔赴”?
- 传统方法:从文档出发 → 提取问题 → 用问题检索 (文档 → 问题 → 检索)
- HyDE:从问题出发 → 生成答案 → 用答案检索 (问题 → 答案 → 检索)
- 两者是反向的,所以称为“双向奔赴”。
- 实际上,这属于“查询改写”的一种高级形式:把自然语言问题转化为语义上更接近文档内容的“虚拟答案”。
(2)为什么要这么去查询
核心矛盾:查询与文档的语义鸿沟
| 维度 | 用户问题 | 真实文档 |
|---|---|---|
| 表达方式 | 简短、口语化、抽象 | 详细、书面化、具体 |
| 信息密度 | 低(只包含意图) | 高(包含事实证据) |
| 关键词 | "为什么""是什么" | 数据、结论、专业术语 |
直接检索的问题:问题和文档在向量空间中距离很远,即使语义相关,相似度分数也不高。
HyDE 解决的根本问题
1. 对齐语义空间
原始问题向量空间 文档向量空间
"为什么经济复苏?" ←——鸿沟——→ "消费支出增长65%..."
↓ ↑
【HyDE桥接】 │
↓ │
"消费支出贡献率65%拉动复苏" ————→ 完美匹配!
2. 具体化模糊意图
| 用户真实意图 | 表面问题 | HyDE生成的假设答案 |
|---|---|---|
| 找原因 | "为什么业绩好?" | "Q3新上线会员体系使复购率提升30%" |
| 找数据 | "去年销量如何?" | "2024年总销量突破100万台,同比增长25%" |
| 找方法 | "怎么降本增效?" | "通过供应链数字化改造降低库存成本15%" |
假设答案把模糊意图转化为具体语义内容,更容易命中文档。
对比:传统检索 vs HyDE
传统检索(问题→文档)
用户问:"公司为什么盈利?"
↓
检索 query:"公司为什么盈利"
↓
召回结果:
❌ "公司盈利预测模型研究..."
❌ "盈利性分析的方法论..."
❌ "上市公司盈利能力排名..." ← 相关但不够精准
HyDE(答案→文档)
用户问:"公司为什么盈利?"
↓
生成假设答案:"主要是因为新产品线毛利率达45%,拉动整体利润增长"
↓
检索 query:"新产品线毛利率45%拉动利润增长"
↓
召回结果:
✅ "Q3财报:新产品线毛利率45.2%,贡献利润增长12亿元..." ← 精准命中证据
HyDE 的深层价值
| 价值 | 说明 |
|---|---|
| 意图扩展 | 把用户的"为什么"扩展成可能的"因为..." |
| 术语对齐 | 把口语映射到文档中的专业表达 |
| 证据预演 | 先"猜"答案长什么样,再去找证据 |
| 噪声过滤 | 假设答案的语义更聚焦,减少召回无关内容 |
一句话总结
HyDE 的本质:用"答案的语言"去检索"答案的载体,用户的问题如果过于模糊,ai直接去知识库查询太难了,他不知道用户到底想要什么方面的数据,但是如果我们根据问题,用ai模拟生成几个答案,比如用户问"公司为什么盈利了",他会生成几个答案,比如"原因是公司制度好","公司产品好"等等,接着他就有方向去查询数据了,比如去查询制度类的,去查询产品营收类的,这种方式的本质还是将用户的问题更加具体化,给出可能要查询的方向,不至于没有头绪的去查找
用户用"问题语言"提问,但文档用"答案语言"写作。HyDE 让大模型充当翻译器,把问题翻译成答案可能的模样,从而跨越语义鸿沟,实现精准检索。
这也是为什么它被称为"查询改写的高级形式"——不是改几个关键词,而是直接改写语义空间。
(3)具体存在的问题
HyDE 的核心风险:幻觉传染
用户问题:"2024年新能源补贴退坡影响"
↓
大模型生成假设答案:"补贴退坡导致比亚迪Q1销量下滑30%"
↓
⚠️ 问题:如果知识库里根本没有这个信息?
↓
结果:检索失败 或 召回无关内容,甚至引入错误前提
本质矛盾:HyDE 依赖大模型的"先验知识"生成假设,但这份知识可能与你的私域知识库不一致。
三种失败场景
| 场景 | 大模型假设 | 知识库真相 | 结果 |
|---|---|---|---|
| 幻觉假设 | "消费增长65%" | 实际增长是35% | 检索不到,或召回错误数据 |
| 过时假设 | "使用GPT-3架构" | 已升级为GPT-4 | 召回旧文档,信息过时 |
| 领域错配 | 生成通用答案 | 知识库是专业垂直领域 | 语义不匹配,检索失败 |
业界如何缓解这个问题
1. RAG-Fusion:多假设+投票
不生成一个假设,而是生成多个 diverse 的假设答案,分别检索后融合:
假设1:"消费增长驱动复苏" → 检索结果A
假设2:"出口回暖拉动经济" → 检索结果B
假设3:"基建投资加大促进" → 检索结果C
↓
合并去重,综合排序
优势:单个假设错了,其他假设可能命中。
2. 假设与原文对比验证
检索回来后,让大模型对比"假设答案" vs "召回文档":
步骤1:生成假设答案
步骤2:用假设答案检索
步骤3:{召回文档} 是否真的支持 {假设答案}?
步骤4:如果不支持,丢弃假设,用原始问题重试
3. 降低假设权重:混合检索
同时用原始问题和假设答案检索,合并结果:
| 检索方式 | 权重 | 作用 |
|---|---|---|
| 原始问题 | 40% | 保底,防止假设跑偏 |
| HyDE假设 | 60% | 提升精准度 |
4. 领域微调:让假设更靠谱
用知识库内容微调生成假设的模型,使其生成风格与私域文档一致。
关键结论
| 问题 | 答案 |
|---|---|
| HyDE 会出错吗? | 会,假设答案本质是大模型幻觉 |
| 还能用吗? | 能,但需要容错机制 |
| 最佳实践? | 作为增强手段,而非唯一手段 |
建议策略:
- 简单/常见/通用问题 → 直接用原始问题检索
- 复杂/抽象/语义鸿沟大的问题 → HyDE + 多假设 + 验证
- 高敏感场景(医疗/法律/金融)→ 慎用,或强制人工校验
2.子问题分解
1.定义:
使用大模型把用户的复杂问题分解成多个简单子问题,然后分别检索,最后合并结果。
适用场景: 当用户提出一个综合性强、涉及多个方面的问题时,比如: “请分析2024年新能源汽车市场的发展趋势、政策影响和技术瓶颈。”
这种问题包含三个子主题: 发展趋势 政策影响 技术瓶颈 如果直接检索,很容易漏掉某些部分。
复杂问题 → 大模型 → 分解为多个子问题 → 并行检索 → 合并结果 → 生成综合回答
2. 举个例子:
用户问:
“2024年新能源汽车市场的发展趋势、政策影响和技术瓶颈是什么?”
步骤1:分解为子问题(由大模型生成)
年新能源汽车市场的销量和渗透率如何?
国家有哪些支持新能源汽车的政策?
当前技术面临哪些瓶颈(如电池续航、充电速度等)?
步骤2:并行检索
对每个子问题分别进行向量检索
得到各自的上下文片段
步骤3:合并结果
将所有相关文档整合
用大模型生成统一的回答
3.多路召回
1. 什么是多路召回?
用一个生活场景来理解:
想象你是一个侦探,要调查一宗案件:
- 单路召回:你只询问一个目击者(单一检索方式)。如果这个目击者记忆有误或者有偏见,你的调查结果就可能出错。
- 多路召回:你同时询问目击者、查看监控录像、分析物证、调查手机记录(多种检索方式)。然后综合所有线索,得出最可靠的结论。
多路召回正是这个思路:同时使用多种检索策略,从不同角度、不同数据源或不同表示形式中召回相关信息,最后合并、去重、重排,得到最优结果集,将他返回给ai
┌─────────────────┐
│ 用户查询 │
└────────┬────────┘
│
┌────┴────┬────────┬────────┐
▼ ▼ ▼ ▼
向量检索 关键词检索 图谱检索 规则匹配
(语义) (精确) (关系) (业务)
│ │ │ │
└────┬────┴────────┴────────┘
▼
结果融合 + 重排序
▼
最终返回Top-K
2.为什么需要多路召回
场景:某公司的智能客服系统
假设你正在为一家大型企业开发智能客服机器人,需要回答员工关于公司政策、IT支持、HR福利等各种问题。
单一检索方式的致命缺陷
用户提问:"我的笔记本电脑太慢了,能申请换新吗?"
让我们看看只用一种检索方式会出什么问题:
// ========== 方案A:只用向量检索(语义相似)==========
// 优点:能理解"慢"和"性能差"是相似的
// 缺点:可能检索出这些结果:
检索结果1: "电脑性能优化指南" (相似度高,但不是换新政策)
检索结果2: "如何清理电脑垃圾文件" (相似度高,但不是换新政策)
检索结果3: "IT资产报废标准" (相似度中,包含换新条件)
检索结果4: "公司电脑配置标准" (相似度中,没有换新流程)
检索结果5: "软件安装规范" (完全不相关,但出现了"电脑")
// 问题:向量检索不知道"换新"是个精确操作
// 结果:可能找到一堆性能优化文档,但就是找不到换新政策
// ========== 方案B:只用关键词检索(BM25/全文搜索)==========
// 优点:精确匹配关键词
// 缺点:太死板,找不到同义词
检索结果1: "新员工电脑申请流程" (匹配"新",但不是换新)
检索结果2: "设备更换申请表" (匹配"换",完美!)
检索结果3: "新年促销活动" (匹配"新",完全不相关)
检索结果4: "更新系统补丁通知" (匹配"新",不相关)
// 问题:如果用户说"电脑太卡想换一台",没有"换新"关键词就搜不到
// 结果:可能完全漏掉"设备更换申请表"这个关键文档
// ========== 方案C:只用元数据过滤 ==========
// 优点:精确,速度快
// 缺点:太死板,需要用户知道精确条件
假设元数据过滤规则:doc_type='replacement_policy' AND device_type='laptop'
用户问题:"我的笔记本电脑太慢了"
// 系统无法自动提取出这些元数据条件
// 结果:一条都搜不到,因为没有指定任何过滤条件
可以看见,单纯走某一条路去查询数据,他都会有一些局限性,比如某些知识无法查询到,某些内容完全不相关,但是如果多路一起并行查询,将他们返回的内容合并,过滤,筛选,重排序,取topk,那么经过多路查询后,最终返回的查询数据,其数据准确性将大幅度提高
根本原因:没有一种检索方式是完美的
| 检索方式 | 擅长 | 不擅长 | 打个比方 |
|---|---|---|---|
| 向量检索 | 理解语义、找相似概念,取语义最相似的 | 精确匹配、专业术语 | 懂你意思,但记性不好 |
| 关键词检索 | 精确匹配(查询包含关键字的数据)、速度快 | 同义词、语义理解 | 记性好,但太死板 |
| 元数据过滤 | 精确筛选、范围限定 | 模糊查询、自由表达 | 按规矩办事,不懂变通 |
| 规则检索 | 处理特定业务逻辑 | 覆盖面窄 | 专家但偏科 |
3.多路召回具体有哪些"路"?
一、核心5大召回路径
| 召回路径 | 核心原理 | 适用场景 | 代码示例 |
|---|---|---|---|
| 1. 向量检索 | 将文本转为向量,计算语义相似度 | 开放式问题、意图模糊、同义词替换 | embedding -> vectorDB.search() |
| 2. 关键词检索 | BM25/倒排索引,精确匹配关键词 | 专业术语、产品型号、标准流程 | esClient.match().field("content") |
| 3. 元数据过滤 | 利用文档标签精确筛选 | 按部门/时间/类型限定范围 | filter: department='IT' AND page>5 |
| 4. 规则检索 | 基于业务逻辑的硬编码规则 | 特定场景、合规检查、异常处理 | if (query.contains("合同") && query.contains("金额")) |
| 5. 知识图谱 | 实体关系查询 | 需要推理、多跳问题、关系查询 | graph.query("张三 负责 哪个项目") |
二、每条路的详细拆解
第1路:向量检索(语义相似),用嵌入模型,即文本转化为向量,存入向量库,再将用户问题转化为向量,检索topK
// 工作原理:将文本转为向量,找"意思相近"的
输入:"电脑太卡了"
找出的结果:
✅ "笔记本运行缓慢" (语义相似)
✅ "设备性能不足" (语义相似)
✅ "电脑卡顿怎么办" (语义相似)
❌ "电脑清洁指南" (虽然也有"电脑",但语义不同)
第2路:关键词检索(BM25/全文搜索)
// 工作原理:精确匹配关键词,计算词频权重
输入:"电脑太卡了"
找出的结果:
✅ "电脑更换申请表" (匹配"电脑")
✅ "电脑卡顿处理流程" (匹配"电脑"+"卡")
✅ "电脑性能优化" (匹配"电脑")
❌ "笔记本电脑促销" (匹配"电脑",但意图不对)
可用ElasticSearch实现,底层就用到了精确查询算法
要实现精准匹配,首先需要在索引文档时,将需要精确查询的字段(如ID、状态码、分类标签)的映射类型设置为 keyword。text 类型用于全文检索(会被分词),而 keyword 类型则会被当作一个完整的、不可分割的单元进行存储和匹配。
// 创建索引时设置映射 (DSL示例)
PUT /products
{
"mappings": {
"properties": {
"product_name": { "type": "text" }, // 全文检索,会被分词
"status": { "type": "keyword" }, // 精确匹配,如 "ACTIVE"
"product_id": { "type": "keyword" } // 精确匹配,如 "P-1001"
}
}
}
具体精确检索实现 (java引入ES的操作类,构建查询方式,指定精确查询字段)
//构建精准查询: 方式term查询查找 status = "ACTIVE"
SearchResponse<Object> response = client.search(s -> s
.index("products") // 指定索引
.query(q -> q // 构建查询
.term(t -> t
.field("status") // 字段名,必须是keyword类型
.value("ACTIVE") // 精确值
)
),
Object.class // 结果映射类型
);
🚀 实战:三种主流Java实现
第3路:元数据过滤
// 工作原理:利用文档的标签字段精确筛选
元数据字段:
- document_type: "IT政策" | "HR手册" | "财务制度"
- department: "技术部" | "市场部" | "人事部"
- created_date: 2023-01-01
- version: "v2.1"
- status: "active" | "archived"
// 使用场景:用户说"找一下去年IT部门的电脑政策"
filter: department='IT' AND document_type='政策' AND year>=2023
第4路:规则检索(业务硬编码)
// 工作原理:针对特定业务场景写死规则
public List<Document> ruleBasedRetrieve(String query) {
List<Document> results = new ArrayList<>();
// 规则1:合同金额查询
if (query.contains("合同") && query.contains("金额")) {
results.addAll(findContractAmountDocs(query));
}
// 规则2:离职流程咨询
if (query.contains("离职") || query.contains("辞职")) {
results.addAll(findHRDocsByType("离职流程"));
}
// 规则3:密码重置
if (query.contains("密码") && (query.contains("忘记") || query.contains("重置"))) {
results.addAll(findITDocsByTag("password_reset"));
}
return results;
}
第5路:知识图谱检索
// 工作原理:查询实体之间的关系
知识图谱结构:
(张三) -[就职于]-> (技术部)
(技术部) -[负责]-> (A项目)
(A项目) -[使用]-> (电脑设备)
// 用户问:"张三用什么电脑?"
推理路径:张三 → 技术部 → A项目 → 电脑设备
// 返回:A项目配备的ThinkPad X1
4.常用的多路检测方式
根据工业界实践,混合检索主要有以下4种经典组合:
| 方式 | 组合 | 适用场景 | 效果 |
|---|---|---|---|
| 方式1 | 向量检索 + BM25关键词 | 通用场景,最主流 | ⭐⭐⭐⭐⭐ |
| 方式2 | 向量检索 + 元数据过滤 | 多维度筛选(按部门/时间/类型) | ⭐⭐⭐⭐ |
| 方式3 | 向量检索 + 关键词 + 元数据 | 企业知识库、复杂业务 | ⭐⭐⭐⭐⭐ |
3.检索后优化
6大检索后优化技术
| 优化技术 | 核心思想 | 解决什么问题 | 效果提升 |
|---|---|---|---|
| 1. 重排序 | 重新计算相关性分数 | 初排不准 | +15-25% |
| 2. 上下文压缩 | 精简文档内容 | Token太长、噪声多 | 减少50%+Token |
| 3. 去重 | 移除重复内容 | 冗余信息 | +10% |
| 4. 文档融合 | 多文档信息聚合 | 答案分散 | +15% |
| 5. 动态上下文选择 | 按需选择内容 | 无关信息干扰 | +20% |
| 6. 验证与修正 | 检查答案准确性 | 幻觉问题 | +30% |
我们还可以设计promt提示词,比如不许构造虚假的检索后的信息,严格输入输出形式等给他加约束
或者再设计一个专门的client,给他设计专门的提示词,让他结合我们是知识库,上下文信息,给我们检索出来的信息打分
也可以多次调试,不断优化提示词