企业制度、产品手册和内部代码通常不在大模型的训练数据中,直接提问容易得到缺少依据甚至错误的回答。RAG 通过先检索知识库、再结合命中文档生成答案,为私有知识问答提供了一条可落地的路径。下面将以 Spring AI 为基础,从最小示例开始拆解入库、检索、重排与生成等关键环节。
用 Spring AI 从零搭一个「会查资料再回答」的知识库问答系统:从原理到代码,从跑通到上线。
你想让大模型回答「内部知识」——公司的制度、手册、产品文档、代码库。但大模型没学过这些,直接问它就会一本正经地瞎编。
RAG(Retrieval-Augmented Generation,检索增强生成)就是干这个的:先查资料,再照着资料答。这篇文章教你把这件事用 Spring AI 完整落地——从「为什么」到「怎么写代码」,再到「怎么把它调到生产可用」。
不需要你懂机器学习,只要会写 Java 就行。
整篇文章走一条主线——Advanced RAG 的完整链路:
离线入库:文档加载 → 解析 → 分片 → 向量化 → 建索引
在线问答:提问 → 改写 → 向量化 → 召回 → 重排 → 拼 prompt → 生成
所有代码都围绕同一个例子(后文反复用到):
知识库里有一条「退货正策」:「7 天无理由退货:签收后 7 天内、未使用可申请,审核通过后 3~5 天退款」。用户问:「这东西咋退啊?」
从入库到回答,你会看到这条知识怎么被切块、向量化、命中、重排、喂给模型,最后给出带溯源的回答。
目标:先看到结果,再回头理解原理。 这里用内存向量库 + 云端大模型(qwen-max),不装本地模型、不碰数据库,配好 API Key 就能跑通。
# ① 云端大模型:阿里百炼开通 qwen-max,创建 API Key(推荐)
export OPENAI_API_KEY=sk-xxxx
# ② 生产要用的 pgvector(这一步可以先跳过,等跑通再装)
docker run -d --name pgvector -e POSTGRES_PASSWORD=postgres -p 5432:5432 pgvector/pgvector:pg16
docker exec -it pgvector psql -U postgres -c "CREATE EXTENSION vector;"
没有 API Key、想离线跑?也可以用本地 Ollama(
ollama pull qwen2.5:1.5b+ollama pull nomic-embed-text),再把第 3 节的配置换回 Ollama 即可。
完整环境说明见附录 A。
新建一个 Spring Boot 项目,pom 里只加三个依赖(spring-ai-starter-model-openai、spring-ai-rag、spring-ai-advisors-vector-store,完整 pom 见附录 A),然后写这个 CommandLineRunner:
@SpringBootApplication
public class QuickStartApplication {
public static void main(String[] args) {
SpringApplication.run(QuickStartApplication.class, args);
}
@Bean
CommandLineRunner run(EmbeddingModel embeddingModel, ChatModel ch@tModel) {
return args -> {
// ① 离线入库:内存向量库(重启即丢,只用来跑通)
VectorStore store = SimpleVectorStore.builder(embeddingModel).build();
// 一条退货正策,切块入库
Document doc = new Document(
"7 天无理由退货:签收后 7 天内、未使用可申请,审核通过后 3~5 天退款。");
List<Document> chunks = TokenTextSplitter.builder()
.withChunkSize(800)
.withMinChunkSizeChars(350)
.build()
.apply(List.of(doc));
store.add(chunks);
// ② 在线问答:挂一个朴素 RAG Advisor
QuestionAnswerAdvisor qa = QuestionAnswerAdvisor.builder(store)
.searchRequest(SearchRequest.builder()
.topK(5)
.similarityThreshold(0.45)
.build())
.build();
ChatClient client = ChatClient.builder(ch@tModel)
.defaultAdvisors(qa)
.build();
// ③ 提问
String answer = client.prompt().user("这东西咋退啊?").call().content();
System.out.println("回答:" + answer);
};
}
}
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
ch@t:
options:
model: qwen-max
embedding:
options:
model: qwen3.7-text-embedding # 1024 维
dimensions: 1024
回答:根据退货正策,商品在签收后 7 天内、未使用的情况下可以申请无理由退货,
审核通过后 3~5 天退款。
看到这个,你就已经跑通了 RAG 的完整链路:入库(切块 → 向量化 → 存)→ 召回(向量相似度命中「退货正策」)→ 生成(照资料答)。
这段代码是「朴素 RAG」——黑盒、不可控、只够演示。后面 10 章就是把它一步步拆开、逐个环节优化成生产可用的 Advanced RAG:
| 你现在要解决的问题 | 去哪看 |
|---|---|
| 文档是扫描件/图片,读不进来 | 第 2 章(OCR) |
| 切块太糙、语义断裂 | 第 3 章 |
| 换中文强 Embedding、上 pgvector | 第 4 章 |
| 口语化「咋退」查不到 | 第 5 章(查询改写) |
| 召回太糙、想要精排 | 第 6 章 |
| 模型瞎编、要溯源 | 第 7 章 |
| 效果还是差、要系统调优 | 第 8 章 |
本章解决什么问题:让你在动手前,搞懂 RAG 是什么、什么时候该用它、以及整套系统长什么样。读完这章,你能画出 RAG 的全景图,并判断「我的场景到底该不该上 RAG」。
大模型很强,但有三件事它天生做不好:
这三件事有一个共同解法:别让模型凭空答,先给它查好资料。这就是 RAG。
RAG = Retrieval(检索)+ Augmented(增强)+ Generation(生成):先查资料,再照着资料答。
用户提问 → 检索知识库(找资料)→ 资料塞进提示词 → LLM 照资料答
一句话:把「查资料」和「照着答」接起来,补齐大模型「不知道私有知识」的短板。
RAG 分离线和在线两条线,先分清这两条线,后面所有章节都挂在上面:
【离线 · 入库线】(一次性,知识更新时重跑)
文档加载 → 解析/清洗 → 分片 → 向量化 → 建索引 → 存入向量库
【在线 · 查询线】(每次提问实时执行)
用户提问 → 查询改写 → 向量化 → 召回 → 重排 → 拼 prompt → LLM 生成 → 回答
各阶段做什么、关键点在哪:
| 阶段 | 做什么 | 关键点 |
|---|---|---|
| 文档加载 | 读入 PDF/Word/HTML/Markdown | 格式多样 |
| 解析/清洗 | 提取纯文本,去页眉页脚 | 图片会丢、表格易碎 |
| 分片 | 长文档切成小块 | 块大小决定检索精度 |
| 向量化 | 文本转成高维向量 | Embedding 模型决定上限 |
| 建索引 | 向量入库 + 建索引 | HNSW / IVFFLAT |
| 查询改写 | 口语化提问改成书面检索式 | 「咋退」→「如何退货」 |
| 召回 | 取 topK 最相关片段 | 阈值 / topK 调参 |
| 重排 | 用 reranker 精排候选 | 粗召回 + 精排 |
| 生成 | 拼 prompt + 照资料答 | 防幻觉 |
| 概念 | 一句话理解 |
|---|---|
| Embedding | 把文本变成一串数字(向量),语义近的向量距离近 |
| 向量库 | 每条记录存「向量 + 原始文本 + 元数据」三样东西,向量只用来检索 |
| Chunk(分片) | 文档切成的小块,检索的基本单位 |
| 召回 | 从库里捞出最相关的 topK 块 |
| Rerank(重排) | 对召回结果重新打分排序 |
| Context(上下文) | 拼进提示词的那段资料 |
| 幻觉 | 模型没有依据地瞎编 |
| 溯源(citation) | 回答时指出依据哪份文档 |
完整术语表见附录 D。
新手误区:向量库不是只存向量。 入库时每条记录同时保存三样东西——向量 embedding(只用于相似度检索)、原始文本、metadata 元数据(来源、时间等)。检索命中后,取出来喂给 LLM 的是原始文本,不是那串向量——向量只是「找」的索引,文本才是「答」的原料。
判断「要不要上 RAG」,看三件事:有哪些典型场景、你的场景适不适合、问题有多复杂(单跳还是多跳)。
典型场景:
| 场景 | 例子 | 知识来源 |
|---|---|---|
| 知识库问答 | 「报销流程是什么」 | 制度、Wiki |
| 智能客服 | 「怎么退货」 | 话术、正策 |
| 企业助手 | 「入职要办啥」 | 手册、SOP |
| 文档/合同助手 | 「合同违约条款」 | 合同、法务文档 |
| 垂直行业 | 「这药禁忌」「这法条解读」 | 药典、法典、研报 |
| 代码助手 | 「鉴权逻辑在哪」 | 代码、技术文档 |
| Agent 增强 | 给 Agent 提供事实依据 | 各类知识源 |
适不适合:
单跳 vs 多跳(判断你的问题复杂度):
| 问题特征 | 例子 | 技术 |
|---|---|---|
| 单跳、关键词明确 | 「P0 故障怎么处理」 | 纯向量 / 混合检索 |
| 单跳、语义口语化 | 「咋退」「咋报销」 | 纯向量 + 查询改写 |
| 精确编号/型号 | 订单号、SKU | 混合检索(向量 + BM25) |
| 多跳推理 | 「A 和 B 什么关系」 | GraphRAG / 图检索(进阶) |
单跳 = 答案就在一条知识里,检索一次命中即可答;多跳 = 答案分散在几条知识里,要「查 A → 根据 A 再查 B → 拼起来」才能答,向量检索搞不定,需图检索(GraphRAG)或 Agent 多轮检索。普通知识库问答,先「纯向量 + 查询改写 + 重排」就够(见第 8 章)。
先给结论:「灌知识」用 RAG,微调现在基本不干这事了——它的用途已经变成「改输出风格/格式、适配领域术语、把强模型蒸馏到小模型降成本」,用它灌知识更新慢、成本高、易过拟合。
| 维度 | RAG | 微调 | 长上下文 |
|---|---|---|---|
| 原理 | 检索外部知识注入 prompt | 私有数据再训练 | 资料全塞进上下文 |
| 知识更新 | 秒级 | 慢(要重训) | 秒级 |
| 可溯源 | 强 | 弱 | 弱 |
| 适用 | 私有知识、实时、要溯源 | 改风格/格式、降本 | 单篇长文精读 |
RAG 落地最容易卡在这四件事,后面章节分别解决:
| 痛点 | 表现 | 对策 | 对应章节 |
|---|---|---|---|
| 文档解析 | PDF 乱码/扫描件、表格拆散、图片丢失 | 换解析器、OCR、图片转文字摘要 | 第 2 章 |
| 颗粒度 | 块太大噪声多、太小语义断裂 | 块 300~1000 token、父子分块 | 第 3 章 |
| 检索准确率 | 查不到 / 查不准 | 阈值、查询改写、混合检索、重排 | 第 5、6 章 |
| 提问模糊性 | 口语化、指代不明,和文档用词对不上 | 查询改写、多轮记忆 | 第 5、7 章 |
这里指 ch@t 生成模型;embedding 选型见第 4 章、rerank 见第 6 章。
选型主要看三个能力:
| 能力 | 说明 |
|---|---|
| 逻辑推理能力 | 能否从多段资料里综合、推理出结论 |
| 指令遵循能力 | 能否严格照 prompt 约束执行(如「资料没有就说不知道」) |
| 防幻觉能力 | 没有依据时会不会硬编 |
评估标准:用「忠实度(有无幻觉)、检索命中率、延迟、成本」量化,先搭评测集再测(第 8 章)。
优先选这三项都强的模型(Qwen / DeepSeek 等),再按成本、延迟收敛。
演进路线:
Naive RAG → Advanced RAG(预检索/检索/后检索三段优化)→ Modular RAG → 前沿
本文主线是 Advanced RAG:预检索(查询改写)、检索(混合)、后检索(重排)。
Modular RAG(模块化):把检索、改写、重排、生成等环节拆成可插拔模块,自由增删/替换/编排——Spring AI 的 RetrievalAugmentationAdvisor 四段式就是它的落地(第 7 章)。
前沿方向简单了解即可:
| 方向 | 一句话 | 解决什么 |
|---|---|---|
| GraphRAG | 把知识建成「实体 + 关系」的图再检索 | 多跳推理(「A 和 B 什么关系」) |
| Agentic RAG | 让 Agent 自主决定何时检索、检索什么 | 复杂任务、多轮检索 |
| 多模态 RAG | 文档里的图片/表格也参与检索 | 图、表信息不丢失 |
本章要点回顾
- RAG 补上大模型三个短板(幻觉 / 知识过期 / 私有知识缺失):先查资料,再照着资料答。
- 两条线:离线入库(加载 → 解析 → 分片 → 向量化 → 建索引)、在线查询(改写 → 向量化 → 召回 → 重排 → 生成)。
- 决策:私有知识 / 实时更新 / 要溯源 → 用 RAG;「灌知识」用 RAG、别用微调。
- 落地四关:文档解析、颗粒度、检索准确率、提问模糊性,分别对应第 2~7 章。
- 主线:Advanced RAG = 预检索(查询改写)+ 检索(混合)+ 后检索(重排)。
第 2~6 章为导读:每章点明「解决什么问题、关键坑在哪」,完整实操与术语(混合检索、BM25、Reranker 等)在对应掘金专栏里;可直接落地的代码集中在第 7 章和附录 B。
本章解决什么问题:离线入库的第一步——把各种格式的文档(PDF/Word/HTML/Markdown,以及扫描件)读进来、洗干净,变成第 3 章能直接切块的干净纯文本。没有这一步,后面分片、索引全是白搭。
详细内容见专栏:RAG检索增强生成:文档加载解析 · RAG检索增强生成:文本清洗
本章解决什么问题:文档太长,整篇塞不进模型、也查不准。本章教你怎么把文档切成合适的小块——块太大/太小的后果、怎么选参数、有哪些策略、以及不同文档类型怎么切才切得好。
详细内容见专栏:RAG检索增强生成:文本分片
本章解决什么问题:把第 3 章切好的文本块变成「机器能算相似度」的向量,并组织成能快速检索的索引。核心是选对 Embedding 模型(决定 RAG 上限)和配好向量库(维度对齐是头号坑)。
详细内容见专栏:RAG检索增强生成:向量化 & 索引
本章解决什么问题:用户问「这东西咋退」,怎么从向量库里捞出真正相关的那几条。本章覆盖三种召回方式、召回质量指标、topK/阈值调参,以及最重要的「查询侧优化」(查询改写)。
详细内容见专栏:RAG检索增强生成:召回
本章解决什么问题:向量召回是「粗筛」,排序不够准。本章用更准的 reranker 模型对候选精排,让真正相关的排到最前面。
详细内容见专栏:RAG检索增强生成:重排(Reranking)
本章解决什么问题:把重排后的资料拼进 prompt,让模型照着资料答、没有依据就拒答,并带出溯源。核心是防幻觉和模块化组装。
[系统提示:基于资料答] + [资料:重排后的文档] + [问题] → LLM → 回答
请基于以下资料回答,资料没有的就说「不知道」,不要编造。
问题:{query}
资料:{question_answer_context}
// 自定义模板必须含 {query} 和 {question_answer_context}
QuestionAnswerAdvisor qa = QuestionAnswerAdvisor.builder(vectorStore)
.promptTemplate(customTemplate).build();
| 手段 | 做法 |
|---|---|
| 空上下文拒答 | allowEmptyContext(false),没资料就说不知道 |
| 提示词强制 | 模板写死「资料没有就说不确定」 |
| 降温度 | temperature 调到 0.2~0.3,回答更保守 |
入库时把来源写进 metadata(source),回答时带出来。注意 QuestionAnswerAdvisor 默认只注入正文、不注入 metadata,溯源要用模块化 RAG(7.7)手动拿 docs 拼 source。
追问「那它的价格呢」要结合上文。做法:ChatClient 同时挂「记忆 + RAG」两个 Advisor:
ChatClient.builder(ch@tModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(ch@tMemory).build(), // 记忆带历史
qaAdvisor) // RAG
.build();
历史会帮消解「它」指什么;配查询改写效果更好;记忆会涨 token,需窗口截断或摘要。
| 手段 | 说明 |
|---|---|
| 控 topK / 重排取前 N | 源头限制注入条数 |
| 截断 | 每篇只取前 N token |
| 摘要压缩 | context 先总结再注入 |
QuestionAnswerAdvisor):黑盒,代码少,环节不可控。RetrievalAugmentationAdvisor):四段可插拔。RetrievalAugmentationAdvisor.builder()
.queryTransformers(rewrite) // ①查询改写(可选)
.documentRetriever( // ②检索(必填)
VectorStoreDocumentRetriever.builder().vectorStore(vs).topK(10).similarityThreshold(0.35).build())
.documentPostProcessors(rerankPostProcessor) // ③后处理 Rerank(可选)
.queryAugmenter( // ④增强(空上下文防幻觉)
ContextualQueryAugmenter.builder().allowEmptyContext(false).build())
.build();
四段只有「检索」必填,其余按需加(改写/重排都多一次模型调用,有成本)。
本章解决什么问题:效果不满意时,八成问题在检索侧。本章给出一份按性价比排序的优化清单、问题诊断表,以及「先有度量再优化」的评测体系。
| 顺序 | 手段 | 成本 | 说明 |
|---|---|---|---|
| 1 | 调阈值/topK | 零 | 先打印 score 分布再定 |
| 2 | 查询改写 | 一次 LLM | 口语化命中率提升最明显 |
| 3 | 元数据过滤 | 零 | 缩小范围、减少误召回 |
| 4 | 混合检索 | 中 | 解决编号/型号查不准 |
| 5 | 重排 | 一次 rerank | 精排、压缩候选 |
| 6 | 换更强 Embedding | 重建索引 | Embedding 不行上面全白搭 |
先便宜后贵:1→3→2→5→4→6,别一上来就换模型、上混合检索。
| 症状 | 根因 | 对策 |
|---|---|---|
| 检索不到(召回低) | 相关文档没搜出 | 降阈值 / 查询改写 / 换 Embedding / 混合检索 |
| 检索不准(精准低) | 搜出一堆无关 | 升阈值 / 过滤 / 重排 |
| 幻觉 | 资料没有却乱编 | 空上下文拒答 / 提示词强制 / 降温度 |
排查顺序:先看检索(命中、score),再看生成(照不照资料答)。
评测集:准备「问题 → 标准答案 → 应命中文档」三元组,作回归测试。
RAGAS 三指标:
| 指标 | 含义 | 查什么 |
|---|---|---|
| Faithfulness | 答案是否忠于上下文(有没有编) | 幻觉 |
| Answer Relevancy | 答案是否切题 | 答非所问 |
| Context Relevancy | 检索上下文是否相关 | 检索准不准 |
三者对应 8.2 三类问题:Context Relevancy 查检索、Faithfulness 查幻觉、Answer Relevancy 查切题。RAGAS 是 Python 库,Java 侧无同级框架,生产用 Python 离线跑或「LLM as Judge」自打分。
三层:底层 DEBUG 日志 → 自定义 Advisor 拦截 → 直接打印检索结果。
logging.level:
[email protected]: DEBUG
org.springframework.ai.vectorstore: DEBUG
// 最直观:直接打印检索结果看 score
for (Document d : vectorStore.similaritySearch(
SearchRequest.builder().query(q).topK(10).build())) {
System.out.println(d.getScore() + " / " + d.getText());
}
排查顺序:先打印检索结果(命中/分数)→ 再看 prompt 拼接 → 最后看生成。
本章解决什么问题:从「能跑」到「能上线转前」。三阶段推进、算账、选型、工程质量四件事、数据治理,以及一个贯穿全书的电商客服案例。
| 阶段 | 目标 | 退出标准 |
|---|---|---|
| PoC | 证明检索+生成能答对 | 最小数据跑通,命中率达预期 |
| 试点 | 验证真实稳定性 | 小流量观察命中率/成本/反馈 |
| 全量 | 规模化运营 | 接评测、坚控、运维体系 |
单次问答成本 ≈ 生成 token × 生成单价 + 检索成本(极小)+ 摊薄的入库成本。生成是大头,和 context 长度、模型档次强相关——所以控 topK、压缩 context 不只是质量问题是成本问题。
定价:按 token / 按调用 / 订阅 / SaaS 多租户。 降本:模型分级(简单问题用小模型)、缓存、本地开发云生产、提示词瘦身、上下文压缩。
| 组件 | 建议 |
|---|---|
| ch@t | 生产云(质量稳),开发本地 |
| embedding | 中文选 bge / qwen3.7(第 4 章) |
| rerank | 云 API 最省事(第 6 章) |
| 向量库 | 有 PG 用 pgvector,大规模用 Milvus |
| 关注点 | 做法 |
|---|---|
| Prompt 注入 | 分隔符隔离用户输入,内容安全组件兜底 |
| 数据隔离 | 按租户/conversationId 隔离,身份走服务端注入 |
| 权限溯源 | 检索按租户过滤,回答带 source |
| 降级兜底 | 检索空/超时/异常有兜底话术或转人工 |
入库去重(稳定 id、先删后加)、过期清理(按 updated_at)、权限隔离、版本管理。
目标:客服机器人答「退货/配送/发票」等制度问题,降低人工坐席
架构:ChatClient 挂 记忆→RAG(改写→检索→重排→增强)→日志;业务表+向量表共用 PG+pgvector
模型:ch@t=qwen-max、embedding=qwen3.7(1024)、rerank=qwen3-rerank
节奏:PoC 灌 4 条知识验证「咋退」→试点接真实日志→全量加评测+坚控+转人工
这个案例贯穿全书各章节;附录 B 是可跑通的朴素闭环 Demo,进阶环节(查询改写 / 重排 / 溯源)见第 7 章。
本章解决什么问题:出了问题,按「数据解析 → 分片 → 向量化 → 召回 → 生成 → 工程化」全链路逐段定位,每段给你「症状 → 根因 → 解法」。
数据解析(10.2) → 分片(10.3) → 向量化/索引(10.4) → 召回(10.5) → 生成(10.6) → 工程化(10.7)
| 问题 | 解法 |
|---|---|
| PDF 乱码/扫描件 | 扫描件 OCR,换解析器 |
| 表格 | 整表解析保表头 |
| 图片信息丢失 | 提取图片 → 调多模态模型生成文字描述 → 当普通文本入库 |
| Tika 提取不干净 | 解析后清洗页眉页脚/广告 |
图片是最隐蔽的坑:Tika 只提取文字,图片直接跳过且不报错。带业务信息的图片必须「转文字摘要」入库;完整多模态 RAG(检索出原图传给 VL)成本高,生产少用。
语义断裂 → 调块大小/重叠;表格切碎 → 整表;标题丢失 → contextual headers;代码切碎 → 按函数切。(详见第 3 章)
| 问题 | 解法 |
|---|---|
| 维度不匹配(静默失败/报错) | 对齐 dimensions(第 4 章) |
| Embedding 不匹配领域(中文/术语召回差) | 换中文强/领域 Embedding |
| 索引类型选错(慢/召回低) | HNSW 默认,大规模/省内存用 IVFFLAT |
| 数据漂移(知识更新库没跟上) | 建立「先删后加 + 定期清理」机制 |
检索不到 / 检索不准——最高频,根因对策见第 8.2 诊断表,此处不展开。
| 问题 | 解法 |
|---|---|
| 幻觉 | 空上下文拒答 + 提示词强制 + 降温度(第 7 章) |
| 答非所问 | 回到检索侧优化(第 8 章) |
| 上下文过长 | 控 topK、重排取前 N、压缩 |
| 引用错误 | metadata 带稳定 source |
| 问题 | 解法 |
|---|---|
| 成本失控 | 模型分级、缓存、压缩 context |
| 延迟高 | 控 topK、模型分级、异步化 |
| 并发撑不住 | 限流、连接池、降级 |
| 多租户串号 | 按租户/会话隔离,身份服务端注入 |
| 数据安全 | 权限过滤、输入隔离、内容安全 |
完整版见附录 C。
需要四样:JDK 21+、Maven 3.6+、PostgreSQL(pgvector)、模型(本地 Ollama 或云端 API Key)。
① PostgreSQL + pgvector(Docker 一键)
docker run -d --name pgvector
-e POSTGRES_PASSWORD=postgres
-p 5432:5432
pgvector/pgvector:pg16
# 启用向量扩展
docker exec -it pgvector psql -U postgres -c "CREATE EXTENSION vector;"
pgvector 是「PostgreSQL + vector 扩展」,不装扩展就没有向量能力。
② 本地 Ollama(免 Key,先跑通)
ollama pull qwen2.5:1.5b # 聊天模型(约 1GB;更小试 0.5b,更好用 3b)
ollama pull nomic-embed-text # Embedding 模型(768 维)
③ 云端百炼(可选):创建 API Key,设环境变量 OPENAI_API_KEY。
<properties>
<spring-boot.version>3.5.0</spring-boot.version>
<spring-ai.version>1.1.2</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 本地模型 Ollama(免 Key) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<!-- 云端模型 OpenAI 兼容端点(阿里百炼 Qwen) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- 向量库 pgvector -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 文档解析(底层 Apache Tika) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
<!-- RAG 模块(查询改写 / 模块化 Advisor) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-rag</artifactId>
</dependency>
<!-- 朴素 RAG 的 QuestionAnswerAdvisor -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
spring:
ai:
# 同时引入 Ollama 和 OpenAI 时,用这个开关决定激活哪个
model:
ch@t: ollama # 改 openai 即切云端
embedding: ollama
ollama:
base-url: http://localhost:11434
ch@t:
options:
model: qwen2.5:1.5b
embedding:
options:
model: nomic-embed-text # 768 维
openai:
api-key: ${OPENAI_API_KEY}
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
ch@t:
options:
model: qwen-max
embedding:
options:
model: qwen3.7-text-embedding # 1024 维(可调 256~2560)
dimensions: 1024
vectorstore:
pgvector:
initialize-schema: true
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 768 # 与激活的 Embedding 模型维度一致
datasource:
url: jdbc:postgresql://localhost:5432/postgres
username: postgres
password: postgres
⚠️
dimensions必须和 Embedding 模型维度一致:用nomic-embed-text填 768,用qwen3.7-text-embedding填 1024(上面激活 Ollama,所以是 768)。
一个可跑通的分层项目:把「离线入库」和「在线问答」拆成两个 Service,支持 pgvector 持久化 + 模块化 RAG。可直接 copy,配好环境就能跑。
rag-demo/
├── pom.xml # 依赖(见附录 A)
├── data/
│ └── return-policy.md # 示例知识文档
└── src/main/
├── java/com/example/rag/
│ ├── RagApplication.java # 启动类 + CommandLineRunner 演示
│ ├── IngestService.java # 离线入库
│ └── QueryService.java # 在线问答
└── resources/
└── application.yml # 配置(见附录 A)
# 退货正策
## 无理由退货条件
7 天无理由退货:签收后 7 天内、未使用可申请,审核通过后 3~5 天退款。
## 退货流程
进入「我的订单」→ 选择订单 → 申请退货 → 填写原因 → 等待审核 → 寄回商品 → 确认收货 → 退款到账。
package com.example.rag;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class RagApplication {
public static void main(String[] args) {
SpringApplication.run(RagApplication.class, args);
}
/** 演示入口:入库一条正策 → 提一个问题 → 打印回答 */
@Bean
CommandLineRunner demo(IngestService ingestService, QueryService queryService) {
return args -> {
ingestService.ingest("data/return-policy.md");
String answer = queryService.ask("这东西咋退啊?");
System.out.println("回答:" + answer);
};
}
}
package com.example.rag;
import java.util.List;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
@Service
public class IngestService {
private final VectorStore vectorStore;
public IngestService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
/** 离线入库:读 → 切 → 存(写入时自动向量化) */
public void ingest(String filePath) {
List<Document> docs = new TikaDocumentReader("file:" + filePath).get();
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(500)
.withMinChunkSizeChars(200)
.withPunctuationMarks(List.of('。', '?', '!', ';')) // 中文标点
.build();
vectorStore.add(splitter.apply(docs));
}
}
package com.example.rag;
import [email protected];
import [email protected];
import [email protected];
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
@Service
public class QueryService {
private final ChatClient ch@tClient;
public QueryService(ChatModel ch@tModel, VectorStore vectorStore) {
// 朴素 RAG:黑盒、代码少,先跑通
QuestionAnswerAdvisor qa = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5)
.similarityThreshold(0.45)
.build())
.build();
this.ch@tClient = ChatClient.builder(ch@tModel)
.defaultAdvisors(qa)
.build();
}
/** 在线问答:召回 + 生成 */
public String ask(String question) {
return [email protected]()
.user(question)
.call()
.content();
}
}
# 1. 装环境(附录 A):Ollama + pgvector
# 2. 启动
mvn spring-boot:run
# 3. 预期输出
回答:根据退货正策,签收后 7 天内、未使用可申请退货,审核通过后 3~5 天退款。
| 症状 | 阶段 | 根因 | 解决 | 章节 |
|---|---|---|---|---|
| 查不到任何文档 | 召回 | 阈值过高 | 降阈值、打印 score | 5/8 |
| 查到一堆无关 | 召回 | 阈值过低/未过滤 | 升阈值、过滤、重排 | 5/6/8 |
| 编号/型号查不准 | 召回 | 纯向量不抓精确词 | 混合检索 | 5 |
| 口语化不命中 | 召回 | query 与文档用词不一致 | 查询改写 | 5 |
| 答案缺上下文 | 分片 | 块太小/断裂 | 父子分块、调块大小 | 3 |
| 表格答错 | 分片 | 表格被切碎 | 整表切 | 3 |
| 图片信息丢 | 解析 | 解析器只取文字 | 图片转文字摘要 | 2 |
| 检索报错/静默失败 | 索引 | 维度不匹配 | 对齐 dimensions | 4 |
| 中文召回差 | 索引 | Embedding 不匹配 | 换中文强 Embedding | 4 |
| 模型瞎编 | 生成 | 空上下文硬答 | 拒答+提示词+降温度 | 7 |
| 成本暴涨 | 工程化 | context 过长/模型贵 | 压缩、分级、缓存 | 9 |
| 串号 | 工程化 | 隔离键写死 | 按租户/会话隔离 | 9 |
| 词 | 一句话 |
|---|---|
| RAG | 检索增强生成:先查资料再照着答 |
| Embedding | 把文本变成向量,语义近的向量距离近 |
| Embedding 模型 | 负责文本 → 向量的模型,决定 RAG 上限 |
| 向量库 | 存「向量 + 原文 + 元数据」,向量只用来检索 |
| Chunk(分片) | 文档切的小块,检索基本单位 |
| Chunk size / overlap | 块大小 / 相邻块重叠,分片两参数 |
| 召回(Retrieval) | 从库里捞出最相关 topK 块 |
| Recall / Precision | 召回率 / 准确率,检索评测指标 |
| Hit Rate / MRR / nDCG | 命中率 / 首个相关排名倒数均值 / 排序质量 |
| topK | 每次召回取前 K 条 |
| similarityThreshold | 相似度阈值,低于则丢弃 |
| Rerank(重排) | 用 reranker 对候选重新打分排序 |
| Reranker | 交叉编码器模型,比 embedding 准但慢 |
| BM25 | 关键词相关性算法,擅长精确词匹配 |
| RRF | 倒数排名融合,合并多路检索结果 |
| HyDE | 先让 LLM 生成「假设答案」再拿去检索 |
| 查询改写 | LLM 把口语改写成书面检索式 |
| Context(上下文) | 拼进提示词的那段资料 |
| 幻觉 | 模型没依据地瞎编 |
| 溯源(citation) | 回答时指出依据哪份文档 |
| HNSW / IVFFLAT | 向量索引类型:分层图 / 倒排+平坦 |
| 余弦距离 | 衡量向量方向是否一致,文本语义默认 |
| pgvector | PostgreSQL 的向量扩展插件 |
| GraphRAG | 知识建图(实体+关系)再检索,解多跳推理 |
| Agentic RAG | Agent 自主决定何时检索、检索什么 |
| 多模态 RAG | 图片/表格也参与检索 |
| RAGAS | RAG 评测框架(Faithfulness / Relevancy 等) |
| Naive RAG | 朴素 RAG:检索 + 生成直连,黑盒 |
| Advanced RAG | 预检索/检索/后检索三段优化 |
| Modular RAG | 环节拆成可插拔模块,自由编排 |
如何写好 CLAUDE.md:一份精简实用指南
跃迁至 Ubuntu 20.04 LTS 发行版 Ubuntu Touch OTA1 Focal首批适配机型曝光
Ubuntu 22.04.2 LTS 维护版本更新发布 升至 Linux 5.19
Linux5.19内核大提升! Ubuntu 22.04 LTS 现可升级至 Linux Kernel 5.19
从零搭建RAG知识库问答系统
Aider 自定义 API 配置指南:终端配对编程接入与排错