

🔥个人主页:代码不加冰(欢迎来访)
🎬作者简介:java后端学习者
❄️个人专栏:LeetCode刷题日记 ,苍穹外卖日记,SSM框架深入,JavaWeb,
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
大家好,我是代码不加冰,好久没有发文章了,最近开始回归更新,让我们一起进步。这篇主要给大家分享一下RAG的相关内容。不是仅仅是一个词汇,而是带你从项目的源码看起,分析内部的逻辑。
RAG = 把用户的问题变成向量 → 去知识库找相关内容 → 把找到的内容塞回 Prompt → 再让大模型回答。
而 LangChain4j 做的事情,就是把这套流程拆成了一组非常清晰的抽象。
而我们今天主要侧重第一步Embedding,也就是文本向量化,文字怎么变成向量,刚听到这些词的时候我也会觉得莫名其妙,这明明是毫不相关的领域,到底是什么联系的,这篇文章就带你深入了解。
一、RAG 到底是什么
RAG = Retrieval-Augmented Generation
翻译成人话:
先检索,再增强,最后生成。
它解决的核心问题是:
LLM 的知识是死的(训练数据截止日期之后的事它不知道),
但 RAG 可以在用户提问时,从外部知识库实时检索相关信息,塞进 Prompt,让 LLM 基于这些信息回答。
所以 RAG 不是重新训练 LLM,而是给 LLM 配一个"外挂知识库"。
二、RAG 整体链路
RAG 有两条链路,一上一下,缺一不可:
text
RAG
│
┌──────────┴──────────┐
│ │
Ingestion Retrieval
数据入库 数据检索
链路一:Ingestion(数据入库)------ 提前准备好知识
text
公司员工手册.pdf
↓
Document
↓
DocumentSplitter(切分)
↓
TextSegment(文本片段)
↓
EmbeddingModel(向量化) ← 这里就是 Embedding
↓
Embedding(向量)
↓
EmbeddingStore(向量数据库)
最终库里存的是:
text
TextSegment: "员工请假需要提前3天提交申请..."
Embedding: [0.023, -0.182, 0.731, ...]
Ingestion 的本质 :把人类能理解的知识,转换成机器能进行语义检索的向量。
链路二:Retrieval(检索增强)------ 用户提问时实时检索
text
用户:"请假需要提前多久?"
↓
UserMessage
↓
Query
↓
QueryTransformer(查询改写)
↓
QueryRouter(路由选择)
↓
ContentRetriever(真正检索) ← 这里会用到 Embedding 进行相似度搜索
↓
EmbeddingStore(向量库)
↓
Top-K 相关内容
↓
ContentAggregator(汇总排序)
↓
ContentInjector(注入 Prompt)
↓
增强后的 UserMessage
↓
ChatModel(LLM)
↓
Answer
三、现在,我们聚焦Embedding
上面的链路中,Ingestion 的 EmbeddingModel 和 Retrieval 的相似度搜索,都围绕同一个核心概念:
Embedding(文本向量化)
四、Embedding 到底是什么
在 LangChain4j 源码中,Embedding 就是一个 float[]:
java
public class Embedding {
private final float[] vector;
}
例如一个 384 维的向量:
java
float[] vector = {
0.12f, -0.52f, 0.83f, 0.19f, ...
}; // 长度 = 384
Embedding ≠ 文本
Embedding 是文本经过模型编码后得到的一组数字。
维度取决于模型:
| 模型 | 维度 |
|---|---|
| 某些轻量模型 | 384 |
| OpenAI text-embedding-ada-002 | 1536 |
| Cohere 某些模型 | 768 |
所以:
text
Embedding = float[dimension]
五、为什么一定要 Embedding
这是最核心的问题。
用户问:
"请假需要提前多久申请?"
知识库里可能根本没有这句话,它写的是:
"员工休假须至少提前三个工作日提交审批。"
如果用传统字符串匹配(关键词搜索),基本匹配不到。
但如果用 Embedding:
text
问题:"请假需要提前多久申请?"
↓
Embedding
↓
[0.21, 0.83, -0.14, 0.52, ...]
知识:"员工休假须至少提前三个工作日提交审批。"
↓
Embedding
↓
[0.19, 0.79, -0.11, 0.48, ...]
这两个向量在向量空间中距离很近,所以能被检索到。
text
请假需要提前多久?
↓
Embedding
↓
[0.21, 0.83...]
↓
向量数据库进行相似度搜索
↓
找到:"员工休假须至少提前三个工作日......"
这就是 Semantic Search(语义搜索):
不是字面匹配,而是意思匹配。
六、CosineSimilarity:怎么判断两个向量像不像
向量数据库找到了候选结果,但怎么排序呢
靠相似度计算 ,最常用的是 余弦相似度。
源码中的计算逻辑:
java
double dotProduct = 0.0;
double normA = 0.0;
double normB = 0.0;
for (int i = 0; i < vectorA.length; i++) {
dotProduct += vectorA[i] * vectorB[i];
normA += vectorA[i] * vectorA[i];
normB += vectorB[i] * vectorB[i];
}
return dotProduct / Math.sqrt(normA) * Math.sqrt(normB);
用人话翻译:
判断两个向量的"方向"有多一致。
比如:
text
A ────────────────>
B ──────────────>
方向非常接近 → 相似度高 ✅
text
A ────────────────>
B <────────────────
方向相反 → 相似度低 ❌
七、为什么用余弦,而不是算距离
因为 Embedding 关注的是语义方向 ,而不是长度。
举个例子:
text
A = [1, 1]
B = [10, 10]
虽然 |A| ≠ |B|(长度不同),但方向完全一样。
余弦相似度:
text
cos θ = 1
所以它认为 A 和 B 非常相似 ,这非常适合语义向量------
"很小的事"和"很大的事"在语义上可能是同一类。
八、为什么 LangChain4j 又搞了一个 RelevanceScore
余弦相似度的范围是 [-1, 1],但 RAG 开发中更希望看到 [0, 1]。
所以 LangChain4j 做了转换:
java
return (cosineSimilarity + 1) / 2;
映射关系:
| 余弦相似度 | RelevanceScore |
|---|---|
| -1 | 0 |
| -0.5 | 0.25 |
| 0 | 0.5 |
| 0.5 | 0.75 |
| 1 | 1 |
然后就可以做阈值过滤:
java
if (match.score() >= 0.7) {
// 认为比较相关,采纳
}
九、不同 Embedding 模型为什么不能混用
这是一个非常容易踩的坑。
-
模型 A:维度 = 1536
-
模型 B:维度 = 384
如果你的向量数据库 Collection 定义的是 dimension = 1536,却塞入 384 维向量:
text
❌ 报错
可以这样理解:
text
EmbeddingModel.dimension() = 数据库表结构的 Schema
就像 age INT 不能塞字符串一样,向量维度必须严格匹配。
十、Embedding 在 RAG 中的两个位置
理解了 Embedding,你就能看懂它在 RAG 中的两次出现:
text
RAG
│
┌──────────┴──────────┐
│ │
Ingestion Retrieval
│ │
▼ ▼
文档 → EmbeddingModel 问题 → EmbeddingModel
│ │
▼ ▼
Embedding Embedding
│ │
▼ ▼
EmbeddingStore 相似度搜索(Cosine)
│
▼
找到相关内容
| 阶段 | Embedding 的作用 |
|---|---|
| Ingestion | 把文档变成向量,存入数据库 |
| Retrieval | 把用户问题变成向量,去数据库里找最相似的 |
十一、整体回顾
text
┌─────────────────────────────────────────────────────────────┐
│ RAG 完整链路 │
├──────────────────────────┬──────────────────────────────────┤
│ Ingestion │ Retrieval │
├──────────────────────────┼──────────────────────────────────┤
│ │ │
│ PDF → Document │ 用户问题 → Query │
│ ↓ │ ↓ │
│ DocumentSplitter │ QueryTransformer │
│ ↓ │ ↓ │
│ TextSegment │ QueryRouter │
│ ↓ │ ↓ │
│ ★ EmbeddingModel ★ │ ContentRetriever │
│ ↓ │ ↓ │
│ ★ Embedding ★ │ ★ 相似度搜索(Cosine)★ │
│ ↓ │ ↓ │
│ EmbeddingStore │ ContentAggregator │
│ │ ↓ │
│ │ ContentInjector │
│ │ ↓ │
│ │ ChatModel → Answer │
└──────────────────────────┴──────────────────────────────────┘
下一篇预告
这一篇我们重点攻克了 Embedding 是什么、为什么、怎么算相似度。
下一篇继续深入:向量数据库的存储与索引