十二.RAG检索增强生成
12.1 基础概念
那么我们可以通过一个案例来告诉大家什么是RAG。假设我们现在有一个需求,就是AI智能运维助手,通过提供的错误编码,给出异常解释来辅助运维人员更好的定位问题和维护系统。
比如我们现在提供的错误代码中,00000代表success,A0001代表网络故障,A0002代表系统限流。但是传统的LLM是有缺陷的,比如LLM的知识不是实时的,不具备知识更新;LLM可能不知道你私有的领域/业务知识;LLM有时会在回答中生成看似合理但实际上是错误的信息。
LLM的知识仅限于它所接受的训练数据。如果你想让一个LLM了解特定领域的知识或专有数据,降低大模型出现幻觉的概率,你可以使用RAG;
简单来说,RAG(检索增强生成)是一种从你的数据中查找相关信息,并在将提示发送给LLM之前将其注入到提示中的方法。这样一来,LLM就能获得(希望是)相关的信息,并基于这些信息进行回答,从而降低产生幻觉的概率。
RAG技术就像给AI大模型装上了「实时百科大脑」,为了让大模型获取足够的上下文,以便获得更加广泛的信息源,通过先查资料后回答的机制,让AI摆脱传统模型的"知识遗忘和幻觉回复"困境。
类似考试时有不懂的,给你准备了小抄,对大模型知识盲区的一种补充
12.2 流程
RAG流程分为两个不同的阶段:索引(Index)和检索(Retrieval)
索引(Index)
索引首先清理和提取各种格式的原始数据,如PDF,HTML等,然后将其转换为统一的纯文本格式。为了适应语言模型的上下文限制,文本被分割为更小的,可消化的块(chunk)。然后使用嵌入模型将块编码成向量表示,并存储在向量数据库中。这一步对于在随后的检索阶段实现高效的相似性搜索至关重要。知识库被分割成chunks,并将chunks向量化至向量库中。
检索(Retrieval)
检索阶段通常在线进行,此时用户提交的问题应使用检索文档来回答。
在收到用户查询之后,RAG系统采用与索引阶段相同的编码模型将查询转化为向量表示,然后计算索引语料库中查询向量和块向量的相似性得分。该系统优先级和检索最高k(Top-K)块,显示最大的相似性查询。
12.3 实战案例
需求: AI智能运维助手,通过提供的错误代码,给出异常解释辅助运维人员更好的定位问题和维护系统(SpringAl+阿里百炼嵌入模型text-embedding-v3+向量数据库RedisStack+DeepSeek来实现RAG功能)
步骤:
1.和以往一样,还是建立Module;和上一节向量一样加pom;yml也类似,不过改了模型变成了deepseek;然后就是主启动;原本的多模型modelclient和model的config类不变
2.接下来就是需要重视的业务类的编写了:
- 提供ops.txt脚本放到resources中,让他存入向量数据库RedisStack,形成文档知识库
版本1:
1. 新建一个InitVectorDatabaseConfig类
初始化向量数据库对应的数据。即将上述的ops.text中的数据以文本向量化的形式作为向量化数据存储入向量数据库中
package ai.study.config; import jakarta.annotation.PostConstruct; import org.springframework.ai.document.Document; import org.springframework.ai.reader.TextReader; import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import java.nio.charset.Charset; import java.util.List; @Configuration public class InitVectorDatabaseConfig { @Autowired private VectorStore vectorStore; @Value("classpath:ops.txt") private Resource sqlFile;// 读取的文件 @PostConstruct public void init() { // 1.读取文件 TextReader textReader = new TextReader(sqlFile); textReader.setCharset(Charset.defaultCharset());//文档最好要设置一个编码,默认是utf-8,防止乱码 // 2.文件转换成向量(开启分词) List<Document> list = new TokenTextSplitter().transform(textReader.read()); // 3.写入向量数据库(Redis),无法去重复版 vectorStore.add(list); }}2.此时建立一个controller
package ai.study.controller; import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor; import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; @RestController public class RagController { @Resource(name = "qwenChatClient") private ChatClient chatClient; @Resource private VectorStore vectorStore; /** * http://localhost:8012/rag4aiops?msg=00000 * http://localhost:8012/rag4aiops?msg=C2222 * @param msg * @return */ @GetMapping("/rag4aiops") public Flux<String> rag(String msg) { String systemInfo = """ 你是一个运维工程师,按照给出的编码给出对应故障解释,否则回复找不到信息。 """; //顾问增强器(属于chatmemory中的内容) RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder() .documentRetriever(//检索 VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .build() ) .build(); return chatClient.prompt() .system(systemInfo)//ai的身份 .user(msg)//用户输入 .advisors(advisor) // RAG功能,向量数据库查询 .stream() .content(); } }但是上述版本会出现一个问题,就是每次我重启一次微服务,就会重新加载一次,然后redisstack中就会新增一次数据;因此需要重复数据过滤清洗的操作
版本二:---->向量数据库去重问题解决
使用redis setnx去重
1.先加入RedisConfig
package ai.study.config; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @Slf4j public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactor) { RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactor); //设置key序列化方式string redisTemplate.setKeySerializer(new StringRedisSerializer()); //设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化 redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; } }2. 修改InitVectorDatabaseConfig:
@Configuration public class InitVectorDatabaseConfig { @Autowired private RedisTemplate<String,String> redisTemplate; ...... // 3.写入向量数据库(Redis),无法去重复版 //vectorStore.add(list); //4 去重复版本 String sourceMetadata = (String)textReader.getCustomMetadata().get("source"); String textHash = SecureUtil.md5(sourceMetadata); String redisKey = "vector-xxx:" + textHash; // 判断是否存入过,redisKey如果可以成功插入表示以前没有过,可以假如向量数据 Boolean retFlag = redisTemplate.opsForValue().setIfAbsent(redisKey, "1"); System.out.println("****retFlag : "+retFlag); if(Boolean.TRUE.equals(retFlag)) { //键不存在,首次插入,可以保存进向量数据库 vectorStore.add(list); }else { //键已存在,跳过或者报错 //throw new RuntimeException("---重复操作"); System.out.println("------向量初始化数据已经加载过,请不要重复操作"); }}十三.ToolCalling
一句话概率,toolcalling就是LLM的外部utils工具类。ToolCalling(也称为FunctionCalling)它允许大模型与一组API或工具进行交互,将LLM的智能与外部工具或API无缝连接,从而增强大模型其功能。
但是注意,LLM本身并不执行函数,它只是指示应该调用哪个函数以及如何调用。如果不使用toolCalling,则会出现:
所以我们知道tool calling可以实现:
- 访问实时数据
- 执行某种工具类/辅助类操作
工作流程可以大致参考下述:
接下来就是开发步骤:
1. 建子模块Module,改pom(dashscope就行),写yml,主启动和以往一样
接下来是业务类
2.新建Tool工具类,类似于Utils工具类
package ai.study.utils; import org.springframework.ai.tool.annotation.Tool; import java.time.LocalDateTime; public class DateTimeTools { /** * 1.定义 function call(tool call) * 2. returnDirect * true = tool直接返回不走大模型,直接给客户 * false = 拿到tool返回的结果,给大模型,最后由大模型回复 */ //必须加描述description,LLM 靠这个判断调用哪个 @Tool(description = "获取当前时间", returnDirect = false) public String getCurrentTime() { return LocalDateTime.now().toString(); } }
描述(Description)就是“咒语”: LLM 决定调不调用你的函数,完全取决于你写的
@Description。如果描述写得模糊(比如只写“获取信息”),模型可能就不知道什么时候该用它。
3.controller类
import ai.study.utils.DateTimeTools; import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.tool.ToolCallingChatOptions; import org.springframework.ai.support.ToolCallbacks; import org.springframework.ai.tool.ToolCallback; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class ToolCallingController { @Resource private ChatModel chatModel; @GetMapping("/toolcall/chat") public String chat(@RequestParam(name = "msg",defaultValue = "你是谁,现在几点了") String msg) { // 1.工具注册到工具集合里 //这是把你的 Java 对象“翻译”成大模型能理解的 JSON Schema 格式 ToolCallback[] tools = ToolCallbacks.from(new DateTimeTools()); // 2.将工具集配置进ChatOptions对象 ChatOptions options = ToolCallingChatOptions.builder().toolCallbacks(tools).build(); // 3.构建提示词 Prompt prompt = new Prompt(msg, options); // 4.调用大模型 return chatModel.call(prompt).getResult().getOutput().getText(); } }结果如下:
十四.MCP
之前的ToolCalling中每个大模型(如DeepSeek、ChatGPT)需要为每个工具单独开发接口(FunctionCalling),导致重复劳动。
而MCP就可以解决上述问题,它提供了一种标准化的方式来连接LLMs需要的上下文,MCP就类似于一个Agent时代的Type-C协议,希望能将不同来源的数据、工具、服务统一起来供大模型调用。更多概念详情,可查看:MCP简单介绍
Tool Calling:是“技能点”。适合简单的、项目内部的、特定的功能扩展。
MCP:是“生态圈”。它试图统一所有 AI 工具的接入标准。
14.1 本地MCP开发步骤
1. MCP-Server服务端的实现
仍然是建Module,然后就是改写pom
<dependencies> <!--注意事项(重要) spring-ai-starter-mcp-server-webflux不能和<artifactId>spring-boot-starter-web</artifactId>依赖并存, 否则会使用tomcat启动,而不是netty启动,从而导致mcpserver启动失败,但程序运行是正常的,mcp客户端连接不上。 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!--mcp-server-webflux--> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>写yml
# spring.ai.dashscope.api-key=${aliQwen-api} # ====mcp-server Config============= spring.ai.mcp.server.type=async spring.ai.mcp.server.name=customer-define-mcp-server spring.ai.mcp.server.version=1.0.0然后就是主启动,接下来就是业务类的编写
a. 天气预报WeatherService服务类
package ai.study.service; import org.springframework.ai.tool.annotation.Tool; import org.springframework.stereotype.Service; import java.util.Map; @Service public class WeatherService { @Tool(description = "根据城市名称获取天气预报") public String getWeatherByCity(String city) { Map<String, String> map = Map.of( "北京", "11111降雨频繁,其中今天和后天雨势较强,部分地区有暴雨并伴强对流天气,需注意", "上海", "22222多云,15℃~27℃,南风3级,当前温度27℃。", "深圳", "333333多云40天,阴16天,雨30天,晴3天" ); return map.getOrDefault(city, "抱歉:未查询到对应城市!"); } }b.ToolCallbackProvider接口配置类
package ai.study.config; import ai.study.service.WeatherService; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class McpServerConfig { /** * 将工具方法暴露给外部 mcp client 调用 * @param weatherService * @return */ @Bean public ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder() .toolObjects(weatherService) .build(); } }在之前的 Tool Calling 里,我们要手动注册工具;而这里的MethodToolCallbackProvider像是一个“雷达探测器”,它会自动扫描WeatherService中所有带@Tool注解的方法,并打包发布。外部 Client 只要一连接,就能立刻看到这些“技能清单”。
2. MCP-Client客户端的实现
客户端的职责是:发现服务端 --------握手连接--------托管给 LLM。
接下来还是新建子模块Module,然后就是改pom
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--spring-ai-alibaba dashscope--> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> </dependency> <!-- 2.mcp-clent 依赖 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency>然后就是写yml: 客户端需要知道 Server 在哪里
# ====SpringAIAlibaba Config============= spring.ai.dashscope.api-key=${aliQwen-api} # ====mcp-client Config============= spring.ai.mcp.client.type=async spring.ai.mcp.client.request-timeout=60s spring.ai.mcp.client.toolcallback.enabled=true # 指向你的 MCP Server 地址 spring.ai.mcp.client.sse.connections.mcp-server1.url=http://localhost:8014mcp-server1是给这个连接起的别名。你可以配置多个 URL,连接来自全世界的MCP Server
主启动编写完,就是业务类的编写:
1. LLMConfig并添加tool调用
package ai.study.config; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SaaLLMConfig { @Bean public ChatClient chatClient(ChatModel chatModel, ToolCallbackProvider tools) { return ChatClient.builder(chatModel) .defaultToolCallbacks(tools.getToolCallbacks()) //mcp协议,配置见yml文件 .build(); } }ToolCallbackProvider tools是从哪来的?这就是 MCP Starter 的“魔法”。当你开启了 MCP 客户端,Spring 会自动创建一个名为tools的 Bean,里面包含了从远程8014服务器抓取到的所有工具。defaultToolCallbacks: 你把这些抓到的工具直接塞进了ChatClient的默认配置里。这意味着,只要你用这个chatClient说话,它就天生自带了远程服务器上的所有技能。
2.controller
package ai.study.controller; import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; 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 reactor.core.publisher.Flux; @RestController public class McpClientController { @Resource private ChatClient chatClient;//使用mcp支持 @Resource private ChatModel chatModel;//没有纳入tool支持,普通调用 // http://localhost:8015/mcpclient/chat?msg=上海 @GetMapping("/mcpclient/chat") public Flux<String> chat(@RequestParam(name = "msg",defaultValue = "北京") String msg) { System.out.println("使用了mcp"); return chatClient.prompt(msg).stream().content(); } @RequestMapping("/mcpclient/chat2") public Flux<String> chat2(@RequestParam(name = "msg",defaultValue = "北京") String msg) { System.out.println("未使用mcp"); return chatModel.stream(msg); } }此时访问即为:
14.2 远程MCP增强案例-对接互联网通用MCP服务(百度地图)
如果我们想要本地当MCP Client,然后远程的百度地图来MCP Server,那么此时我们先要能到可以调用上万个通用MCP的地方:https://mcp.so/zh
原理说明:
那么在此,我们环境的配置选择的是NodeJS,接着我们应该注册百度地图账号+申请API-key
https://lbsyun.baidu.com/
接下来,我们加入编码开发:
- 建完module之后,修改pom
<!-- 1.大模型依赖 --> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> </dependency> <!-- 2.mcp-clent 依赖 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency>- 写yml
server.port=8016 # 设置全局编码格式 server.servlet.encoding.enabled=true server.servlet.encoding.force=true server.servlet.encoding.charset=UTF-8 spring.application.name=springAI-16chat-mcpclient-call-baidumcp # ====LLM Config============= spring.ai.dashscope.api-key=${aliQwen-api} # ====mcp-client Config============= spring.ai.mcp.client.request-timeout=20s spring.ai.mcp.client.toolcallback.enabled=true # spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-server.json5- 写mcp-server.json5,放在resources下:
{ "mcpServers": { "baidu-map": { "command": "cmd", "args": ["/c", "npx", "-y", "@baidumap/mcp-server-baidu-map"], "env": {"BAIDU_MAP_API_KEY": "你猜"} } } }注意此时用json5而不是json的一大原因在于json不能写注释
- 写主启动启动,此时可以看到后台mcp配置,然后写业务类
- 业务类中SaaLLMConfig,controller和上节一摸一样,此处就不再演示
此时查询:http://localhost:8016/mcpclient/chat?msg=查询北京39.9042归到位置
十五.SAA生态篇,后续不再更新
具体看上方资源