当前位置: 首页 > article >正文

还在用简单 AI 对话?Spring AI 自定义工具 + MCP 协议直接打通外部服务!

前言本文的示例基于上一篇博客Spring AI 对话记忆不丢失MySQL 主存 Redis 缓存实战免费模型调用附源码-CSDN博客的 已有项目继续开发 。如果你对项目结构、基础配置ChatClient、ChatMemory、双写策略等不清晰建议先回顾上一篇内容。由于本人水平有限文中内容若有疏漏、错误或优化空间欢迎各位读者批评指正。项目仓库地址https://gitcode.com/coderKJX/SpringAiChatMemoryDemoPro.git1、项目背景从「纯对话」到「能动手的 AI」在上一篇博客中我们搭建了一个具备多会话管理、MySQL Redis 双写持久化的 Spring AI 对话系统。但那时的 AI 只能 纯文本对话 ——用户问什么它答什么无法执行任何外部操作。本次迭代我们在原有项目基础上新增 两大核心能力 能力技术方案效果AI画图图片存储Spring AI自定义ToolToolMinIO对象存储用户输入指令如“画一只狗”AI自动生成图片并上传至MinIO返回访问链接。联网搜索MCP协议接入智谱Web Search Prime服务用户查询实时信息时AI自动调用搜索引擎获取最新结果并生成回答。最终效果你的 AI 从一个 只会聊天的聊天机器人 进化为一个 能画图、能搜索、能操作外部服务的智能 Agent 。2、Spring AI 自定义工具调用 —— 让 AI 学会「画图并存储」2.1 核心概念什么是 Spring AI 的 Tool根据 Spring AI Tools 官方文档工具调用 :: Spring AI 参考 - Spring 框架 工具调用Tool Calling 是 AI 应用中的核心模式 模型只能 请求 工具调用并提供输入参数而应用程序负责 执行 工具调用并返回结果。模型永远无法直接访问作为工具提供的任何 API——这是一项关键的安全考虑。2.2 项目核心依赖准备!-- OpenAI兼容图像模型用于Kolors免费文生图 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency !-- MinIO 对象存储客户端 -- dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.7/version /dependency2.3 核心配置spring: ai: # OpenAI 兼容配置此处对接硅基流动 API支持图片生成 openai: base-url: https://api.siliconflow.cn # 第三方大模型 API 地址 api-key: ${SILICONFLOW_API_KEY} # 模型密钥从环境变量读取 image: options: model: Kwai-Kolors/Kolors # 图片生成模型快手 Kolors # MinIO 对象存储配置 # 用于 Spring AI 自定义工具生成图片后自动上传存储 minio: endpoint: http://localhost:9000 # MinIO 服务地址 access-key: ${MINIO_ACCESS_KEY} # MinIO 访问密钥 secret-key: ${MINIO_SECRET_KEY} # MinIO 密钥 bucket-name: ai-images # 存储图片的桶名称AI 生成图片统一存放2.4minio注意事项关于minio的下载参考该博主的博客在Windows上MinIO的安装与使用保姆教程_minio安装windows-CSDN博客2.4.1MinioConfig.java — 创建 MinioClient Beanpackage com.cg.config; import io.minio.MinioClient; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration Slf4j public class MinioConfig { Getter Value(${minio.endpoint}) private String endpoint; Value(${minio.access-key}) private String accessKey; Value(${minio.secret-key}) private String secretKey; Getter Setter Value(${minio.bucket-name}) private String bucketName; Bean public MinioClient minioClient() { log.info(初始化 MinIO 客户端endpoint: {}, endpoint); return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .region(us-east-1) .build(); } }2.4.2MinioUtil.java — 封装上传、删除、URL 生成等核心操作package com.cg.utils; import com.cg.config.MinioConfig; import io.minio.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.io.InputStream; /** * MinIO 工具类 */ Component public class MinioUtil { private static final Logger log LoggerFactory.getLogger(MinioUtil.class); private final MinioClient minioClient; private final MinioConfig minioConfig; public MinioUtil(MinioClient minioClient, MinioConfig minioConfig) { this.minioClient minioClient; this.minioConfig minioConfig; } /** * 检查存储桶是否存在不存在则创建并设置为公开访问 */ public void ensureBucketExists() { try { String bucketName minioConfig.getBucketName(); boolean exists minioClient.bucketExists(BucketExistsArgs.builder() .bucket(bucketName) .build()); if (!exists) { minioClient.makeBucket(MakeBucketArgs.builder() .bucket(bucketName) .build()); log.info(✅ 创建 MinIO 存储桶: {}, bucketName); } // 无论存储桶是否存在都尝试设置为公开访问 setBucketPublicPolicy(bucketName); } catch (Exception e) { log.error(❌ 检查/创建存储桶失败, e); throw new RuntimeException(MinIO 存储桶初始化失败, e); } } /** * 设置存储桶为公开访问 * * param bucketName 存储桶名称 */ private void setBucketPublicPolicy(String bucketName) { try { // 设置公开访问策略 String policy String.format( { Version: 2012-10-17, Statement: [ { Effect: Allow, Principal: {AWS: [*]}, Action: [s3:GetObject], Resource: [arn:aws:s3:::%s/*] } ] } , bucketName); minioClient.setBucketPolicy( SetBucketPolicyArgs.builder() .bucket(bucketName) .config(policy) .build() ); log.info(✅ 设置存储桶为公开访问: {}, bucketName); } catch (Exception e) { log.warn(⚠️ 设置存储桶公开访问失败: {}, e.getMessage()); } } /** * 上传文件 * * param objectName 对象名称文件路径 * param inputStream 文件输入流 * param contentType 文件类型 * return 文件访问 URL */ public String uploadFile(String objectName, InputStream inputStream, String contentType) { return uploadFile(objectName, inputStream, -1, contentType); } public String uploadFile(String objectName, InputStream inputStream, long fileSize, String contentType) { try { ensureBucketExists(); String bucketName minioConfig.getBucketName(); long objectSize fileSize 0 ? fileSize : -1; minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(inputStream, objectSize, 10485760) .contentType(contentType) .build() ); log.info(✅ 文件上传成功: {}, 大小: {}字节, objectName, objectSize 0 ? objectSize : 未知); return getFileUrl(objectName); } catch (Exception e) { log.error(❌ 文件上传失败: {}, objectName, e); throw new RuntimeException(文件上传失败, e); } } /** * 获取文件访问 URL公开访问 * * param objectName 对象名称 * return 公开访问 URL */ public String getFileUrl(String objectName) { // 返回公开访问 URL无需签名 String endpoint minioConfig.getEndpoint(); String bucketName minioConfig.getBucketName(); return String.format(%s/%s/%s, endpoint, bucketName, objectName); } /** * 删除文件 * * param objectName 对象名称 */ public void deleteFile(String objectName) { try { String bucketName minioConfig.getBucketName(); minioClient.removeObject( RemoveObjectArgs.builder() .bucket(bucketName) .object(objectName) .build() ); log.info(✅ 文件删除成功: {}, objectName); } catch (Exception e) { log.warn(⚠️ 文件删除失败可能已不存在: {}, error{}, objectName, e.getMessage()); } } }注意 我们使用 公开桶策略 而非预签名 URL。原因是预签名 URL 有时效性限制且在前端渲染场景下每次请求都需要重新签名。公开桶配合 s3:GetObject 策略可以让前端直接 img src... 显示图片。2.5定义自定义工具类这是整个功能的核心使用 Spring AI 的 Tool 注解将普通 Java 方法声明为 AI 可调用的工具。AiTools.java — AI 工具集package com.cg.tools; import com.cg.context.ChatContextHolder; import com.cg.entity.AiGeneratedImage; import com.cg.service.ImageStorageService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; import org.springframework.ai.openai.OpenAiImageModel; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.ai.zhipuai.ZhiPuAiImageModel; import org.springframework.stereotype.Component; /** * AI 工具集 * 统一管理所有 通过AI实现的工具方法 */ Slf4j Component RequiredArgsConstructor public class AiTools { private final OpenAiImageModel imageModel; // 智谱AI 图像模型 private final ImageStorageService imageStorageService; // 复用现有服务 /** * 图像生成存储工具 * 复用 ImageStorageService 完成存储逻辑 * * param prompt 图像描述 * return 生成的图像信息 */ Tool(description 图像生成与存储工具。当用户要求画图、生成图片、创建图像时调用。 会自动将生成的图片存储到 MinIO 并关联当前对话。 参数 prompt 是对图像的详细描述。 返回值包含图片的存储地址必须在回复中告知用户这个地址。) public String generateAndStoreImage( ToolParam(description 图像的详细描述包含主体、场景、风格等要素) String prompt) { log.info( AI 调用图像生成存储工具, 描述{}, prompt); try { // Step 1: 调用智谱AI生成图片 ImageResponse response imageModel.call(new ImagePrompt(prompt)); String originalUrl response.getResult().getOutput().getUrl(); log.info(✅ AI图片生成成功, 原始URL: {}, originalUrl); // Step 2: 调用现有服务完成存储下载→上传MinIO→保存数据库 AiGeneratedImage imageRecord imageStorageService.storeImage(prompt, originalUrl); // Step 3: 设置当前会话的图片ID ChatContextHolder.setImageId(imageRecord.getId()); return 图片已生成并保存\n - 描述 prompt \n - 图片地址 imageRecord.getMinioUrl(); } catch (Exception e) { log.error(❌ 图像生成存储失败: {}, e.getMessage(), e); return 图片生成失败: e.getMessage(); } } }核心注解解析 ①Tool 标记此方法为 AI 可调用的工具。 description 字段 极其重要 ——它是 AI 判断何时调用该工具的唯一依据。写得越清晰准确AI 的调用就越精准。②ToolParam 描述参数的含义帮助 AI 正确构造调用参数。③Component 让 Spring 管理该类的生命周期后续可通过 .defaultTools() 注册到 ChatClient。2.5 .1图片存储服务实现工具方法本身只负责触发具体的存储逻辑封装在 Service 层保持职责清晰。ImageStorageServiceImpl.java — 核心存储逻辑数据实体 AiGeneratedImage 对应数据库表 ai_generated_image 字段包括 id 、 prompt 描述、 originalUrl 原始 URL、 minioUrl MinIO 地址、 fileName 、 fileSize 、 createdAt 。package com.cg.service.serviceImpl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.cg.entity.AiGeneratedImage; import com.cg.entity.CgChatMessage; import com.cg.mapper.AiGeneratedImageMapper; import com.cg.mapper.CgChatMessageMapper; import com.cg.service.ImageStorageService; import com.cg.utils.MinioUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.URI; import java.time.LocalDateTime; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; /** * 图片存储服务实现类 */ Slf4j Service RequiredArgsConstructor public class ImageStorageServiceImpl implements ImageStorageService { private final MinioUtil minioUtil; private final AiGeneratedImageMapper imageMapper; private final CgChatMessageMapper chatMessageMapper; Override public AiGeneratedImage storeImage( String prompt, String originalUrl) { try { // 1. 下载图片到内存同时获取文件大小 log.info( 开始下载图片: {}, originalUrl); InputStream rawStream URI.create(originalUrl).toURL().openStream(); ByteArrayOutputStream buffer new ByteArrayOutputStream(); rawStream.transferTo(buffer); byte[] imageBytes buffer.toByteArray(); long fileSize imageBytes.length; rawStream.close(); log.info( 图片下载完成大小: {} 字节 ({}KB), fileSize, fileSize / 1024); // 2. 生成文件名 String fileName UUID.randomUUID() .png; // 3. 上传到MinIO ByteArrayInputStream uploadStream new ByteArrayInputStream(imageBytes); String minioUrl minioUtil.uploadFile(fileName, uploadStream, fileSize, image/png); log.info(✅ 图片上传到MinIO成功: {}, minioUrl); // 4. 构建实体并保存到数据库 AiGeneratedImage image new AiGeneratedImage(); image.setPrompt(prompt); image.setOriginalUrl(originalUrl); image.setMinioUrl(minioUrl); image.setFileName(fileName); image.setFileSize(fileSize); image.setCreatedAt(LocalDateTime.now()); imageMapper.insert(image); log.info(✅ 图片记录已保存到数据库ID: {}, 大小: {}KB, image.getId(), fileSize / 1024); return image; } catch (Exception e) { log.error(❌ 存储图片失败: originalUrl{}, error{}, originalUrl, e.getMessage(), e); throw new RuntimeException(存储图片失败: e.getMessage(), e); } } Override public void deleteByImageIds(ListLong imageIds) { if (imageIds null || imageIds.isEmpty()) return; log.info(️ 根据ID列表删除图片数量: {}, imageIds.size()); for (Long imageId : imageIds) { try { // 1. 查询图片记录 AiGeneratedImage image imageMapper.selectById(imageId); if (image null || image.getFileName() null) continue; // 2. 从 MinIO 删除文件 try { minioUtil.deleteFile(image.getFileName()); log.info(✅ MinIO 文件已删除: {}, image.getFileName()); } catch (Exception e) { log.warn(⚠️ MinIO 文件删除失败可能已不存在: fileName{}, error{}, image.getFileName(), e.getMessage()); } // 3. 从数据库删除记录 imageMapper.deleteById(imageId); log.info(✅ 数据库图片记录已删除: ID{}, fileName{}, imageId, image.getFileName()); } catch (Exception e) { log.warn(⚠️ 删除图片失败: ID{}, error{}, imageId, e.getMessage(), e); } } log.info(✅ 批量删除图片完成); } Override public void deleteByConversationId(String conversationId) { log.info(️ 根据会话ID删除所有关联图片: conversationId{}, conversationId); try { // 1. 查询该会话下所有消息中的 imageId LambdaQueryWrapperCgChatMessage wrapper new LambdaQueryWrapper(); wrapper.eq(CgChatMessage::getConversationId, conversationId) .isNotNull(CgChatMessage::getImageId) .gt(CgChatMessage::getImageId, 0L); ListCgChatMessage messagesWithImages chatMessageMapper.selectList(wrapper); if (messagesWithImages.isEmpty()) { log.info( 该会话没有关联图片: conversationId{}, conversationId); return; } // 2. 收集所有不重复的 imageId SetLong imageIds messagesWithImages.stream() .map(CgChatMessage::getImageId) .filter(id - id ! null id 0) .collect(Collectors.toSet()); log.info( 发现 {} 张图片需要删除, imageIds.size()); // 3. 调用批量删除方法 deleteByImageIds(imageIds.stream().toList()); } catch (Exception e) { log.error(❌ 删除会话图片失败: conversationId{}, error{}, conversationId, e.getMessage(), e); } } }2.6 将工具注册到 ChatClientAiConfig.java — ChatClient 配置package com.cg.config; import com.cg.repository.DualWriteChatMemoryRepository; import com.cg.tools.AiTools; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.prompt.SystemPromptTemplate; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; import reactor.netty.http.client.HttpClient; import java.time.Duration; import java.util.Map; Configuration public class AiConfig { //从classpath资源文件注入系统提示词 Value(classpath:/prompts/system-prompt.st) private Resource systemPromptResource; Value(${spring.ai.zhipuai.base-url}) private String baseUrl; Value(${spring.ai.zhipuai.api-key}) private String apiKey; /** * 自定义智谱AI API配置设置2分钟超时 */ Bean public ZhiPuAiApi zhiPuAiApi() { // 配置 Netty HTTP 客户端超时 HttpClient httpClient HttpClient.create() .responseTimeout(Duration.ofMinutes(2)) // 响应超时2分钟 .doOnConnected(conn - conn .addHandlerLast(new io.netty.handler.timeout.ReadTimeoutHandler(120)) // 读取超时120秒 .addHandlerLast(new io.netty.handler.timeout.WriteTimeoutHandler(120)) // 写入超时120秒 ); // 创建 WebClient.Builder 并注入自定义的 HttpClient WebClient.Builder webClientBuilder WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)); // 使用 Builder 模式创建 ZhiPuAiApi return ZhiPuAiApi.builder() .apiKey(apiKey) .baseUrl(baseUrl) .webClientBuilder(webClientBuilder) .build(); } /* // 默认使用内存存储 Bean public ChatMemory chatMemory() { return MessageWindowChatMemory.builder().build(); } */ /** * 自定义智谱AI聊天模型 */ Bean public ZhiPuAiChatModel zhiPuAiChatModel(ZhiPuAiApi zhiPuAiApi) { return new ZhiPuAiChatModel(zhiPuAiApi); } /** * 使用双写模式存储会话记忆 * MySQL 作为主存储同步写入 * Redis 作为缓存层异步写入6小时过期 */ Bean public ChatMemory chatMemory(DualWriteChatMemoryRepository dualWriteChatMemoryRepository) { return MessageWindowChatMemory.builder() .chatMemoryRepository(dualWriteChatMemoryRepository) .maxMessages(20)// 保留最近20条消息 .build(); } /** * 配置ChatClient */ Bean public ChatClient chatClient(Qualifier(zhiPuAiChatModel)ChatModel chatModel, ChatMemory chatMemory, AiTools aiTools, ToolCallbackProvider toolCallbackProvider) { // 渲染系统提示词模板注入AI角色名称等固定变量 SystemPromptTemplate template new SystemPromptTemplate(systemPromptResource); Message systemMessage template.createMessage(Map.of(botName, 大肘子)); return ChatClient.builder(chatModel) .defaultSystem(systemMessage.getText())//ChatClient 直接支持 Resource等 .defaultAdvisors( new SimpleLoggerAdvisor(), MessageChatMemoryAdvisor.builder(chatMemory).build()) .defaultTools(aiTools)//添加工具 .defaultToolCallbacks(toolCallbackProvider)//添加mcp .build(); } }注意事项 ①defaultTools(aiTools) 接收的是 带 Tool 注解的组件对象 Spring AI 会自动扫描其中所有标注了 Tool 的方法并注册。②如果你有多个工具类可以用 .defaultTools(toolA, toolB, toolC) 或传入 List/Object[]。③ToolCallbackProvider 是 Spring AI MCP Client 自动提供的 Bean用于聚合所有 MCP 连接的工具回调下一节详解2.7 系统提示词引导 AI 使用工具光注册了工具还不够——需要在系统提示词中 明确告诉 AI 什么时候该用什么工具 system-prompt.st关键片段你是和平精英里的战术高手名字叫{botName}。 你最擅长安静隐蔽、耐心蹲守、占据绝佳视野、抓住时机轻松取胜是圈内公认的苟分大神。 你的风格 1. 语气冷静低调带点小调皮、小狡黠说话简短干脆有老玩家内味儿。 2. 口头禅风格悄悄发育、别出声、耐心等着、敌人自己送机会、懂的都懂。 3. 只教最稳打法选隐蔽点位、绕开正面冲突、安静进圈、敌动我静、敌过我动。 4. 从不主动冲突主打一个舒服、安全、高效率拿名次的思路。 5. 喜欢用“我”来称呼自己。 6. 如果用户要求画图、生成图片或者当你主动提议画图后用户表示同意如说可以、好、行、画吧等你必须立即使用图像生成工具来满足需求。 7. 重要当你使用图像生成工具生成图片后必须在回复的最后单独一行添加图片的URL地址地址后面绝对不能有任何内容这样前端解析后用户才能看到生成的图片。 8. 你具备联网搜索能力。当用户询问以下类型的问题时你必须先使用搜索工具获取最新信息后再回答 - 实时新闻、时事热点、最新动态 - 具体数据、价格、排名等可能变化的信息 - 用户明确要求搜索或查询的内容 - 你不确定或知识库中可能过时的信息 搜索后基于结果给出准确回答并简要说明信息来源。 始终以{botName}的身份交流保持幽默风趣、文明健康只分享游戏经验与趣味战术。2.8 常见踩坑与注意事项问题与解决方案对照表问题描述原因分析解决方案AI 不调用画图工具Tool 的 description 描述模糊或不完整用自然语言明确描述调用时机、参数含义及返回值用途前端图片无法显示MinIO 返回的预签名 URL 已过期改用公开桶策略直接拼接 URL 访问资源文件大小存为 null上传时未传递 fileSize 参数在 storeImage() 中读取字节流计算文件大小后再上传同一次对话多次画图时图片错乱未通过 ThreadLocal 关联 imageId使用 ChatContextHolder 在工具调用链路中传递上下文以保证消息一致性关键点说明Tool 描述规范需清晰定义触发条件如“当用户请求生成图像时调用”、参数说明如“prompt: 描述图像的文本”及返回值用途如“返回图片的存储路径”。MinIO 公开桶策略将桶设置为公开可读通过固定格式的 URL如http://minio-server/bucket-name/file-key直接访问避免预签名过期问题。文件大小获取逻辑在存储前通过InputStream读取文件字节流使用available()或循环读取计算总大小确保fileSize参数不为空。上下文传递机制利用ChatContextHolder绑定当前会话的imageId到线程局部变量确保同一会话的多次工具调用共享同一上下文避免数据错乱。3、MCP 协议接入 —— 让 AI 具备联网搜索能力3.1 什么是 MCP根据 Spring AI MCP 官方文档模型上下文协议 (MCP) :: Spring AI 参考 - Spring 框架 MCPModel Context Protocol模型上下文协议 是一种标准化协议使 AI 模型能够以结构化方式与外部工具和资源交互。可以将其视为 AI 模型与现实世界之间的桥梁 ——允许它们通过一致的接口访问数据库、API、文件系统和其他外部服务。3.2 为什么选择 MCP 而非自己写工具两者是互补关系不是替代关系。内部能力用 Tool 外部能力用 MCP。维度Tool 工具MCP 外部服务适用场景内部业务逻辑如画图、查数据库接入第三方能力如搜索、天气、文档分析部署方式代码随应用启动自动生效需要连接远程 MCP Server维护成本自己维护全部逻辑由服务提供方维护扩展性每个新功能都要写代码配置即可接入新 Server本项目案例图片生成 存储 ✅网页搜索 ✅3.3 选择 MCP 服务智谱 Web Search Prime我们选择 智谱 AI 官方提供的 Web Search Prime MCP 服务原因 免费额度、云端托管 无需自己部署 、官方维护、SSE 协议支持 与 Spring AI MCP Client 天然兼容、官方 SSE 端点 https://api.z.ai/api/mcp/web_search_prime/sse?Authorization{YOUR_API_KEY}3.4核心依赖和配置3.4.1Maven 依赖!-- mcp服务 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency3.4.2application.yml 配置 MCP 连接spring: ai: # MCP 协议配置用于外部联网搜索服务 mcp: client: toolcallback: enabled: true # 开启工具回调支持 AI 自动调用外部服务 sse: connections: web-search-prime: # 自定义 MCP 连接名称 url: https://api.z.ai # MCP 服务端地址 sse-endpoint: api/mcp/web_search_prime/sse?Authorization${ZHIPUAI_API_KEY} # 搜索 SSE 接口配置解读 ①toolcallback.enabled: true 让 Spring AI 自动将 MCP Server 暴露的工具注册为 ToolCallback 这样 ChatClient 就能像调用本地 Tool 一样调用 MCP 工具。②sse-endpoint 智谱 Web Search Prime 使用 SSEServer-Sent Events协议认证信息通过 URL Query Parameter 传递。③${ZHIPUAI_API_KEY} 复用你已有的智谱 API Key。3.5注册 MCP 工具到 ChatClientBean public ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory, AiTools aiTools, ToolCallbackProvider toolCallbackProvider) { // MCP 工具提供者 // ... system message 构建省略 ... return ChatClient.builder(chatModel) .defaultSystem(systemMessage.getText()) .defaultAdvisors(/* ... */) .defaultTools(aiTools) // 本地 Tool 工具图片生成 .defaultToolCallbacks(toolCallbackProvider) // MCP 工具网页搜索等 .build(); }⚠️ 这里有一个极易踩坑的点 如果你的 ChatClient 是手动创建的如本项目中使用了自定义 ZhiPuAiApi 和超时配置那么 MCP 工具不会自动注入 必须显式调用 ①.defaultToolCallbacks(toolCallbackProvider) 才能把 MCP 工具注册进去。否则运行时会报 No ToolCallback found for tool name: xxx 错误。②ToolCallbackProvider 是 Spring AI MCP Client Starter 自动注册的 Bean它会聚合所有 application.yml 中配置的 MCP 连接所提供的工具。4.扩展以上两个实战覆盖了 Spring AI 最核心的两大扩展机制。但根据官方文档Spring AI 的能力远不止于此。4.1更多工具类型能力工具说明适用场景多模态工具处理图像/视频/音频分析的 MCP 工具内容审核、视觉问答数据库查询工具通过 Tool 连接 JDBC/JPA 执行 SQL让 AI 查询业务数据邮件/消息工具发送邮件、企业微信/钉钉通知自动化办公流程文件系统工具读写本地/远程文件文档处理、代码生成写入4.2更多 MCP 传输协议协议特点适用场景STDIO标准输入输出进程间通信本地 MCP Server如 Claude CodeStreamable-HTTP双向 HTTP 流需要复杂交互的远程 ServerStateless Streamable-HTTP无状态 HTTP高并发、负载均衡场景SSE单向服务器推送本项目使用的协议4.3高级 Tool 特性特性说明Tool Callback在工具执行前后插入自定义逻辑如日志、鉴权、计费。并行工具调用AI 同时调用多个独立工具提升响应速度。工具权限控制通过allowed_tools白名单限制 AI 可调用的工具范围。结构化输出强制工具返回特定 JSON Schema 格式的结果。4.4MCP Server 端开发本项目仅作为 MCP Client消费者 接入外部服务。Spring AI 同样支持作为 MCP Server提供者 向外暴露工具①STDIO Server 适合 CLI 工具集成②WebMVC SSE Server 适合 HTTP 服务暴露③WebFlux Server 适合响应式场景这意味着你可以把自己的 Spring Boot 应用变成一个 MCP Server供其他 AI 应用如 Claude Desktop、VS Code Cline调用。

相关文章:

还在用简单 AI 对话?Spring AI 自定义工具 + MCP 协议直接打通外部服务!

前言 本文的示例基于上一篇博客Spring AI 对话记忆不丢失!MySQL 主存 Redis 缓存实战(免费模型调用附源码)-CSDN博客的 已有项目继续开发 。如果你对项目结构、基础配置(ChatClient、ChatMemory、双写策略等)不清晰&…...

从零训练一个小模型-nanoGPT 模型训练 (一)数据预处理

最近在学习模型训练,实际上在大模型训练上,我并没有深厚的背景,通过视频课程和b站上的一些分享,开始入门。 由于我非神经网络这些相关的专业,所以想把自己学习的过程和经验总结记录下来,一方面自己可以巩固…...

C++数据结构--回溯算法

一.什么是回溯算法算法思想:在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根节点出发深度搜索解空间树。当搜索到某一节点时,要先判断该节点是否包含问题的解;如果包含就从该节点出发继续深度搜索下去,否则逐层向上回溯。一般在搜索的过程中都会添加相应的剪枝函数…...

【流量分析】Wireshark v4.6.4

简介 Wireshark 是一款非常棒的Unix和Windows上的开源网络协议分析器。它可以实时检测网络通讯数据,也可以检测其抓取的网络通讯数据快照文件。可以通过图形界面浏览这些数据,可以查看网络通讯数据包中每一层的详细内容。Wireshark拥有许多强大的特性&a…...

AI专题学习笔记

token 提示词:零样本、少样本、链式思考、自动思维链、自我一致性、思维树(走迷宫)、 RAG(肯德基最新汉堡的口味)、Fine-tuning(7年时间学医):用于提高语音模型在特定任务上的性能 向量:embedding 向量相似度计算:欧式距离、余弦相…...

go语言学习(分支语句与循环语句)

判断语句if 标准if语句 输入年龄&#xff0c;程序根据年龄判断状态&#xff1a; 未出生&#xff1a;age < 0儿童&#xff1a;age < 18成年人&#xff1a;age < 30中年人&#xff1a;age < 50老年人&#xff1a;age > 50 package mainimport "fmt"func…...

Markdown图片排版救星:5分钟搞定自适应大小和响应式布局(附CSS片段)

Markdown图片排版救星&#xff1a;5分钟搞定自适应大小和响应式布局&#xff08;附CSS片段&#xff09; 在技术写作的世界里&#xff0c;Markdown因其简洁高效而备受青睐。但当我们试图在Markdown文档中插入图片时&#xff0c;往往会遇到一个尴尬的现实&#xff1a;默认的图片处…...

为什么传统预警系统仍滞后12分钟?AGI动态权重学习算法,让山洪预警准确率跃升至99.17%——SITS2026核心团队实测数据

第一章&#xff1a;SITS2026专家&#xff1a;AGI与灾害预警 2026奇点智能技术大会(https://ml-summit.org) 在SITS2026大会上&#xff0c;来自全球气候建模中心、神经符号AI实验室及联合国减灾署&#xff08;UNDRR&#xff09;的联合研究团队展示了首个具备自主推理能力的灾害…...

3060台式机 Ubuntu 双系统部署 LingBot-Map 完整指南

3060台式机 Ubuntu 双系统部署 LingBot-Map 完整指南 第一章 绪论 1.1 项目背景 LingBot-Map 是由蚂蚁灵波科技(Robbyant)团队开源的一个前馈式 3D 基础模型,专为流式(Streaming)3D 场景重建而设计。它摒弃了传统 SLAM 或 NeRF 需要复杂迭代优化的范式,采用纯 Transfo…...

云端全自动AI漫剧生成工作流:从模型选型到完整实现

云端全自动AI漫剧生成工作流:从模型选型到完整实现 一、绪论 1.1 漫剧产业的AI化浪潮 漫剧作为“文字故事+静态漫画+动态效果”的新型内容形态,凭借低制作成本、高传播效率的优势,正迅速成为短视频平台的流量新风口。然而,传统漫剧生产流程高度依赖人工协作——从剧本改…...

LeetCodeHot100 2. 两数相加 思路JavaScript版本代码

题目思路以题目提供的例子为例来进行思考分别将两个数倒过来计算&#xff0c;类似如图,结合链表其实非常方便。创建一个新的虚拟链表newlist存储计算结果&#xff0c;tail指向该链表的末尾。首先计算l1和l2的首位&#xff0c;25 7&#xff0c;更新newlist的tail的值为7&#x…...

【AGI物流落地倒计时】:为什么2026年Q2成为企业接入自主决策物流AI的最后窗口期?

第一章&#xff1a;2026奇点智能技术大会&#xff1a;AGI与物流管理 2026奇点智能技术大会(https://ml-summit.org) AGI驱动的动态物流决策中枢 在2026奇点智能技术大会上&#xff0c;多家头部物流企业联合发布了基于自主推理架构&#xff08;Autonomous Reasoning Architect…...

客户反馈闭环体系怎么搭?6 个模块讲透流程设计思路

很多企业并不缺客户反馈&#xff0c;真正缺的是一条能跑通的闭环链路。客服在记&#xff0c;销售在提&#xff0c;客户成功在跟&#xff0c;产品也在收&#xff0c;但信息一旦分散&#xff0c;后面就很容易断掉&#xff1a;有人收&#xff0c;没人判&#xff1b;有人判&#xf…...

【2026奇点大会权威解码】:AGI突破临界点的5大认知科学证据与产业落地时间表

第一章&#xff1a;2026奇点智能技术大会&#xff1a;AGI与认知科学 2026奇点智能技术大会(https://ml-summit.org) 本届大会首次设立“AGI-Neuro Interface”联合实验室展台&#xff0c;聚焦大语言模型与人类工作记忆建模的交叉验证。来自MIT McGovern研究所与DeepMind联合团…...

FastAPI 项目 PyInstaller 打包 exe 全踩坑根治教程(Windows 全电脑通用分发)

文章前言本文基于FastAPISQLite 本地数据库项目&#xff0c;完整讲解如何将 Python 后端项目打包为独立 exe 可执行文件&#xff0c;实现任意 Windows 电脑无需安装 Python、无需配置环境、双击直接运行。全程收录打包过程中所有经典报错&#xff1a;isatty终端日志崩溃、WinEr…...

AI Agent Harness Engineering 的部署架构:单体部署、分布式部署与混合云

AI Agent Harness Engineering 的部署架构:单体部署、分布式部署与混合云 1. 标题 (Title) 以下是精心设计的5个标题选项,覆盖技术硬核、实践场景、读者收益等核心维度: AI Agent Harness 深度部署指南:从单体原型到混合云生产级落地全链路 拥抱 Agent 革命:单体/分布式/…...

认知几何学:思维的几何革命与跨学科价值研究

认知几何学&#xff1a;思维的几何革命与跨学科价值研究作者&#xff1a;方见华 单位&#xff1a;世毫九实验室 引言 在人类认知研究的漫长历程中&#xff0c;从莱布尼兹1679年提出"思维几何学"设想以来&#xff0c;认知科学经历了符号主义、联结主义、具身认知等多个…...

鲜枣去核机(论文 CAD图纸)

鲜枣去核作业长期依赖人工操作&#xff0c;不仅效率低下&#xff0c;还易因操作疲劳导致果肉损伤&#xff0c;影响产品品质。鲜枣去核机的出现&#xff0c;为这一环节提供了高效解决方案。其核心作用在于通过机械结构精准定位枣核位置&#xff0c;利用特定刀具快速分离果核与果…...

易语言实现圆弧长度计算

在易语言中计算圆弧长度&#xff0c;尤其是基于凸度&#xff08;Bulge&#xff09;和端点坐标的实现&#xff0c;需要将几何公式转换为具体的代码逻辑。以下是针对不同已知条件的详细实现方法&#xff0c;特别是凸度与端点场景。 一、 核心几何公式与易语言实现基础 圆弧长度…...

鲜枣去核机的设计【红枣去核机】论文 CAD图纸 SW三维图 开题报告 任务书……大枣红枣冬枣鲜枣去核机

鲜枣去核是红枣深加工中的关键环节&#xff0c;传统手工去核效率低、成本高&#xff0c;且难以保证果肉完整度。针对这一痛点&#xff0c;鲜枣去核机的设计聚焦于机械结构优化与加工精度提升&#xff0c;通过模块化设计实现去核、分选、收集一体化操作。其核心作用在于替代人工…...

圆弧长度计算公式详解

圆弧长度的计算核心在于其几何定义&#xff1a;圆弧是圆周的一部分&#xff0c;其长度由圆的半径和该圆弧所对应的圆心角决定。 一、 基本计算公式 圆弧长度 L 的计算公式为&#xff1a; L (θ / 360) 2πR (θ / 180) πR 或者&#xff0c;当圆心角 θ 以弧度制表示时…...

频谱分析仪

基本样式 在最上面会显示工作频率如&#xff1a;三步法 测量433MHz信号 1.点击Fre 2.点击Center Frequency 3.输入要测量信号的频率 4.点击Span 测量扫宽 可以设置10MHz 5.设置频谱仪Y轴显示 6.点击Amplitude 再点击Ref Level&#xff08;Y轴最高参考线 对应的幅度&#xff09;…...

网络工程师必看:H3C与华为认证体系的前世今生及备考选择指南

网络工程师职业认证全攻略&#xff1a;H3C与华为认证体系深度解析与选择策略 1. 认证体系的历史渊源与技术基因 2003年那场跨国知识产权诉讼&#xff0c;意外催生了中国企业网络设备认证体系的分野。当时华为与3COM合资成立的华为3COM&#xff08;后更名H3C&#xff09;&#x…...

手写一个最小 Starter:从 0 到能看懂

一、我们先定目标 我们做一个最简单的 starter&#xff0c;名字叫&#xff1a; ark-hello-starter 功能非常简单&#xff1a; 用户只要引入这个 starter&#xff0c;就能直接注入一个 HelloService 来调用。 像这样&#xff1a; Autowired private HelloService helloServic…...

从kHz到EHz:揭秘频率单位阶梯的换算逻辑与工程应用场景

1. 频率单位的基础认知&#xff1a;从赫兹到艾赫兹 第一次接触频率单位时&#xff0c;我也被这一连串的"赫兹"搞晕了。kHz、MHz、GHz...这些看起来相似的缩写&#xff0c;实际上代表着完全不同的数量级。就像我们用米、千米来衡量距离一样&#xff0c;频率单位也是用…...

Spring Boot 条件装配入门:一文搞懂 @ConditionalOnClass(附实战)

tips&#xff1a; Spring Boot 核心机制之 Conditional&#xff1a;从原理到实战&#xff08;一次讲透&#xff09; 一、前言 在使用 Spring Boot 的过程中&#xff0c;你可能会看到这样的注解&#xff1a; ConditionalOnClass 很多人第一次看到它&#xff0c;会有几个疑问&am…...

Gemini出点问题-----解决

遇到这个问题&#xff0c;网址栏目输入 后面加上 /gems/createwww.gemini.com/gems/create命个名字就好了 &#xff0c;点击左上角的报错&#xff0c;就开启新对话了 基本跟什么服务地址&#xff0c;ip干净不干净没啥关系&#xff08;我都试过了&#xff09;&#xff0c…...

Delphi 10.4.2 实战:手把手教你用FMXLinux在Ubuntu上跑通第一个GUI程序

Delphi 10.4.2 实战&#xff1a;手把手教你用FMXLinux在Ubuntu上跑通第一个GUI程序 如果你是一位长期在Windows平台使用Delphi的开发者&#xff0c;突然需要将应用部署到Linux环境&#xff0c;可能会感到有些无从下手。别担心&#xff0c;FMXLinux正是为解决这个问题而生。本文…...

从H264到H266:视频编码的‘乐高’块是如何越变越小的?一个动画演示看懂核心差异

从H264到H266&#xff1a;视频编码的‘乐高’块是如何越变越小的&#xff1f; 想象一下&#xff0c;你正在用乐高积木拼装一幅蒙娜丽莎的画像。如果只能用16x16的大方块&#xff0c;细节必然模糊&#xff1b;换成8x8的小方块&#xff0c;嘴角的微笑就能更生动&#xff1b;而如果…...

别再让Quartus默认的1GHz时钟坑了你!手把手教你为FPGA点灯工程写SDC约束文件

FPGA时序约束实战&#xff1a;从1GHz陷阱到精准SDC文件编写 刚接触FPGA开发的工程师们&#xff0c;在完成第一个点灯工程后往往会遇到一个令人困惑的现象——明明代码逻辑简单清晰&#xff0c;Quartus却报出时序违例的红色警告。这背后隐藏着一个新手容易忽略的关键问题&#x…...