Spring AI Alibaba学习

114 阅读16分钟

Spring AI 官网:引言 : Spring AI Reference(中文版)

Spring AI Alibaba 官网:java2ai.com  

Spring AI Alibaba 仓库:github.com/alibaba/spr…

Spring AI Alibaba 官方示例仓库:github.com/springaiali…

Spring AI 1.0 GA 文章:java2ai.com/blog/spring…

Spring AI 仓库:github.com/spring-proj…

大模型服务平台百炼控制台

LangChain4j

Hello World入门

使用的是阿里云百炼平台,阿里云百炼是一个大模型服务平台,它的核心目标是让企业、开发者和个人能够以最低的成本、最简单的操作,快速使用和集成各种顶尖的大语言模型来构建自己的AI应用。

  • 阿里云百炼:正是一个现代化、综合性的“大模型食堂”

  • 模型提供商(通义、ChatGLM等) :就是来自五湖四海的星级厨师团队,各有拿手菜系(有的擅长写代码,有的擅长写文案,有的精通多语言)。

  • (用户) :就是来就餐的顾客。 在这个食堂里,您享受的服务远超一个普通食堂:

  1. “自助餐”与“单点” :您可以直接“点餐”(调用某个现成模型),也可以选择“自助餐”,品尝不同厨师的招牌菜,看看哪道最合您的口味(对比不同模型的输出)。
  2. “定制私房菜” :您不仅可以点菜单上的菜,还可以带着自己的独家食材(您的业务数据) ,请任何一位厨师根据您的口味,为您量身定制一道私房菜(模型精调) 。比如,您提供公司的产品手册,厨师就能做出完全符合您公司风格的“宣传文案”这道菜。
  3. “万能调味料” :食堂还提供一种神奇的  “调味料”(RAG和Prompt工程)  。即使厨师不知道某个冷知识,您通过这种调味料,可以瞬间让他查阅并理解您带来的一本参考书(您的知识库),然后做出专业回答。
  4. “免厨房”体验:您完全不需要关心后厨有多少口锅、灶火有多旺、食材怎么采购(即无需管理GPU服务器、运维环境等基础设施)。您只需要告诉食堂您想吃什么,他们就会把做好的菜端到您面前

环境配置

引入相关的依赖,包括:Spring AI和Spring AI Alibaba。

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
    <java.version>21</java.version>
    <!-- Spring Boot 新建2025.9-->
    <spring-boot.version>3.5.5</spring-boot.version>
    <!-- Spring AI 新建2025.9-->
    <spring-ai.version>1.0.0</spring-ai.version>
    <!-- Spring AI Alibaba 新建2025.9-->
    <SpringAIAlibaba.version>1.0.0.2</SpringAIAlibaba.version>
</properties>

<dependencyManagement>
    <dependencies>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- Spring AI Alibaba -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-bom</artifactId>
            <version>${SpringAIAlibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- Spring AI -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

在百炼控制台上获取到对应的API-Key和模型代码之后,在application.yml文件中进行配置,有两种方式:dashscope协议或者openai访问:

# ====SpringAIAlibaba Config=============
# 通过dashscope协议访问
#spring.ai.dashscope.api-key=${aliQwen-api}
#spring.ai.dashscope.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1
#spring.ai.dashscope.chat.options.model=deepseek-v3


# ====SpringAI Config=============
#通过openai协议调用通义千问
#spring.ai.openai.api-key=${aliQwen-api}
#spring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode
#spring.ai.openai.chat.options.model=qwen-plus

注意,如果要使用openAI的方式,那么依赖需要变为:

<!-- spring-ai-openai -- >
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

创建一个配置类,注入对应的DashScopeApi对象:

@Configuration
public class SaaLLMConfig
{

    /**
     * 方式1:${}
     * 持有yml文件配置:spring.ai.dashscope.api-key=${aliQwen-api}
     */
    @Value("${spring.ai.dashscope.api-key}")
    private String apiKey;

    @Bean
    public DashScopeApi dashScopeApi()
    {
        return DashScopeApi.builder().apiKey(apiKey).build();
    }
}

对话模型(Chat Model)

对话模型(Chat Model)

对话模型(Chat Model)接收一系列消息(Message)作为输入,与模型LLM服务进行交互,并接收返回的聊天消息(Chat Message)作为输出。相比于普通的程序输入,模型的输入与输出消息(Message)不止支持纯字符文本,还支持包括语音、图片、视频等作为输入输出。同时,在SpringAl Alibaba中,消息中还支持包含不同的角色,帮助底层模型区分来自模型、用户和系统指令等的不同消息。

Spring Al Alibaba 复用了Spring Al抽象的Model API,并与通义系列大模型服务进行适配(如通义千 问、通义万相等),目前支持纯文本聊天、文生图、文生语音、语音转文本等。以下是框架定义的几个 核心 API:

  • ChatModel,文本聊天交互模型,支持纯文本格式作为输入,并将模型的输出以格式化文本形式 返回。
  • CImageModel,接收用户文本输入,并将模型生成的图片作为输出返回。
  • CAudioModel,接收用户文本输入,并将模型合成的语音作为输出返回。
@RestController
public class ChatHelloController
{

    @Resource // 对话模型,调用阿里云百炼平台
    private ChatModel chatModel;

    /**
     * 通用调用
     * @param msg
     * @return
     */
    @GetMapping(value = "/hello/dochat")
    public String doChat(@RequestParam(name = "msg",defaultValue="你是谁") String msg)
    {
        String result = chatModel.call(msg);
        return result;
    }

    /**
     * 流式返回调用
     * @param msg
     * @return
     */
    @GetMapping(value = "/hello/streamchat")
    public Flux<String> stream(@RequestParam(name = "msg",defaultValue="你是谁") String msg)
    {
        return chatModel.stream(msg);
    }
}

Ollama

Ollama是一个功能强大的开源框架,旨在简化在Docker容器中部署和管理大型语言模型(LLM)的过程。它帮助用户快速在本地运行大模型,通过简单的安装指令,用可以执行一条命令就在本地运行开源大型语言模型,如LLama 2

Ollama极大地简化了在Docker容器内部署和管理LLM的过程,它优化了设置和配置细节,包括GPU使用情况,并 将模型权重、配置和数据捆绑到一个包中,定义成Modelfile。此外,Ollama还还提供了多种大型语言模型的开源仓 库,用户可以通过简单的命令行操作来下载和运行这些模型。总的来说,Ollama是一个用于在本地高效运行大型语言模型的平台,为开发者和研究人员提供了极大的便利。

image.png

image.png

当在本地安装好Ollama平台之后,并且利用Ollama安装好了对应的模型之后,在调用之前需要引入以下依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-ollama</artifactId>
    <version>1.0.0</version>
</dependency>

配置文件如下所示:

# ====ollama Config=============
spring.ai.dashscope.api-key=${aliQwen-api}
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.model=qwen2.5:latest

JAVA的Controller代码如下所示:

@RestController
public class OllamaController
{
    @Resource(name = "ollamaChatModel")
    private ChatModel chatModel;
    
    /**
     * http://localhost:8002/ollama/chat?msg=你是谁
     * @param msg
     * @return
     */
    @GetMapping("/ollama/chat")
    public String chat(@RequestParam(name = "msg") String msg)
    {
        String result = chatModel.call(msg);
        System.out.println("---结果:" + result);
        return result;
    }

    @GetMapping("/ollama/streamchat")
    public Flux<String> streamchat(@RequestParam(name = "msg",defaultValue = "你是谁") String msg)
    {
        return chatModel.stream(msg);
    }
}

ChatClient

ChatClient提供了与AI模型通信的Flueiit API,它支持同步和反应式(Reactive)编程模型。与ChatModel、Message、ChatMemory 等原子API相比,使用 ChatClient 可以将与LLM及其他组件交互的复杂性隐藏在背后,因为基于LLM的应用程序通常要多个组件协同工作(例如,提示词模板、聊天记忆、LLM Model、输出解析器、RAG组件:嵌入模型和存储),并且通常涉及多个交互,因此协调它们会让编码变得繁琐。当然使用 ChatModel 等原子API可以为应用程序带来更多的灵活性,成本就是您需要编写大量样板代码。

ChatClient类似于应用程序开发中的服务层,它为应用程序直接提供 Al服务,开发者可以使用ChatClient Fluent API快速完成一整套AI交互流程的组装。包括一些基础功能,如:

  • 定制和组装模型的输入(Prompt)
  • 格式化解析模型的输出(Structured Output)
  • 调整模型交互参数(ChatOptions)

还支持更多高级功能:

  • 聊天记忆(Chat Memory)
  • 工具/函数调用(Function Calling)
  • RAG

ChatModel是底层接口,直接与具体大语言模型交互,提供cal()和stream()方法,适合简单大模型交互场景。 ChatClient是高级封装,基于ChatModel构建,适合快速构建标准化复杂AI服务,支持同步和流式交互,集成多种高级功能。

由于在Spring AI中不支持ChatClient的自动注入,因此需要自己创建一个实例存放进容器中:

@Bean
public ChatClient chatClient(ChatModel dashscopeChatModel){
    return ChatClient.builder(dashscopeChatModel).build();
}

调用方式为:

@Resource
private ChatClient dashScopechatClientv2;

/**
    * http://localhost:8003/chatclientv2/dochat
    * @param msg
    * @return
    */
@GetMapping("/chatclientv2/dochat")
public String doChat(@RequestParam(name = "msg",defaultValue = "你是谁") String msg){
    String result = dashScopechatClientv2.prompt().user(msg).call().content();
    System.out.println("ChatClient响应:" + result);
    return result;
}

SSE流式输出

Server-Sent Events(SSE)是一种允许服务端可以持续推送数据片段(如逐词或逐句)到前端的Web技术。通过单向的HTTP长连接,使用一个长期存在的连接,让服务器可以主动将数据“推”给客户端,SSE是轻量级的单向通信协议,适合AI对话这类服务端主导的场量

  • SSE的核心思想是:客户端发起一个请求,服务器保持这个连接打开并在有新数据时,通过这个连接将数据发送给客户端。这与传统的请求-响应模式(客户端请求一次,服务器响应一次,连接关闭)有本质区别。

流式输出:是一种逐步返回大模型生成结果的技术,生成一点返回一点,许服务器将响应内容分批次实时传输给客户端,而不是等待全部内容生成完毕后再一次性返回。这种机制能显著提升用户体验,尤其适用于大模型响应较慢的场景(如生成长文本或复杂推理结果)。

image.png

SSE 非常适合需要服务器向客户端实时推送更新的场景,例如:

  • 实时通知:股票行情、新闻推送、聊天应用(仅接收消息)、系统告警。
  • 状态更新:长时间运行的任务进度(如文件上传、后台处理)。
  • 实时数据流:传感器数据、监控仪表盘。
  • 协作应用:显示其他用户的在线状态或操作(但不包括发送操作)。
//同时兼容多个chatModel
@Configuration
public class SaaLLMConfig {
    // 模型名称常量定义,一套系统多模型共存
    private final String DEEPSEEK_MODEL = "deepseek-v3";
    private final String QWEN_MODEL = "qwen-max";

    @Bean(name = "deepseek")
    public ChatModel deepSeek() {
        return DashScopeChatModel.builder()
                .dashScopeApi(DashScopeApi.builder().apiKey(System.getenv("aliQwen-api")).build())
                .defaultOptions(DashScopeChatOptions.builder().withModel(DEEPSEEK_MODEL).build())
                .build();
    }

    @Bean(name = "qwen")
    public ChatModel qwen() {
        return DashScopeChatModel.builder()
                .dashScopeApi(DashScopeApi.builder().apiKey(System.getenv("aliQwen-api")).build())
                .defaultOptions(DashScopeChatOptions.builder().withModel(QWEN_MODEL).build())
                .build();
    }

    @Bean(name = "deepseekChatClient")
    public ChatClient deepseekChatClient(@Qualifier("deepseek") ChatModel deepseek) {
        return
                ChatClient.builder(deepseek)
                        .defaultOptions(ChatOptions.builder().model(DEEPSEEK_MODEL).build())
                        .build();
    }

    @Bean(name = "qwenChatClient")
    public ChatClient qwenChatClient(@Qualifier("qwen") ChatModel qwen) {
        return
                ChatClient.builder(qwen)
                        .defaultOptions(ChatOptions.builder().model(QWEN_MODEL).build())
                        .build();
    }
}

Controller方法实现流式处理:

@GetMapping(value = "/stream/chatflux3")
public Flux<String> chatflux3(@RequestParam(name = "question", defaultValue = "你是谁") String question) {
    return deepseekChatClient.prompt(question).stream().content();
}

@GetMapping(value = "/stream/chatflux4")
public Flux<String> chatflux4(@RequestParam(name = "question", defaultValue = "你是谁") String question) {
    return qwenChatClient.prompt(question).stream().content();
}

提示词

Prompt是引导AI模型生成特定输出的输入格式,Prompt的设计和措辞会显著影响模型的响应。

Prompt最开始只是简单的字符串,随着时间的推移,prompt逐渐开始包含特定的占位符,例如AI模型可以识别的“USER:”、“SYSTEM:”等。阿里云通义模型可通过将多个消息字符串分类为不同的角色,然后再由AI模型处理,为prompt引入了更多结构。每条消息都分配有特定的角色,这些角色对消息进行分类,明确AI模型提示的每个部分的上下文和目的。这种结构化方法增强了与AI沟通的细微差别和有效性,因为prompt的每个部分在交互中都扮演着独特且明确的角色。

通常使用ChatModel的call()方法,该方法接受Prompt实例并返回ChatResponse。Prompt类充当有组织的一系列Message对象ChatOptions对象的容器。每条消息在提示中都体现了独特的角色,其内容和意图各不相同。这些角色可以包含各种元素,从用户查询到AI生成的响应再到相关背景信息。这种安排可以实现与AI模型的复杂而详细的交互,因为提示是由多条消息构成的,每条消息都被分配了在对话中扮演的特定角色。

public class Prompt implements ModelRequest<List<Message>> {
    private final List<Message> messages;
    @Nullable
    private ChatOptions chatOptions;
}

Message对象与四大角色

Message 是对话中的基本通信单元,代表单条对话内容。主要特性:

  • 内容(Content) : 消息的实际文本
  • 类型(MessageType) : 消息的角色类型
  • 属性(Properties) : 可选的元数据

Message对象一共有4种角色,每个角色都有对应的具体Message实现类

public enum MessageType {
    USER,       // 用户消息
    ASSISTANT,  // AI助手回复
    SYSTEM,     // 系统指令
    FUNCTION    // 函数调用结果
}

SYSTEM

SYSTEM:设定AI行为边界/角色/定位。指导AI的行为和响应方式,设置AI如何解释和回复输入的。比如设定AI的角色为律师,只回答法律相关的问题。特点

  • 设定AI的行为、角色、回复风格等
  • 通常放在对话的最开始
  • 对整个对话会话有持久影响
  • 不是用户可见的对话内容

USER

用户原始提问输入。代表用户的输入他们向AI提出的问题、命令或陈述。*特点

  • 代表用户输入的问题、指令或对话内容
  • 是AI模型需要回应的主要对象
  • 通常是最新的一条消息

ASSISTANT

AI返回的响应信息,定义为“助手角色”消息。用它可以确保上下文能够连贯的交互,记忆对话,积累回答。特点

  • 代表AI模型生成的回复内容
  • 在多轮对话中作为历史对话记录
  • 在响应中,这是AI新生成的内容

image.png

TOOL

桥接外部服务,可以进行函数调用如,支付/数据查询等操作,类以调用第三方util工具类。这一部分主要是避免大模型的幻读或者已读乱回问题。特点

  • 包含工具函数或外部API的调用结果
  • 用于将函数执行结果反馈给AI模型
  • 在函数调用流程中使用

ChatOptions

ChatOptions 充当了一个统一的、模型无关的配置接口。它抽象了不同AI供应商(如OpenAI、Anthropic、Azure OpenAI等)各自特有的API参数,使得开发者可以用一套通用的配置方式来与各种模型交互。通过 ChatOptions,你可以控制以下关键方面:

  1. 创造性 / 确定性
    • temperature: 控制输出的随机性。值越高(如0.8-1.0),输出越随机、有创意;值越低(如0.0-0.2),输出越确定、一致。
  2. 输出多样性
    • topP: 核采样。值越低,模型仅从最可能的少数词汇中选择;值越高,选择范围更广。通常与 temperature 配合使用。
  3. 生成长度控制
    • maxTokens / maxOutputTokens: 设置模型响应中最多能生成多少个令牌,有效控制回应的长度
  4. 停止序列
    • stopSequences: 提供一个字符串列表。当模型生成的文本包含其中任何一个序列时,会立即停止生成。常用于格式化输出或控制对话流程。
  5. 存在与频率惩罚
    • presencePenalty: 惩罚模型提及新主题,鼓励它围绕当前话题展开。
    • frequencyPenalty: 惩罚模型重复使用相同的词汇,鼓励使用更多样的表达。
  6. 特定模型的高级功能
    • JSON Mode: 强制模型以合法的JSON格式输出。
    • Function Calling / Tools: 配置模型可以调用的函数或工具。

示例代码

//system+user示例
// http://localhost:8005/prompt/chat?question=火锅介绍下
@GetMapping("/prompt/chat")
public Flux<String> chat(String question){
    return deepseekChatClient.prompt()
            // AI 能力边界
            .system("你是一个法律助手,只回答法律问题,其它问题回复,我只能回答法律相关问题,其它无可奉告")
            .user(question)
            .stream()
            .content();
}

// Assistant角色示例
    /**
    * http://localhost:8005/prompt/chat4?question=葫芦娃
    * @param question
    * @return
    */
@GetMapping("/prompt/chat4")
public String chat4(String question){
    AssistantMessage assistantMessage = deepseekChatClient.prompt()
                .user(question)
                .call()
                .chatResponse()
                .getResult()
                .getOutput();

    return assistantMessage.getText();

}

//tool角色示例
@GetMapping("/prompt/chat5")
public String chat5(String city){

    String answer = deepseekChatClient.prompt()
            .user(city + "未来3天天气情况如何?")
            .call()
            .chatResponse()
            .getResult()
            .getOutput()
            .getText();
    ToolResponseMessage toolResponseMessage =
            new ToolResponseMessage(
                    List.of(new ToolResponseMessage.ToolResponse("1","获得天气",city)
                    )
            );
    String toolResponse = toolResponseMessage.getText();
    String result = answer + toolResponse;
    return result;
}

PromptTemplate

PromptTemplate 是一个非常重要的工具类,用于动态生成Prompt对象。它通过模板化方式简化了Prompt的创建过程,特别适合需要插入动态变量的场景。PromptTemplate 的主要作用是:

  • 定义包含占位符的消息模板
  • 将动态数据注入到模板中
  • 自动生成结构化的Prompt对象
/**
 * @Description: PromptTemplate基本使用,使用占位符设置模版 PromptTemplate
 * @Auther: zzyybs@126.com
 * 测试地址
 * http://localhost:8006/prompttemplate/chat?topic=java&output_format=html&wordCount=200
 */
@GetMapping("/prompttemplate/chat")
public Flux<String> chat(String topic, String output_format, String wordCount){ 
    //模板,使用占位符
    PromptTemplate promptTemplate = new PromptTemplate("" +
            "讲一个关于{topic}的故事" +
            "并以{output_format}格式输出," +
            "字数在{wordCount}左右");

    // PromptTempate -> Prompt
    Prompt prompt = promptTemplate.create(Map.of(
            "topic", topic,
            "output_format",output_format,
            "wordCount",wordCount));

    return deepseekChatClient.prompt(prompt).stream().content();
}

如果模板内容比较长,或者模板数量较多,可以采用配置文件或者配置中心的方式,相关对象是:Resource。

//从配置文件中创建一个Resource对象,Resource对象可以用于创建PromptTemplate
@Value("classpath:/prompttemplate/atguigu-template.txt")
private org.springframework.core.io.Resource userTemplate;

/**
    * @Description: PromptTemplate读取模版文件实现模版功能
    * @Auther: zzyybs@126.com
    * 测试地址
    * http://localhost:8006/prompttemplate/chat2?topic=java&output_format=html
    */
@GetMapping("/prompttemplate/chat2")
public String chat2(String topic,String output_format){
    PromptTemplate promptTemplate = new PromptTemplate(userTemplate);
    Prompt prompt = promptTemplate.create(Map.of("topic", topic, "output_format", output_format));
    return deepseekChatClient.prompt(prompt).call().content();
}

PromptTemplate可以创建包含多个消息的复杂Prompt:

@GetMapping("/prompttemplate/chat3")
public String chat3(String sysTopic, String userTopic)
{
    // 1.SystemPromptTemplate
    SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate("你是{systemTopic}助手,只回答{systemTopic}其它无可奉告,以HTML格式的结果。");
    Message sysMessage = systemPromptTemplate.createMessage(Map.of("systemTopic", sysTopic));
    // 2.PromptTemplate
    PromptTemplate userPromptTemplate = new PromptTemplate("解释一下{userTopic}");
    Message userMessage = userPromptTemplate.createMessage(Map.of("userTopic", userTopic));
    // 3.组合【关键】 多个 Message -> Prompt
    Prompt prompt = new Prompt(List.of(sysMessage, userMessage));
    // 4.调用 LLM
    return deepseekChatClient.prompt(prompt).call().content();
}

格式化输出

如果您想从LLM接收结构化输出,Structured Output可以协助将 ChatModel/ChatClient 方法的返回类型从String 更改为其他类型。LLM生成结构化输出的能力对于依赖可靠解析输出值的下游应用程序非常重要。开发人员希望快速将Al模型的结果转换为可以传递给其他应用程序函数和方法的数据类型,例如JSON、XML或Java类。Spring Al 结构化输出转换器有助于将LLM的输出转换为结构化格式。下面将LLM的输出转换为Record对象:

Record 是一种新的类声明形式,其核心思想是:当你需要一个类的主要作用只是作为数据的透明载体时,可以用更简洁的方式来实现。总结来说:Record = entityClass + Lombok。

public record Person(String name, int age, String email) { }

编译器会自动为Record生成:

  • 规范构造函数:接收所有组件参数
  • 访问器方法name()age()email()(注意:不是getXxx格式)
  • equals() 和 hashCode() :基于所有组件
  • toString() :包含所有组件信息的字符串表示

相关的API为:ChatClient.prompt().user(Consumer<PromptUserSpec> consumer)。示例代码有两种:

/**
    * 方式一
    * http://localhost:8007/structuredoutput/chat?sname=李四&email=zzyybs@126.com
    * @param sname
    * @return
    */
@GetMapping("/structuredoutput/chat")
public StudentRecord chat(@RequestParam(name = "sname") String sname,
                            @RequestParam(name = "email") String email) {

    return qwenChatClient.prompt().user(new Consumer<ChatClient.PromptUserSpec>()
    {
        @Override
        public void accept(ChatClient.PromptUserSpec promptUserSpec)
        {
            promptUserSpec.text("学号1001,我叫{sname},大学专业计算机科学与技术,邮箱{email}")
                    .param("sname",sname)
                    .param("email",email);
        }
    }).call().entity(StudentRecord.class);
}


/**
    * 方式二
    * http://localhost:8007/structuredoutput/chat2?sname=孙伟&email=zzyybs@126.com
    * @param sname
    * @return
    */
@GetMapping("/structuredoutput/chat2")
public StudentRecord chat2(@RequestParam(name = "sname") String sname,
                            @RequestParam(name = "email") String email) {

    String stringTemplate = """
            学号1002,我叫{sname},大学专业软件工程,邮箱{email}            
            """;

    return qwenChatClient.prompt()
            .user(promptUserSpec -> promptUserSpec.text(stringTemplate)
            .param("sname",sname)
            .param("email",email))
            .call()
            .entity(StudentRecord.class);
}

image.png

Chat_Memory

“大模型的对话记忆”这一概念,根植于人工智能与自然语言处理领域,特别是针对具有深度学习能力的大型语言模型而言,它指的是模型在与用户进行交互式对话过程中,能够追踪、理解并利用先前对话上下文的能力。此机制使得大模型不仅能够响应即时的输入请求,还能基于之前的交流内容能够在对话中记住先前的对话内容,并根据这些信息进行后续的响应。这种记忆机制使得模型能够在对话中持续跟踪和理解用户的意图和上下文,从而实现更自然和连贯的对话。

大型语言模型(LLMs)是无状态的,这意味着它们不保留有关先前交互的信息。当您想要在多个交互中维护上下文或状态时,这可能是一个限制。为了解决这个问题,Spring AI 提供了聊天内存功能,允许您存储和检索与 LLM 的多个交互中的信息。ChatMemory 抽象允许您实现各种类型的内存来支持不同的用例。消息的底层存储由 ChatMemoryRepository 处理,其唯一职责是存储和检索消息。由 ChatMemory 实现决定保留哪些消息以及何时删除它们。策略示例可能包括保留最后 N 条消息、保留特定时间段的消息或保留达到特定令牌限制的消息。在选择内存类型之前,了解聊天内存和聊天历史之间的区别很重要:

  • 聊天历史(Chat History)

    • 是什么:完整的对话记录,包含所有轮次的用户输入和AI响应
    • 存储方式:通常持久化存储(数据库、文件等)
    • 生命周期:长期存在,跨越多个会话
    • 主要用途:数据分析、审计、模型训练、上下文检索
  • 聊天内存(Chat Memory)

    • 是什么:当前会话的上下文窗口,用于维护对话连贯性
    • 存储方式:通常是内存中或临时存储
    • 生命周期:会话级别,会话结束即销毁(或部分保留)
    • 主要用途:维持对话连贯性,提供上下文理解

ChatMemory 抽象旨在管理聊天内存。它允许您存储和检索与当前对话上下文相关的消息。但是,它不适合存储聊天历史。如果您需要维护所有交换消息的聊天历史,应该考虑使用不同的方法,例如依赖 Spring Data 来高效存储和检索完整的聊天历史。此处以Redis作为持久化存储为例,需要引入以下依赖:

<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-memory-redis</artifactId>
</dependency>
<!--jedis-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

ChatMemory 接口

核心职责:管理与一个 特定对话会话 相关的消息历史。ChatMemory 是一个具体的“记忆单元”。它内部维护了一个消息列表(List<Message>),并提供了添加、获取、清除这些消息的方法。

  • 作用范围单个会话。每个独立的对话(例如,每个用户、每个聊天窗口、每个线程)都应该有自己的 ChatMemory 实例。
  • 关键方法
    • add(): 向记忆中添加一条消息。
    • get(): 获取当前记忆中的所有消息(可能会经过处理,比如只返回最近的N条)。
    • clear(): 清空当前会话的记忆。
  • 实现策略ChatMemory 接口的不同实现决定了记忆的策略。
    • SimpleChatMemory: 简单的内存实现。
    • VectorStoreChatMemory: 将消息存储在向量数据库中,用于基于相似性的记忆检索。
    • WindowChatMemory: 只保留最近N条消息(滑动窗口)。

简单比喻:如果把一次AI对话比作一个“聊天窗口”,那么 ChatMemory 就是这个窗口的“聊天记录” 。

ChatMemoryRepository接口

ChatMemoryRepository 是 Spring AI 中聊天内存存储的核心抽象接口,它定义了聊天内存数据的持久化操作规范。核心作用:

  • 抽象存储层:将内存管理逻辑与具体存储技术解耦
  • 统一操作接口:为不同的存储后端提供一致的 API
  • 会话管理:基于会话 ID 管理不同对话的上下文
  • 生命周期管理:提供消息的增删改查操作 我们引入了Redis相关的Memory依赖,Spring AI Alibaba已经创建好了对应的实现类RedisChatMemoryRepository,如果要自定义存储介质,那么就需要自己实现一个:
public interface ChatMemoryRepository {
    //查找所有存在的会话 ID
    List<String> findConversationIds();
    //根据会话 ID 查询该会话的所有消息
    List<Message> findByConversationId(String conversationId);
    //保存或更新指定会话的所有消息
    void saveAll(String conversationId, List<Message> messages);
    //删除指定会话的所有消息
    void deleteByConversationId(String conversationId);
}

MessageWindowChatMemory类

MessageWindowChatMemory维护一个消息窗口,最多可达指定的最大大小。当消息数超过最大值时,将删除较旧的消息,同时保留系统消息。默认窗口大小为20条消息。它是ChatMemory的一个具体实现

MessageWindowChatMemory memory = MessageWindowChatMemory.builder()
      .maxMessages(10)
      .build();

Advisor

要构建高效的AI应用程序,一系列支持功能至关重要。模型增强的概念(如下图所示)正是为此而生,它为基础模型添加了数据检索(RAG)、对话记忆(Memory)和工具调用(Tool)等功能。这些功能允许您将自己的数据和外部API直接引入模型的推理过程。Spring AI中实现这一目标的关键组件就是Advisor。Advisor是一个拦截器链设计模式,允许你通过注入检索数据(Retrieval Context)和对话历史(Chat Memory)来修改传入的Prompt。

  • MessageChatMemoryAdvisor。管理对话内存。在每次交互中,它都会从内存中检索对话历史记录,并将其作为消息集合包含在提示中。
  • PromptChatMemoryAdvisor。管理对话内存。在每次交互时,它都会从内存中检索对话历史记录,并将其作为纯文本附加到系统提示中。
  • VectorStoreChatMemoryAdvisor。管理对话内存。在每次交互时,它都会从矢量存储中检索对话历史记录,并将其作为纯文本追加到系统消息中。
ChatMemory chatMemory = MessageWindowChatMemory.builder().build();
ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
        .build();

当执行调用时,内存将由自动管理。将根据指定的对话ID从内存中检索对话历史记录:

String conversationId = "007";
chatClient.prompt()
    .user("Do I have license to code?")
    .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
    .call()
    .content();

示例代码

//配置RedisChatMemoryRepository,并注入到容器中
@Configuration
public class RedisMemoryConfig {
    @Value("${spring.data.redis.host}")
    private String host;
    @Value("${spring.data.redis.port}")
    private int port;

    @Bean
    public RedisChatMemoryRepository redisChatMemoryRepository() {
        return RedisChatMemoryRepository.builder()
                .host(host)
                .port(port)
                .build();
    }
}

//将RedisChatMemoryRepository绑定到对应的ChatClient上
@Bean(name = "qwenChatClient")
public ChatClient qwenChatClient(@Qualifier("qwen") ChatModel qwen, RedisChatMemoryRepository redisChatMemoryRepository) {
    //设置这个ChatClient的策略为
    MessageWindowChatMemory windowChatMemory = MessageWindowChatMemory.builder()
            .chatMemoryRepository(redisChatMemoryRepository)
            .maxMessages(10)
            .build();

    return ChatClient.builder(qwen)
            .defaultOptions(ChatOptions.builder().model(QWEN_MODEL).build())
            .defaultAdvisors(MessageChatMemoryAdvisor.builder(windowChatMemory).build())
            .build();
}


/**
    * zzyybs@126.com
    *
    * @param msg 用户发起的提问内容
    * @param userId 发起提问的用户ID
    * @return
    */
@GetMapping("/chatmemory/chat")
public String chat(String msg, String userId) {
    return qwenChatClient
            .prompt(msg)
            //param()方法就是将userID存放到一个Map中,key为固定值CONVERSATION_ID,value = userId
            .advisors(advisorSpec -> advisorSpec.param(CONVERSATION_ID, userId))
            .call()
            .content();
}

向量化

Vector是向量或矢量的意思,向量是数学里的概念,而矢量是物理果的概念,但二者描述的是同一件事。

  • 定义:向量是用于表示具有大小和方向的量

向量可以在不同的维度空间中定义,最常见的是二维和三维空间中的向量,但理论上也可以有更高维的向量。例如,在二维平面上的一个向量可以写作(x,y),这里x和y分别表示该向量沿两个坐标轴方向上的分量;而在三维空间里,则会有一个额外的z坐标,即(x,y,z)。

image.png

Embedding Model

嵌入(Embedding)的工作原理是将文本、图像和视频转换为称为向量(Vectors)的浮点数数组,嵌入数组的长度称为向量的维度(Dimensionality)。量旨在捕捉文本、图像和视频的含义。

嵌入模型Embedding Model是一种机器学习模型,旨在在连续的低维向量空间中表示数据(例如文本、图像或其他形式的信息)。这些嵌入可以捕获数据之间的语义或上下文相似性,使机器能够更有效地执行比较、聚类或分类等任务。 假设你想描述不同的水果。你不用长篇大论,而是用数字来描述甜度、大小和颜色等特征。例如,苹果可能是[8,5,7],而香蕉是[9,7,4]。这些数字使比较或对相似的水果进行分组变得更容易。

当前EmbeddingModel的接口主要用于将文本转换为数值向量,接口的设计主要围绕这两个目标展开:

  • 可移植性:该接口确保在各种嵌入模型之间的轻松适配。它允许开发者在不同的嵌入技术或模型之间切换,所需的代码更改最小化。这一设计与Spring模块化和互换性的理念一致。
  • 简单性:嵌入模型简化了文本转换为嵌入的过程。通过提供如embed(String text)和embed(Documentdocument)这样简单的方法,它去除了处理原始文本数据和嵌入算法的复杂性。这个设计更容易在他们的应用程序中使用嵌入,而无需深入了解其底层机制。

如我们所见,每个数值向量都有x和y坐标(或者在多维系统中是x、y、Z, ... )。X、y、z ... 是这个向量空间的轴,称为维度。对于我们想要表示为向量的一些非数值实体,我们首先需要决定这些维度,并为每个实体在每个维度上分配一个值。 例如,在一个交通工具数据集中,我们可以定义四个维度:“轮子数量”、“是否有发动机”、“是否可以在地上开动”和“最大乘客数”。然后我们可以将一些车辆表示为:

image.png

因此我们的汽车Car向量将是(4.yes,yes,5),或者用数值表示为(4,1,1,5)(将yes设为1,no设为0)。向量的每个维度代表数据的不同特性,维度越多对事务的描述越精确,我们可以使用“是否有翅膀”、“是否使用柴油”、“最高速度”、“平均重量”、“价格”等等更多的维度信息。

如何确定哪些是最相似的?

每个向量都有一个长度和方向。例如,在这个图中,p和a指向相同的方向,但长度不同。p和b正好指向相反的方向,但有相同的长度。然后还有c,长度比p短一点,方向不完全相同,但很接近。通过余弦相似度来判断是否相似, 如果“相似”仅仅意味着指向相似的方向,那么a是最接近p的、接下来是c、b是最不相似的,因为它正好指向与p相反的方向。如果“相似“仅仅意味着相似的长度,那么b是最接近p的(因为它有相同的长度),接下来是c,然后是a。由于向量通常用于描述语义意义,仅仅看长度通常无法满足需求。大多数相似度测量要么仅依赖于方向,要么同时考虑方向和太小。

image.png

向量存储VectorStore

向量存储(VectorStore)是一种用于存储和检索高维向量数据的数据库或存储解决方案,它特别适用于处理那些经过嵌入模型转化后的数据。在VectorStore中,查询与传统关系数据库不同。它们执行相似性搜索,而不是精确匹配。当给定一个向量作为查询时,VectorStore返回与查询向量“相似”的向量。

  • 其核心功能是通过高效的索引结构和相似性计算算法,支持大规模向量数据的快速查询与分析,向量数据库维度越高,查询精准度也越高,查询效果也越好。

VectorStore用于将您的数据与AI模型集成。在使用它们时的第一步是将您的数据加载到矢量数据库中。然后,当要将用户查询发送到AI模型时,首先检索一组相似文档。然后,这些文档作为用户问题的上下文,并与用户的查询一起发送到AI模型。这种技术被称为检索增强生成(Retrieval AugmentedGeneration,RAG )。SpringAI支持的向量数据库

编码

此处使用Redis作为向量数据库,首先引入相关依赖,并不是原来的Redis,是增强版的Redis。

<!-- 添加 Redis 向量数据库依赖 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-redis</artifactId>
</dependency>

需要对配置文件进行修改:

# ====SpringAIAlibaba Config=============
spring.ai.dashscope.api-key=${aliQwen-api}
# qwen-plus用于与用户打交道
spring.ai.dashscope.chat.options.model=qwen-plus
# text-embedding-v3提供向量化功能
spring.ai.dashscope.embedding.options.model=text-embedding-v3


# =======Redis Stack==========
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.username=default
spring.data.redis.password=default
# redis的额外配置
spring.ai.vectorstore.redis.initialize-schema=true
spring.ai.vectorstore.redis.index-name=custom-index
spring.ai.vectorstore.redis.prefix=custom-prefix

示例代码如下。Spring AI 通过VectorStore接口提供了与向量数据库交互的抽象API,要将数据插入向量数据库,需要将其封装在 Document 对象中。 Document类封装了来自数据源(如 PDF 或 Word 文档)的内容,并包含以字符串形式表示的文本。它还包含以键值对形式存储的元数据,包括文件名等详细信息。

import java.util.List;

/**
 * @auther zzyybs@126.com
 * @create 2025-07-29 19:54
 * @Description TODO
 */
@RestController
@Slf4j
public class Embed2VectorController {

    @Resource
    private VectorStore vectorStore; //用于存储

    /**
     * 文本向量化 后存入向量数据库RedisStack
     */
    @GetMapping("/embed2vector/add")
    public void add() {
        List<Document> documents = List.of(
                new Document("i study LLM"),
                new Document("i love java")
        );

        vectorStore.add(documents);
    }


    /**
     * 从向量数据库RedisStack查找,进行相似度查找
     */
    @GetMapping("/embed2vector/get")
    public List getAll(@RequestParam(name = "msg") String msg) {
        //构建请求
        SearchRequest searchRequest = SearchRequest.builder()
                //按照msg进行查找
                .query(msg)
                //查找最相似的两条
                .topK(2)
                .build();
        //返回结果
        List<Document> list = vectorStore.similaritySearch(searchRequest);
        return list;
    }
}

向量数据库的作用是存储和查询这些向量的相似性搜索。它本身不生成向量入。对于向量的生成,应该使用 EmbeddingModel。VectorStore会使用容器中配置好的EmbeddingModel来进行生成(也就是配置文件中指定的text-embedding-v3)。如果需要指定,需要通过以下方式:

@Bean
public VectorStore vectorStore(JedisPooled jedisPooled, EmbeddingModel embeddingModel) {
    return RedisVectorStore.builder(jedisPooled, embeddingModel) //指定EmbeddingModel
        .indexName("custom-index")                // Optional: defaults to "spring-ai-index"
        .prefix("custom-prefix")                  // Optional: defaults to "embedding:"
        .metadataFields(                         // Optional: define metadata fields for filtering
            MetadataField.tag("country"),
            MetadataField.numeric("year"))
        .initializeSchema(true)                   // Optional: defaults to false
        .batchingStrategy(new TokenCountBatchingStrategy()) // Optional: defaults to TokenCountBatchingStrategy
        .build();
}

// This can be any EmbeddingModel implementation
@Bean
public EmbeddingModel embeddingModel() {
    return new OpenAiEmbeddingModel(new OpenAiApi(System.getenv("OPENAI_API_KEY")));
}

如果你有需要,也可以直接使用EmbeddingModel进行向量化得到对应的结果:

@RestController
@Slf4j
public class Embed2VectorController {
    @Resource
    private EmbeddingModel embeddingModel; //用于文本向量化


    /**
     * 将目标文本向量化
     */
    @GetMapping("/text2embed")
    public EmbeddingResponse text2Embed(String msg) {
        //实际操作代码
        EmbeddingResponse embeddingResponse = embeddingModel.call(new EmbeddingRequest(List.of(msg),
                DashScopeEmbeddingOptions.builder()
                        .withModel("text-embedding-v3")
                        .build())
        );
        System.out.println(Arrays.toString(embeddingResponse.getResult().getOutput()));
        return embeddingResponse;
    }
}

image.png

RAG

RAG即检索增强生成,简单来说,通过引入外部知识源来增强LLM的输出能力,传统的LLM通常基于其训练数据生成响应,但这些数据可能过时或不够全面。RAG允许模型在生成答案之前,从特定的知识库中检索相关信息,从而提供更准确和上下文相关的回答。它的核心作用可以概括为以下几点:

  1. 提供实时、外部知识
    • 作用:大语言模型的知识来源于其训练数据,存在一个“知识截止日期”。RAG可以突破这个限制,通过检索外部知识库(如公司内部文档、最新新闻、行业报告、数据库等),为模型提供它“从未见过”的最新信息。
    • 例子:你可以问:“苹果公司昨天发布的财报主要亮点是什么?” 即使模型的训练数据只到2023年,RAG也能检索到昨天的最新财报,并让模型基于此生成回答。
  2. 提高事实准确性与可信度
    • 作用:LLM有时会“一本正经地胡说八道”,产生幻觉或事实错误。RAG通过提供来自权威、可信来源的检索结果作为依据,强制模型“言之有物”,极大地减少了捏造信息的可能性。
    • 例子:在医疗或法律咨询等严肃领域,RAG可以确保模型的回答严格基于最新的医学指南或法律条文,而不是凭空想象。
  3. 解决“知识孤岛”问题,赋能专业领域
    • 作用:每个企业或组织都有大量非公开的私有数据(如产品手册、会议纪要、客户邮件、代码库等)。直接训练一个精通这些内部知识的模型成本极高。RAG可以快速地将这些私有知识库作为检索来源,瞬间让一个通用模型变成一个精通你公司业务的专家。
    • 例子:新员工可以问公司内部的RAG助手:“我们项目A的第三季度技术架构图在哪里?核心挑战是什么?” 助手能直接从公司的Confluence、Notion或SVN中检索相关信息并生成总结。
  4. 提供信息来源,增强可追溯性
    • 作用:RAG系统在生成答案时,可以明确告知用户其回答是参考了哪些源文件或数据片段。这方便用户去核实信息的真实性,增加了系统的透明度和可信度。
    • 例子:在生成一个市场分析报告后,RAG助手可以附上引用的来源链接或文档名,方便用户进一步查阅。
  5. 控制成本与提升效率
    • 作用:相比于为了学习新知识而频繁地对大模型进行微调,RAG方案的实现成本和计算开销要低得多。它更像是一个“即插即用”的扩展,无需改动模型本身。

索引与检索

索引(Indexing):索引首先清理和提取各种格式的原始数据,如PDF、HTML、Word和Markdown,然后将其转换为统一的纯文本格式。为了适应语言模型的上下文限制,文本被分割成更小的、可消化的块(chunk)。然后使用嵌入模型将块编码成向量表示,并存储在向量数据库中。这一步对于在随后的检索阶段实现高效的相似性搜索至关重要。知识库分割成chunks,并将chunks向量化至向量库中。

image.png

检索:检索阶段通常在线进行,此过程可能因所使用的信息检索方法而异。对于向量搜索,这通常涉及嵌入用户的查询(question)以及在嵌入存储中执行相似性搜索。然后将相关句段(原始文档的片段)注入到提示中并发送到LLM。此时用户提交的问题应使用索引文档来回答。

image.png

在收到用户查询(Query)后,RAQ系统采用与索引阶段相同的编码模型将查询转换为向量表示,然后计算索引语料库中查询向量与块向量的相似性得分。该系统优先级和检索最高k(Top-K)块,显示最大的相似性查询。

编码示例

AI智能运维助手,通过提供的错误编码,给出异常解释辅助运维人员更好的定位问题和维护系统。已有的LLM不包含此部分知识,因此需要使用RAG来加强LLM。提供ErrorCode脚本让他存入向量数据库RedisStack,形成文档知识库。这个ErrorCode脚本就使用最简单的txt存储=>ops.txt。

00000 系统OK正确执行后的返回
A0001 用户端错误一级宏观错误码
A0100 用户注册错误二级宏观错误码
B1111 支付接口超时
C2222 Kafka消息解压严重

RAG的配置类,主要是读取目标文件内容并且放进向量数据库中:


@Configuration
public class InitVectorDatabaseConfig {
    @Autowired
    private VectorStore vectorStore;
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Value("classpath:ops.txt")
    private Resource opsFile;

    @PostConstruct
    public void init() {
        //1 读取文件
        TextReader textReader = new TextReader(opsFile);
        textReader.setCharset(Charset.defaultCharset());

        //2 文件转换为向量(开启分词)
        List<Document> list = new TokenTextSplitter().transform(textReader.read());

        //3 写入向量数据库RedisStack
        //vectorStore.add(list);

        // 解决上面第3步,向量数据重复问题,使用redis setnx命令处理

        //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 {
            //键已存在,跳过或者报错
            System.out.println("------向量初始化数据已经加载过,请不要重复操作");
        }
    }
}

Controller类代码如下:

@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 = """
                你是一个运维工程师,按照给出的编码给出对应故障解释,否则回复找不到信息。
                """;

        RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder()
                //指定对应的向量数据库
                .documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(vectorStore).build())
                .build();

        return chatClient
                .prompt()
                .system(systemInfo)
                .user(msg)
                .advisors(advisor)
                .stream()
                .content();
    }
}

ToolCalling

ToolCalling(也称为FunctionCalling)它允许大模型与一组API或工具进行交互,将LLM的智能与外部工具或API无缝连接,从而增强大模型其功能。LLM本身并不执行函数,它只是指示应该调用哪个函数以及如何调用。相反,它们会在响应中表达调用特定工具的意图(而不是以纯文本回应)。然后,应用程序应该执行这个工具,并报告工具执行的结果给模型。当LLM可以访问工具时,它可以在合适的情况下决定调用其中一个工具,这是一个非常强大的功能。

Screenshot 2025-11-12 111314.jpg

Screenshot 2025-11-12 111329.jpg

编码演示

相关的注解为@Tool。该注解有个主要属性returnDirect,默认情况下,工具调用的返回值会再次回传到AI模型进一步处理。但在一些场景中需要将结果直接返回给调用方而非模型,比如数据搜索。定义方法工具时,可以通过 @Tool 注解的 returnDirect 参数置 true 来启动直接返回。

public class DateTimeTools {
    /**
     * 1.定义 function call(tool call)
     * 2. returnDirect
     * true = tool直接返回不走大模型,直接给客户
     * false = 默认值,拿到tool返回的结果,给大模型,最后由大模型回复
     */
    @Tool(description = "获取当前时间", returnDirect = false)
    public String getCurrentTime() {
        return LocalDateTime.now().toString();
    }
}

Controller类中,代码结构如下所示,ChatModel和ChatClient两种实现方式:

@RestController
public class ToolCallingController
{
    @Resource
    private ChatModel chatModel;

    @GetMapping("/toolcall/chat")
    public String chat(@RequestParam(name = "msg",defaultValue = "你是谁现在几点") String msg)
    {
        // 1.工具注册到工具集合里
        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();
    }

    @Resource
    private ChatClient chatClient;

    @GetMapping("/toolcall/chat2")
    public Flux<String> chat2(@RequestParam(name = "msg",defaultValue = "你是谁现在几点") String msg)
    {
        return chatClient.prompt(msg)
                .tools(new DateTimeTools())
                .stream()
                .content();
    }
}

MCP

之前每个大模型(如DeepSeek、ChatGPT)需要为每个工具单独开发接口(FunctionCalling),导致重复劳动。MCP是一种开放协议,它标准化了应用程序如何向大型语言模型(LLMs)提供上下文。可以将MCP想象成AI应用的USB-C端口。就像USB-C提供了一种标准化的方式将你的设备连接到各种外围设备和配件一样,MCP提供了一种标准化的方式将AI模型连接到不同的数据源和工具。(相当于Spring Cloud Feign,用于大模型与大模型之间的服务调用).MCP 仓库

image.png

架构以及核心概念

  • MCP主机(MCP Hosts):发起请求的AI应用程序,比如聊天机器人、AI驱动的IDE等。
  • MCP 客户端(MCP Clients):在主机程序内部,与MCP服务器保持1:1的连接。
  • MCP服务器(MCP Servers):为MCP客户端提供上下文、工具和提示信息。
  • 本地资源(Local Resources):本地计算机中可供MCP服务器安全访问的资源,如文件、数据库。
  • 远程资源(Remote Resources):MCP服务器可以连接到的远程资源,如通过API提供的数据

image.png

在MCP通信协议中,一般有两种模式:

  • STDIO(标准输入/输出)支持标准输入和输出流进行通信,主要用于本地集成、命令行工具等场景
  • SSE (Server-Sent Events)支持使用 HTTP POST请求进行服务器到客户端流式处理,以实现客户端到服务器的通信

image.png

本地MCP服务端+客户端

服务端需要引入依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>

spring-ai-starter-mcp-server-webflux不能和spring-boot-starter-web依赖并存,否则会使用tomcat启动,而不是netty启动,从而导致mcpserver启动失败,但程序运行是正常的,mcp客户端连接不上。配置文件需要添加信息:

# 异步访问
spring.ai.mcp.server.type=async
# MCP Server的名字,自定义
spring.ai.mcp.server.name=customer-define-mcp-server
# MCP Server的版本,自定义
spring.ai.mcp.server.version=1.0.0

定义工具方法:

@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, "抱歉:未查询到对应城市!");
    }
}

还需要将工具方法暴露给外部 mcp client 调用:

@Configuration
public class McpServerConfig {
    /**
     * 将工具方法暴露给外部 mcp client 调用
     *
     * @param weatherService
     * @return
     */
    @Bean
    public ToolCallbackProvider weatherTools(WeatherService weatherService) {
        return MethodToolCallbackProvider.builder()
                .toolObjects(weatherService)
                .build();
    }
}

服务端模块定义完毕之后,开始定义客户端Client,对于客户端而言,需要引入以下依赖。对于客户端而言,不需要排除spring-boot-starter-web依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>

配置文件需要添加信息:

spring.ai.mcp.client.type=async
# 连接超时事件
spring.ai.mcp.client.request-timeout=60s
spring.ai.mcp.client.toolcallback.enabled=true
# 对应的MCP服务端的地址
spring.ai.mcp.client.sse.connections.mcp-server1.url=http://localhost:8014

配置类如下所示:

@Configuration
public class SaaLLMConfig {
    @Bean
    public ChatClient chatClient(ChatModel chatModel, ToolCallbackProvider tools) {
        return ChatClient.builder(chatModel)
                .defaultToolCallbacks(tools.getToolCallbacks())  //mcp协议,配置见yml文件
                .build();
    }
}

Controller代码如下所示:

@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);
    }
}