本文是对「RAG 基础概念 + 本地知识库 Demo」的整理稿:上半部分讲清楚 什么是 RAG 和 流程图在说什么;下半部分保留可运行的 Spring AI + 智谱 示例代码,并补一段 为何不精准 和 往生产走时多考虑啥。
适合已经会写 Spring Boot、正准备接大模型做企业内部问答的 Java 同学。
1. 一句话定义
RAG(Retrieval-Augmented Generation,检索增强生成):在让大模型 生成答案之前,先从你自己的知识源里 检索 一段相关内容,塞进 Prompt,再交给 LLM 生成。这样模型既用得上 最新、私有、领域 的材料,又能在一定程度上抑制 空口编造(幻觉)。
它不是替代微调,而是 外挂图书馆:考试允许翻书,翻到的页码就是检索到的 chunk。
2. RAG 在干什么:对照流程图理解
下面这张示意图把 RAG 拆成两条时间线:离线建索引 和 在线问答案。原图常见于各类 RAG 教程,这里在仓库中的引用路径为:

2.1 离线索引(Indexing,图左侧虚线框)
| 步骤 | 在干什么 |
|---|---|
| docs | 原始材料:URL、PDF、TXT、数据库导出等,本质是你的 本地 / 企业知识库。 |
| Parsing + preprocessing | 解析格式、清洗噪声,变成可切的纯文本。 |
| Chunking | 切成 小块(chunks):模型上下文有限,且小块更利于 精准命中 某一段说法。 |
| Embedding Model | 把每个 chunk 变成 向量(高维数值),语义相近的文本在向量空间里通常 离得近。 |
| Vector store + Indexing | 向量(常附带原文、元数据)写入 向量数据库或索引,供后续检索。 |
个人理解(Java 老鸟的直觉):这一步像给海量日志做 倒排索引,只不过索引键从「词」换成了 语义向量;建索引进度慢、占存储,但问的时候是在 近邻搜索,不是在全库扫字符串。
2.2 在线检索与生成(Retrieval & Generation,图右侧)
| 步骤 | 在干什么 | 白话解释(中文) |
|---|---|---|
| query | 用户问题。 | 用户用自然语言提问,是整个 在线阶段 的入口;后面所有步骤都围绕「这句话要找什么、答什么」。 |
| Embedding Model(Vectorize) | 问题也编成 同维度向量,和库里的向量 可比。 | 把「一句话」变成 一串数字(向量),和建库时 chunk 用的 同一套模型、同一维度,才能在同一空间里谈「近不近」。 |
| Retrieve | 在向量库里做 相似度检索(常见:余弦相似度、点积;库内往往用 HNSW 等索引加速)。 | 在知识库里 按语义远近 捞出与问题最相关的若干段材料,相当于 翻书翻到最相关那几页(不必整本书扫字)。 |
| Relevant docs + prompt | 把 Top-K 片段拼进提示词模板,约束模型 只结合给定上下文作答。 | 把捞出来的片段当 参考资料,写进 Prompt,告诉模型:先信这些,再组织语言;减少空口编造。 |
| LLM → Generate → response | 大模型推理,输出连贯回答。 | 大模型在 给定上下文 + 用户问题 下做生成,输出用户看得懂的 最终答复(仍可能需后处理与审核)。 |
名词:HNSW 指什么?
HNSW(Hierarchical Navigable Small World)是向量检索里常用的一种 近似最近邻(ANN) 算法:在高维向量空间里建 分层图索引,查询时从上层「粗跳」快速接近目标区域,再在下层细查,从而在 不全表暴力扫描 的前提下,尽快找到与查询向量 最接近 的若干条记录。
- 为何需要它:全库两两算相似度是 O(数据量),数据一大就不可用;向量库(Milvus、Qdrant、pgvector 等)内部用 HNSW、IVF 等索引做 加速召回。
- 近似:结果是 近似 最近邻,用少量精度换 延迟与吞吐;重要场景可再叠 Rerank 精排。
-
和 Demo 的关系:教程里
for循环算余弦相似度是 朴素全表;生产应交给 带 ANN 索引的 Vector Store,而不是在应用里自己扫List。
落地实现:从「手写循环」到「带索引的 Vector Store」
1. 典型数据流(索引何时出现)
-
建库 / 灌库:文档 chunk →
EmbeddingModel.embed()→ 向量 →VectorStore.add/addAll;存储在落盘后 构建或更新 HNSW(或 IVF 等),后续查询 不再 对全量向量做朴素两两比较。 -
在线检索:用户问题 → 同维度 query 向量 →
VectorStore.similaritySearch(SearchRequest)(或各实现类等价 API)→ 返回 Top-KDocument。 - 距离度量:与库侧索引 ops 一致(余弦 / L2 / 内积),且与 Embedding 模型是否归一化 的假设一致,否则召回会飘。
2. Spring AI 中的抽象
-
VectorStore:屏蔽PgVectorStore、MilvusVectorStore、SimpleVectorStore等;业务只面对Document(text + metadata) 与SearchRequest(query / topK / filter)。 -
SimpleVectorStore:多作小数据、单测或本地 demo;不等于 生产级持久化 + HNSW,但可快速验证 RAG 链路。 - PgVector / Milvus / Qdrant:在库里 建表 + 建向量索引(部分由 starter 自动 DDL,部分需你按厂商文档先建 HNSW)。
3. 各后端「HNSW」长什么样(概念级)
| 后端 | 实现要点 |
|---|---|
| pgvector | 列类型如 vector(1536);索引示例:CREATE INDEX … ON tbl USING hnsw (embedding vector_cosine_ops);(ops 与距离一致)。可调 m、ef_construction 等(以 pgvector 当前文档为准)。 |
| Milvus | Collection 上 INDEX,index_type 常选 HNSW,params 含 M、efConstruction;查询侧 ef 影响精度与延迟。 |
| Qdrant | 向量参数 + HNSW(如 m、ef_construct),查询可设 hnsw_ef。 |
4. 调参直觉
- M:邻居多 → 召回更好,建索引 更慢、占内存更多。
- efConstruction / ef(查询):搜索范围大 → 更准更慢。
- Top-K:可先 略大(如 20)再 Rerank 压到 5,比单次 K=2 稳。
5. 代码形态(示意)
// 灌库:chunk → Document(metadata 带 source、chunkId) → add
// vectorStore.add(List.of(new Document(text, Map.of("source", "poetry.txt"))));
// 检索:由 VectorStore 内部调 Embedding 或传入 query 文本(视实现)
// List<Document> hits = vectorStore.similaritySearch(
// SearchRequest.builder().query("古代诗歌常用意象有哪些?").topK(8).build());
6. 与本文 Demo 的对应
| Demo | 生产 |
|---|---|
List<float[]> + for + cosineSimilarity
|
VectorStore + 库内 ANN 索引
|
内存 docs
|
持久化表 + metadata,向量可 重建 |
探究:RAG 解决的是 「知识从哪来」;「话怎么说得体」 仍靠 Prompt、模型能力和后处理。上下文给错了,模型照样能一本正经胡说——所以 检索质量 往往是瓶颈,而不是 Chat API 调得花不花。
3. Demo 案例目标(Spring AI + 智谱)
案例中用到:
-
Spring AI:统一抽象
EmbeddingModel、ChatClient等,类似当年用JdbcTemplate统一数据源。 - 智谱 AI Embedding:把文本变成向量。
- 智谱 Chat(如 GLM-4-Flash):结合检索到的片段回答问题。
实现思路(与教程一致):
- 本地知识文件放在
resources,ClassPathResource加载。 - 按分隔符
----手工切分片段(教学向;生产慎用这种粗切法)。 - 启动时对每段 embed,向量放 内存列表(教学向;重启即失、量大必挂)。
- 用户提问 embed 后,与所有片段向量算 余弦相似度,取 Top-K(示例代码里
TOP_K = 3,可按题调整)拼上下文。 -
系统提示 + 用户提示 交给
ChatClient同步调用返回文案。
4. 配置与资源
4.1 Chat 模型(application.properties)
# 使用智谱 AI Chat 模型(pom.xml 需引入对应 spring-ai-starter 与智谱 BOM/依赖)
spring.ai.zhipuai.chat.options.model=GLM-4-Flash
4.2 本地知识文件
在 resources 下放置 古代诗歌常用意象.txt(注意原文小标题里有时写作「意向」,文件名建议统一为 意象,与诗文术语一致)。
内容结构上,文档用 ---- 分成多块,涵盖 植物类 / 动物类 / 景象类 / 人文类 等意象说明——Demo 里 每一块会被整体 embed 一次,块太大或切法不对会直接影响检索颗粒度。
5. 核心代码:RagService
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Service
public class RagService {
/** 检索拼进 Prompt 的片段条数;枚举类问题可适当调大 */
private static final int TOP_K = 3;
private final EmbeddingModel em; // 嵌入模型,用于生成文本的向量
private final ChatClient chatClient; // 聊天客户端,用于与 AI 进行交互
private final List<String> docs = new ArrayList<>();// 存储本地文档内容
private final List<float[]> vectors = new ArrayList<>();// 存储本地文档向量
public RagService(EmbeddingModel embeddingModel, ChatClient.Builder chatBuilder) throws IOException {
this.em = embeddingModel;
this.chatClient = chatBuilder.build(); // 创建智谱 AI Chat 客户端
Resource res = new ClassPathResource("古代诗歌常用意象.txt");
String content = new String(res.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
for (String part : content.split("----")) {
System.out.println("part: " + part);
if (part.isBlank()) continue;
docs.add(part);
vectors.add(em.embed(part)); // 将文档生成 embedding 并存储
}
}
public String answer(String q) {
if (vectors.isEmpty()) {
return "知识库为空。";
}
float[] qv = em.embed(q);
int k = Math.min(TOP_K, vectors.size());
List<Integer> topIndices = topKSimilarIndices(qv, k);
StringBuilder ctx = new StringBuilder();
for (int i = 0; i < topIndices.size(); i++) {
if (i > 0) {
ctx.append("\n---\n");
}
ctx.append(docs.get(topIndices.get(i)));
}
String prompt = "以下是知识内容:\n" + ctx + "\n请基于上述知识回答用户问题:「" + q + "」";
var response = chatClient
.prompt()
.system("你是知识助手,结合上下文回答问题")
.user(prompt)
.call();
return response.content();
}
/**
* Top-K:先对每个 chunk 算与问题的余弦相似度,再按分数降序取前 k 个下标。
* 数据量极大时可改为「最小堆维护前 K」避免全量排序,此处教学向保持可读。
*/
private List<Integer> topKSimilarIndices(float[] queryVector, int k) {
int n = vectors.size();
double[] score = new double[n];
for (int i = 0; i < n; i++) {
score[i] = cosineSimilarity(queryVector, vectors.get(i));
}
Integer[] order = new Integer[n];
for (int i = 0; i < n; i++) {
order[i] = i;
}
Arrays.sort(order, (i, j) -> Double.compare(score[j], score[i]));
List<Integer> top = new ArrayList<>(k);
for (int i = 0; i < k; i++) {
top.add(order[i]);
}
return top;
}
// 余弦相似度:值域通常在 [-1, 1],越接近 1 越相似(向量若已 L2 归一化则点积即余弦)
private double cosineSimilarity(float[] a, float[] b) {
double dot = 0, na = 0, nb = 0;
for (int i = 0; i < a.length; i++) {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
return dot / (Math.sqrt(na) * Math.sqrt(nb));
}
}
防呆:实现手写相似度时,dot 必须是 (\sum a_i b_i);若误写成 dot += a[i] * a[i],Top-K 排序会完全失真——排错片段比「模型笨」更致命。
6. RagController
import com.example.springaiembedding.service.RagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/rag")
public class RagController {
@Autowired
private RagService ragService;
@GetMapping("/ask")
public Map<String, String> ask(@RequestParam("question") String question) {
String answer = ragService.answer(question);
return Map.of("question", question, "answer", answer);
}
}
自测 URL
http://localhost:8080/rag/ask?question=古代诗歌常用意象有哪些?
若答案 只覆盖部分内容 或 偏题,多半不是「模型不行」四个字能糊弄过去的,下面分条说原因。
7. 为何 Demo 容易「不够准」?
| 原因 | 说明 |
|---|---|
| 切分策略过粗 | 按 ---- 切,单块可能仍很长,语义混杂;检索命中的是「整坨」里最接近问题的平均语义,颗粒度不对。 |
| Top-K = 2 太小 | 「常用意象有哪些」是 枚举型 问题,两条 chunk 可能盖不全植物/动物/景象/人文。RAG 不是只配 Top2,可以先大 K 再压缩。 |
| 线性扫全库 |
for 循环算相似度是 O(n),教学够用;数据一大就要 向量索引(HNSW 等),否则延迟和内存都扛不住(HNSW 含义见上文 §2.2)。 |
| 实现细节 bug | 手写相似度时点积维度搞错、向量维度不一致等,会直接导致 召回错乱。 |
| 仅有向量、没有词面 | 专有名词、生僻书名,向量检索偶尔会飘;生产常用 混合检索(BM25 + Vector) 补短板。 |
| 缺少「不知道」约束 | Prompt 里应明确:上下文没有的信息 不要编;可显著减少「像那么回事」的幻觉。 |
8. 往生产走:RAG 相关实现补全( checklist )
下面这些和「会不会调 Chat Completion」关系不大,和 数据工程 关系更大。
向量持久化
使用 pgvector / Milvus / Qdrant / Redis Stack 等,配合 Spring AI 的VectorStore抽象;避免「重启 embed 一遍」和「单机 List 存全量」。Chunk 策略
按 固定 token、段落/标题、或 结构化字段 切;必要时 重叠窗口(overlap) 保留上下文;长文档先 分段索引。混合检索与重排
初召回:向量 Top 50 + 关键词 Top 50;再用 Rerank 模型 压成 Top 5 进 Prompt,命中率 往往上一台阶。元数据与过滤
索引里带tenantId、docVersion、permission,检索前 先过滤 再相似度排序,企业里这是 合规 问题而不只是效果问题。观测与评测
记录 query、召回 id、得分、最终回答;用固定问集合做回归,否则每次改分块都像在黑盒炼丹。异步与成本
建索引(批量 embed)可走 异步任务;查询链路上区分 轻量模型 embed 与 大模型 generate 的配额与超时。
9. 小结
- RAG = 索引(Embedding + 向量存储)+ 检索(相似度/混合)+ 增强 Prompt + LLM 生成。
- 流程图把 离线 与 在线 分清楚,你在架构评审时也可以同样画:左边数据管道,右边查询 SLAs。
- Demo 的价值是 跑通链路;要上生产,重点会挪到 chunk、向量库、混合检索、权限与评测。
引用与延伸阅读
- 文中 RAG 工作流程示意:见本文配图
images/rag-workflow-architecture.png(经典 Indexing / Retrieval & Generation 二分结构)。 -
Spring AI:Spring AI 官方文档(Embedding、
VectorStore、ChatClient等)。 - 智谱 AI:以官方开放平台说明为准,注意 Embedding 与 Chat 模型名、维度、计费 分离配置。
- 同仓库若已有 《Spring AI / Embedding》 小册章节,可与本文 交叉阅读(余弦相似度、向量维度、批处理 embed 等)。