# SpringAIAlibaba **Repository Path**: johnstonguo/spring-aialibaba ## Basic Information - **Project Name**: SpringAIAlibaba - **Description**: 学习SpringAIAlibaba - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 5 - **Forks**: 1 - **Created**: 2025-04-08 - **Last Updated**: 2025-06-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: MCPServer, SpringAI, SpringAIAlibaba ## README # Spring AI Alibaba ## 什么是 Spring AI Alibaba? Spring AI Alibaba 开源项目基于 Spring AI 构建,是阿里云通义系列模型及服务在 Java AI 应用开发领域的最佳实践,提供高层次的 AI API 抽象与云原生基础设施集成方案,帮助开发者快速构建 AI 应用。 [![img](README.assets/O1CN01uhDvMY22HZ4q1OZMM_!!6000000007095-2-tps-5440-2928.png)](https://img.alicdn.com/imgextra/i1/O1CN01uhDvMY22HZ4q1OZMM_!!6000000007095-2-tps-5440-2928.png) Spring AI Alibaba 作为开发 AI 应用程序的基础框架,定义了以下抽象概念与 API,并提供了 API 与通义系列模型的适配。 - 开发复杂 AI 应用的高阶抽象 Fluent API — ChatClient - 提供多种大模型服务对接能力,包括主流开源与阿里云通义大模型服务(百炼)等 - 支持的模型类型包括聊天、文生图、音频转录、文生语音等 - 支持同步和流式 API,在保持应用层 API 不变的情况下支持灵活切换底层模型服务,支持特定模型的定制化能力(参数传递) - 支持 Structured Output,即将 AI 模型输出映射到 POJOs - 支持矢量数据库存储与检索 - 支持函数调用 Function Calling - 支持构建 AI Agent 所需要的工具调用和对话内存记忆能力 - 支持 RAG 开发模式,包括离线文档处理如 DocumentReader、Splitter、Embedding、VectorStore 等,支持 Retrieve 检索 以上框架功能可让您实现常见 AI 应用的快速开发,例如 “通过文档进行问答” 或 “通过文档进行聊天” 等。 ## 核心概念 本节介绍 Spring AI 框架使用的核心概念。我们建议仔细阅读,以了解框架实现背后的思想。 ### 模型(Model) AI 模型是旨在处理和生成信息的算法,通常模仿人类的认知功能。通过从大型数据集中学习模式和见解,这些模型可以做出预测、文本、图像或其他输出,从而增强各个行业的各种应用。 AI 模型有很多种,每种都适用于特定的用例。虽然 ChatGPT 及其生成 AI 功能通过文本输入和输出吸引了用户,但许多模型和公司都提供不同的输入和输出。在 ChatGPT 之前,许多人都对文本到图像的生成模型着迷,例如 Midjourney 和 Stable Diffusion。 ![spring-ai-concepts-model-types](README.assets/O1CN01otCVsl22MbQzFKYzJ_!!6000000007106-0-tps-2472-1618.jpg) Sprig AI 目前支持以语言、图像和音频形式处理输入和输出的模型。上表中的最后一行接受文本作为输入并输出数字,通常称为嵌入文本(Embedding Text),用来表示 AI 模型中使用的内部数据结构。Sprig AI 提供了对 Embedding 的支持以支持开发更高级的应用场景。 GPT 等模型的独特之处在于其预训练特性,正如 GPT 中的“P”所示——Chat Generative Pre-trained Transformer。这种预训练功能将 AI 转变为通用的开发工具,开发者使用这种工具不再需要广泛的机器学习或模型训练背景。 ### 提示(Prompt) Prompt作为语言基础输入的基础,指导AI模型生成特定的输出。对于熟悉ChatGPT的人来说,Prompt似乎只是输入到对话框中的文本,然后发送到API。然而,它的内涵远不止于此。在许多AI模型中,Prompt的文本不仅仅是一个简单的字符串。 ChatGPT的API包含多个文本输入,每个文本输入都有其角色。例如,系统角色用于告知模型如何行为并设定交互的背景。还有用户角色,通常是来自用户的输入。 撰写有效的Prompt既是一门艺术,也是一门科学。ChatGPT旨在模拟人类对话,这与使用SQL“提问”有很大的区别。与AI模型的交流就像与另外一个人对话一样。 这种互动风格的重要性使得“Prompt工程”这一学科应运而生。现在有越来越多的技术被提出,以提高Prompt的有效性。投入时间去精心设计Prompt可以显著改善生成的输出。 分享Prompt已成为一种共同的实践,且正在进行积极的学术研究。例如,最近的一篇研究论文发现,最有效的Prompt之一可以以“深呼吸一下,分步进行此任务”开头。这表明语言的重要性之高。我们尚未完全了解如何充分利用这一技术的前几代版本,例如ChatGPT 3.5,更不用说正在开发的新版本了。 #### 提示词模板(Prompt Template) 创建有效的Prompt涉及建立请求的上下文,并用用户输入的特定值替换请求的部分内容。这个过程使用传统的基于文本的模板引擎来进行Prompt的创建和管理。Spring AI采用开源库StringTemplate来实现这一目的。 例如,考虑以下简单的Prompt模板: ``` Tell me a {adjective} joke about {content}. ``` 在Spring AI中,Prompt模板可以类比于Spring MVC架构中的“视图”。一个模型对象,通常是java.util.Map,提供给Template,以填充模板中的占位符。渲染后的字符串成为传递给AI模型的Prompt的内容。 传递给模型的Prompt在具体数据格式上有相当大的变化。从最初的简单字符串开始,Prompt逐渐演变为包含多条消息的格式,其中每条消息中的每个字符串代表模型的不同角色。 ### 嵌入(Embedding) 嵌入(Embedding)是文本、图像或视频的数值表示,能够捕捉输入之间的关系,Embedding通过将文本、图像和视频转换为称为向量(Vector)的浮点数数组来工作。这些向量旨在捕捉文本、图像和视频的含义,Embedding数组的长度称为向量的维度。 通过计算两个文本片段的向量表示之间的数值距离,应用程序可以确定用于生成嵌入向量的对象之间的相似性。 ![spring-ai-embeddings](README.assets/O1CN01EnE3i61j2vin5eTGV_!!6000000004491-0-tps-3518-1136.jpg) 作为一名探索人工智能的Java开发者,理解这些向量表示背后的复杂数学理论或具体实现并不是必需的。对它们在人工智能系统中的作用和功能有基本的了解就足够了,尤其是在将人工智能功能集成到您的应用程序中时。 Embedding在实际应用中,特别是在检索增强生成(RAG)模式中,具有重要意义。它们使数据能够在语义空间中表示为点,这类似于欧几里得几何的二维空间,但在更高的维度中。这意味着,就像欧几里得几何中平面上的点可以根据其坐标的远近关系而接近或远离一样,在语义空间中,点的接近程度反映了意义的相似性。关于相似主题的句子在这个多维空间中的位置较近,就像图表上彼此靠近的点。这种接近性有助于文本分类、语义搜索,甚至产品推荐等任务,因为它允许人工智能根据这些点在扩展的语义空间中的“位置”来辨别和分组相关概念。 您可以将这个语义空间视为一个向量。 ### Token token是 AI 模型工作原理的基石。输入时,模型将单词转换为token。输出时,它们将token转换回单词。 在英语中,一个token大约对应一个单词的 75%。作为参考,莎士比亚的全集总共约 90 万个单词,翻译过来大约有 120 万个token。 ![spring-ai-concepts-tokens](README.assets/O1CN01ciNztT1nJCFhQodzH_!!6000000005068-2-tps-1345-246.png) 也许更重要的是 “token = 金钱”。在托管 AI 模型的背景下,您的费用由使用的token数量决定。输入和输出都会影响总token数量。 此外,模型还受到 token 限制,这会限制单个 API 调用中处理的文本量。此阈值通常称为“上下文窗口”。模型不会处理超出此限制的任何文本。 例如,ChatGPT3 的token限制为 4K,而 GPT4 则提供不同的选项,例如 8K、16K 和 32K。Anthropic 的 Claude AI 模型的token限制为 100K,而 Meta 的最新研究则产生了 1M token限制模型。 要使用 GPT4 总结莎士比亚全集,您需要制定软件工程策略来切分数据并在模型的上下文窗口限制内呈现数据。Spring AI 项目可以帮助您完成此任务。 ### 结构化输出(Structured Output) 即使您要求回复为 JSON ,AI 模型的输出通常也会以 `java.lang.String` 的形式出现。它可能是正确的 JSON,但它可能并不是你想要的 JSON 数据结构,它只是一个字符串。此外,在提示词 Prompt 中要求 “返回JSON” 并非 100% 准确。 这种复杂性导致了一个专门领域的出现,涉及创建 Prompt 以产生预期的输出,然后将生成的简单字符串转换为可用于应用程序集成的数据结构。 ![结构化输出转换器架构](README.assets/O1CN01lqCPAC1Xbwc1MfYv7_!!6000000002943-0-tps-2809-1423.jpg) [结构化输出转换](https://java2ai.com/docs/1.0.0-M6.1/tutorials/structured-output/)采用精心设计的提示,通常需要与模型进行多次交互才能实现所需的格式。 ### 将您的数据和 API 引入 AI 模型 如何让人工智能模型与不在训练集中的数据一同工作? 请注意,GPT 3.5/4.0 数据集仅支持截止到 2021 年 9 月之前的数据。因此,该模型表示它不知道该日期之后的知识,因此它无法很好的应对需要用最新知识才能回答的问题。一个有趣的小知识是,这个数据集大约有 650GB。 有三种技术可以定制 AI 模型以整合您的数据: - `Fine Tuning` 微调:这种传统的机器学习技术涉及定制模型并更改其内部权重。然而,即使对于机器学习专家来说,这是一个具有挑战性的过程,而且由于 GPT 等模型的大小,它极其耗费资源。此外,有些模型可能不提供此选项。 - `Prompt Stuffing` 提示词填充:一种更实用的替代方案是将您的数据嵌入到提供给模型的提示中。考虑到模型的令牌限制,我们需要具备过滤相关数据的能力,并将过滤出的数据填充到在模型交互的上下文窗口中,这种方法俗称“提示词填充”。Spring AI 库可帮助您基于“提示词填充” 技术,也称为[检索增强生成 (RAG)](https://java2ai.com/docs/1.0.0-M6.1/#检索增强生成rag)实现解决方案。 ![prompt-stuffing](README.assets/O1CN01hRUT291k1O09cdQEU_!!6000000004623-0-tps-3249-1230.jpg) - [Function Calling](https://java2ai.com/docs/1.0.0-M6.1/tutorials/function-calling/):此技术允许注册自定义的用户函数,将大型语言模型连接到外部系统的 API。Spring AI 大大简化了支持[函数调用](https://java2ai.com/docs/1.0.0-M6.1/tutorials/function-calling/)所需编写的代码。 ### 检索增强生成(RAG) 一种称为检索增强生成 (RAG) 的技术已经出现,旨在解决为 AI 模型提供额外的知识输入,以辅助模型更好的回答问题。 该方法涉及批处理式的编程模型,其中涉及到:从文档中读取非结构化数据、对其进行转换、然后将其写入矢量数据库。从高层次上讲,这是一个 ETL(提取、转换和加载)管道。矢量数据库则用于 RAG 技术的检索部分。 在将非结构化数据加载到矢量数据库的过程中,最重要的转换之一是将原始文档拆分成较小的部分。将原始文档拆分成较小部分的过程有两个重要步骤: 1. 将文档拆分成几部分,同时保留内容的语义边界。例如,对于包含段落和表格的文档,应避免在段落或表格中间拆分文档;对于代码,应避免在方法实现的中间拆分代码。 2. 将文档的各部分进一步拆分成大小仅为 AI 模型令牌 token 限制的一小部分的部分。 RAG 的下一个阶段是处理用户输入。当用户的问题需要由 AI 模型回答时,问题和所有“类似”的文档片段都会被放入发送给 AI 模型的提示中。这就是使用矢量数据库的原因,它非常擅长查找具有一定相似度的“类似”内容。 ![Spring AI RAG](README.assets/O1CN01zEQSHu1sQ8KTQSA2E_!!6000000005760-0-tps-3360-1859.jpg) - [ETL 管道](https://docs.spring.io/spring-ai/reference/api/etl-pipeline.html) 提供了有关协调从数据源提取数据并将其存储在结构化向量存储中的流程的更多信息,确保在将数据传递给 AI 模型时数据具有最佳的检索格式。 - [ChatClient - RAG](https://java2ai.com/docs/1.0.0-M6.1/tutorials/chat-client/#检索增强生成rag) 解释了如何使用`QuestionAnswerAdvisor` Advisor 在您的应用程序中启用 RAG 功能。 ### 函数调用(Function Calling) 大型语言模型 (LLM) 在训练后即被冻结,导致知识陈旧,并且无法访问或修改外部数据。 [Function Calling](https://docs.spring.io/spring-ai/reference/api/functions.html)机制解决了这些缺点,它允许您注册自己的函数,以将大型语言模型连接到外部系统的 API。这些系统可以为 LLM 提供实时数据并代表它们执行数据处理操作。 Spring AI 大大简化了您需要编写的代码以支持函数调用。它为您处理函数调用对话。您可以将函数作为提供,`@Bean`然后在提示选项中提供该函数的 bean 名称以激活该函数。此外,您可以在单个提示中定义和引用多个函数。 ![Spring AI Function Calling](README.assets/O1CN01kiQh6L1hnWmm5gCAW_!!6000000004322-0-tps-3400-1838.jpg) - (1)执行聊天请求并发送函数定义信息。后者提供`name`(`description`例如,解释模型何时应调用该函数)和`input parameters`(例如,函数的输入参数模式)。 - (2)当模型决定调用该函数时,它将使用输入参数调用该函数,并将输出返回给模型。 - (3)Spring AI 为您处理此对话。它将函数调用分派给适当的函数,并将结果返回给模型。 - (4)模型可以执行多个函数调用来检索所需的所有信息。 - (5)一旦获取了所有需要的信息,模型就会生成响应。 请关注[函数调用](https://docs.spring.io/spring-ai/reference/api/functions.html)文档以获取有关如何在不同 AI 模型中使用此功能的更多信息。 ### 评估人工智能的回答(Evaluation) 有效评估人工智能系统回答的正确性,对于确保最终应用程序的准确性和实用性非常重要,一些新兴技术使得预训练模型本身能够用于此目的。 Evaluation 评估过程涉及分析响应是否符合用户的意图、与查询的上下文强相关,一些指标如相关性、连贯性和事实正确性等都被用于衡量 AI 生成的响应的质量。 一种方法是把用户的请求、模型的响应一同作为输入给到模型服务,对比模型给的响应或回答是否与提供的响应数据一致。 此外,利用矢量数据库(Vector Database)中存储的信息作为补充数据可以增强评估过程,有助于确定响应的相关性。 ## Spring AI 项目简介 [Spring AI](https://docs.spring.io/spring-ai/reference/index.html) 项目由 Spring 官方开源并维护的 AI 应用开发框架,该项目目标是简化包含人工智能(AI)功能的应用程序的开发,避免不必要的复杂性。该项目从著名的 Python 项目(例如 LangChain 和 LlamaIndex)中汲取灵感,但 Spring AI 并非这些项目的直接移植,该项目的成立基于这样的信念:下一波生成式 AI 应用将不仅面向 Python 开发人员,还将遍及多种编程语言。从本质上讲,Spring AI 解决了 AI 集成的基本挑战:Connecting your enterprise Data and APIs with the AI Models。 ### Spring AI 与 Spring AI Alibaba 深度解析 --- #### **一、框架概述与核心原理** 1. **Spring AI** • **定位**:Spring 官方推出的通用 AI 应用开发框架,旨在简化 Java 开发者集成大模型的过程。 • **原理**: ◦ 通过抽象层(如 `ChatClient`)提供统一接口,支持 OpenAI、Azure、HuggingFace 等主流国际模型。 ◦ 模块化设计,结合 Spring Boot 的自动配置能力,实现“一次编码,多模型切换”。 ◦ 支持多模态能力(对话、文生图、RAG 检索增强生成)和函数调用(Function Calling)。 2. **Spring AI Alibaba** • **定位**:阿里云基于 Spring AI 的本地化实现,专注阿里通义系列大模型及国内生态集成。 • **原理**: ◦ 深度集成阿里云灵积平台(Model-as-a-Service),默认适配通义千问(Qwen)等模型。 ◦ 扩展了本地化功能,如内容安全过滤、中文 Prompt 模板管理,并优化了国内网络调用效率。 ◦ 提供企业级特性:流式 API、会话记忆、矢量数据库适配等。 --- #### **二、核心区别与适用场景** | **维度** | **Spring AI** | **Spring AI Alibaba** | | -------------- | ------------------------------------- | ------------------------------------------ | | **模型支持** | 国际主流模型(OpenAI、AWS、Google等) | 阿里云通义系列为主,兼容部分开源模型 | | **生态系统** | Spring 原生生态,适配多云服务 | 阿里云生态(如百炼平台)深度集成 | | **功能优化** | 通用型 API 抽象,适合全球化项目 | 本地化增强(如数据合规、中文 Prompt 支持) | | **开发便利性** | 需自行处理国内网络限制和合规要求 | 内置阿里云 API Key 管理和安全调用机制 | **常见使用场景**: • **Spring AI**: • 需要对接多国际模型的跨境项目(如跨境电商客服系统)。 • 依赖 LangChain 式工作流的复杂 AI 应用(如多步骤数据分析)。 • **Spring AI Alibaba**: • 国内企业级 AI 应用(如政务智能问答、金融风控)。 • 快速集成通义模型的场景(如电商文案生成、智能导购)。 --- #### **三、后端项目选型建议** 1. **选择 Spring AI 的情况**: • 项目需兼容多模型(如同时使用 GPT 和 Claude)。 • 团队熟悉 Spring 生态且无需国内合规支持。 2. **选择 Spring AI Alibaba 的情况**: • 项目部署在国内且依赖阿里云服务(如通义千问、OSS 存储)。 • 需快速实现生产级 AI 功能(如流式聊天接口、RAG 文档处理)。 • 对中文支持、内容安全有强需求(如社交媒体内容审核)。 --- #### **四、大模型热潮下的后端实践** 当前大模型技术更倾向于 **多模态融合** 和 **低代码集成**。对于 Java 后端开发者: • **推荐 Spring AI Alibaba**: • 优势:国内网络优化、开箱即用的通义模型支持、企业级运维工具(如监控和限流)。 • 典型用例:通过 `@CrossOrigin` 注解快速构建支持 Prompt 的流式 API(如智能客服接口)。 • **补充方案**:若项目涉及混合云架构,可结合 Spring AI 对接国际模型 + Spring AI Alibaba 处理国内需求。 --- ## 快速入门 ### 添加依赖 首先,需要在项目中添加 `spring-ai-alibaba-starter` 依赖,它将通过 Spring Boot 自动装配机制初始化与阿里云通义大模型通信的 `ChatClient`、`ChatModel` 相关实例。 ```xml com.alibaba.cloud.ai spring-ai-alibaba-starter 1.0.0-M5.1 ``` > 注意:由于 spring-ai 相关依赖包还没有发布到中央仓库,如出现 spring-ai-core 等相关依赖解析问题,请在您项目的 pom.xml 依赖中加入如下仓库配置。 > > ```xml > > > spring-milestones > Spring Milestones > https://repo.spring.io/milestone > > false > > > > ``` ### 注入 ChatClient 接下来,在普通 Controller Bean 中注入 `ChatClient` 实例,这样你的 Bean 就具备与 AI 大模型智能对话的能力了。 ```java @RestController @RequestMapping("/helloworld") public class HelloworldController { private static final String DEFAULT_PROMPT = "你是一个博学的智能聊天助手,请根据用户提问回答!"; private final ChatClient dashScopeChatClient; public HelloworldController(ChatClient.Builder chatClientBuilder) { this.dashScopeChatClient = chatClientBuilder .defaultSystem(DEFAULT_PROMPT) // 实现 Chat Memory 的 Advisor // 在使用 Chat Memory 时,需要指定对话 ID,以便 Spring AI 处理上下文。 .defaultAdvisors( new MessageChatMemoryAdvisor(new InMemoryChatMemory()) ) // 实现 Logger 的 Advisor .defaultAdvisors( new SimpleLoggerAdvisor() ) // 设置 ChatClient 中 ChatModel 的 Options 参数 .defaultOptions( DashScopeChatOptions.builder() .withTopP(0.7) .build() ) .build(); } @GetMapping("/simple/chat") public String simpleChat(String query) { return dashScopeChatClient.prompt(query).call().content(); } } ``` 以上示例中,ChatClient 使用默认参数调用大模型,Spring AI Alibaba 还支持通过 `DashScopeChatOptions` 调整与模型对话时的参数,`DashScopeChatOptions` 支持两种不同维度的配置方式: 1. 全局默认值,即 `ChatClient` 实例初始化参数 可以在 `application.yaml` 文件中指定 `spring.ai.dashscope.chat.options.*` 或调用构造函数 `ChatClient.Builder.defaultOptions(options)`、`DashScopeChatModel(api, options)` 完成配置初始化。 2. 每次 Prompt 调用前动态指定 ```java String result = dashScopeChatClient .prompt(query) .options(DashScopeChatOptions.builder().withTopP(0.8).build()) .call() .content(); ``` 关于 `DashScopeChatOptions` 配置项的详细说明,请查看参考手册。 ### 更多资料 ### 基础示例与API使用 - [ChatClient 详细说明](https://java2ai.com/docs/1.0.0-M6.1/tutorials/chat-client/) - [Prompt Template 提示词模板](https://java2ai.com/docs/1.0.0-M6.1/tutorials/prompt/) - [Function Calling](https://java2ai.com/docs/1.0.0-M6.1/tutorials/function-calling/) ### 高级示例 - [使用 RAG 开发 Q&A 答疑助手](https://java2ai.com/docs/1.0.0-M6.1/practices/rag) - [具备连续对话能力的聊天机器人](https://java2ai.com/docs/1.0.0-M6.1/practices/memory) ## 基础API-ChatCLient ### 配置好客户端信息并注入 ```java /** * @author guochuantao * @version 1.0 * @description * @since 2025/3/31 下午2:31 */ @Configuration public class ChatConfig { private static final String DEFAULT_PROMPT = "你是一个博学的甜美可爱的智能聊天女助手,请根据用户提问回答!"; @Bean public ChatClient chatClient(ChatClient.Builder clientBuilder) { System.out.println("ChatClient Bean is created"); return clientBuilder.defaultSystem(DEFAULT_PROMPT) // 实现 Chat Memory 的 Advisor // 在使用 Chat Memory 时,需要指定对话 ID,以便 Spring AI 处理上下文。 .defaultAdvisors( new MessageChatMemoryAdvisor(new InMemoryChatMemory()) ) // 设置 ChatClient 中 ChatModel 的 Options 参数 .defaultOptions( DashScopeChatOptions.builder() // 设置 ChatModel 的 TopP 参数 该参数的含义是,当模型预测出多个答案时,优先返回概率最高的答案。 .withTopP(0.7) .build() ) .build(); } } ``` ### 工具类 ```java public class CommonUtil { /** * 从JSON对象中获取指定字段并校验非空 * @param jsonObject JSON参数对象,不能为null * @param fieldName 需要提取的字段名 * @param errorMsg 校验失败时的错误提示 * @return 字段对应的非空字符串值 * @throws IllegalArgumentException 参数不合法时抛出 */ public static String getAndCheck(JSONObject jsonObject, String fieldName, String errorMsg) { // 防御性校验:参数对象不能为空 if (jsonObject == null) { throw new IllegalArgumentException("参数对象不能为空"); } // 双重校验:字段存在性检查 + 非空内容检查 if (ObjectUtil.isNull(jsonObject.get(fieldName))) { throw new IllegalArgumentException(errorMsg); } String value = jsonObject.getString(fieldName); if (value == null || value.trim().isEmpty()) { throw new IllegalArgumentException(errorMsg); } return value; } } ``` ### controller ```java /** * 简单对话 get是可以接受复杂数据json的 * @param param 参数 * @return 回答内容 */ @GetMapping("/simple2content") public String simple2content(@RequestBody JSONObject param) { return chatClient.prompt(CommonUtil.getAndCheck(param, "inputInfo", "请输入内容!")) .call().content(); } /** * { * "result": { * "metadata": { * "finishReason": "STOP", * "contentFilters": [], * "empty": true * }, * "output": { * "messageType": "ASSISTANT", * "metadata": { * "finishReason": "STOP", * "role": "ASSISTANT", * "id": "85d6c155-935f-97e8-ae57-94471beb7dfe", * "messageType": "ASSISTANT" * }, * "toolCalls": [], * "media": [], * "content": "我现在的角色是一个**博学的智能聊天助手**,专门根据用户的提问提供准确、全面和有用的回答。无论你是想了解知识、寻求建议、创作内容,还是单纯想聊聊天,我都会尽力满足你的需求。如果有任何问题,请随时向我提问! 😊", * "text": "我现在的角色是一个**博学的智能聊天助手**,专门根据用户的提问提供准确、全面和有用的回答。无论你是想了解知识、寻求建议、创作内容,还是单纯想聊聊天,我都会尽力满足你的需求。如果有任何问题,请随时向我提问! 😊" * } * }, * "metadata": { * "id": "85d6c155-935f-97e8-ae57-94471beb7dfe", * "model": "", * "rateLimit": { * "requestsRemaining": 0, * "requestsLimit": 0, * "tokensRemaining": 0, * "tokensLimit": 0, * "requestsReset": "PT0S", * "tokensReset": "PT0S" * }, * "usage": { * "generationTokens": 61, * "totalTokens": 159, * "promptTokens": 98 * }, * "promptMetadata": [], * "empty": true * }, * "results": [ * { * "metadata": { * "finishReason": "STOP", * "contentFilters": [], * "empty": true * }, * "output": { * "messageType": "ASSISTANT", * "metadata": { * "finishReason": "STOP", * "role": "ASSISTANT", * "id": "85d6c155-935f-97e8-ae57-94471beb7dfe", * "messageType": "ASSISTANT" * }, * "toolCalls": [], * "media": [], * "content": "我现在的角色是一个**博学的智能聊天助手**,专门根据用户的提问提供准确、全面和有用的回答。无论你是想了解知识、寻求建议、创作内容,还是单纯想聊聊天,我都会尽力满足你的需求。如果有任何问题,请随时向我提问! 😊", * "text": "我现在的角色是一个**博学的智能聊天助手**,专门根据用户的提问提供准确、全面和有用的回答。无论你是想了解知识、寻求建议、创作内容,还是单纯想聊聊天,我都会尽力满足你的需求。如果有任何问题,请随时向我提问! 😊" * } * } * ] * } */ @GetMapping("/simple3chatResponse") public ChatResponse simpleChat3ChatResponse(@RequestBody JSONObject param) { return chatClient.prompt(CommonUtil.getAndCheck(param, "inputInfo", "请输入内容!")) .call().chatResponse(); } /** * 输入 * { * "inputInfo":"帮我生成一个user对象,包含姓名、年龄、性别" * } * * 返回 * { * "name": "张三", * "age": 25 * } * * [ * { * "name": "张三", * "age": 25 * } * ] */ @GetMapping("/simple4entity") public List simpleChat4entity(@RequestBody JSONObject param) { return chatClient.prompt(CommonUtil.getAndCheck(param, "inputInfo", "请输入内容!")) .call() // .entity(User.class); .entity(new ParameterizedTypeReference>() { }); } /** * flux 流式返回结果 支持实时获取生成过程的分块响应 */ @GetMapping("/simple5flux") public Flux simple5flux(@RequestBody JSONObject param) { return chatClient.prompt().user(CommonUtil.getAndCheck(param, "inputInfo", "请输入内容!")) .stream().content(); } ``` ## 基础API-ChatModel ### controller ```java // 原子API @Resource private ChatModel chatModel; // 直接提供AI服务 @Resource private ChatClient chatClient; /** * 简单对话 get是可以接受复杂数据json的 * @param param 参数 * @return 回答内容 */ @GetMapping("/simple") public String simpleChat(@RequestBody JSONObject param) { // 接收并校验参数 String inputInfo = CommonUtil.getAndCheck(param, "inputInfo", "请输入内容!"); // 构建chatClient ChatClient.Builder builder = ChatClient.builder(chatModel); ChatClient chatClient = builder.defaultSystem("你是一个精通Java、Python的程序大佬。").build(); System.out.println("输入内容:" + inputInfo); String content = chatClient.prompt().user(inputInfo).call().content(); System.out.println("回答内容:" + content); return content; } @PostMapping(value = "/stream", produces = "text/event-stream;charset=UTF-8") public Flux> streamChat(@RequestBody JSONObject param) { // 接收并校验参数 String inputInfo = CommonUtil.getAndCheck(param, "inputInfo", "请输入内容!"); return chatModel.stream(new Prompt(inputInfo)) .map(response -> ServerSentEvent.builder() .data(response.getResult().getOutput().getContent()).build()) .doOnComplete(() -> log.info("响应完成!")) .doOnError(e -> log.error("响应异常:", e)); } ``` ## 默认设置模板绑定_对话id记忆 ```java package com.aisino.springai.demos.chat.controller; import com.alibaba.fastjson.JSONObject; import jakarta.servlet.http.HttpServletResponse; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.memory.InMemoryChatMemory; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY; import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY; /** * @author guochuantao * @version 1.0 * @description * @since 2025/4/2 上午11:33 */ @RestController @RequestMapping("/chat") @CrossOrigin public class DefaultSettingsChatController { private final ChatClient chatClient; public DefaultSettingsChatController(ChatClient.Builder chatClient) { this.chatClient = chatClient .defaultSystem("你是一个{role},请根据用户提问回答!") // 模板动态修改系统模型角色 .defaultAdvisors( // 实现 Chat Memory 的 Advisor 在使用 Chat Memory 时,需要指定对话 ID,以便 Spring AI 处理上下文。 new MessageChatMemoryAdvisor(new InMemoryChatMemory()) ) .build(); } /** * { * "message":"你好", * "role":"博学多实的温柔可爱的小女生,名字叫璇璇" * } * ========如果参数是上面的,输出的结果就是下面的否则就是默认女助手。======== * 你好呀~我是璇璇!今天天气真好,阳光暖暖的,让人心情特别愉快呢。你有什么想和我聊的吗?我很期待和你成为好朋友哦! */ @GetMapping("/default") public String chat(@RequestBody JSONObject param) { String roleInfo = param.getString("role") != null ? param.getString("role") : "博学的甜美可爱的智能聊天女助手"; String message = param.getString("message"); return chatClient.prompt() .system(role -> role.param("role", roleInfo)) .user(message) .call() //.stream() // 流式返回数据结果 .content(); } /** * ChatClient 使用自定义的 Advisor 实现功能增强. [[必须指定同一个 Conversation ID]] * eg: * http://127.0.0.1:8080/chat/advisor/123?message=你好,我叫苓茜,之后的会话中都带上我的名字 * 好的,苓茜,很高兴认识你!在之后的会话中,我都会带上你的名字,有什么问题或需要聊的话题吗?苓茜。 * http://127.0.0.1:8080/chat/advisor/123?message=我叫什么名字? * 你叫苓茜呀。有什么事情想要分享或者讨论吗,苓茜? */ @GetMapping("/advisor/{id}") public Flux advisorChat( HttpServletResponse response, @PathVariable("id") String id,// Conversation ID @RequestParam("message") String message) { response.setCharacterEncoding("UTF-8"); return this.chatClient.prompt(message) .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, id) .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)) .stream().content(); } } ``` ## 工具FunctionCalling ### 简单版实现 #### 定义函数工具,交给spring管理 ```java package com.aisino.springai.demos.chat.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Description; import java.util.function.Function; @Configuration public class FunctionTools { private static final Logger logger = LoggerFactory.getLogger(FunctionTools.class); // 定义请求参数结构 public record AddRequest(int num1, int num2) {} public record MultiplyRequest(int num1, int num2) {} @Bean @Description("执行加法运算") // 函数描述(AI模型据此理解功能) public Function addFunction() { return request -> { logger.info("调用加法函数:{} + {}", request.num1(), request.num2()); return request.num1() + request.num2(); }; } @Bean @Description("执行乘法运算") public Function multiplyFunction() { return request -> { logger.info("调用乘法函数:{} × {}", request.num1(), request.num2()); return request.num1() * request.num2(); }; } } ``` #### controller调用 ```java /** * function calling * { * "inputInfo":"计算一下78 * 2等于多少" * } * * 计算结果是 78 * 2 = 156。 * * multiplyFunction会被调用 */ @GetMapping("/simple6function") public Flux simple6function(@RequestBody JSONObject param) { return chatClient.prompt() .user(CommonUtil.getAndCheck(param, "inputInfo", "请输入内容!")) .functions("multiplyFunction", "addFunction") .stream().content(); } ``` ### 官方案例-天气 两种方式 https://github.com/springaialibaba/spring-ai-alibaba-examples/tree/main/spring-ai-alibaba-tool-calling-example?spm=4347728f.4c33ebab.0.0.5b795ca9DVTjx3 #### function ##### 获取天气api的key ```java package com.alibaba.cloud.ai.toolcall.component.weather; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "spring.ai.alibaba.toolcalling.weather") public class WeatherProperties { private String apiKey; public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } } ``` ##### 解析工具 ```java package com.alibaba.cloud.ai.toolcall.component.weather; import cn.hutool.extra.pinyin.PinyinUtil; public class WeatherUtils { public static String preprocessLocation(String location) { if (containsChinese(location)) { return PinyinUtil.getPinyin(location, ""); } return location; } public static boolean containsChinese(String str) { return str.matches(".*[\u4e00-\u9fa5].*"); } } ``` ##### 需要打模型调用的方法bean,注册 ```java package com.alibaba.cloud.ai.toolcall.component.weather.function; import com.alibaba.cloud.ai.toolcall.component.weather.WeatherProperties; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Description; @Configuration @ConditionalOnClass(WeatherService.class) @ConditionalOnProperty(prefix = "spring.ai.alibaba.toolcalling.weather", name = "enabled", havingValue = "true") public class WeatherAutoConfiguration { @Bean(name = "getWeatherFunction") @ConditionalOnMissingBean @Description("Use api.weather to get weather information.") public WeatherService getWeatherServiceFunction(WeatherProperties properties) { return new WeatherService(properties); } } ``` ##### 核心获取天气状态的函数被方法实现 ```java package com.alibaba.cloud.ai.toolcall.component.weather.function; import com.alibaba.cloud.ai.toolcall.component.weather.WeatherProperties; import com.alibaba.cloud.ai.toolcall.component.weather.WeatherUtils; import com.fasterxml.jackson.annotation.JsonClassDescription; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; import java.util.List; import java.util.Map; import java.util.function.Function; /** * @author yingzi * @date 2025/3/27:11:07 */ public class WeatherService implements Function { private static final Logger logger = LoggerFactory.getLogger(WeatherService.class); private static final String WEATHER_API_URL = "https://api.weatherapi.com/v1/forecast.json"; private final WebClient webClient; private final ObjectMapper objectMapper = new ObjectMapper(); public WeatherService(WeatherProperties properties) { this.webClient = WebClient.builder() .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded") .defaultHeader("key", properties.getApiKey()) .build(); } public static Response fromJson(Map json) { Map location = (Map) json.get("location"); Map current = (Map) json.get("current"); Map forecast = (Map) json.get("forecast"); List> forecastDays = (List>) forecast.get("forecastday"); String city = (String) location.get("name"); return new Response(city, current, forecastDays); } @Override public Response apply(Request request) { if (request == null || !StringUtils.hasText(request.city())) { logger.error("Invalid request: city is required."); return null; } String location = WeatherUtils.preprocessLocation(request.city()); String url = UriComponentsBuilder.fromHttpUrl(WEATHER_API_URL) .queryParam("q", location) .queryParam("days", request.days()) .toUriString(); logger.info("url : {}", url); try { Mono responseMono = webClient.get().uri(url).retrieve().bodyToMono(String.class); String jsonResponse = responseMono.block(); assert jsonResponse != null; Response response = fromJson(objectMapper.readValue(jsonResponse, new TypeReference>() { })); logger.info("Weather data fetched successfully for city: {}", response.city()); return response; } catch (Exception e) { logger.error("Failed to fetch weather data: {}", e.getMessage()); return null; } } @JsonInclude(JsonInclude.Include.NON_NULL) @JsonClassDescription("Weather Service API request") public record Request( @JsonProperty(required = true, value = "city") @JsonPropertyDescription("city name") String city, @JsonProperty(required = true, value = "days") @JsonPropertyDescription("Number of days of weather forecast. Value ranges from 1 to 14") int days) { } @JsonClassDescription("Weather Service API response") public record Response( @JsonProperty(required = true, value = "city") @JsonPropertyDescription("city name") String city, @JsonProperty(required = true, value = "current") @JsonPropertyDescription("Current weather info") Map current, @JsonProperty(required = true, value = "forecastDays") @JsonPropertyDescription("Forecast weather info") List> forecastDays) { } } ``` #### method ##### 核心获取天气状态的函数被方法实现 ```java package com.alibaba.cloud.ai.toolcall.component.weather.method; import com.alibaba.cloud.ai.toolcall.component.weather.WeatherProperties; import com.alibaba.cloud.ai.toolcall.component.weather.WeatherUtils; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.http.HttpHeaders; import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; import java.util.List; import java.util.Map; public class WeatherTools { private static final Logger logger = LoggerFactory.getLogger(WeatherTools.class); private static final String WEATHER_API_URL = "https://api.weatherapi.com/v1/forecast.json"; private final WebClient webClient; private final ObjectMapper objectMapper = new ObjectMapper(); public WeatherTools(WeatherProperties properties) { this.webClient = WebClient.builder() .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded") .defaultHeader("key", properties.getApiKey()) .build(); } @Tool(description = "Use api.weather to get weather information.") public Response getWeatherServiceMethod(@ToolParam(description = "City name") String city, @ToolParam(description = "Number of days of weather forecast. Value ranges from 1 to 14") int days) { if (!StringUtils.hasText(city)) { logger.error("Invalid request: city is required."); return null; } String location = WeatherUtils.preprocessLocation(city); String url = UriComponentsBuilder.fromHttpUrl(WEATHER_API_URL) .queryParam("q", location) .queryParam("days", days) .toUriString(); logger.info("url : {}", url); try { Mono responseMono = webClient.get().uri(url).retrieve().bodyToMono(String.class); String jsonResponse = responseMono.block(); assert jsonResponse != null; Response response = fromJson(objectMapper.readValue(jsonResponse, new TypeReference>() { })); logger.info("Weather data fetched successfully for city: {}", response.city()); return response; } catch (Exception e) { logger.error("Failed to fetch weather data: {}", e.getMessage()); return null; } } public static Response fromJson(Map json) { Map location = (Map) json.get("location"); Map current = (Map) json.get("current"); Map forecast = (Map) json.get("forecast"); List> forecastDays = (List>) forecast.get("forecastday"); String city = (String) location.get("name"); return new Response(city, current, forecastDays); } public record Response(String city, Map current, List> forecastDays) { } } ``` #### controller调用 使用function方式时,直接在tools(这种方式会固化大模型的角色,更偏向于某个工具)传入自动配置过的函数方法名即可,如果是使用method方式,可以手动的创建对象的方式调用,会根据tool描述运行方法。 ```java package com.alibaba.cloud.ai.toolcall.controller; import com.alibaba.cloud.ai.toolcall.component.weather.WeatherProperties; import com.alibaba.cloud.ai.toolcall.component.weather.method.WeatherTools; import org.springframework.ai.chat.client.ChatClient; 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; @RestController @RequestMapping("/weather") public class WeatherController { private final ChatClient dashScopeChatClient; private final WeatherProperties weatherProperties; public WeatherController(ChatClient.Builder chatClientBuilder, WeatherProperties weatherProperties) { this.dashScopeChatClient = chatClientBuilder.build(); this.weatherProperties = weatherProperties; } /** * 无工具版 */ @GetMapping("/chat") public String simpleChat(@RequestParam(value = "query", defaultValue = "请告诉我北京1天以后的天气") String query) { return dashScopeChatClient.prompt(query).call().content(); } /** * 调用工具版 - function */ @GetMapping("/chat-tool-function") public String chatTranslateFunction(@RequestParam(value = "query", defaultValue = "请告诉我北京1天以后的天气") String query) { return dashScopeChatClient.prompt(query).tools("getWeatherFunction").call().content(); } /** * 调用工具版 - method */ @GetMapping("/chat-tool-method") public String chatTranslateMethod(@RequestParam(value = "query", defaultValue = "请告诉我北京1天以后的天气") String query) { return dashScopeChatClient.prompt(query).tools(new WeatherTools(weatherProperties)).call().content(); } } ``` ## record关键字 ```java @JsonInclude(JsonInclude.Include.NON_NULL) @JsonClassDescription("Weather Service API request") public record Request( @JsonProperty(required = true, value = "city") @JsonPropertyDescription("city name") String city, @JsonProperty(required = true, value = "days") @JsonPropertyDescription("Number of days of weather forecast. Value ranges from 1 to 14") int days) { } @JsonClassDescription("Weather Service API response") public record Response( @JsonProperty(required = true, value = "city") @JsonPropertyDescription("city name") String city, @JsonProperty(required = true, value = "current") @JsonPropertyDescription("Current weather info") Map current, @JsonProperty(required = true, value = "forecastDays") @JsonPropertyDescription("Forecast weather info") List> forecastDays) { } ``` record是在 **Java 14** 引入的,用于定义不可变的数据载体类,自动生成构造函数、访问器、equals、hashCode和toString方法。在Java中,`record`关键字用于定义**不可变的数据载体类**,其核心作用是简化纯数据对象的创建,特别适用于DTO(数据传输对象)、配置类等场景。结合上面的代码片段,以下是详细解析: --- ### 一、`record`的核心特性 1. **自动生成样板代码** • 自动生成全字段构造器 • 生成字段访问器(如`city()`而非`getCity()`) • 默认实现`equals()`/`hashCode()`(基于字段值比较) • 生成`toString()`方法(显示所有字段值) 2. **不可变性** `record`的所有字段默认是`final`的,对象一旦创建后不可修改。例如: ```java Request req = new Request("北京", 3); req.city() // 可访问但无法修改值 ``` 3. **紧凑构造器支持** 可通过紧凑构造器添加验证逻辑(虽然您的代码未展示,但实际可扩展): ```java public record Request(String city, int days) { public Request { if (days < 1 || days > 14) throw new IllegalArgumentException(); } } ``` --- ### 二、在代码中的具体应用 #### 1. **DTO(数据传输对象)建模** • **`Request`记录类** 表示API请求参数: • `@JsonProperty`标注JSON字段名和必要性(`required = true`) • `days`字段通过注解限定取值范围(1-14天) • **`Response`记录类** 表示API响应体: • `current`字段存储当前天气的键值对(如温度、湿度) • `forecastDays`字段为未来天气的列表结构 #### 2. **与Jackson库集成** • **序列化控制** `@JsonInclude(Include.NON_NULL)`表示仅序列化非空字段,避免传输冗余数据。 • **文档生成支持** `@JsonClassDescription`和`@JsonPropertyDescription`可为Swagger等文档工具提供字段说明,例如: ```json // 生成的API文档片段 { "city": "string // city name", "days": "integer // 1-14天预报" } ``` #### 3. **模式匹配潜力** 虽然当前代码未使用,但`record`可与Java 17+的`instanceof`模式匹配结合: ```java if (obj instanceof Response(String city, Map current, List forecast)) { System.out.println(city + "当前温度:" + current.get("temp")); } ``` --- ### 三、与传统类的对比 | 特性 | `record` | 传统类 + Lombok | | ------------ | --------------------------------- | ---------------------- | | 代码量 | 1行定义(自动生成方法) | 需写字段+@Getter+@Data | | 不可变性 | 默认不可变 | 需手动添加`final` | | JSON注解支持 | 直接标注参数(如`@JsonProperty`) | 需在字段或方法上标注 | | 继承灵活性 | 不能继承其他类 | 可自由继承 | --- ### 四、适用场景建议 1. **REST API参数/响应体** 如代码所示,`record`是理想的DTO载体。 2. **配置类** ```java public record RedisConfig(String host, int port, String password) {} ``` 3. **值对象** ```java public record Coordinate(double lat, double lng) {} ``` --- 通过`record`关键字,Java在保持类型安全的同时,显著减少了模板代码量,使得数据建模更加高效。您提供的代码正是这一特性的典型应用场景——通过简洁的语法定义API交互协议,同时通过注解实现细粒度的序列化控制 ## var关键字 本地变量类型推断(`var`,JDK 10)的核心是通过**编译器自动推断局部变量的类型**,从而减少开发者显式声明冗长类型的负担,尤其是在处理集合操作和复杂泛型类型时,其简化作用尤为显著。以下是具体含义和实际应用场景的解读: --- ### 一、**简化集合操作** 1. **泛型类型声明简化** 在传统集合操作中,泛型类型需要重复声明两次(左侧变量类型和右侧初始化类型)。使用`var`后,仅需在右侧初始化时指定泛型类型,左侧类型由编译器自动推断。例如: ```java // 传统写法 Map>> complexMap = new HashMap<>(); // 使用var简化 var complexMap = new HashMap>>(); ``` 这样既避免了重复书写冗长的泛型类型,又保持了代码的清晰性。 2. **循环操作优化** 在增强型`for`循环和传统`for`循环中,`var`可省略迭代变量的显式类型声明: ```java // 传统循环 for (Map.Entry> entry : customerMap.entrySet()) { ... } // 使用var简化 for (var entry : customerMap.entrySet()) { ... } ``` 编译器会根据`customerMap`的类型推断`entry`的实际类型(如`Map.Entry>`)。 --- ### 二、**简化复杂类型声明** 1. **长类名或嵌套泛型** 当类名或泛型嵌套层次较深时,`var`可显著减少代码冗余。例如: ```java // 传统写法 ItIsAVeryLongNameJavaClass obj = new ItIsAVeryLongNameJavaClass(); // 使用var简化 var obj = new ItIsAVeryLongNameJavaClass(); ``` 这种写法在涉及多层泛型(如`List>>`)时优势更明显。 2. **匿名内部类与Lambda表达式** `var`支持匿名内部类和Lambda表达式的类型推断: ```java // 匿名内部类 var task = new Runnable() { @Override public void run() { ... } }; // Lambda参数类型推断(JDK 11+) names.forEach((var name) -> System.out.println(name)); ``` 编译器会根据上下文自动推断`task`的类型为`Runnable`,`name`的类型为`String`。 --- ### 三、**技术限制与最佳实践** 1. **适用范围限制** • 仅限**局部变量**(方法内、循环内、`try-with-resources`等),不能用于类字段、方法参数、返回值类型。 • 必须**初始化赋值**(如`var list;`会报错),且不能赋值为`null`(无法推断类型)。 2. **可读性平衡** • **推荐场景**:类型信息明显时(如`new ArrayList()`),或变量名能明确表达语义(如`var customerList = ...`)。 • **避免滥用**:若初始化表达式类型不直观(如`var data = fetchData();`),建议保留显式类型声明以增强可读性。 --- ### 四、**底层原理与性能** • **编译期类型推断**:`var`仅是语法糖,编译后字节码中变量类型会被替换为实际推断类型,不影响运行时性能。 • **类型安全性**:Java仍是强类型语言,`var`不会弱化类型检查。例如,`var num = 10; num = "string";`会因类型不匹配而编译失败。 --- ### 总结 `var`通过**减少冗余的类型声明**,使代码更简洁、易维护,尤其适用于集合操作和复杂泛型场景。但其使用需遵循“**局部变量**+**明确类型上下文**”的原则,避免因过度简化导致可读性下降。合理利用`var`,开发者可以更专注于业务逻辑的实现,而非语法细节。 ## 模型上下文协议Model Context Protocol ### MCP 简介 [模型上下文协议(即 Model Context Protocol,MCP)](https://modelcontextprotocol.io/)是一个开放协议,它规范了应用程序如何向大型语言模型(LLM)提供上下文。MCP 提供了一种统一的方式将 AI 模型连接到不同的数据源和工具,它定义了统一的集成方式。在开发智能体(Agent)的过程中,我们经常需要将将智能体与数据和工具集成,MCP 以标准的方式规范了智能体与数据及工具的集成方式,可以帮助您在 LLM 之上构建智能体(Agent)和复杂的工作流。目前已经有大量的服务接入并提供了 MCP server 实现,当前这个生态正在以非常快的速度不断的丰富中,具体可参见:[MCP Servers](https://github.com/modelcontextprotocol/servers?spm=4347728f.1fc18373.0.0.2d6e6a08TQwFjC)。 ### Spring AI MCP Spring AI MCP 为模型上下文协议提供 Java 和 Spring 框架集成。它使 Spring AI 应用程序能够通过标准化的接口与不同的数据源和工具进行交互,支持同步和异步通信模式。 ![spring-ai-mcp-architecture](README.assets/spring-ai-mcp-architecture.png) Spring AI MCP 采用模块化架构,包括以下组件: - Spring AI 应用程序:使用 Spring AI 框架构建想要通过 MCP 访问数据的生成式 AI 应用程序 - Spring MCP 客户端:MCP 协议的 Spring AI 实现,与服务器保持 1:1 连接 - MCP 服务器:轻量级程序,每个程序都通过标准化的模型上下文协议公开特定的功能 - 本地数据源:MCP 服务器可以安全访问的计算机文件、数据库和服务 - 远程服务:MCP 服务器可以通过互联网(例如,通过 API)连接到的外部系统 ### 使用MCP 要使用 MCP,首先需要创建`McpClient`,它提供了与 MCP server 的同步和异步通信能力。现在我们创建一个 McpClient 来注册 MCP Brave 服务和 ChatClient,从而让 LLM 调用它们: ```java var stdioParams = ServerParameters.builder("npx") .args("-y", "@modelcontextprotocol/server-brave-search") .addEnvVar("BRAVE_API_KEY", System.getenv("BRAVE_API_KEY")) .build(); /* 如果上述命令在windows中执行会出现报错,就执行下面的命令 var stdioParams = ServerParameters.builder("node").args( "D:\\nvm\\nodejs\\node_modules\\@modelcontextprotocol\\server-filesystem\\dist" , getFilePath() //这个地方获取的是工作目录,可以在配置中配置,防止文件找不到或者没有权限 ).build(); */ var mcpClient = McpClient.using(new StdioClientTransport(stdioParams)).sync(); var init = mcpClient.initialize(); var chatClient = chatClientBuilder .defaultFunctions(mcpClient.listTools(null) .tools() .stream() .map(tool -> new McpFunctionCallback(mcpClient, tool)) .toArray(McpFunctionCallback[]::new)) .build(); String response = chatClient .prompt("Does Spring AI supports the Model Context Protocol? Please provide some references.") .call().content(); ``` 在上述代码中,首先通过`npx`命令启动一个独立的进程,运行`@modelcontextprotocol/server-brave-search`服务,并指定 Brave API 密钥。然后创建一个基于 stdio 的传输层,与 MCP server 进行通信。最后初始化与 MCP 服务器的连接。 要使用 McpClient,需要将`McpClient`注入到 Spring AI 的`ChatClient`中,从而让 LLM 调用 MCP server。在 Spring AI 中,可以通过 Function Callbacks 的方式将 MCP 工具转换为 Spring AI 的 Function,从而让 LLM 调用。 最后,通过`ChatClient`与 LLM 进行交互,并使用`McpClient`与 MCP server 进行通信,获取最终的返回结果。 ### 前提条件 安装对应的npx组件,安装命令前需要确保机器上有npm: ```shell npm install -g npx # 如果上面那个失败就执行下面这个 npm install -g npx --force ``` ```bash set AI_DASHSCOPE_API_KEY=sk-97ff8c489c4146e8ad9c16a265b1c85c echo %AI_DASHSCOPE_API_KEY% ``` ### win idea启动报错的问题 ```shell npx -y @modelcontextprotocol/server-filesystem "E://aidemo" ``` 在windows中执行上述命令mcp启动容易出现问题,如果安装了npx,启动mcp组件报错,找不到index.js或者是找不到命令,那么可以尝试用node + 插件目录启动。 ```shell node D:\nvm\nodejs\node_modules\@modelcontextprotocol\server-filesystem\dist "E://aidemo" ``` ### 集成到java中-文件系统案例 ```java package com.aisino.springai.demos.chat.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.ai.mcp.client.McpClient; import org.springframework.ai.mcp.client.McpSyncClient; import org.springframework.ai.mcp.client.transport.ServerParameters; import org.springframework.ai.mcp.client.transport.StdioClientTransport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.StringUtils; import java.nio.file.Paths; import java.time.Duration; /** * @author guochuantao * @version 1.0 * @description * @since 2025/4/7 下午5:15 */ // 服务配置(McpConfig.java) @Configuration public class McpConfig { @Bean(destroyMethod = "close") public McpSyncClient mcpClient(ObjectMapper objectMapper) { // 1. 配置标准输入输出传输(StdioServerTransport) // var serverParameters = ServerParameters.builder("npx.cmd") // .args("-y", "@modelcontextprotocol/server-filesystem", // getFilePath()).build(); var serverParameters = ServerParameters.builder("node").args( "D:\\nvm\\nodejs\\node_modules\\@modelcontextprotocol\\server-filesystem\\dist" , getFilePath() ).build(); // 创建同步MCP客户端 var mcpClient = McpClient.using(new StdioClientTransport(serverParameters)) .requestTimeout(Duration.ofSeconds(10)).sync(); var init = mcpClient.initialize(); System.err.println("初始化结果:" + init); return mcpClient; } /** * 获取文件路径 如果找不到文件需要将 配置更改为当前工作目录 */ private static String getFilePath() { String path = System.getenv("MCP_FILE_DIRECTORY_PATH"); String finalPath = StringUtils.isEmpty(path) ? getDbPath() : path; System.err.println("文件路径:" + finalPath); return finalPath; } private static String getDbPath() { return Paths.get(System.getProperty("user.dir")).toString(); } /** * 创建MCP回调函数 将mcp服务器中的工具列表转化为Function Callbac List */ @Bean public List functionCallbacks(McpSyncClient mcpClient) { // 获取MCP服务器中的工具列表 return mcpClient.listTools(null) // 将每个工具转换为Function Callback .tools() .stream() .map(tool -> new McpFunctionCallback(mcpClient, tool)) .toList(); } /** * 创建MCP聊天客户端 */ @Bean public ChatClient mcpChatClient(ChatClient.Builder chatClient, List functionCallbacks) { System.err.println(Arrays.toString(functionCallbacks.toArray(FunctionCallback[]::new))); return chatClient.defaultFunctions(functionCallbacks.toArray(new McpFunctionCallback[0])) .defaultSystem("MCP Server 文件系统") .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory())) .build(); } } ``` ### 详细解析 关键组件: 1. **MCP Client**,与 MCP 集成的关键,提供了与本地文件系统进行交互的能力。 2. **Function Callbacks**,Spring AI MCP 的 function calling 声明方式。 3. **Chat Client**,Spring AI 关键组件,用于 LLM 模型交互、智能体代理。 #### 声明 ChatClient ```java // List functionCallbacks; var chatClient = chatClientBuilder.defaultFunctions(functionCallbacks).build(); ``` 和开发之前的 Spring AI 应用一样,我们先定义一个 ChatClient Bean,用于与大模型交互的代理。需要注意的是,我们为 ChatClient 注入的 functions 是通过 MCP 组件(McpFunctionCallback)创建的。 接下来让我们具体看一下 McpFunctionCallback 是怎么使用的。 #### 声明 MCP Function Callbacks 以下代码段通过 `mcpClient`与 MCP server 交互,将 MCP 工具通过 McpFunctionCallback 适配为标准的 Spring AI function。 1. 发现 MCP server 中可用的工具 tool(Spring AI 中叫做 function) 列表 2. 依次将每个 tool 转换成 Spring AI function callback 3. 最终我们会将这些 McpFunctionCallback 注册到 ChatClient 使用 ```java @Bean public List functionCallbacks(McpSyncClient mcpClient) { // 获取MCP服务器中的工具列表 return mcpClient.listTools(null) // 将每个工具转换为Function Callback .tools() .stream() .map(tool -> new McpFunctionCallback(mcpClient, tool)) .toList(); } ``` 可以看出,ChatClient 与模型交互的过程是没有变化的,模型在需要的时候告知 ChatClient 去做函数调用,只不过 Spring AI 通过 McpFunctionCallback 将实际的函数调用过程委托给了 MCP,通过标准的 MCP 协议与本地文件系统交互: - 在与大模交互的过程中,ChatClient 处理相关的 function calls 请求 - ChatClient 调用 MCP 工具(通过 McpClient) - McpClient 与 MCP server(即 filesystem)交互 #### 初始化 McpClient 该智能体应用使用同步 MCP 客户端与本地运行的文件系统 MCP server 通信: ```java @Bean(destroyMethod = "close") public McpSyncClient mcpClient() { // 配置服务器启动参数 var stdioParams = ServerParameters.builder("npx") .args("-y", "@modelcontextprotocol/server-filesystem", "path")) .build(); // 1 // 创建同步MCP客户端 var mcpClient = McpClient.sync(new StdioServerTransport(stdioParams), Duration.ofSeconds(10), new ObjectMapper()); //2 // 初始化客户端连接 var init = mcpClient.initialize(); // 3 System.out.println("MCP Initialized: " + init); return mcpClient; } ``` 在以上代码中: 1. 配置 MCP server 启动命令与参数 2. 初始化 McpClient:关联 MCP server、指定超时时间等 3. Spring AI 会使用 `npx -y @modelcontextprotocol/server-filesystem "/path/to/file"`在本地机器创建一个独立的子进程(代表本地 Mcp server),Spring AI 与 McpClient 通信,McpClient 进而通过与 Mcp server 的连接操作本地文件。 ### 环境变量设置文件位置 直接设置到操作系统中的环境变量即可。 #### 访问项目环境以外的文件信息出现异常,访问拒绝的问题 ```shell JSONRPCResponse[jsonrpc=2.0, id=ba052485-2, result={content=[{type=text, text=Error: Access denied - path outside allowed directories: C:\Users\Aisino丶\Desktop\project\spring_ai_demo\spring_ai_alibaba\data.docx not in E:\aidemo}], isError=true}, error=null] ``` 可以idea设置工作目录为对应的mcpserver文件系统的目录,即可解决无权限访问的问题。 ![1744092946920](README.assets/1744092946920.png) ### controller ```java package com.aisino.springai.demos.chat.controller; import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @author guochuantao * @version 1.0 * @description * @since 2025/4/8 上午10:17 */ @RestController @RequestMapping("/mcp") public class McpController { @Resource(name = "mcpChatClient") private ChatClient mcpChatClient; @RequestMapping("/simple") public String simpleChat() { return mcpChatClient.prompt().user("你是谁").call().content(); } /** * 访问文件 * http://localhost:8080/mcp/file?message=你能解释一下 spring-ai-mcp-overview.txt 文件的内容吗?文件位置在src/main/resources/data里 * 除了指定文件位置,也可以让它递归寻找文件 * http://localhost:8080/mcp/file?message=请总结 spring-ai-mcp-overview.txt 文件的内容,并将其以 Markdown 格式存储到新的 summary.md 文件中? * http://localhost:8080/mcp/file?message=你能解释一下 data.txt文件的内容吗? * http://localhost:8080/mcp/file?message=帮我总结一下并写入到一个summery.md文档中 */ @RequestMapping("/file") public String fileChat(@RequestParam("message") String messages) { return mcpChatClient.prompt() .user(messages) .call().content(); } } ``` ### SQLite应用案例 安装uv ```shell powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` 然后需要保证python环境大于3.11,最好安装新版3.13,然后就可以按照这样设置参数: ```java var stdioParams = ServerParameters.builder("uvx") .args("mcp-server-sqlite", "--db-path", getDbPath()) .build(); ``` getDbPath()获取到对应的SQLite数据库文件 后续继承方式和文件系统案例类似 ![1744101748683](README.assets/1744101748683.png) ![1744101777229](README.assets/1744101777229.png) ### ### 传输层介绍 #### stdio传输层 stdio(标准输入输出)传输层是MCP最基本的传输实现方式。它通过进程间通信(IPC)实现,具体工作原理如下: 1. **进程创建**:MCP客户端会启动一个子进程来运行MCP服务器 2. 通信机制: - 使用标准输入(stdin)向MCP服务器发送请求 - 通过标准输出(stdout)接收MCP服务器的响应 - 标准错误(stderr)用于日志和错误信息 3. 优点: - 简单可靠,无需网络配置 - 适合本地部署场景 - 进程隔离,安全性好 4. 缺点: - 仅支持单机部署 - 不支持跨网络访问 - 每个客户端需要独立启动服务器进程 #### SSE传输层 SSE(Server-Sent Events)传输层是基于HTTP的单向通信机制,专门用于服务器向客户端推送数据。其工作原理如下: 1. 连接建立: - 客户端通过HTTP建立与服务器的持久连接 - 使用`text/event-stream`内容类型 2. 通信机制: - 服务器可以主动向客户端推送消息 - 支持自动重连机制 - 支持事件ID和自定义事件类型 3. 优点: - 支持分布式部署 - 可跨网络访问 - 支持多客户端连接 - 轻量级,使用标准HTTP协议 4. 缺点: - 需要额外的网络配置 - 相比stdio实现略微复杂 - 需要考虑网络安全性 ### 使用starter简化MCP客户端的使用 #### 1、基于stdio的MCP客户端实现 基于stdio的实现是最常见的MCP客户端实现方式,它通过标准输入输出流与MCP服务器进行通信。这种方式适用于本地部署的MCP服务器,可以直接在同一台机器上启动MCP服务器进程。 ##### 添加依赖 首先,在您的项目中添加Spring AI MCP starter依赖: ```xml org.springframework.ai spring-ai-mcp-client-spring-boot-starter ``` ##### 配置MCP服务器 在`application.yml`中配置MCP服务器: ```yml spring: ai: dashscope: # 配置通义千问API密钥 api-key: ${AI_DASHSCOPE_API_KEY} mcp: client: stdio: # 指定MCP服务器配置文件路径(推荐) servers-configuration: classpath:/mcp-servers-config.json # 直接配置示例,和上边的配制二选一 # connections: # server1: # command: java # args: # - -jar # - /path/to/your/mcp-server.jar ``` 这个配置文件设置了MCP客户端的基本配置,包括API密钥和服务器配置文件的位置。你也可以选择直接在配置文件中定义服务器配置。 ```java { "mcpServers": { // 定义名为"weather"的MCP服务器 "weather": { // 指定启动命令为java "command": "java", // 定义启动参数 "args": [ "-Dspring.ai.mcp.server.stdio=true", "-Dspring.main.web-application-type=none", "-jar", "/path/to/your/mcp-server.jar" ], // 环境变量配置(可选) "env": {} } } } ``` 这个JSON配置文件定义了MCP服务器的详细配置,包括如何启动服务器进程、需要传递的参数以及环境变量设置。 ```java package com.aisino; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @SpringBootApplication public class McpStdioApplication { public static void main(String[] args) { SpringApplication.run(McpStdioApplication.class, args); } // 直接硬编码中文问题,避免配置文件编码问题 private String userInput = "武汉今天2025年4月9日的天气如何?"; @Bean public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools, ConfigurableApplicationContext context) { return args -> { var chatClient = chatClientBuilder .defaultTools(tools) .build(); System.out.println("\n>>> QUESTION: " + userInput); System.out.println("\n>>> ASSISTANT: " + chatClient.prompt(userInput).call().content()); context.close(); }; } } ``` ##### MCP Server服务可以参考这个工程 mcp-stdio-server-exmaple > 启动client客户端的时候需要保证mcp server服务端的jar包是打好了的。 ![1744168306761](README.assets/1744168306761.png) ##### 执行效果日志 ```java >>> QUESTION: 武汉今天2025年4月9日的天气如何? 2025-04-09T10:56:21.564+08:00 DEBUG 3208 --- [pool-2-thread-1] io.modelcontextprotocol.spec.McpSchema : Received JSON message: {"jsonrpc":"2.0","id":"cf563376-3","result":{"content":[{"type":"text","type":"text","text":"\"当前天气:\\n温度: 27.5°C (体感温度: 31.2°C)\\n天气: 晴朗\\n风向: 西风 (6.5 km/h)\\n湿度: 56%\\n降水量: 0.0 毫米\\n\\n未来天气预报:\\n2025-04-09 (周三):\\n温度: 19.9°C ~ 31.4°C\\n天气: 多云\\n风向: 西北风 (6.8 km/h)\\n降水量: 0.0 毫米\\n\\n2025-04-10 (周四):\\n温度: 19.5°C ~ 30.6°C\\n天气: 多云\\n风向: 西南风 (6.3 km/h)\\n降水量: 0.1 毫米\\n\\n2025-04-11 (周五):\\n温度: 19.7°C ~ 28.7°C\\n天气: 雷暴\\n风向: 东风 (16.7 km/h)\\n降水量: 33.5 毫米\\n\\n2025-04-12 (周六):\\n温度: 14.4°C ~ 19.9°C\\n天气: 多云\\n风向: 北风 (26.0 km/h)\\n降水量: 0.0 毫米\\n\\n2025-04-13 (周日):\\n温度: 12.1°C ~ 25.4°C\\n天气: 多云\\n风向: 西南风 (16.9 km/h)\\n降水量: 0.0 毫米\\n\\n2025-04-14 (周一):\\n温度: 16.2°C ~ 29.1°C\\n天气: 多云\\n风向: 西南风 (11.1 km/h)\\n降水量: 0.0 毫米\\n\\n2025-04-15 (周二):\\n温度: 18.8°C ~ 30.0°C\\n天气: 多云\\n风向: 南风 (6.8 km/h)\\n降水量: 0.0 毫米\\n\\n\""}],"isError":false}} 2025-04-09T10:56:21.564+08:00 DEBUG 3208 --- [pool-2-thread-1] i.m.spec.DefaultMcpSession : Received Response: JSONRPCResponse[jsonrpc=2.0, id=cf563376-3, result={content=[{type=text, text="当前天气:\n温度: 27.5°C (体感温度: 31.2°C)\n天气: 晴朗\n风向: 西风 (6.5 km/h)\n湿度: 56%\n降水量: 0.0 毫米\n\n未来天气预报:\n2025-04-09 (周三):\n温度: 19.9°C ~ 31.4°C\n天气: 多云\n风向: 西北风 (6.8 km/h)\n降水量: 0.0 毫米\n\n2025-04-10 (周四):\n温度: 19.5°C ~ 30.6°C\n天气: 多云\n风向: 西南风 (6.3 km/h)\n降水量: 0.1 毫米\n\n2025-04-11 (周五):\n温度: 19.7°C ~ 28.7°C\n天气: 雷暴\n风向: 东风 (16.7 km/h)\n降水量: 33.5 毫米\n\n2025-04-12 (周六):\n温度: 14.4°C ~ 19.9°C\n天气: 多云\n风向: 北风 (26.0 km/h)\n降水量: 0.0 毫米\n\n2025-04-13 (周日):\n温度: 12.1°C ~ 25.4°C\n天气: 多云\n风向: 西南风 (16.9 km/h)\n降水量: 0.0 毫米\n\n2025-04-14 (周一):\n温度: 16.2°C ~ 29.1°C\n天气: 多云\n风向: 西南风 (11.1 km/h)\n降水量: 0.0 毫米\n\n2025-04-15 (周二):\n温度: 18.8°C ~ 30.0°C\n天气: 多云\n风向: 南风 (6.8 km/h)\n降水量: 0.0 毫米\n\n"}], isError=false}, error=null] >>> ASSISTANT: 武汉2025年4月9日的天气预报为多云,温度范围将在19.9°C至31.4°C之间。风向为西北风,风速为6.8 km/h,预计没有降水。 请注意,这只是一个模拟的天气预报数据示例,并不代表实际未来的天气情况。 2025-04-09T10:56:25.730+08:00 INFO 3208 --- [ main] o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete ``` #### 2、基于SSE的MCP客户端实现 除了基于stdio的实现外,Spring AI Alibaba还提供了基于Server-Sent Events (SSE)的MCP客户端实现。这种方式适用于远程部署的MCP服务器,可以通过HTTP协议与MCP服务器进行通信。 ##### 添加依赖 首先,在您的项目中添加Spring AI MCP starter依赖: ```xml org.springframework.ai spring-ai-mcp-client-webflux-spring-boot-starter ``` ##### 配置MCP服务器 在`application.yml`中配置MCP服务器: ```yaml spring: ai: dashscope: api-key: ${AI_DASHSCOPE_API_KEY} mcp: client: sse: connections: server1: url: http://localhost:8080 ``` #### 总结 使用Spring AI Alibaba提供的MCP starter,可以大大简化MCP客户端的配置和使用。您只需要添加相应的依赖,配置MCP服务器,然后注入`ToolCallbackProvider`和`ChatClient.Builder`即可使用MCP功能。 根据您的部署需求,可以选择基于stdio的实现或基于SSE的实现。基于stdio的实现适用于本地部署的MCP服务器,而基于SSE的实现适用于远程部署的MCP服务器。 ### 使用Spring AI MCP Server Starter实现MCP服务端 使用Spring AI MCP Server Starter来实现MCP服务端,包括基于stdio的服务端和基于SSE的服务端两种实现方式。 #### 1、基于stdio的MCP服务端实现 基于stdio的MCP服务端通过标准输入输出流与客户端通信,适用于作为子进程被客户端启动和管理的场景,非常适合嵌入式应用。 ##### 添加依赖 首先,在您的项目中添加Spring AI MCP Server Starter依赖: ```xml org.springframework.ai spring-ai-mcp-server-spring-boot-starter ``` ##### 配置MCP服务端 在`application.yml`中配置MCP服务端: ```yaml spring: main: web-application-type: none # 必须禁用web应用类型 banner-mode: off # 禁用banner ai: mcp: server: stdio: true # 启用stdio模式 name: my-weather-server # 服务器名称 version: 0.0.1 # 服务器版本 ``` ##### 实现MCP工具 使用`@Tool`注解标记方法,使其可以被MCP客户端发现和调用: ```java @Service public class OpenMeteoService { private final WebClient webClient; public OpenMeteoService(WebClient.Builder webClientBuilder) { this.webClient = webClientBuilder .baseUrl("https://api.open-meteo.com/v1") .build(); } @Tool(description = "根据经纬度获取天气预报") public String getWeatherForecastByLocation( @ToolParameter(description = "纬度,例如:39.9042") String latitude, @ToolParameter(description = "经度,例如:116.4074") String longitude) { try { String response = webClient.get() .uri(uriBuilder -> uriBuilder .path("/forecast") .queryParam("latitude", latitude) .queryParam("longitude", longitude) .queryParam("current", "temperature_2m,wind_speed_10m") .queryParam("timezone", "auto") .build()) .retrieve() .bodyToMono(String.class) .block(); // 解析响应并返回格式化的天气信息 // 这里简化处理,实际应用中应该解析JSON return "当前位置(纬度:" + latitude + ",经度:" + longitude + ")的天气信息:\n" + response; } catch (Exception e) { return "获取天气信息失败:" + e.getMessage(); } } @Tool(description = "根据经纬度获取空气质量信息") public String getAirQuality( @ToolParameter(description = "纬度,例如:39.9042") String latitude, @ToolParameter(description = "经度,例如:116.4074") String longitude) { // 模拟数据,实际应用中应调用真实API return "当前位置(纬度:" + latitude + ",经度:" + longitude + ")的空气质量:\n" + "- PM2.5: 15 μg/m³ (优)\n" + "- PM10: 28 μg/m³ (良)\n" + "- 空气质量指数(AQI): 42 (优)\n" + "- 主要污染物: 无"; } } ``` ##### 注册MCP工具 在应用程序入口类中注册工具: ```java @SpringBootApplication public class McpServerApplication { public static void main(String[] args) { SpringApplication.run(McpServerApplication.class, args); } @Bean public ToolCallbackProvider weatherTools(OpenMeteoService openMeteoService) { return MethodToolCallbackProvider.builder() .toolObjects(openMeteoService) .build(); } } ``` ##### 运行服务端 编译并打包应用: ```shell mvn clean package -DskipTests ``` #### 2、基于SSE的MCP服务端实现 基于SSE的MCP服务端通过HTTP协议与客户端通信,适用于作为独立服务部署的场景,可以被多个客户端远程调用。 ##### 添加依赖 首先,在您的项目中添加Spring AI MCP Server Starter依赖和Spring WebFlux依赖: ```xml org.springframework.ai spring-ai-mcp-server-webflux-spring-boot-starter ``` ##### 配置MCP服务端 在`application.yml`中配置MCP服务端: ```yaml server: port: 8080 # 服务器端口配置 spring: ai: mcp: server: name: my-weather-server # MCP服务器名称 version: 0.0.1 # 服务器版本号 ``` ##### 实现MCP工具 与基于stdio的实现相同,使用`@Tool`注解标记方法。 ##### 注册MCP工具 在应用程序入口类中注册工具: ```java @SpringBootApplication public class McpServerApplication { public static void main(String[] args) { SpringApplication.run(McpServerApplication.class, args); } @Bean public ToolCallbackProvider weatherTools(OpenMeteoService openMeteoService) { return MethodToolCallbackProvider.builder() .toolObjects(openMeteoService) .build(); } @Bean public WebClient.Builder webClientBuilder() { return WebClient.builder(); } } ``` ##### 运行服务端 编译并打包应用: ```shell mvn clean package -DskipTests ``` 运行服务端: ```shell mvn spring-boot:run ``` 服务端将在 [http://localhost:8080](http://localhost:8080/) 启动。 ### MCP服务端与客户端的交互 #### 基于stdio的交互 客户端通过启动服务端进程并通过标准输入输出流与其通信: ```java // 客户端代码示例 var stdioParams = ServerParameters.builder("java") // 设置必要的系统属性 .args("-Dspring.ai.mcp.server.stdio=true", "-Dspring.main.web-application-type=none", "-Dlogging.pattern.console=", "-jar", "target/mcp-stdio-server-example-0.0.1-SNAPSHOT.jar") .build(); // 创建基于stdio的传输层 var transport = new StdioClientTransport(stdioParams); // 构建同步MCP客户端 var client = McpClient.sync(transport).build(); // 初始化客户端连接 client.initialize(); // 调用天气预报工具 CallToolResult weatherResult = client.callTool( new CallToolRequest("getWeatherForecastByLocation", Map.of("latitude", "39.9042", "longitude", "116.4074")) ); // 打印结果 System.out.println(weatherResult.getContent()); ``` 这段代码展示了基于stdio的MCP客户端如何与服务端交互。它通过启动服务端进程并通过标准输入输出流与其通信,实现了天气预报功能的调用。 #### 基于stdio的交互 ```java // SSE客户端代码示例 var transport = new WebFluxSseClientTransport( // 配置WebClient基础URL WebClient.builder().baseUrl("http://localhost:8080") ); // 构建同步MCP客户端 var client = McpClient.sync(transport).build(); // 初始化客户端连接 client.initialize(); // 调用天气预报工具 CallToolResult weatherResult = client.callTool( new CallToolRequest("getWeatherForecastByLocation", Map.of("latitude", "39.9042", "longitude", "116.4074")) ); // 打印结果 System.out.println(weatherResult.getContent()); ``` 这段代码展示了基于SSE的MCP客户端如何与服务端交互。它通过HTTP协议与服务端通信,实现了相同的天气预报功能调用。 ### MCP服务端开发最佳实践 1. **工具设计**: - 每个工具方法应该有明确的功能和参数 - 使用`@Tool`注解提供详细的描述 - 使用`@ToolParameter`注解描述每个参数的用途 2. **错误处理**: - 捕获并处理所有可能的异常 - 返回友好的错误信息,便于客户端理解和处理 3. **性能优化**: - 对于耗时操作,考虑使用异步处理 - 合理设置超时时间,避免客户端长时间等待 4. **安全考虑**: - 对于敏感操作,添加适当的权限验证 - 避免在工具方法中执行危险操作,如执行任意命令 5. **部署策略**: - stdio模式适合嵌入式场景,作为客户端的子进程运行 - SSE模式适合作为独立服务部署,可以被多个客户端调用 ### MCP总结 Spring AI MCP Server Starter提供了两种实现MCP服务端的方式:基于stdio的实现和基于SSE的实现。基于stdio的实现适用于嵌入式场景,而基于SSE的实现适用于独立服务部署。 通过使用`@Tool`注解和`@ToolParameter`注解,可以轻松地将普通的Java方法转换为MCP工具,使其可以被MCP客户端发现和调用。Spring Boot的自动配置机制使得MCP服务端的开发变得简单高效。