ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring AI 中的 RAG 实现方式:手写 similaritySearch vs 官方QuestionAnswerAdvisor

Spring AI 中的 RAG 实现方式:手写 similaritySearch vs 官方QuestionAnswerAdvisor

Spring AI 中的 RAG 检索原理:Embedding 模型调用流程与检索实现

在 Spring AI 中,RAG(检索增强生成)的核心流程无非是两步:① 检索(Retrieval,查出相关文档)+② 生成(Generation,让大模型阅读文档并回答)。

如果你写了如下的similaritySearch方法:

publicList<Document>similaritySearch(Stringquery){returnvectorStore.similaritySearch(SearchRequest.builder().query(query).topK(5).similarityThreshold(0.7f).build());}

这就意味着:你通过这个方法自己实现了 RAG 中的“检索”能力。如果你再写代码把这个方法查出来的文档拼到 Prompt 里发给大模型,你实际上就是“自己实现了一套简易版的 QuestionAnswerAdvisor”。


💡 核心澄清:基础语义检索 vs. 把问题转化成多种问法

在讨论如何实现检索前,我们需要区分 RAG 中的两种不同层次的语义优化:

  1. 向量层面的语义理解(通过similaritySearch或QuestionAnswerAdvisor实现):
    因为哪怕不改写问题,直接把用户的原话通过 Embedding 向量模型变成向量去搜,向量模型本身就能听懂同义词(比如“报销”和“补贴”在向量空间里本来就挨得很近)。这属于向量层面的语义理解。
    你写的similaritySearch方法正是利用了这种能力。

  2. 文本层面的同义扩写(把问题转化成多种问法 / Multi-Query Expansion):
    如果用户的问题非常口语化、含糊不清、或者包含多个子问题,单靠把原话转成一次向量可能不够稳妥。这时才需要在检索前让大模型先把 1 个问题“改写/裂变成 3 种不同角度的问法”(需要使用更高级的RetrievalAugmentationAdvisor配合MultiQueryExpander),然后分别去向量库检索并合并去重。


一、 核心底层:调用 Embedding 模型的详细流程与代码

无论是你手动调similaritySearch,还是通过QuestionAnswerAdvisor间接去查,只要触发了向量检索,底层必定会执行以下调用 Embedding 模型的流程:

[ 用户输入的字符串 Query ] │ ▼ (1. 注入并调用 EmbeddingModel) [ 调用 embeddingModel.embed(query) ] │ ▼ (2. 模型计算并输出数学特征) [ 高维浮点数数组向量 (float[] 向量) ] │ ▼ (3. 携带 topK 和阈值参数) [ 访问向量数据库进行余弦相似度匹配并返回结果 ]

简明代码示例:

在 Spring AI 中,你可以直接注入EmbeddingModel显式调用,也可以通过VectorStore隐式触发:

@AutowiredprivateEmbeddingModelembeddingModel;publicvoidtestEmbedding(Stringquery){// 1. 显式调用:将文本转为高维浮点数向量float[]vector=embeddingModel.embed(query).getVector();// 2. 向量库检索(内部会自动执行上述的 embed 流程)List<Document>docs=vectorStore.similaritySearch(SearchRequest.builder().query(query).build());}

二、 对比:你是怎么处理similaritySearch的?

方式 A:【自己写方法】手动串联(类似于自己实现 Advisor)

如果在业务代码中是这样调用的:

@AutowiredprivateVectorStorevectorStore;@AutowiredprivateChatClientchatClient;publicStringmanualRag(StringuserQuery){// 1. 调用你自己写的语义检索方法(内部会自动触发上述的 Embedding 模型调用流程)List<Document>docs=this.similaritySearch(userQuery);// 2. 手动把查出来的文档拼成字符串Stringcontext=docs.stream().map(Document::getText).collect(Collectors.joining("\n---\n"));// 3. 手动塞给大模型生成回答returnchatClient.prompt().system("请根据以下参考资料回答:"+context).user(userQuery).call().content();}
  • 特点:逻辑完全由你掌控,每一步干了什么一清二楚。

方式 B:【使用 Spring AI 官方的】QuestionAnswerAdvisor(自动帮你调用similaritySearch)

如果不希望每次都手动写拼字符串的代码,可以直接用官方提供的QuestionAnswerAdvisor。它在底层自动帮你封装了调用similaritySearch的过程:

@OverridepublicvoidafterPropertiesSet()throwsException{// 官方的 QuestionAnswerAdvisor 内部会拿着配置的 SearchRequest 去自动执行类似你写的那套向量检索QuestionAnswerAdvisorquestionAnswerAdvisor=QuestionAnswerAdvisor.builder(vectorStore).searchRequest(SearchRequest.builder().similarityThreshold(0.7f).topK(5).build()).build();this.chatClient=ChatClient.builder(chatModel).defaultAdvisors(questionAnswerAdvisor)// 挂载后,发消息时自动触发检索.build();}
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进