你是一位旅游大师,为用户提供专业旅游咨询服务,涵盖行程规划、目的地推荐、游玩问题解答等
现实中用户咨询旅游问题时,专业 "旅游大师" 会主动引导挖掘需求。因此系统提示词可设计为,让 AI 主动抛出引导性问题,深入了解用户背景,示例引导问题:
你最近有没有特别想去旅行的目的地类型呀,比如自然风光、历史人文还是海滨度假?
这次旅行你大概计划安排几天时间呢?
同行人员有什么特点不,像亲子、情侣、朋友结伴,这会影响行程规划哦〜
通过这类引导,能更精准捕捉用户需求,输出贴合的旅游方案。
参考 Spring AI 官方文档,Spring AI 提供 ChatClient API 与 AI 大模型交互。相较于之前直接用 Spring Boot 注入 ChatModel 调用大模型,自行构造 ChatClient 可打造功能更丰富、灵活的 AI 对话客户端,推荐以此方式调用 AI ,支持复杂灵活的链式调用Fluent API 。
在这里插入图片描述
简要流程说明:
// 基础用法(ChatModel)
ChatResponse response = chatModel.call(new Prompt("你好"));
// 高级用法(ChatClient)
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("你是旅游顾问")
.build();
String response = chatClient.prompt().user("你好").call().content();
// 方式1:使用构造器注入
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是旅游顾问")
.build();
}
}
// 方式2:使用建造者模式
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("你是旅游顾问")
.build();
public static void main(String[] args) {
// 1. 初始化 ChatModel(需替换为实际大模型配置,如 OpenAI、通义千问等)
ChatModel chatModel = initializeChatModel(); // 2. 构建 ChatClient,配置默认系统提示词(旅游大师角色)
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("You are a professional travel master, providing travel consultation services. " +
"Offer itinerary planning, destination recommendations, and travel tips. " +
"Interact in a {voice} style.")
.build();
// 3. 动态调整系统提示词变量(设置交互风格为 "专业且热情")
String voiceStyle = "professional and enthusiastic";
// 4. 示例 1:返回 ChatResponse(含元数据,如 token 用量)
ChatResponse responseWithMeta = chatClient.prompt()
.system(sp -> sp.param("voice", voiceStyle))
.user("帮我规划去冰岛 7 天的旅行行程,包含极光观测、冰川徒步")
.call()
.chatResponse();
System.out.println("=== 示例 1:ChatResponse 输出 ===");
System.out.println("AI 回复内容:" + responseWithMeta.getResult().getOutput().getContent());
System.out.println("本次调用消耗 token:" + responseWithMeta.getUsage().getTotalTokens());
// 5. 示例 2:返回单个实体对象(自动映射 AI 输出)
DestinationRecommend singleEntity = chatClient.prompt()
.system(sp -> sp.param("voice", voiceStyle))
.user("推荐一个适合秋季旅行的国内城市,说明理由和必去景点")
.call()
.entity(DestinationRecommend.class);
System.out.println("\n=== 示例 2:单个实体对象输出 ===");
System.out.println("推荐城市:" + singleEntity.city());
System.out.println("推荐理由:" + singleEntity.reason());
System.out.println("必去景点:" + String.join(", ", singleEntity.attractions()));
// 6. 示例 3:返回泛型集合(多城市玩法推荐)
List<CityActivity> multiEntity = chatClient.prompt()
.system(sp -> sp.param("voice", voiceStyle))
.user("分别推荐三亚(夏季)、哈尔滨(冬季)的特色玩法")
.call()
.entity(new ParameterizedTypeReference<List<CityActivity>>() {});
System.out.println("\n=== 示例 3:泛型集合输出 ===");
multiEntity.forEach(ca ->
System.out.println(ca.city() + " 特色玩法:" + ca.activity())
);
// 7. 示例 4:流式返回文本内容(模拟打字机效果)
Flux<String> streamText = chatClient.prompt()
.system(sp -> sp.param("voice", voiceStyle))
.user("详细描述丝绸之路自驾 15 天的行程故事,包含沿途文化、美食、风景")
.stream()
.content();
System.out.println("\n=== 示例 4:流式文本输出 ===");
streamText.subscribe(
segment -> System.out.print(segment),
error -> System.err.println("流式失败:" + error)
);
// 8. 示例 5:流式返回 ChatResponse(含元数据)
Flux<ChatResponse> streamWithMeta = chatClient.prompt()
.system(sp -> sp.param("voice", voiceStyle))
.user("详细介绍京都全年不同季节的旅行亮点")
.stream()
.chatResponse();
System.out.println("\n=== 示例 5:流式 ChatResponse 输出 ===");
streamWithMeta.subscribe(
chatResp -> {
String segment = chatResp.getResult().getOutput().getContent();
int token = chatResp.getUsage().getTotalTokens();
System.out.println("片段内容:" + segment + "(本段 token:" + token + ")");
},
error -> System.err.println("流式失败:" + error)
);
/**
* 初始化 ChatModel(需替换为实际大模型配置,以下为伪代码示例)
* 真实场景中,需引入对应大模型依赖,配置 API 密钥、模型名称等
*/
private static ChatModel initializeChatModel() {
// 示例:若使用 OpenAI,需配置 apiKey、baseUrl 等
// return new OpenAiChatModel(apiKey, baseUrl, modelName);
// 此处为简化演示,返回空实现(实际需替换为真实逻辑)
return new ChatModel() {};
}
在这里插入图片描述
在这里插入图片描述
1、作用:初始Spring AI 的 Advisors(顾问 / 拦截器) 可在 调用 AI 前 / 后 执行增强逻辑,适配旅游大师场景:
2、 示例:MessageChatMemoryAdvisor和QuestionAnswerAdvisor
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory), // 对话记忆 advisor
new QuestionAnswerAdvisor(vectorStore) // RAG 检索增强 advisor
)
.build();
String response = this.chatClient.prompt()
// 对话时动态设定拦截器参数,比如指定对话记忆的 id 和长度
.advisors(advisor -> advisor.param("chat_memory_conversation_id", "4548")
.param("chat_memory_response_size", 10))
.user(userText)
.call()
.content();
public class TravelMasterApp {
private final ChatClient chatClient;
// 系统提示词:定义旅游大师角色与服务逻辑
private static final String SYSTEM_PROMPT = "扮演深耕旅游规划领域的专家。开场向用户表明身份,告知可咨询旅行难题。" +
"围绕独行、结伴、家庭游三种场景提问:独行关注安全保障、小众玩法探索困扰;" +
"结伴关注行程协调、成员意见分歧矛盾;家庭游关注亲子/长辈适配项目、行程节奏问题。" +
"引导用户详述旅行意向、人员构成、特殊需求,以便定制专属旅行方案。";
public TravelMasterApp(ChatModel dashscopeChatModel) {
// 初始化基于内存的对话记忆,存储多轮对话上下文
ChatMemory chatMemory = new InMemoryChatMemory();
// 构建 ChatClient,配置系统提示词、对话记忆拦截器
chatClient = ChatClient.builder(dashscopeChatModel)
.defaultSystem(SYSTEM_PROMPT) // 注入旅游大师系统提示词
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory) // 对话记忆拦截器,维持多轮上下文
)
.build();
}
// 可扩展对话方法,如接收用户提问并获取 AI 回复
public String getTravelAdvice(String userQuestion) {
return chatClient.prompt()
.user(userQuestion)
.call()
.content();
}
}
/**
* 处理用户旅游咨询的对话方法
* @param message 用户提问内容(如"求川西 5 天亲子游行程")
* @param chatId 对话唯一标识(用于关联多轮对话记忆,如"TRAVEL_PARENT_202408")
* @return AI 生成的旅游建议回复
*/
public String doChat(String message, String chatId) {
ChatResponse response = chatClient.prompt()
.user(message) // 传入用户旅游咨询内容
.advisors(spec -> spec
.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) // 指定对话记忆 ID,关联多轮对话
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 20) // 控制对话记忆检索长度(按需调整)
)
.call() // 发起 AI 调用
.chatResponse(); // 获取包含元数据的完整响应
String content = response.getResult().getOutput().getText();
log.info("用户提问:{},AI 回复:{}", message, content);
return content;
}
}
public class MyCustomAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
// 实现方法...
}
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
// 1. 处理请求(前置处理)
AdvisedRequest modifiedRequest = processRequest(advisedRequest);
// 2. 调用链中的下一个Advisor
AdvisedResponse response = chain.nextAroundCall(modifiedRequest);
// 3. 处理响应(后置处理)
return processResponse(response);
}
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
// 1. 处理请求
AdvisedRequest modifiedRequest = processRequest(advisedRequest);
// 2. 调用链中的下一个Advisor并处理流式响应
return chain.nextAroundStream(modifiedRequest)
.map(response -> processResponse(response));
}
@Override
public int getOrder() {
// 值越小优先级越高,越先执行
return 100;
}
logging:
level:
org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor: debug
/**
* 自定义日志 Advisor
* 打印 info 级别日志、只输出单次用户提示词和 AI 回复的文本
*/
@Slf4j
public class MyLoggerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
@Override
public String getName() {
return this.getClass().getSimpleName();
}
@Override
public int getOrder() {
return 0;
}
private AdvisedRequest before(AdvisedRequest request) {
log.info("AI提问: {}", request.userText());
return request;
}
private void observeAfter(AdvisedResponse advisedResponse) {
log.info("AI 响应: {}", advisedResponse.response().getResult().getOutput().getText());
}
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
advisedRequest = this.before(advisedRequest);
AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);
this.observeAfter(advisedResponse);
return advisedResponse;
}
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
advisedRequest = this.before(advisedRequest);
Flux<AdvisedResponse> advisedResponses = chain.nextAroundStream(advisedRequest);
// aroundStream 方法利用 MessageAggregator 工具类将 Flux 流式响应聚合成单个 AdvisedResponse,用于需要观测完整响应的场景.
return (new MessageAggregator()).aggregateAdvisedResponse(advisedResponses, this::observeAfter);
}
}
2、原理围绕 Spring AI 的 StructuredOutputConverter 接口展开
4、官方示例
在这里插入图片描述
// 定义一个记录类
record ActorsFilms(String actor, List<String> movies) {}
// 使用高级 ChatClient API
ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
.user("Generate 5 movies for Tom Hanks.")
.call()
.entity(ActorsFilms.class);
Map<String, Object> result = ChatClient.create(chatModel).prompt()
.user(u -> u.text("Provide me a List of {subject}")
.param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'"))
.call()
.entity(new ParameterizedTypeReference<Map<String, Object>>() {});
List<String> flavors = ChatClient.create(chatModel).prompt()
.user(u -> u.text("List five {subject}")
.param("subject", "ice cream flavors"))
.call()
.entity(new ListOutputConverter(new DefaultConversionService()));
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-generator</artifactId>
<version>4.38.0</version>
</dependency>
record TravelReport(String title, List<String> suggestions) {
}
public LoveReport doChatWithReport(String message, String chatId) {
LoveReport loveReport = chatClient
.prompt()
.system(SYSTEM_PROMPT + "每次对话后都要生成旅游结果,标题为{用户名}的旅游报告,内容为建议列表")
.user(message)
.advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
.call()
// 关键步骤
.entity(LoveReport.class);
log.info("loveReport: {}", loveReport);
return loveReport;
}
4、编写单元测试用例,代码测试。
@Test
void doChatWithReport() {
String chatId = UUID.randomUUID().toString();
String message = "你好,我是爱健身的王嘉尔,我想去英国旅游,给出一份旅游报告";
TravelApp.TravelReport TravelReport = TravelApp.doChatWithReport(message, chatId);
Assertions.assertNotNull(TravelReport);
}
5、结果验证。
在这里插入图片描述
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.6.2</version>
</dependency>
/**
* 基于文件持久化的对话记忆
*/
public class FileBasedChatMemory implements ChatMemory {
private final String BASE_DIR;
private static final Kryo kryo = new Kryo();
static {
kryo.setRegistrationRequired(false);
// 设置实例化策略
kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
}
// 构造对象时,指定文件保存目录
public FileBasedChatMemory(String dir) {
this.BASE_DIR = dir;
File baseDir = new File(dir);
if (!baseDir.exists()) {
baseDir.mkdirs();
}
}
@Override
public void add(String conversationId, List<Message> messages) {
List<Message> conversationMessages = getOrCreateConversation(conversationId);
conversationMessages.addAll(messages);
saveConversation(conversationId, conversationMessages);
}
@Override
public List<Message> get(String conversationId, int lastN) {
List<Message> allMessages = getOrCreateConversation(conversationId);
return allMessages.stream()
.skip(Math.max(0, allMessages.size() - lastN))
.toList();
}
@Override
public void clear(String conversationId) {
File file = getConversationFile(conversationId);
if (file.exists()) {
file.delete();
}
}
private List<Message> getOrCreateConversation(String conversationId) {
File file = getConversationFile(conversationId);
List<Message> messages = new ArrayList<>();
if (file.exists()) {
try (Input input = new Input(new FileInputStream(file))) {
messages = kryo.readObject(input, ArrayList.class);
} catch (IOException e) {
e.printStackTrace();
}
}
return messages;
}
private void saveConversation(String conversationId, List<Message> messages) {
File file = getConversationFile(conversationId);
try (Output output = new Output(new FileOutputStream(file))) {
kryo.writeObject(output, messages);
} catch (IOException e) {
e.printStackTrace();
}
}
private File getConversationFile(String conversationId) {
return new File(BASE_DIR, conversationId + ".kryo");
}
}
public LoveApp(ChatModel dashscopeChatModel) {
// 初始化基于文件的对话记忆
String fileDir = System.getProperty("user.dir") + "/chat-memory";
ChatMemory chatMemory = new FileBasedChatMemory(fileDir);
chatClient = ChatClient.builder(dashscopeChatModel)
.defaultSystem(SYSTEM_PROMPT)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory)
)
.build();
}
在这里插入图片描述