示例目标是使用 Spring AI Alibaba 框架开发一个智能机票助手,它可以帮助消费者完成机票预定、问题解答、机票改签、取消等动作,具体要求为:
- 基于 AI 大模型与用户对话,理解用户自然语言表达的需求
- 支持多轮连续对话,能在上下文中理解用户意图
- 理解机票操作相关的术语与规范并严格遵守,如航空法规、退改签规则等
- 在必要时可调用工具辅助完成任务
使用 RAG 增加机票退改签规则
基于以上架构图,应用是由 AI 模型理解用户问题,决策下一步动作、驱动业务流程。但任何一个通用的大模型都能帮我们解决机票相关的问题吗?依赖模型的决策是可靠的吗?比如有用户提出了机票改签的诉求,模型一定是能够很好的理解用户的意图的,这点没有疑问。但它怎么知道当前用户符不符合退票规则呢?要知道每个航空公司的改签规则可能都是不一样的;它怎么知道改签手续费的规定那?在这样一个可能带来经济纠纷、法律风险的应用场景下,AI模型必须要知道改签规则的所有细节,并逐条确认用户信息符合规则后,才能最终作出是否改签的决策。
很显然,单纯依赖 AI 模型本身并不能替我们完成上面的要求,这个时候就要用到 RAG(检索增强)模式了。通过 RAG 我们可以把机票退改签相关的领域知识输入给应用和 AI 模型,让 AI 结合这些规则与要求辅助决策
使用 Function Calling 执行业务动作
AI 智能体可以帮助应用理解用户需求并作出决策,但是它没法代替应用完成决策的执行,决策的执行还是要由应用自己完成,这一点和传统应用并没有区别,不论是智能化的还是预先编排好的应用,都是要由应用本身去调用函数修改数据库记录实现数据持久化。
通过 Spring AI 框架,我们可以将模型的决策转换为对某个具体函数的调用,从而完成机票的最终改签或者退票动作,将用户数据写入数据库,这就是我们前面提到的 Function Calling 模式。
使用 Chat Memory 增加多轮对话能力
最后一点是关于多轮连续对话的支持,我们要记住一点,大模型是无状态的,它看到的只有当前这一轮对话的内容。因此如果要支持多轮对话效果,需要应用每次都要把之前的对话上下文保留下来,并与最新的问题一并作为 prompt 发送给模型。这时,我们可以利用 Spring AI Alibaba 提供的内置 Conversation Memory 支持,方便的维护对话上下文。
总结起来,我们在这个智能机票助手应用中用到了 Spring AI Alibaba 的核心如下能力:
- 基本模型对话能力,通过 Chat Model API 与阿里云通义模型交互
- Prompt 管理能力
- Chat Memory 聊天记忆,支持多轮对话
- RAG、Vector Store,机票预定、改签、退票等相关规则
1、jdk17 ,spring-ai-alibaba版本 1.0.0-M6.1,使用阿里云大模型,若使用其他模型添加对应依赖即可
pom文件
<properties>
<java.version>17</java.version>
<vaadin.version>24.4.7</vaadin.version>
<maven-deploy-plugin.version>3.1.1</maven-deploy-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Other spring dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2、application.properties 内容如下
# spring.ai.chat.client.enabled=false
server.port=19000
spring.threads.virtual.enabled=true
spring.mvc.static-path-pattern=/templates/**
spring.thymeleaf.cache=false
###################
# Anthropic Claude 3
###################
# spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY}
# spring.ai.openai.chat.options.model=llama3-70b-8192
# spring.ai.anthropic.chat.options.model=claude-3-5-sonnet-20240620
###################
# Groq
###################
# spring.ai.openai.api-key=${GROQ_API_KEY}
# spring.ai.openai.base-url=https://api.groq.com/openai
# spring.ai.openai.chat.options.model=llama3-70b-8192
###################
# dashscope
###################
spring.ai.dashscope.api-key=${AI_DASHSCOPE_API_KEY}
spring.ai.dashscope.chat.options.model=qwen-plus
spring.ai.dashscope.embedding.enabled=true
spring.ai.dashscope.embedding.options.model=text-embedding-v2
###################
# OpenAI
###################
# spring.ai.openai.chat.options.functions=getBookingDetails,changeBooking,cancelBooking
# spring.ai.openai.chat.enabled=false
# Disable the OpenAI embedding when the local huggingface embedding (e.g. spring-ai-transformers-spring-boot-starter) is used.
# spring.ai.openai.embedding.enabled=false
###################
# Azure OpenAI
###################
# spring.ai.azure.openai.api-key=${AZURE_OPENAI_API_KEY}
# spring.ai.azure.openai.endpoint=${AZURE_OPENAI_ENDPOINT}
# spring.ai.azure.openai.chat.options.deployment-name=gpt-4o
###################
# Mistral AI
###################
# spring.ai.mistralai.api-key=${MISTRAL_AI_API_KEY}
# spring.ai.mistralai.chat.options.model=mistral-small-latest
# spring.ai.mistralai.chat.options.model=mistral-small-latest
# spring.ai.mistralai.chat.options.functions=getBookingDetails,changeBooking,cancelBooking
# # spring.ai.retry.on-client-errors=true
# # spring.ai.retry.exclude-on-http-codes=429
###################
# Vertex AI Gemini
###################
# spring.ai.vertex.ai.gemini.project-id=${VERTEX_AI_GEMINI_PROJECT_ID}
# spring.ai.vertex.ai.gemini.location=${VERTEX_AI_GEMINI_LOCATION}
# spring.ai.vertex.ai.gemini.chat.options.model=gemini-1.5-pro-001
# # spring.ai.vertex.ai.gemini.chat.options.model=gemini-1.5-flash-001
# spring.ai.vertex.ai.gemini.chat.options.transport-type=REST
# spring.ai.vertex.ai.gemini.chat.options.functions=getBookingDetails,changeBooking,cancelBooking
###################
# Milvus Vector Store
###################
# Change the dimentions to 384 if the local huggingface embedding (e.g. spring-ai-transformers-spring-boot-starter) is used.
# spring.ai.vectorstore.milvus.embedding-dimension=384
###################
# PGVector
###################
# spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
# spring.datasource.username=postgres
# spring.datasource.password=postgres
###################
# QDrant
###################
# spring.ai.vectorstore.qdrant.host=localhost
# spring.ai.vectorstore.qdrant.port=6334
###################
# Chroma
###################
# spring.ai.vectorstore.chroma.client.host=http://localhost
# spring.ai.vectorstore.chroma.client.port=8000
3、知识库文本文件 terms-of-service.txt 内容如下
These Terms of Service govern your experience with Funnair. By booking a flight, you agree to these terms.
1. Booking Flights
- Book via our website or mobile app.
- Full payment required at booking.
- Ensure accuracy of personal information (Name, ID, etc.) as corrections may incur a $25 fee.
2. Changing Bookings
- Changes allowed up to 24 hours before flight.
- Change via online or contact our support.
- Change fee: $50 for Economy, $30 for Premium Economy, Free for Business Class.
3. Cancelling Bookings
- Cancel up to 48 hours before flight.
- Cancellation fees: $75 for Economy, $50 for Premium Economy, $25 for Business Class.
- Refunds processed within 7 business days.
4、初始化向量库和对话记忆等
package ai.spring.demo.ai.playground.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.web.client.RestClient;
@Configuration
public class InitConfig {
private static final Logger logger = LoggerFactory.getLogger(InitConfig.class);
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
//初始化向量库
return SimpleVectorStore.builder(embeddingModel).build();
}
@Bean
CommandLineRunner ingestTermOfServiceToVectorStore(VectorStore vectorStore,
@Value("classpath:rag/terms-of-service.txt") Resource termsOfServiceDocs) {
//加载机票票务相关知识到向量库
return args -> {
// Ingest the document into the vector store
vectorStore.write(new TokenTextSplitter().transform(new TextReader(termsOfServiceDocs).read()));
vectorStore.similaritySearch("Cancelling Bookings").forEach(doc -> {
logger.info("Similar Document: {}", doc.getText());
});
};
}
@Bean
public ChatMemory chatMemory() {
//对话记忆
return new InMemoryChatMemory();
}
@Bean
@ConditionalOnMissingBean
public RestClient.Builder restClientBuilder() {
return RestClient.builder();
}
}
5、机票信息、机票类型、机票状态、等相关实体类
package ai.spring.demo.ai.playground.data;
public enum BookingClass {
// 机票类型 头等 商务
FIRST ,BUSINESS
}
package ai.spring.demo.ai.playground.data;
public enum BookingStatus {
//机票状态 已确认 已使用 已取消
CONFIRMED, COMPLETED, CANCELLED
}
package ai.spring.demo.ai.playground.data;
import java.time.LocalDate;
/**
* 机票信息
*/
public class Booking {
/**
* 订单号
*/
private String bookingNumber;
/**
* 订单日期
*/
private LocalDate date;
/**
* 购票信息
*/
private Customer customer;
/**
* 出发城市
*/
private String from;
/**
* 到达城市
*/
private String to;
/**
* 票务状态
*/
private BookingStatus bookingStatus;
/**
* 票务种类
*/
private BookingClass bookingClass;
public Booking(String bookingNumber, LocalDate date, Customer customer, BookingStatus bookingStatus, String from,
String to, BookingClass bookingClass) {
this.bookingNumber = bookingNumber;
this.date = date;
this.customer = customer;
this.bookingStatus = bookingStatus;
this.from = from;
this.to = to;
this.bookingClass = bookingClass;
}
public String getBookingNumber() {
return bookingNumber;
}
public void setBookingNumber(String bookingNumber) {
this.bookingNumber = bookingNumber;
}
public LocalDate getDate() {
return date;
}
public void setDate(LocalDate date) {
this.date = date;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public BookingStatus getBookingStatus() {
return bookingStatus;
}
public void setBookingStatus(BookingStatus bookingStatus) {
this.bookingStatus = bookingStatus;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public BookingClass getBookingClass() {
return bookingClass;
}
public void setBookingClass(BookingClass bookingClass) {
this.bookingClass = bookingClass;
}
}
package ai.spring.demo.ai.playground.data;
import java.util.ArrayList;
import java.util.List;
/**
* 购票信息
*/
public class Customer {
/**
* 购票人姓名
*/
private String name;
/**
* 购票人的票务信息
*/
private List<Booking> bookings = new ArrayList<>();
public Customer() {
}
public Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Booking> getBookings() {
return bookings;
}
public void setBookings(List<Booking> bookings) {
this.bookings = bookings;
}
}
下面的类 是模拟数据库
package ai.spring.demo.ai.playground.data;
import java.util.ArrayList;
import java.util.List;
/**
* 模拟数据库表 包含票务相关信息
*/
public class BookingData {
private List<Customer> customers = new ArrayList<>();
private List<Booking> bookings = new ArrayList<>();
public List<Customer> getCustomers() {
return customers;
}
public void setCustomers(List<Customer> customers) {
this.customers = customers;
}
public List<Booking> getBookings() {
return bookings;
}
public void setBookings(List<Booking> bookings) {
this.bookings = bookings;
}
}
6、机票服务类(包含数据初始化、退票、改签等业务方法)、智能体工具和智能体实现
package ai.spring.demo.ai.playground.services;
import ai.spring.demo.ai.playground.data.*;
import ai.spring.demo.ai.playground.services.BookingTools.BookingDetails;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 模拟票务业务层
*/
@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 < 13; 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);
}
/**
* 获取数据库所有票务信息
* @return
*/
public List<BookingDetails> getBookings() {
return db.getBookings().stream().map(this::toBookingDetails).toList();
}
/**
* 根据订单号 合购票者姓名查找票务信息
* @param bookingNumber
* @param name
* @return
*/
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"));
}
/**
* 根据订单号 合购票者姓名查找票务信息
* @param bookingNumber
* @param name
* @return
*/
public BookingDetails getBookingDetails(String bookingNumber, String name) {
var booking = findBooking(bookingNumber, name);
return toBookingDetails(booking);
}
/**
* 改签票务
* @param bookingNumber
* @param name
* @param newDate
* @param from
* @param to
*/
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);
}
/**
* 退票
* @param bookingNumber
* @param name
*/
public void cancelBooking(String bookingNumber, String name) {
var booking = findBooking(bookingNumber, name);
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());
}
}
package ai.spring.demo.ai.playground.services;
import java.time.LocalDate;
import java.util.function.Function;
import ai.spring.demo.ai.playground.data.BookingStatus;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
import org.springframework.core.NestedExceptionUtils;
@Configuration
public class BookingTools {
private static final Logger logger = LoggerFactory.getLogger(BookingTools.class);
@Autowired
private FlightBookingService flightBookingService;
/**
* 获取票务信息工具请求
* @param bookingNumber
* @param name
*/
public record BookingDetailsRequest(String bookingNumber, String name) {
}
/**
* 改签票务工具请求
* @param bookingNumber
* @param name
* @param date
* @param from
* @param to
*/
public record ChangeBookingDatesRequest(String bookingNumber, String name, String date, String from, String to) {
}
/**
* 退票工具请求
* @param bookingNumber
* @param name
*/
public record CancelBookingRequest(String bookingNumber, String name) {
}
/**
* 获取票务信息工具返回
* @param bookingNumber
* @param name
* @param date
* @param bookingStatus
* @param from
* @param to
* @param bookingClass
*/
@JsonInclude(Include.NON_NULL)
public record BookingDetails(String bookingNumber, String name, LocalDate date, BookingStatus bookingStatus,
String from, String to, String bookingClass) {
}
/**
* 获取机票预定详细信息 工具
* @return
*/
@Bean
@Description("获取机票预定详细信息")
public Function<BookingDetailsRequest, BookingDetails> getBookingDetails() {
return request -> {
try {
return flightBookingService.getBookingDetails(request.bookingNumber(), request.name());
}
catch (Exception e) {
logger.warn("Booking details: {}", NestedExceptionUtils.getMostSpecificCause(e).getMessage());
return new BookingDetails(request.bookingNumber(), request.name(), null, null, null, null, null);
}
};
}
/**
* 修改机票预定日期工具
* @return
*/
@Bean
@Description("修改机票预定日期")
public Function<ChangeBookingDatesRequest, String> changeBooking() {
return request -> {
flightBookingService.changeBooking(request.bookingNumber(), request.name(), request.date(), request.from(),
request.to());
return "";
};
}
/**
* 取消机票预定 工具
* @return
*/
@Bean
@Description("取消机票预定")
public Function<CancelBookingRequest, String> cancelBooking() {
return request -> {
flightBookingService.cancelBooking(request.bookingNumber(), request.name());
return "";
};
}
}
/*
* Copyright 2024-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ai.spring.demo.ai.playground.services;
import java.time.LocalDate;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
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;
/**
* 智能机票助手 agent
*/
@Service
public class CustomerSupportAssistant {
private final ChatClient chatClient;
public CustomerSupportAssistant(ChatClient.Builder modelBuilder, VectorStore vectorStore, ChatMemory chatMemory) {
// 初始化 Agent 智能体
this.chatClient = modelBuilder
.defaultSystem("""
您是“Funnair”航空公司的客户聊天支持代理。请以友好、乐于助人且愉快的方式来回复。
您正在通过在线聊天系统与客户互动。
您能够支持已有机票的预订详情查询、机票日期改签、机票预订取消等操作,其余功能将在后续版本中添加,如果用户问的问题不支持请告知详情。
在提供有关机票预订详情查询、机票日期改签、机票预订取消等操作之前,您必须始终从用户处获取以下信息:预订号、客户姓名。
在询问用户之前,请检查消息历史记录以获取预订号、客户姓名等信息,尽量避免重复询问给用户造成困扰。
在更改预订之前,您必须确保条款允许这样做。
如果更改需要收费,您必须在继续之前征得用户同意。
使用提供的功能获取预订详细信息、更改预订和取消预订。
如果需要,您可以调用相应函数辅助完成。
请讲中文。
今天的日期是 {current_date}.
""")
.defaultAdvisors(
//对话记忆
new PromptChatMemoryAdvisor(chatMemory), // Chat Memory
// new VectorStoreChatMemoryAdvisor(vectorStore)),
//RAG知识库
new QuestionAnswerAdvisor(vectorStore, SearchRequest.builder().topK(4).similarityThresholdAll().build()), // RAG
// new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()
// .withFilterExpression("'documentType' == 'terms-of-service' && region in ['EU', 'US']")),
// logger
new SimpleLoggerAdvisor()
)
//定义工具 对应 BookingTools中的三个方法
.defaultFunctions("getBookingDetails", "changeBooking", "cancelBooking") // FUNCTION CALLING
.build();
}
public Flux<String> chat(String chatId, String userMessageContent) {
return this.chatClient.prompt()
// current_date defaultSystem中的变量
.system(s -> s.param("current_date", LocalDate.now().toString()))
.user(userMessageContent)
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100))
.stream()
.content();
}
}
7、机票列表接口和智能体对话接口
package ai.spring.demo.ai.playground.client;
import ai.spring.demo.ai.playground.services.BookingTools.BookingDetails;
import ai.spring.demo.ai.playground.services.FlightBookingService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@Controller
@RequestMapping("/")
public class BookingController {
private final FlightBookingService flightBookingService;
public BookingController(FlightBookingService flightBookingService) {
this.flightBookingService = flightBookingService;
}
@RequestMapping("/")
public String index() {
return "index";
}
/**
* 获取数据库所有票务信息
* @return
*/
@RequestMapping("/api/bookings")
@ResponseBody
public List<BookingDetails> getBookings() {
return flightBookingService.getBookings();
}
}
package ai.spring.demo.ai.playground.client;
import ai.spring.demo.ai.playground.services.CustomerSupportAssistant;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RequestMapping("/api/assistant")
@RestController
public class AssistantController {
private final CustomerSupportAssistant agent;
public AssistantController(CustomerSupportAssistant agent) {
this.agent = agent;
}
@RequestMapping(path="/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(String chatId, String userMessage) {
return agent.chat(chatId, userMessage);
}
}
8、页面展示如下
访问 http://127.0.0.1:19000/