Skip to content
鼓励作者:欢迎打赏犒劳

spring-ai

https://gitee.com/lzh1995/spring-ai-demo

依赖

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>ai-demo3</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo3</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>21</java.version>
        <spring-ai.version>1.0.0-M6</spring-ai.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <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>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

对接ollama

第一个对话接口

application.properties

properties
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.model=deepseek-r1:1.5b

配置

java
package com.example.ai.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CommonConfiguration {

    @Bean
    public ChatClient chatClient(OllamaChatModel model) {
        return ChatClient
                .builder(model)
                .defaultSystem("你是一个热心、可爱的智能助手,你的名字叫小林同学,请以小林同学的身份和语气回答问题。")
                .build();
    }
}

web

java
package com.example.ai.web;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/ai")
public class ChatController {
    @Autowired
    private ChatClient chatClient;

    /**
     * 同步返回
     * @param prompt
     * @return
     */
    @RequestMapping(value = "/chat")
    public String chat(@RequestParam("prompt") String prompt) {
        return chatClient.prompt().user(prompt).call().content();
    }

    /**
     * 流式返回
     * @param prompt
     * @return
     */
    @RequestMapping(value = "/chat2", produces = "text/html;charset=utf-8")
    public Flux<String> chat2(@RequestParam("prompt") String prompt) {
        return chatClient.prompt().user(prompt).stream().content();
    }
}

分别访问:http://localhost:8080/ai/chat?prompt=你是谁
http://localhost:8080/ai/chat2?prompt=你是谁

会话日志

如果我们想打印请求ai的入参和出参,可以增加一个ai自带的拦截器即可。注意需要将日志设置成debug模式

properties
logging.level.org.springframework.ai=debug
java
@Configuration
public class CommonConfiguration {

    @Bean
    public ChatClient chatClient(OllamaChatModel model) {
        return ChatClient
                .builder(model)
                .defaultSystem("你是一个热心、可爱的智能助手,你的名字叫小林同学,请以小林同学的身份和语气回答问题。")
                // 新增自带的拦截器
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }
}

会话记忆

其实上就是每次问ai的时候,将之前对话都给ai传过去,ai就知道上下文了。

会话记忆肯定是根据会话来的,如果你新建一个会话,就不会知道你上一个会话说的什么了。

那么如何存储会话呢?spring-ai框架自带的有会话内存存储,当然你也可以存在mysql,redis中,只需要继承实现该ChatMemory类就行。

大致实现的原理就是,我们需要指定一个Advisors,表明我们要用内存会话管理,然后每次会话的时候,也需要指定一个Advisors,这个Advisors的作用是 用来告诉ai当前对话是哪个会话id,框架就会在每次请求的时候根据会话id获取之前的会话信息组装之后请求ai了。

角色描述示例
system优先于user指令之前的指令,也就是给大模型设定角色和任务背景的系统指令你是一个乐于助人的编程助手,你的名字叫小团团,请以小团团的风格来回答用户的问题。
user终端用户输入的指令(类似于你在ChatGPT聊天框输入的内容)你好,你是谁?
assistant由大模型生成的消息,可能是上一轮对话生成的结果注意,用户可能与模型产生多轮对话,每轮对话模型都会生成不同结果。

配置文件

java
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CommonConfiguration {

    //指定聊天记忆 - 基于内存的方式
    @Bean
    public ChatMemory chatMemory() {
        return new InMemoryChatMemory();
    }
    @Bean
    public ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory) {
        return ChatClient
                .builder(model)
                .defaultSystem("你是一个热心、可爱的智能助手,你的名字叫小林同学,请以小林同学的身份和语气回答问题。")
                .defaultAdvisors(
                        new SimpleLoggerAdvisor(),
                        new MessageChatMemoryAdvisor(chatMemory)
                )
                .build();
    }
}

接口

java
@RequestMapping(value = "/chat2", produces = "text/html;charset=utf-8")
public Flux<String> chat2(@RequestParam("prompt") String prompt,@RequestParam("chatId") String chatId) {
    return chatClient.prompt()
            .user(prompt)
            // CHAT_MEMORY_CONVERSATION_ID_KEY 是规定死的key,代表 【聊天_记忆_对话key】, 然后去找对应的chatId
            .advisors(advisorSpec -> advisorSpec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
            .stream().content();
}

会话历史

会话历史其实就是将用户的chatId保存一下,这个很简单。再一个就是根据chatId去查询该会话的内容,这个我们之前是保存到了内存中,所以我们 找的话,也是需要去内存中寻找的。我们之前指定了ChatMemory,这个类正好有提供List<Message> get(String conversationId, int lastN);方法 ,很简单,传递chatId和要查询的条数就行。需要注意的是,我们需要将它返回的格式转换成我们需要的格式。

主要接口

java
import com.example.ai.entity.vo.MessageVO;
import com.example.ai.service.ChatHistoryService;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/ai/history")
public class ChatHistoryController {

    @Autowired
    private ChatHistoryService chatHistoryService;

    @Autowired
    private ChatMemory chatMemory;

    @GetMapping("/{type}")
    public List<String> getChatIds(@PathVariable("type") String type) {
        return chatHistoryService.getChatIds(type);
    }

    @GetMapping("/{type}/{chatId}")
    public List<MessageVO> getChatHistory(@PathVariable("type") String type, @PathVariable("chatId") String chatId) {
        List<Message> messages = chatMemory.get(chatId, Integer.MAX_VALUE);
        if(messages == null) {
            return List.of();
        }
        return messages.stream().map(MessageVO::new).toList();
    }
}

返回结果类

java
import lombok.Data;
import org.springframework.ai.chat.messages.Message;

@Data
public class MessageVO {
    private String role;
    private String content;

    public MessageVO(Message message) {
        switch (message.getMessageType()) {
            case USER:
                role = "user";
                break;
            case ASSISTANT:
                role = "assistant";
                break;
            default:
                role = "";
                break;
        }
        this.content = message.getText();
    }
}

对接openAI

openAI是一个标准,通义千问就实现了这个标准,所以我们来对接一下阿里的通义千问。

注意 base_url:https://dashscope.aliyuncs.com/compatible-mode

第一个对话接口

依赖

xml
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>

application.properties

properties
## open-ai
spring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode
spring.ai.openai.api-key=sk-xxxxxxxxxxxxxxxxxxxx
spring.ai.openai.chat.options.model=qwen-plus

CommonConfiguration.java

java
package com.example.ai.config;

import com.example.ai.constants.SystemConstants;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CommonConfiguration {

    //指定聊天记忆 - 基于内存
    @Bean
    public ChatMemory chatMemory() {
        return new InMemoryChatMemory();
    }

    @Bean
    public ChatClient gameChatClient(OpenAiChatModel model, ChatMemory chatMemory) {
        return ChatClient
                .builder(model)
                .defaultSystem(SystemConstants.GAME_SYSTEM_PROMPT)
                .defaultAdvisors(
                        new SimpleLoggerAdvisor(),
                        new MessageChatMemoryAdvisor(chatMemory)
                )
                .build();
    }
}

接口

java
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;

@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class GameController {

    private final ChatClient gameChatClient;

    @RequestMapping(value = "/game", produces = "text/html;charset=utf-8")
    public Flux<String> chat(String prompt, String chatId) {
        return gameChatClient.prompt()
                .user(prompt)
                .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
                .stream()
                .content();
    }
}

访问接口:http://localhost:8080/ai/game?prompt=开始游戏&chatId=123

function call

就是定义一个java接口,但是要有注释说明,然后要配置一下,告诉ai模型,ai就会根据用户的意思去调用对应的接口。 很简单,只是写一个tool,然后配置一下就OK

java
import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
import com.example.ai.entity.po.Course;
import com.example.ai.entity.po.CourseReservation;
import com.example.ai.entity.po.School;
import com.example.ai.entity.query.CourseQuery;
import com.example.ai.service.CourseReservationService;
import com.example.ai.service.CourseService;
import com.example.ai.service.SchoolService;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

import java.util.List;

@RequiredArgsConstructor
@Component
public class CourseTools {

    private final CourseService courseService;
    private final SchoolService schoolService;
    private final CourseReservationService reservationService;

    @Tool(description = "根据条件查询课程")
    public List<Course> queryCourse(@ToolParam(description = "查询的条件", required = false) CourseQuery query) {
        if (query == null) {
            return courseService.list();
        }
        QueryChainWrapper<Course> wrapper = courseService.query()
                .eq(query.getType() != null, "type", query.getType()) // type = '编程'
                .le(query.getEdu() != null, "edu", query.getEdu());// edu <= 2
        if (query.getSorts() != null && !query.getSorts().isEmpty()) {
            for (CourseQuery.Sort sort : query.getSorts()) {
                wrapper.orderBy(true, sort.getAsc(), sort.getField());
            }
        }
        return wrapper.list();
    }

    @Tool(description = "查询所有校区")
    public List<School> querySchool() {
        return schoolService.list();
    }

    @Tool(description = "生成预约单,返回预约单号")
    public Integer createCourseReservation(
            @ToolParam(description = "预约课程") String course,
            @ToolParam(description = "预约校区") String school,
            @ToolParam(description = "学生姓名") String studentName,
            @ToolParam(description = "联系电话") String contactInfo,
            @ToolParam(description = "备注", required = false) String remark) {
        CourseReservation reservation = new CourseReservation();
        reservation.setCourse(course);
        reservation.setSchool(school);
        reservation.setStudentName(studentName);
        reservation.setContactInfo(contactInfo);
        reservation.setRemark(remark);
        reservationService.save(reservation);

        return reservation.getId();
    }
}

配置

java
@Bean
public ChatClient serviceChatClient(OpenAiChatModel model, ChatMemory chatMemory, CourseTools courseTools) {
    return ChatClient
            .builder(model)
            .defaultSystem(SystemConstants.SERVICE_SYSTEM_PROMPT)
            .defaultAdvisors(
                    new SimpleLoggerAdvisor(),
                    new MessageChatMemoryAdvisor(chatMemory)
            )
            //重点配置
            .defaultTools(courseTools)
            .build();
}

如有转载或 CV 的请标注本站原文地址