简易聊天机器人设计
1. 引言
Spring AI Alibaba 开源项目基于 Spring AI 构建,是阿里云通义系列模型及服务在 Java AI 应用开发领域的最佳实践,提供高层次的 AI API 抽象与云原生基础设施集成方案,帮助开发者快速构建 AI 应用。
2. 效果展示
源代码地址 DailySmileStart/simple-chatboot (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();
作者:平平无奇加油鸭
来源:juejin.cn/post/7436369701020516363
来源:juejin.cn/post/7436369701020516363