阅读视图

Spring AI实现一个简单的对话机器人

未完待续

本文通过Spring AI基于DeepSeek大模型,以Prompt模式,开发一个智能聊天机器人,并进行对话。Spring AI必须基于jdk-21,因此需要先升级自己的JDK版本

基于jdk-21创建spring-boot项目,引入spring-boot依赖3.5.7,spring-ai依赖1.0.3,以及整合DeepSeek的spring-ai-starter-model-deepseek

<parent>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-parent</artifactId>    <version>3.5.7</version></parent><dependencyManagement>    <dependencies>        <dependency>            <groupId>org.springframework.ai</groupId>            <artifactId>spring-ai-bom</artifactId>            <version>1.0.3</version>            <type>pom</type>            <scope>import</scope>        </dependency>    </dependencies></dependencyManagement><dependencies>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-test</artifactId>        <scope>test</scope>    </dependency>    <dependency>        <groupId>org.springframework.ai</groupId>        <artifactId>spring-ai-starter-model-deepseek</artifactId>    </dependency></dependencies><build>    <plugins>        <plugin>            <groupId>org.apache.maven.plugins</groupId>            <artifactId>maven-compiler-plugin</artifactId>            <configuration>                <source>21</source>                <target>21</target>                <encoding>UTF-8</encoding>            </configuration>        </plugin>    </plugins></build>

application.yml配置中进行配置,并填写DeepSeek的API_KEY,我是从DeepSeek官方(https://platform.deepseek.com/)购买获得,充值后,可以从https://platform.deepseek.com/api_keys页面获得API_KEY

⚠ 为防止误提交代码到公开仓库,spring文档建议将API_KEY写进本机环境变量,yml中设置为api-key: ${DEEPSEEK_API_KEY}

更多配置项,可见官方文档:https://docs.spring.io/spring-ai/reference/api/chat/deepseek-chat.html

spring:  ai:    deepseek:      base-url: https://api.deepseek.com      api-key: sk-02**********************d8666

1.ChatClient

编写一个配置类,声明一个对话客户端,并且注入配置好的DeepSeek模型,通过defaultSystem()来指定大模型的默认角色和任务背景

package org.example;import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.deepseek.DeepSeekChatModel;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class ModelConfig {    @Bean    public ChatClient chatClient(DeepSeekChatModel model) {        return ChatClient.builder(model)                .defaultSystem("你是聪明的智能助手,名字叫小羊")                .build();    }}

在controller中调用

package org.example.controller;import jakarta.annotation.Resource;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.RestController;import reactor.core.publisher.Flux;@RestController@RequestMapping("ai")public class ChatController {    @Resource    private ChatClient chatClient;        @GetMapping(value = "chat-stream")    public String stream(String msg) {        return chatClient.prompt()                .user(msg)                .call()                .content();    }}

通过call()是阻塞的调用,在http请求中使用会出现无限等待的情况,如果要实现不断输出的效果,需要web环境下使用stream()流式调用返回Flux,并设置返回格式为text/html;charset=utf-8,否则输出的中文是乱码

package org.example.controller;import jakarta.annotation.Resource;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.RestController;import reactor.core.publisher.Flux;@RestController@RequestMapping("ai")public class ChatController {    @Resource    private ChatClient chatClient;        @GetMapping(value = "chat-stream", produces = "text/html;charset=utf-8")    public Flux<String> stream(String msg) {        return chatClient.prompt()                .user(msg)                .stream()                .content();    }}

通过使用stream()流式调用返回Flux,可以得到以下效果的输出

2.Advisor

Spring AI通过Advisor(https://docs.spring.io/spring-ai/reference/api/advisors.html)接口提供了会话的增强功能,可以利用其开发更加高级的会话功能

Advisor接口主要用到以下实现类:

  • org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor 简单的日志打印功能
  • org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor 可以实现会话记忆
  • org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor 与RAG知识库功能有关

在使用QuestionAnswerAdvisor时,需要额外添加依赖:

<dependency>   <groupId>org.springframework.ai</groupId>   <artifactId>spring-ai-advisors-vector-store</artifactId></dependency>

可以在创建ChatClient的时候就指定默认的Advisor为SimpleLoggerAdvisor实现输出日志功能

@Beanpublic ChatClient chatClient(DeepSeekChatModel model) {    return ChatClient.builder(model)            .defaultAdvisors(new SimpleLoggerAdvisor())            .defaultSystem("你是聪明的智能助手,名字叫小羊")            .build();}

SimpleLoggerAdvisor日志级别默认为DEBUG,如果要使用SimpleLoggerAdvisor打印日志到控制台,需要修改yml配置文件中的日志级别:

logging:  level:    org.springframework.ai: debug

大模型不具备记忆能力,要想让大模型记住之前的聊天内容,唯一的办法是把之前的聊天内容和新的提示词一并发送给大模型,此时就需要用到MessageChatMemoryAdvisor

使用MessageChatMemoryAdvisor,需要先定义一个ChatMemory接口的实现,来自定义管理会话数据的逻辑(添加,获取,删除),比如可以自己选择维护会话数据到mysql,redis,或者Map中

org.springframework.ai.chat.memory.ChatMemory

public interface ChatMemory {    String DEFAULT_CONVERSATION_ID = "default";    String CONVERSATION_ID = "chat_memory_conversation_id";    default void add(String conversationId, Message message) {        Assert.hasText(conversationId, "conversationId cannot be null or empty");        Assert.notNull(message, "message cannot be null");        this.add(conversationId, List.of(message));    }    void add(String conversationId, List<Message> messages);    List<Message> get(String conversationId);    void clear(String conversationId);}

Spring AI为我们默认实现了一个实现类InMemoryChatMemoryRepository,可将会话保存到本地内存中用于测试,如果我们没有自定义ChatMemory实现类注入,默认的InMemoryChatMemoryRepository将会注入

此处,为了测试功能,就以默认的InMemoryChatMemoryRepository为例

@Beanpublic ChatClient chatClient(DeepSeekChatModel model, ChatMemory chatMemory) {    return ChatClient.builder(model)            .defaultAdvisors(                        SimpleLoggerAdvisor.builder().build(),                        MessageChatMemoryAdvisor.builder(chatMemory).build()            )            .defaultSystem("你是聪明的智能助手,名字叫小羊")            .build();}

Controller的代码需要用户发起聊天时,调用接口传入会话的ID:chatId,并通过.advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, chatId))传递给chatClient

@GetMapping(value = "chat-stream", produces = "text/html;charset=utf-8")public Flux<String> stream(String msg, String chatId) {    return chatClient.prompt()            .user(msg)            .advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, chatId))            .stream()            .content();}

然后测试,先指定会话ID为001,先后两次分别提问“40除以2等于几”和“那除以5呢”,会发现第二次提问没有带上40也得到了正确答案8,再将ID改为002继续问“那乘以3呢”,大模型随即忘记了数字40,失去了记忆,这说明大模型此时通过MessageChatMemoryAdvisor增强,已经有了记忆,并且能够根据不同的会话进行区分!



以“40除以2等于几”和“那除以5呢”这两个问题为例,分析请求日志,其中,messageType=USER的消息代表的是用户的提问,messageType=ASSISTANT代表的是大模型的回复,messageType=SYSTEM代表的则是系统指令,请求日志是这样的:第二个问题并不直接发问,而是将第一个问题的回答的会话历史记录也一并带上在询问第二个问题。这样,自动将整个会话历史回传给大模型从而形成记忆的功能由MessageChatMemoryAdvisor实现了

2025-10-27T20:20:09.211+08:00 DEBUG 19240 --- [oundedElastic-1] o.s.a.c.c.advisor.SimpleLoggerAdvisor    : request: ChatClientRequest[prompt=Prompt{messages=[SystemMessage{textContent='你是聪明的智能助手,名字叫小羊', messageType=SYSTEM, metadata={messageType=SYSTEM}}, UserMessage{content='messageType=40除以2等于几', metadata={messageType=USER}, messageType=USER}], modelOptions=org.springframework.ai.deepseek.DeepSeekChatOptions@34422e1f}, context={chat_memory_conversation_id=111}]2025-10-27T20:20:12.391+08:00 DEBUG 19240 --- [oundedElastic-2] o.s.a.c.c.advisor.SimpleLoggerAdvisor    : response: {  "result" : {    "output" : {      "messageType" : "ASSISTANT",      "metadata" : {        "finishReason" : "STOP",        "id" : "f08c10a5-8bb5-4cda-9c1c-43087452f826",        "role" : "ASSISTANT",        "messageType" : "ASSISTANT"      },      "toolCalls" : [ ],      "media" : [ ],      "text" : "40 除以 2 等于 **20**。  \n如果你有其他问题,随时问我哦! 😊"    },    "metadata" : {      "finishReason" : "STOP",      "contentFilters" : [ ],      "empty" : true    }  },  "metadata" : {    "id" : "f08c10a5-8bb5-4cda-9c1c-43087452f826",    "model" : "deepseek-chat",    "rateLimit" : {      "tokensReset" : 0.0,      "tokensLimit" : 0,      "requestsReset" : 0.0,      "requestsLimit" : 0,      "tokensRemaining" : 0,      "requestsRemaining" : 0    },    "usage" : {      "promptTokens" : 21,      "completionTokens" : 22,      "totalTokens" : 43,      "nativeUsage" : {        "promptTokens" : 21,        "totalTokens" : 43,        "completionTokens" : 22      }    },    "promptMetadata" : [ ],    "empty" : true  },  "results" : [ {    "output" : {      "messageType" : "ASSISTANT",      "metadata" : {        "finishReason" : "STOP",        "id" : "f08c10a5-8bb5-4cda-9c1c-43087452f826",        "role" : "ASSISTANT",        "messageType" : "ASSISTANT"      },      "toolCalls" : [ ],      "media" : [ ],      "text" : "40 除以 2 等于 **20**。  \n如果你有其他问题,随时问我哦! 😊"    },    "metadata" : {      "finishReason" : "STOP",      "contentFilters" : [ ],      "empty" : true    }  } ]}2025-10-27T20:20:25.739+08:00 DEBUG 19240 --- [oundedElastic-2] o.s.a.c.c.advisor.SimpleLoggerAdvisor    : request: ChatClientRequest[prompt=Prompt{messages=[UserMessage{content='messageType=40除以2等于几', metadata={messageType=USER}, messageType=USER}, AssistantMessage [messageType=ASSISTANT, toolCalls=[], textContent=40 除以 2 等于 **20**。  如果你有其他问题,随时问我哦! 😊, metadata={finishReason=STOP, id=f08c10a5-8bb5-4cda-9c1c-43087452f826, role=ASSISTANT, messageType=ASSISTANT}], SystemMessage{textContent='你是聪明的智能助手,名字叫小羊', messageType=SYSTEM, metadata={messageType=SYSTEM}}, UserMessage{content='messageType=那除以5呢', metadata={messageType=USER}, messageType=USER}], modelOptions=org.springframework.ai.deepseek.DeepSeekChatOptions@34422e1f}, context={chat_memory_conversation_id=111}]2025-10-27T20:20:27.328+08:00 DEBUG 19240 --- [oundedElastic-1] o.s.a.c.c.advisor.SimpleLoggerAdvisor    : response: {  "result" : {    "output" : {      "messageType" : "ASSISTANT",      "metadata" : {        "finishReason" : "STOP",        "id" : "81223274-c38b-4d65-b88c-8811abfc743d",        "role" : "ASSISTANT",        "messageType" : "ASSISTANT"      },      "toolCalls" : [ ],      "media" : [ ],      "text" : "40 除以 5 等于 **8**。  \n有其他问题的话,继续问我吧! 😃"    },    "metadata" : {      "finishReason" : "STOP",      "contentFilters" : [ ],      "empty" : true    }  },  "metadata" : {    "id" : "81223274-c38b-4d65-b88c-8811abfc743d",    "model" : "deepseek-chat",    "rateLimit" : {      "tokensReset" : 0.0,      "tokensLimit" : 0,      "requestsReset" : 0.0,      "requestsLimit" : 0,      "tokensRemaining" : 0,      "requestsRemaining" : 0    },    "usage" : {      "promptTokens" : 54,      "completionTokens" : 22,      "totalTokens" : 76,      "nativeUsage" : {        "promptTokens" : 54,        "totalTokens" : 76,        "completionTokens" : 22      }    },    "promptMetadata" : [ ],    "empty" : true  },  "results" : [ {    "output" : {      "messageType" : "ASSISTANT",      "metadata" : {        "finishReason" : "STOP",        "id" : "81223274-c38b-4d65-b88c-8811abfc743d",        "role" : "ASSISTANT",        "messageType" : "ASSISTANT"      },      "toolCalls" : [ ],      "media" : [ ],      "text" : "40 除以 5 等于 **8**。  \n有其他问题的话,继续问我吧! 😃"    },    "metadata" : {      "finishReason" : "STOP",      "contentFilters" : [ ],      "empty" : true    }  } ]}

需要注意,当前使用的InMemoryChatMemoryRepository将会话保存在内存,进程结束即销毁,如果正式的项目需要换成其他的实现来真正的持久化,而且会话的ID应该后台生成并和当前登录用户绑定,而不是由前端随便的传进去。

如果需求包括逐条加载和查看审批历史,可以根据ChatMemory的List<Message> get(String conversationId);方法,传入对话的ID即可获得,返回的List<Message>对象可以进一步包装成自己业务需要的对象数据格式。

package org.example.controller;import jakarta.annotation.Resource;import org.springframework.ai.chat.memory.ChatMemory;import org.springframework.ai.chat.messages.Message;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.List;@RestController@RequestMapping("ai")public class ChatController {    @Resource    private ChatMemory chatMemory;    @GetMapping(value = "chat-history")    public List<Message> history(String chatId) {        return chatMemory.get(chatId);    }}

如果要获得某个用户的所有会话以及会话历史,只需要发起会话时自己记录会话ID到数据库,到时候在查出返回即可。

  •