文章

springai 简易聊天机器人设计

1. 引言

Spring AI Alibaba 开源项目基于 Spring AI 构建,是阿里云通义系列模型及服务在 Java AI 应用开发领域的最佳实践,提供高层次的 AI API 抽象与云原生基础设施集成方案,帮助开发者快速构建 AI 应用。

image-20241112230716389

2. 效果展示

20241112_223517

**源代码 **simple-chatboot: 一个简易的聊天机器人,使用spring ai aibaba (gitee.com)

3. 代码实现

依赖

        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter</artifactId>
            <version>1.0.0-M2</version>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <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>

注意:由于 spring-ai 相关依赖包还没有发布到中央仓库,如出现 spring-ai-core 等相关依赖解析问题,请在您项目的 pom.xml 依赖中加入如下仓库配置。

<repositories>
  <repository>
    <id>spring-milestones</id>
    <name>Spring Milestones</name>
    <url><https://repo.spring.io/milestone></url>
    <snapshots>
      <enabled>false</enabled>
    </snapshots>
  </repository>
</repositories>
@SpringBootApplication
public class SimpleChatbootApplication {
​
    public static void main(String[] args) {
        SpringApplication.run(SimpleChatbootApplication.class, args);
    }
​
}

配置自定义ChatClient

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
@Configuration
public class ChatClientConfig {
​
   static ChatMemory chatMemory = new InMemoryChatMemory();
   @Bean
    public ChatClient chatClient(ChatModel chatModel) {
       return ChatClient.builder(chatModel)
                .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
                .build();
    }
​
}

controller类

import ch.qos.logback.core.util.StringUtil;
import com.hbduck.simplechatboot.demos.function.WeatherService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
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;
import reactor.core.publisher.Flux;
​
import java.util.UUID;
​
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;
​
@RestController
@RequestMapping("/ai")
public class ChatModelController {
​
    private final ChatModel chatModel;
    private final ChatClient chatClient;
​
    public ChatModelController(ChatModel chatModel, ChatClient chatClient) {
        this.chatClient = chatClient;
        this.chatModel = chatModel;
    }
​
    @GetMapping("/stream")
    public String stream(String input) {
​
        StringBuilder res = new StringBuilder();
        Flux<ChatResponse> stream = chatModel.stream(new Prompt(input));
        stream.toStream().toList().forEach(resp -> {
             res.append(resp.getResult().getOutput().getContent());
        });
​
        return res.toString();
    }
    @GetMapping(value = "/memory", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> memory(@RequestParam("conversantId") String conversantId, @RequestParam("input") String input) {
        if (StringUtil.isNullOrEmpty(conversantId)) {
            conversantId = UUID.randomUUID().toString();
        }
        String finalConversantId = conversantId;
​
        Flux<ChatResponse> chatResponseFlux = chatClient
                .prompt()
                .function("getWeather", "根据城市查询天气", new WeatherService())
                .user(input)
                .advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, finalConversantId)
                        .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
                .stream().chatResponse();
​
        return Flux.concat(
                // First event: send conversationId
                Flux.just(ServerSentEvent.<String>builder()
                        .event("conversationId")
                        .data(finalConversantId)
                        .build()),
                // Subsequent events: send message content
                chatResponseFlux.map(response -> ServerSentEvent.<String>builder()
                        .id(UUID.randomUUID().toString())
                        .event("message")
                        .data(response.getResult().getOutput().getContent())
                        .build())
        );
    }
}

配置文件

server:
  port: 8000
​
spring:
  thymeleaf:
    cache: true
    check-template: true
    check-template-location: true
    content-type: text/html
    enabled: true
    encoding: UTF-8
    excluded-view-names: ''
    mode: HTML5
    prefix: classpath:/templates/
    suffix: .html
  ai:
    dashscope:
      api-key: ${AI_DASHSCOPE_API_KEY}
    chat:
      client:
        enabled: false

前端页面

<!DOCTYPE html>
<html>
<head>
    <title>AI Chat Bot</title>
    <style>
        #chatBox {
            height: 400px;
            border: 1px solid #ccc;
            overflow-y: auto;
            margin-bottom: 10px;
            padding: 10px;
        }
        .message {
            margin: 5px;
            padding: 5px;
        }
        .user-message {
            background-color: #e3f2fd;
            text-align: right;
        }
        .bot-message {
            background-color: #f5f5f5;
            white-space: pre-wrap;  /* 保留换行和空格 */
            word-wrap: break-word;  /* 长单词换行 */
        }
    </style>
</head>
<body>
    <h1>AI Chat Bot</h1>
    <div id="chatBox"></div>
    <input type="text" id="userInput" placeholder="Type your message..." style="width: 80%">
    <button onclick="sendMessage()">Send</button>
​
    <script>
        let conversationId = null;
        let currentMessageDiv = null;
​
        function addMessage(message, isUser) {
            const chatBox = document.getElementById('chatBox');
            const messageDiv = document.createElement('div');
            messageDiv.className = `message ${isUser ? 'user-message' : 'bot-message'}`;
            messageDiv.textContent = message;
            chatBox.appendChild(messageDiv);
            chatBox.scrollTop = chatBox.scrollHeight;
            return messageDiv;
        }
​
        async function sendMessage() {
            const input = document.getElementById('userInput');
            const message = input.value.trim();
​
            if (message) {
                addMessage(message, true);
                input.value = '';
​
                // Create bot message container
                currentMessageDiv = addMessage('', false);
​
                const eventSource = new EventSource(`/ai/memory?conversantId=${conversationId || ''}&input=${encodeURIComponent(message)}`);
​
                eventSource.onmessage = function(event) {
                    const content = event.data;
                    if (currentMessageDiv) {
                        currentMessageDiv.textContent += content;
                    }
                };
​
                eventSource.addEventListener('conversationId', function(event) {
                    if (!conversationId) {
                        conversationId = event.data;
                    }
                });
​
                eventSource.onerror = function(error) {
                    console.error('SSE Error:', error);
                    eventSource.close();
                    if (currentMessageDiv && currentMessageDiv.textContent === '') {
                        currentMessageDiv.textContent = 'Sorry, something went wrong!';
                    }
                };
​
                // Close the connection when the response is complete
                eventSource.addEventListener('complete', function(event) {
                    eventSource.close();
                    currentMessageDiv = null;
                });
            }
        }
​
        // Allow sending message with Enter key
        document.getElementById('userInput').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                sendMessage();
            }
        });
    </script>
</body>
</html>

带chat memory 的对话

可以使用 InMemoryChatMemory实现

   //初始化InMemoryChatMemory
   static ChatMemory chatMemory = new InMemoryChatMemory();
   //在ChatClient 配置memory
      @Bean
    public ChatClient chatClient(ChatModel chatModel) {
       return ChatClient.builder(chatModel)
                .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
                .build();
    }
    //调用时配置
        Flux<ChatResponse> chatResponseFlux = chatClient
                .prompt()
                .function("getWeather", "根据城市查询天气", new WeatherService())
                .user(input)
                .advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, finalConversantId)
                        .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
                .stream().chatResponse();

工具

“工具(Tool)”或“功能调用(Function Calling)”允许大型语言模型(LLM)在必要时调用一个或多个可用的工具,这些工具通常由开发者定义。工具可以是任何东西:网页搜索、对外部 API 的调用,或特定代码的执行等。LLM 本身不能实际调用工具;相反,它们会在响应中表达调用特定工具的意图(而不是以纯文本回应)。然后,我们应用程序应该执行这个工具,并报告工具执行的结果给模型。

通过工具来实现获取当前天气

天气获取的类,目前使用硬编码温度

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.hbduck.simplechatboot.demos.entity.Response;
​
import java.util.function.Function;
​
public class WeatherService implements Function<WeatherService.Request, Response> {
​
    @Override
    public Response apply(Request request) {
        if (request.city().contains("杭州")) {
            return new Response(String.format("%s%s晴转多云, 气温32摄氏度。", request.date(), request.city()));
        }
        else if (request.city().contains("上海")) {
            return new Response(String.format("%s%s多云转阴, 气温31摄氏度。", request.date(), request.city()));
        }
        else {
​
            return new Response(String.format("%s%s多云转阴, 气温31摄氏度。", request.date(), request.city()));
        }
    }
​
    @JsonInclude(JsonInclude.Include.NON_NULL)
    @JsonClassDescription("根据日期和城市查询天气")
    public record Request(
            @JsonProperty(required = true, value = "city") @JsonPropertyDescription("城市, 比如杭州") String city,
            @JsonProperty(required = true, value = "date") @JsonPropertyDescription("日期, 比如2024-08-22") String date) {
    }
}

chatClient配置function

Flux<ChatResponse> chatResponseFlux = chatClient
                .prompt()
                .function("getWeather", "根据城市查询天气", new WeatherService())
                .user(input)
                .advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, finalConversantId)
                        .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
                .stream().chatResponse();
License:  CC BY 4.0