一、前言
本文是学习徐庶老师的一个SpringAI+通义大模型课程,所做的一个资料实现,感谢徐庶老师的分享!SpringAI+通义大模型带你撸一个前后端分离智能助手项目
二、准备工作
JDK17+、Node.js 18+、阿里通义大模型api_key(免费)
三、项目实现
1、前端主页代码
<template>
<el-row :gutter="20">
<el-col :span="16">
<el-table :data="tableData" stripe style="width: 100%">
<el-table-column prop="bookingNumber" label="#" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="date" label="Date" />
<el-table-column prop="from" label="From" />
<el-table-column prop="to" label="To" />
<el-table-column prop="bookingStatus" label="Status" >
<template #default="scope">
{{ scope.row.bookingStatus === "CONFIRMED" ? "✅" : "❌"}}
</template>
</el-table-column>
<el-table-column prop="bookingClass" label="Booking class" />
<el-table-column label="Operations" fixed="right" width="180" >
<template #default="scope">
<el-button size="small"
type="primary">
更改预定
</el-button>
<el-button
size="small"
type="danger">
退订
</el-button>
</template>
</el-table-column>
</el-table>
</el-col>
<el-col :span="8" style="background-color: aliceblue;">
<div style="height: 500px;overflow: scroll">
<el-timeline style="max-width: 100%">
<el-timeline-item
v-for="(activity, index) in activities"
:key="index"
:icon="activity.icon"
:type="activity.type"
:color="activity.color"
:size="activity.size"
:hollow="activity.hollow"
:timestamp="activity.timestamp"
>
{{ activity.content }}
</el-timeline-item>
</el-timeline>
</div>
<div id="container">
<div id="chat">
<el-input
v-model="msg"
input-style="width: 100%;height:50px"
:rows="2"
type="text"
placeholder="Please input"
@keydown.enter="sendMsg();"
/>
<el-button @click="sendMsg()">发送</el-button>
</div>
</div>
</el-col>
</el-row>
</template>
<script lang="ts">
import { MoreFilled } from '@element-plus/icons-vue'
import {ref, onMounted} from "vue";
import axios from 'axios'//引入axios
export default {
setup() {
const activities = ref([
{
content: '⭐欢迎来到图灵航空✈!请问有什么可以帮您的?',
timestamp: new Date().toLocaleDateString() + " " + new Date().toLocaleTimeString(),
color: '#0bbd87',
},
]);
const msg = ref('');
const tableData = ref([]);
let count = 2;
let eventSource;
const sendMsg = () => {
if (eventSource) {
eventSource.close();
}
activities.value.push(
{
content: `你:${msg.value}`,
timestamp: new Date().toLocaleDateString() + " " + new Date().toLocaleTimeString(),
size: 'large',
type: 'primary',
icon: MoreFilled,
},
);
activities.value.push(
{
content: 'waiting...',
timestamp: new Date().toLocaleDateString() + " " + new Date().toLocaleTimeString(),
color: '#0bbd87',
},
);
// sse: 服务端推送 Server-Sent Events
eventSource = new EventSource(`http://localhost:8080/ai/generateStreamAsString?message=${msg.value}`);
msg.value='';
eventSource.onmessage = (event) => {
if (event.data === '[complete]') {
count = count + 2;
eventSource.close();
getBookings(); // 每次对话完后刷新列表
return;
}
activities.value[count].content += event.data;
};
eventSource.onopen = () => {
activities.value[count].content = '';
};
};
const getBookings = () => {
axios.get('http://localhost:8080/booking/list')
.then((response) => {
debugger;
tableData.value = response.data;
})
.catch((error) => {
console.error(error);
});
};
// Use onMounted to call getBookings when the component is mounted
onMounted(() => {
getBookings();
});
return {
activities,
msg,
tableData,
sendMsg,
getBookings,
};
},
};
</script>
<style scoped>
* {
margin: 0;
padding: 0;
}
#chat button{
position: absolute;
margin-left: -60px;
margin-top: 19px;
}
</style>
2、主启动类SpringAiDemoApplication
@SpringBootApplication
public class SpringAiDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAiDemoApplication.class, args);
}
@Bean
public ChatMemory chatMemory(){
return new InMemoryChatMemory();
}
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel)
.build();
}
// 启动springboot的时候就会运行
@Bean
CommandLineRunner ingestTermOfServiceToVectorStore(EmbeddingModel embeddingModel, VectorStore vectorStore,
@Value("classpath:rag/terms-of-service.txt") Resource termsOfServiceDocs) {
return args -> {
vectorStore.write( // 3.写入
new TokenTextSplitter().transform( // 2.转换
new TextReader(termsOfServiceDocs).read()) // 1.读取
);
};
}
}
3、控制层BookingController和OpenAiController
BookingController
@RestController
@CrossOrigin
public class BookingController {
private final FlightBookingService flightBookingService;
public BookingController(FlightBookingService flightBookingService) {
this.flightBookingService = flightBookingService;
}
@CrossOrigin
@GetMapping(value = "/booking/list")
public List<BookingDetails> getBookings() {
return flightBookingService.getBookings();
}
}
OpenAiController
@RestController
@CrossOrigin
public class OpenAiController {
private final ChatClient chatClient;
public OpenAiController(ChatClient.Builder chatClientBuilder, ChatMemory chatMemory, VectorStore vectorStore) {
this.chatClient = chatClientBuilder.defaultSystem(
"""
您是“Tuling”航空公司的客户聊天支持代理。请以友好、乐于助人且愉快的方式来回复。
您正在通过在线聊天系统与客户互动。
在提供有关预订或取消预订的信息之前,您必须始终
从用户处获取以下信息:预订号、客户姓名。
在询问用户之前,请检查消息历史记录以获取此信息。
在更改或退订之前,请先获取预订信息并且用户确定之后才进行更改或退订。
请讲中文。
今天的日期是 {current_date}.
"""
)
.defaultAdvisors(
new PromptChatMemoryAdvisor(chatMemory),
new LoggingAdvisor(),
new QuestionAnswerAdvisor(vectorStore, new SearchRequest()) // RAG
)
.defaultFunctions("cancelBooking","getBookingDetails","changeBooking")
.build();
}
@CrossOrigin
@GetMapping(value = "/ai/generateStreamAsString", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> generateStreamAsString(@RequestParam(value = "message", defaultValue = "讲个笑话") String message) {
Flux<String> content = this.chatClient.prompt()
.user(message)
.system(promptSystemSpec -> promptSystemSpec.param("current_date", LocalDate.now().toString()))
.advisors(advisorSpec -> advisorSpec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY,100))
.stream()
.content();
return content.concatWith(Flux.just("[complete]"));
}
}
4、服务层FlightBookingService和BookingTools
FlightBookingService
@Service
public class FlightBookingService {
private final BookingData db;
public FlightBookingService() {
db = new BookingData();
initDemoData();
}
private void initDemoData() {
// 模拟从数据库查询加载数据
List<String> names = List.of("徐庶", "诸葛", "百里", "楼兰", "庄周");
List<String> airportCodes = List.of("北京", "上海", "广州", "深圳", "杭州", "南京", "青岛", "成都", "武汉", "西安", "重庆", "大连",
"天津");
Random random = new Random();
var customers = new ArrayList<Customer>();
var bookings = new ArrayList<Booking>();
for (int i = 0; i < 5; i++) {
String name = names.get(i);
String from = airportCodes.get(random.nextInt(airportCodes.size()));
String to = airportCodes.get(random.nextInt(airportCodes.size()));
BookingClass bookingClass = BookingClass.values()[random.nextInt(BookingClass.values().length)];
Customer customer = new Customer();
customer.setName(name);
LocalDate date = LocalDate.now().plusDays(2 * (i + 1));
Booking booking = new Booking("10" + (i + 1), date, customer, BookingStatus.CONFIRMED, from, to,
bookingClass);
customer.getBookings().add(booking);
customers.add(customer);
bookings.add(booking);
}
// Reset the database on each start
db.setCustomers(customers);
db.setBookings(bookings);
}
// 获取所有已预订航班
public List<BookingDetails> getBookings() {
return db.getBookings().stream().map(this::toBookingDetails).toList();
}
// 根据编号+姓名查询航班
private Booking findBooking(String bookingNumber, String name) {
return db.getBookings()
.stream()
.filter(b -> b.getBookingNumber().equalsIgnoreCase(bookingNumber))
.filter(b -> b.getCustomer().getName().equalsIgnoreCase(name))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Booking not found"));
}
// 根据编号+姓名查询查询航班详情(function-call用)
public BookingDetails getBookingDetails(String bookingNumber, String name) {
var booking = findBooking(bookingNumber, name);
return toBookingDetails(booking);
}
// 更改预定航班
public void changeBooking(String bookingNumber, String name, String newDate, String from, String to) {
var booking = findBooking(bookingNumber, name);
if (booking.getDate().isBefore(LocalDate.now().plusDays(1))) {
throw new IllegalArgumentException("Booking cannot be changed within 24 hours of the start date.");
}
booking.setDate(LocalDate.parse(newDate));
booking.setFrom(from);
booking.setTo(to);
}
// 取消预定航班
public void cancelBooking(String bookingNumber, String name) {
var booking = findBooking(bookingNumber, name);
// 是不是发车前2天
if (booking.getDate().isBefore(LocalDate.now().plusDays(2))) {
throw new IllegalArgumentException("Booking cannot be cancelled within 48 hours of the start date.");
}
booking.setBookingStatus(BookingStatus.CANCELLED);
}
private BookingDetails toBookingDetails(Booking booking) {
return new BookingDetails(booking.getBookingNumber(), booking.getCustomer().getName(), booking.getDate(),
booking.getBookingStatus(), booking.getFrom(), booking.getTo(), booking.getBookingClass().toString());
}
}
BookingTools
@Configuration
public class BookingTools {
@Autowired
FlightBookingService flightBookingService;
@JsonInclude(Include.NON_NULL)
public record BookingDetails(String bookingNumber, String name, LocalDate date, BookingStatus bookingStatus,
String from, String to, String bookingClass) {
}
public record CancelBookingRequest(String bookingNumber,String name){}
public record BookingDetailsRequest(String bookingNumber,String name){}
public record ChangeBookingRequest(String bookingNumber,String name, String newDate, String from, String to){}
@Bean
@Description("处理机票退订")
public Function<CancelBookingRequest,String> cancelBooking(){
return cancelBookingRequest -> {
// apply 调用退订方法
flightBookingService.cancelBooking(cancelBookingRequest.bookingNumber(),cancelBookingRequest.name());
return "退订成功!";
};
}
@Bean
@Description("获取机票预定详细信息")
public Function<BookingDetailsRequest, BookingDetails> getBookingDetails() {
return request -> {
try {
return flightBookingService.getBookingDetails(request.bookingNumber(), request.name());
}
catch (Exception e) {
return new BookingDetails(request.bookingNumber(), request.name(), null, null, null, null, null);
}
};
}
@Bean
@Description("更改预定")
public Function<ChangeBookingRequest,String> changeBooking(){
return changeBookingRequest -> {
// apply 调用退订方法
flightBookingService.changeBooking(changeBookingRequest.bookingNumber(),changeBookingRequest.name(),
changeBookingRequest.newDate,changeBookingRequest.from,changeBookingRequest.to);
return "退订成功!";
};
}
}
四、项目结构和源码




被折叠的 条评论
为什么被折叠?



