第 10 章 · Embedding、VectorStore 与 RAG

版本:Spring AI 2.0.1

目标:走通「文档进库 → 检索增强生成」,分清经典 QA Advisor 与 Modular RAG,写出可运行的最小 ingest / 问答代码。

大模型训练语料里没有你的私有手册与工单。RAG 把「能检索的私有片段」在回答前塞进 Prompt,让模型基于材料说话,而不是凭空编。

整条链可以拆成两段:

  1. 离线 ingest:文档 → 切分 → Embedding → 写入向量库

  2. 在线问答:问题 → Embedding → 相似检索 → 片段进入 Prompt → Chat

Embedding 负责语义距离,VectorStore 负责存与搜,Advisor 负责把搜到的内容接进 ChatClient


10.1 文档进库:ETL 三件套

java 复制代码
DocumentReader → DocumentTransformer(s) → DocumentWriter / VectorStore

Document 带文本、id、metadata;检索命中后还可能带 score。元数据会进入过滤条件,入库前就要定好 tenantIdsourceversion 等键。

输入类型 常见 Reader
Markdown MarkdownDocumentReader
HTML / 网页 JsoupDocumentReader
PDF PagePdfDocumentReader / ParagraphPdfDocumentReader
Office 杂糅 TikaDocumentReader
JSON / 纯文本 JsonReader / TextReader

不要把用户任意 URL 直接丢进 Reader(SSRF、本地文件读取风险)。上传先落到受控存储,再由服务端选 Reader。

最小 ingest(Markdown → 切分 → 写入):

java 复制代码
Resource resource = new ClassPathResource("docs/handbook.md");
​
List<Document> docs = new MarkdownDocumentReader(
        resource,
        MarkdownDocumentReaderConfig.builder()
                .withAdditionalMetadata("tenantId", "acme")
                .withAdditionalMetadata("source", "handbook")
                .build()
).get();
​
TokenTextSplitter splitter = TokenTextSplitter.builder()
        .withChunkSize(800)
        .withMinChunkSizeChars(200)
        .build();
​
List<Document> chunks = splitter.apply(docs);
vectorStore.add(chunks);

切分优先用 TokenTextSplitter.builder();中文语料注意把 。?! 等标点纳入切分规则(按所用 splitter 的配置项)。父文档 metadata 会复制到 chunk。

更新语料时先按条件删旧再写入,避免过期片段残留:

java 复制代码
vectorStore.delete(
        FilterExpressionBuilder.builder()
                .eq("source", "handbook")
                .build()
);
vectorStore.add(chunks);

各 VectorStore 实现的 delete / filter API 略有差异,语义都是「按条件清旧再灌新」。


10.2 EmbeddingModel

java 复制代码
float[] embed(String) / embed(Document)
EmbeddingResponse call(EmbeddingRequest)
int dimensions()

常见来源:OpenAI、Ollama、Mistral、Google、Vertex、Bedrock、PostgresML、Transformers 等。OpenAI 配置示例:

java 复制代码
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      embedding:
        options:
          model: text-embedding-3-small

约束:

  • 维度必须与向量索引一致;换 Embedding 模型通常等于重建索引

  • 入库与查询必须用同一套 Embedding,不要「入库用 A、查询用 B」

  • Chat 与 Embedding 可以来自不同供应商,但同一语料只能共用一套向量空间

  • 大批量 ingest 走批处理,并观察限流与重试

手工调用(调试 / 预热):

java 复制代码
EmbeddingResponse response = embeddingModel.call(
        new EmbeddingRequest(List.of("订单退款规则是什么?"), null));
float[] vector = response.getResult().getOutput();
int dim = embeddingModel.dimensions();

10.3 VectorStore

VectorStore 同时是 Writer 与 Retriever:

java 复制代码
add / delete / similaritySearch(SearchRequest)
java 复制代码
List<Document> hits = vectorStore.similaritySearch(
        SearchRequest.builder()
                .query("如何申请退款?")
                .topK(5)
                .similarityThreshold(0.75)
                .filterExpression("tenantId == 'acme'")
                .build());

SearchRequest 常见字段:query、topK、similarityThreshold、filterExpression。

进程内可用 SimpleVectorStore 做演示与单测。生产按已有基础设施选:

基础设施 倾向
已有 PostgreSQL PgVector
已有 Redis Redis VectorStore
本地实验 Chroma / Qdrant / SimpleVectorStore
托管省运维 Pinecone / 云厂商向量能力
图 + 向量同库 Neo4j

PgVector 依赖印象:

xml 复制代码
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>

个别实现可能没有 dedicated starter,需直接依赖实现模块。语义缓存用的向量库不是业务知识库,不要混成同一个 Store。

多租户硬规则:

xml 复制代码
写入:服务端注入 tenantId,不信客户端
检索:filter 必须带同一 tenantId
删除:按租户 + 业务键,禁止无条件全表扫

filter 字符串若拼接了用户输入,字段与取值都要白名单,防止过滤条件被篡改。


10.4 两条 RAG 装配路径

QuestionAnswerAdvisor(经典短路径)

检索 → 注入 Prompt → 对话。适合 FAQ、手册问答。

java 复制代码
@Bean
ChatClient ragChatClient(ChatModel chatModel, VectorStore vectorStore) {
    return ChatClient.builder(chatModel)
            .defaultAdvisors(
                    QuestionAnswerAdvisor.builder(vectorStore)
                            .searchRequest(SearchRequest.builder()
                                    .topK(5)
                                    .similarityThreshold(0.7)
                                    .build())
                            .build())
            .build();
}
​
String answer = ragChatClient.prompt()
        .user("退货几日内可申请?")
        .advisors(a -> a.param(
                QuestionAnswerAdvisor.FILTER_EXPRESSION,
                "tenantId == 'acme'"))
        .call()
        .content();

FILTER_EXPRESSION 的常量名以依赖版本为准;要点是按请求注入租户过滤,不要把租户写死在全局默认里却忘了多租户场景。

RetrievalAugmentationAdvisor(Modular RAG)

把查询改写、多路扩展、检索、拼接、后处理拆成可插拔积木,适合要精细控制流水线的场景。

java 复制代码
Advisor modularRag = RetrievalAugmentationAdvisor.builder()
        .documentRetriever(VectorStoreDocumentRetriever.builder()
                .vectorStore(vectorStore)
                .topK(5)
                .similarityThreshold(0.7)
                .build())
        // 可选:QueryRewriter / QueryExpander / DocumentPostProcessor ...
        .build();
​
ChatClient client = ChatClient.builder(chatModel)
        .defaultAdvisors(modularRag)
        .build();
QA Advisor Modular RAG
复杂度 中高
可插拔改写 / 多路检索
适合 FAQ、手册 多集合、压缩上下文、可控流水线

流式接口通常是先检索再 stream :首 token 前会有一小段静默,可用 status=retrieving 一类事件提示前端。

另有 VectorStoreChatMemoryAdvisor:用向量库做长期对话记忆,和本章业务知识库 RAG 不是同一条产品线------一个记「这个用户说过什么」,一个记「公司手册写了什么」。


10.5 完整最小问答骨架

java 复制代码
@Service
public class HandbookQaService {
​
    private final ChatClient chatClient;
​
    public HandbookQaService(ChatModel chatModel, VectorStore vectorStore) {
        this.chatClient = ChatClient.builder(chatModel)
                .defaultAdvisors(
                        QuestionAnswerAdvisor.builder(vectorStore)
                                .searchRequest(SearchRequest.builder().topK(5).build())
                                .build())
                .build();
    }
​
    public String ask(String tenantId, String question) {
        return chatClient.prompt()
                .user(question)
                .advisors(a -> a.param(
                        QuestionAnswerAdvisor.FILTER_EXPRESSION,
                        "tenantId == '" + sanitize(tenantId) + "'"))
                .call()
                .content();
    }
​
    private String sanitize(String tenantId) {
        return tenantId.replaceAll("[^a-zA-Z0-9_-]", "");
    }
}

sanitize 只是示范:禁止把原始用户输入直接拼进 filter。生产环境用枚举租户、预编译表达式或参数化 filter API。


10.6 质量与运维

  • 空检索率、过期 chunk、错误的 tenant filter,往往比「换更大模型」更影响体感

  • 换 Embedding 维度要有重建索引的 runbook

  • 打开 VectorStore observation,才能分清慢在检索还是慢在 Chat

  • 语义缓存可省钱,错误答案也要有 TTL / 失效手段

  • 对比实验:同一问题开关 RAG,记录是否引用到文档 id


10.7 小结

RAG 主链是 ingest → Embedding → VectorStore → Advisor 注入 → Chat。先用 QuestionAnswerAdvisor 跑通带租户过滤的手册问答;流水线要改写、多路检索或压缩上下文时,再上 RetrievalAugmentationAdvisor。向量维度一致、过滤条件可信、语料可更新,这三件事比纠结 Advisor 类名更决定能不能上线。

相关推荐
jsl_jsl_jsl1 小时前
《一个 Agent 平台怎么接入多家大模型:Provider 槽位制设计》
人工智能
ι:1 小时前
Codex 自主调用 Visio 绘图完整教程
人工智能·visio·codex
梦帮科技1 小时前
一次性会员支付系统的可靠性设计:幂等、金额校验、Webhook 与链上确认
人工智能·神经网络·mysql·区块链·建造者模式·合成复用原则·加密货币
AgentMaster1 小时前
智能客服系统技术选型实战:从架构设计到落地实施的完整指南
大数据·人工智能
lailai04101 小时前
图像处理的技术路径与实现方式考察
人工智能
回眸&啤酒鸭1 小时前
【回眸】GPT-5.6 Luna 批量处理实战指南
人工智能
Java内核笔记1 小时前
Spring Boot 4 SSRF 防护源码剖析:InetAddressFilter 挡住内网地址与云元数据
java·后端
suaizai_1 小时前
多智能体架构揭秘:从混乱到高效
人工智能
一切皆是因缘际会1 小时前
物质计算机:计算即物理,安全即拓扑
人工智能·ai·计算机架构·计算机系统架构