前言

目前大模型应用开发比较常见的框架是基于Python语言的LangChain,但是对于Java程序员平时使用Spring生态

开发的大量应用来说,适配性就稍微差了些。

而Spring公司推出的SpringAI框架,充分利用了Spring框架中AOP、IOC的能力,可以与现有的Java项目无缝合,

非常方便。

但是Spring AI要求的JDK版本至少是JDK17SpringBoot也必须是3.x的版本才可以,JDK8是不支持使用Spring AI的。

大模型调用基础知识

以DeepSeek官方给出的文档为例:

# Please install OpenAI SDK first: `pip3 install openai`

from openai import OpenAI

# 1.初始化OpenAI客户端,要指定两个参数:api_key、base_url
client = OpenAI(api_key="<DeepSeek API Key>", base_url="https://api.deepseek.com")

# 2.发送http请求到大模型,参数比较多
response = client.chat.completions.create(
    model="deepseek-chat",  # 2.1.选择要访问的模型
    messages=[ 				# 2.2.发送给大模型的消息
        {"role": "system", "content": "You are a helpful assistant"},
        {"role": "user", "content": "Hello"},
    ],
    stream=False 			# 2.3.是否以流式返回结果
)

print(response.choices[0].message.content)
接口说明
  • 请求方式:通常是POST,因为要传递JSON风格的参数
  • 请求路径:与平台有关
    • DeepSeek官方平台:https://api.deepseek.com
    • 阿里云百炼平台:https://dashscope.aliyuncs.com/compatible-mode/v1
    • 本地ollama部署的模型:http://localhost:11434
  • 安全校验:开放平台都需要提供API_KEY来校验权限,本地ollama则不需要
  • 请求参数:参数很多,比较常见的有:
    • model:要访问的模型名称
    • messages:发送给大模型的消息,是一个数组
    • stream:true,代表响应结果流式返回;false,代表响应结果一次性返回,但需要等待
    • temperature:取值范围[0:2),代表大模型生成结果的随机性,越小随机性越低。DeepSeek-R1不支持

注意,这里请求参数中的messages是一个消息数组,而且其中的消息要包含两个属性:

  • role:消息对应的角色
  • content:消息内容

其中消息的内容,也被称为提示词Prompt),也就是发送给大模型的指令

提示词角色

通常消息的角色有三种:

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

其中System类型的消息非常重要!影响了后续AI会话的行为模式。

比如,我们会发现,当我们询问这些AI对话产品“你是谁”这个问题的时候,每一个AI的回答都不一样。

而回答的模式就是遵循System的设定来回答。

会话记忆问题

这里还有一个问题:

我们为什么要把历史消息都放入Messages中,形成一个数组呢?

这是因为大模型是没有记忆的,因此我们调用API接口与大模型对话时,每一次对话信息都不会保留,多次对话之间都是独立的,没有关联的。

但是大家可能发现了,我们使用的AI对话产品却能够记住每一轮对话信息,根据这些信息进一步回答,这是怎么回事呢?

答案就是Messages数组。

我们只需要每一次发送请求时,都把历史对话中每一轮的User消息、Assistant消息都封装到Messages数组中,一起发送给大模型,这样大模型就会根据这些历史对话信息进一步回答,就像是拥有了记忆一样。

这里还有一个问题:

我们为什么要把历史消息都放入Messages中,形成一个数组呢?

这是因为大模型是没有记忆的,因此我们调用API接口与大模型对话时,每一次对话信息都不会保留,多次对话之间都是独立的,没有关联的。

但是大家可能发现了,我们使用的AI对话产品却能够记住每一轮对话信息,根据这些信息进一步回答,这是怎么回事呢?

答案就是Messages数组。

我们只需要每一次发送请求时,都把历史对话中每一轮的User消息、Assistant消息都封装到Messages数组中,一起发送给大模型,这样大模型就会根据这些历史对话信息进一步回答,就像是拥有了记忆一样。

在这里插入图片描述

Spring AI的接入与基本使用

下面的配置基于Spring AI 1.0.1 正式版和Spring Boot 3.5.5。

Maven依赖

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-openai</artifactId>
            <version>1.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>

第一步,直接引入Spring AI中关于openai的起步依赖,这样一来,项目中就可以使用Spring AI接入本地或者各类远程的AI大模型了。

Yaml配置

spring:
  application:
    name: demo-ai
  ai:
    openai:
      base-url: https://api.deepseek.com/
      api-key: 
      chat:
        options:
          model: deepseek-reasoner
          temperature: 0.7

这里需要配置openAI的相关配置:

  • base-url:模型的地址

  • api-key:apply for an API key,调用远程模型时一般都是收费的,如果是本地模型可以没有这个配置

  • model:选择的远程模型种类

  • temperature:温度,意思就是回答的随机性,值越大,回答的随机性越大,这和Transformer神经网络的推理模式有关,根据前文推测出接下来的一个词语后,把这个词语加入前文,再次交给大模型处理,推测下一个字,然后不断重复前面的过程,就可以生成大段的内容了。

这里就以配置Deepseek远程模型为例,后续改成其它远程大模型比如Open AI都是兼容的。

Deepseek远程模型地址和APIKey获取方式

Deepseek API文档当中可以看到官方提供的base-url和两个常见的model。

Deepseek API文档地址:

https://api-docs.deepseek.com/zh-cn/

在这里插入图片描述

Deepseek的ApiKey获取地址:

https://platform.deepseek.com/api_keys

不过这个需要进行充值才可以创建自己的ApiKey。

在这里插入图片描述

增加Configuration配置类配置SpringAI的必需Bean

这里就先展示最基本的ChatClient配置,有了它就可以在代码中使用大模型的基本功能了。

@Configuration
public class CommonConfiguration {

    @Bean("chatClient")
    public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory) {
        return ChatClient.builder(model)
            	.defaultSystem("你是可爱且热情、人见人爱,花见花开的AI助手,你的名字是墩墩,请用墩墩的身份回答用户的问题!")
                .build();
    }

}

defaultSystem可以修改一些系统的默认配置,给AI设定初始的角色,回答问题的范围等等

提供Controller层对外暴露的接口

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

    private final @Qualifier("chatClient")ChatClient chatClient;

    // 非流式的接口,这种情况要等AI大模型完成全部回答再一次性返回结果
    @GetMapping("/chat/simple")
    @CrossOrigin
    public String chat(String prompt) {
        return chatClient.prompt(prompt)
                .user(prompt)
                .call()
                .content();
    }

    // 流式的接口,这种情况就是大模型生成一个字就返回一个字,和我们平时用deepSeek问问题是一样的效果
    // 但是这里一定要设置字符编码,要不然是会乱码的
    @RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
    public Flux<String> chatStream(String prompt) {
        return chatClient.prompt(prompt)
                .user(prompt)
                .stream()
                .content();
    }

}

效果展示

在这里插入图片描述

非流式的接口和流式接口返回的结果都是差不多的,只不过一个是一次性返回,一个是一个字一个字往外蹦。

SpringAI 日志配置

默认情况下,应用于AI的交互时不记录日志的,我们无法得知SpringAI组织的提示词到底长什么样,有没有问题。

这样不方便我们调试。

这里就需要用到SpringAI的Advisor。

Advisor的原理

SpringAI基于AOP机制实现与大模型对话过程的增强、拦截、修改等功能。所有的增强通知都需要实现Advisor接口。

在这里插入图片描述

SpringAI提供了一些Advisor的默认实现,来实现一些基本的增强功能:
在这里插入图片描述

  • SimpleLoggerAdvisor:日志记录的Advisor
  • MessageChatMemoryAdvisor:会话记忆的Advisor
  • QuestionAnswerAdvisor:实现RAG的Advisor

当然,我们也可以自定义Advisor,具体可以参考官方文档:

https://docs.spring.io/spring-ai/reference/1.0/api/advisors.html#_implementing_an_advisor

添加日志Advisor

在配置类中给ChatClient添加日志Advisor。

@Configuration
public class CommonConfiguration {

    @Bean("chatClient")
    public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory) {
        return ChatClient.builder(model)
            	.defaultSystem("你是可爱且热情、人见人爱,花见花开的AI助手,你的名字是墩墩,请用墩墩的身份回答用户的问题!")
            	.defaultAdvisors(
                        new SimpleLoggerAdvisor()	// 添加默认的Advisor,记录日志
                )
                .build();
    }
}

application.yaml中添加日志配置:

logging:
  level:
    org.springframework.ai.chat.client.advisor: DEBUG
    com.xxx.demo.ai: DEBUG

效果展示

在这里插入图片描述

这样一来控制台就可以看到AI会话的日志,就可以方便调试了。

SpringAI原生会话记忆功能

由于将会话记忆持久化的篇幅会多一些,这里就讲解最容易的会话记忆实现方式,主要看看思路。

这里依旧是使用上面提到的Advisor去对会话过程进行增强,从而达到会话记忆的功能。

配置会话记忆Advisor

@Configuration
public class CommonConfiguration {

    @Bean("chatClient")
    public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory) {
        return ChatClient.builder(model)
            	.defaultSystem("你是可爱且热情、人见人爱,花见花开的AI助手,你的名字是墩墩,请用墩墩的身份回答用户的问题!")
            	.defaultAdvisors(
                        new SimpleLoggerAdvisor()	// 添加默认的Advisor,记录日志
            			// 有了下面这句话,会话就有了记忆功能
            			MessageChatMemoryAdvisor.builder(chatMemory).build() 
                )
                .build();
    }

}

此时,会话记忆信息是保存在内存当中的,如果重启了系统,会话记忆依旧是会丢的。

会话历史的实现-内存方式

有了单个会话记忆之后还需要有会话历史的功能

比如deepseek这里就是会话历史的功能

在这里插入图片描述

定义接口
// 保存会话历史的接口
public interface ChatHistoryService {

    /**
     * 保存会话历史记录
     * @param type 会话业务类型   如:chat(聊天)
     * @param chatId 会话ID
     */
    void save(String type, String chatId);

    /**
     * 根据业务类型获取会话id列表
     * @param type 会话业务类型  如:chat(聊天)
     * @return 会话ID列表
     */
    List<String> getChatIds(String type);

}
定义实现类
@Component
@RequiredArgsConstructor
public class InMemoryChatHistoryRepository implements ChatHistoryRepository {

    private Map<String, List<String>> chatHistory;

    @Override
    public void save(String type, String chatId) {
        /*if (!chatHistory.containsKey(type)) {
            chatHistory.put(type, new ArrayList<>());
        }
        List<String> chatIds = chatHistory.get(type);*/
        List<String> chatIds = chatHistory.computeIfAbsent(type, k -> new ArrayList<>());
        if (chatIds.contains(chatId)) {
            return;
        }
        chatIds.add(chatId);
    }

    @Override
    public List<String> getChatIds(String type) {
        /*List<String> chatIds = chatHistory.get(type);
        return chatIds == null ? List.of() : chatIds;*/
        return chatHistory.getOrDefault(type, List.of());
    }
}
Controller层
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {

    private final @Qualifier("chatClient")ChatClient chatClient;

    private final ChatHistoryService chatHistoryService;
    
	@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
    public Flux<String> chatStream(String prompt, String chatId) {
        // 1.保存会话id 
        chatHistoryService.save(ServiceTypeEnum.CHAT.getType(), chatId);
        // 2.请求模型
        return chatClient.prompt(prompt)
                .user(prompt)
            	// 添加一个SpringAOP环绕增强的配置 用作会话ID记忆,这样每次会话的内容就不会串
                .advisors(a -> a.param(CONVERSATION_ID, chatId))  
                .stream()
                .content();
    }
    
    @RequestMapping("/history/{type}")
    public List<String> getChatIds(@PathVariable String type) {
        return chatHistoryService.getChatIds(type);
    }

    @RequestMapping("/history/{type}/{chatId}")
    public List<MessageVO> getChatHistory(@PathVariable String type, @PathVariable String chatId) {
        List<Message> messages = chatMemory.get(chatId);
        if (messages == null) {
            return List.of();
        }
        return messages.stream().map(MessageVO::new).toList();
    }
}
枚举
@Getter
@AllArgsConstructor
public enum ServiceTypeEnum {
    /**
     * 聊天
     */
    CHAT("chat");

    private final String type;

}
消息VO类
@Data
@NoArgsConstructor
public class MessageVO {
    private String role;
    private String content;

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

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐