SpringAI入门及浅实践,实战 Spring AI 调用大模型、提示词工程、对话记忆、Advisor 的使用
上一次写AI学习笔记已经好久之前了,温习温习,这一章讲讲关于Spring AI 调用大模型、对话记忆、Advisor、结构化输出、自定义对话记忆、Prompt 模板的相关知识点。
快速跳转到你感兴趣的地方
- 一、提示词工程(Prompt)
- 1. 基本概念 (提示词、提示词工程)
- 2. 分类 (基于角色、功能)
- 3. Prompt 优化技巧
- 二、对话记忆(多轮对话实现)
- 1. 使用 Chat Client 实现基础对话功能
- 2. Advisor 顾问的使用
- 三、SpringAI会话记忆、Advisor实战
- 1. 使用 InMemoryChatMemory:内存存储 实现对话记忆
- 2. 自定义Advisor的使用
- 3. 使用自定义 Advisor 实现自定义日志 Advisor
- 四、结语(会不断补充,可以收藏一下)
一、提示词工程(Prompt)
1. 基本概念 (提示词、提示词工程)
提示词就是用户输入给AI的指令,但是写好提示词,是一件十分不容易的事情。准确的提示词能够让AI清楚的明白你所要达到的目的,从而生成满意的内容。
而提示词工程就是通过优化输入给AI模型的指令或问题(即“提示词”),以提升模型输出的准确性和相关性。其核心在于理解模型的行为模式,并通过结构化语言引导模型生成更符合预期的结果。
2. 分类 (基于角色、功能)
按照角色来分类可以分为如下三类:
- 用户 Prompt
是用户直接向 AI 提出的具体需求或问题,例如请求写诗、解答问题或生成代码。其核心是传递用户的具体意图,要求 AI 执行明确的任务。 - 系统 Prompt
是开发者或平台设定的隐藏指令,用于定义 AI 的角色、行为边界和回答风格。它不直接面向用户,但决定了 AI 如何响应用户 Prompt。例如,设定 AI 为“专业诗人”或“情感顾问”会直接影响生成内容的专业性、语气和结构。 - 助手 Prompt
在多轮对话中,助手 Prompt(即 AI 的历史回复)会成为后续交互的上下文。预设的助手消息能引导对话方向,例如在客服场景中提前声明服务范围。这种机制增强了对话连贯性,但也需注意避免因历史信息导致回答偏差。
按照功能来分可以分为:
- 指令型提示词: 明确告诉 AI 模型需要执行的任务,通常以命令式语句开头。
将上述中文文本翻译为英文
- 对话型提示词: 模拟自然对话,以问答形式与 AI 模型交互。
你认为AI会在取代人类吗?
- 创意型提示词 引导 AI 模型进行创意内容生成,如故事、诗歌、广告文案等。
写一首描写爱情的诗歌,赞美矢志不渝伟大的爱情
- 角色扮演提示词 让 AI 扮演特定角色或人物进行回答。
你是一位使用Java的程序员,使用java编写一个贪吃蛇游戏
- 少样本学习提示词 提供一些示例,引导 AI 理解所需的输出格式和风格。
将以下句子改写为正式商务语言:
示例1:
原句:这个想法不错。
改写:该提案展现了相当的潜力和创新性。示例2:
原句:我们明天见。
改写:期待明日与您会面,继续我们的商务讨论。现在请改写:这个价格太高了。
3. Prompt 优化技巧
网上和 Prompt 优化相关的资源非常丰富,几乎各大主流 AI 大模型和 AI 开发框架官方文档都有相关的介绍。Spring AI 提示工程指南,智谱 AI Prompt 设计指南等等
这里讲讲我学习整理后的一些心得
这是springAI
的提示词编写教程中的重点,其实重点在三个方面:
在开发提示时,重要的是要集成几个关键组件,以确保清晰度和有效性:
- 指令:向AI提供清晰而直接的指令,类似于你与人沟通的方式。这种清晰度对于帮助AI“理解”预期内容至关重要。
- 外部环境:在必要时,为人工智能的反应提供相关的背景信息或具体指导。这种“外部环境”构成了提示,并帮助AI掌握整个场景。
- 用户输入:这是最直接的部分——用户的直接请求或问题
只要说明白这三点,就算是一个合适的提示词,但是是远远不够的,这里我用AI整理出了优化的重点,感兴趣的可以看一下。
清晰具体地定义需求,避免模糊指令,确保模型准确理解任务目标明确指定输出格式,如JSON或特定结构,便于后续处理或解析将复杂任务拆解为多个简单子任务,逐步递进完成,提高准确率使用分隔符区分不同输入部分,避免混淆,如"""文本内容"""在需要推理的问题中要求展示思维链,分步骤呈现推导过程通过少样本示例引导模型模仿特定风格或行为模式控制输出长度,设定字数限制或篇幅要求,确保简洁性对长文本采用分段归纳再整合的方法,突破上下文限制在技术性任务中先让模型自主生成答案,再进行评估验证必要时隐藏详细推理过程,直接输出最终结果或结构化数据利用外部工具增强能力,如API查询实时数据或访问知识库在长对话中定期总结关键信息,保持上下文连贯性角色扮演方式可提升专业领域回答的准确性为系统提示设定明确的行为模式和任务规范### 优化Prompt的技巧清晰具体地定义需求,避免模糊指令,确保模型准确理解任务目标明确指定输出格式,如JSON或特定结构,便于后续处理或解析将复杂任务拆解为多个简单子任务,逐步递进完成,提高准确率使用分隔符区分不同输入部分,避免混淆,如"""文本内容"""在需要推理的问题中要求展示思维链,分步骤呈现推导过程通过少样本示例引导模型模仿特定风格或行为模式控制输出长度,设定字数限制或篇幅要求,确保简洁性对长文本采用分段归纳再整合的方法,突破上下文限制在技术性任务中先让模型自主生成答案,再进行评估验证必要时隐藏详细推理过程,直接输出最终结果或结构化数据利用外部工具增强能力,如API查询实时数据或访问知识库在长对话中定期总结关键信息,保持上下文连贯性角色扮演方式可提升专业领域回答的准确性为系统提示设定明确的行为模式和任务规范### 优化Prompt的技巧清晰具体地定义需求,避免模糊指令,确保模型准确理解任务目标明确指定输出格式,如JSON或特定结构,便于后续处理或解析将复杂任务拆解为多个简单子任务,逐步递进完成,提高准确率使用分隔符区分不同输入部分,避免混淆,如"""文本内容"""在需要推理的问题中要求展示思维链,分步骤呈现推导过程通过少样本示例引导模型模仿特定风格或行为模式控制输出长度,设定字数限制或篇幅要求,确保简洁性对长文本采用分段归纳再整合的方法,突破上下文限制在技术性任务中先让模型自主生成答案,再进行评估验证必要时隐藏详细推理过程,直接输出最终结果或结构化数据利用外部工具增强能力,如API查询实时数据或访问知识库在长对话中定期总结关键信息,保持上下文连贯性角色扮演方式可提升专业领域回答的准确性为系统提示设定明确的行为模式和任务规范
二、对话记忆(多轮对话实现)
1. 使用 Chat Client 实现基础对话功能
参考 Spring AI
的官方文档,了解到 Spring AI
提供了 ChatClient API
来和 AI
大模型交互。
ChatClient 是使用 ChatClient.Builder 对象创建的。您可以为任何 ChatModel Spring Boot 自动配置获取一个自动配置的 ChatClient.Builder 实例,也可以通过编程方式创建一个
这是官方提供的通过构造器注入的事例:
// 方式1:使用构造器注入
@RestController
class MyController {private final ChatClient chatClient;public MyController(ChatClient.Builder chatClientBuilder) {this.chatClient = chatClientBuilder.build();}@GetMapping("/ai")String generation(String userInput) {return this.chatClient.prompt().user(userInput).call().content();}
}
使用建造者模式
// 方式2:使用建造者模式
ChatClient chatClient = ChatClient.builder(chatModel).defaultSystem("you are a helpful AI").build();
ChatClient 支持多种响应格式,比如返回 ChatResponse 对象、返回实体对象、流式返回:
// ChatClient支持多种响应格式
// 1. 返回 ChatResponse 对象(包含元数据如 token 使用量)
ChatResponse chatResponse = chatClient.prompt().user("Tell me a joke").call().chatResponse();// 2. 返回实体对象(自动将 AI 输出映射为 Java 对象)
// 2.1 返回单个实体
record ActorFilms(String actor, List<String> movies) {}
ActorFilms actorFilms = chatClient.prompt().user("Generate the filmography for a random actor.").call().entity(ActorFilms.class);// 2.2 返回泛型集合
List<ActorFilms> multipleActors = chatClient.prompt().user("Generate filmography for Tom Hanks and Bill Murray.").call().entity(new ParameterizedTypeReference<List<ActorFilms>>() {});// 3. 流式返回(适用于打字机效果)
Flux<String> streamResponse = chatClient.prompt().user("Tell me a story").stream().content();// 也可以流式返回ChatResponse
Flux<ChatResponse> streamWithMetadata = chatClient.prompt().user("Tell me a story").stream().chatResponse();
可以给 ChatClient 设置默认参数,比如系统提示词,还可以在对话时动态更改系统提示词的变量,类似模板的概念:
// 定义默认系统提示词
ChatClient chatClient = ChatClient.builder(chatModel).defaultSystem("You are a friendly chat bot that answers question in the voice of a {voice}").build();// 对话时动态更改系统提示词的变量
chatClient.prompt().system(sp -> sp.param("voice", voice)).user(message).call().content());
2. Advisor 顾问的使用
Advisor在Spring AI中扮演着类似AOP(面向切面编程)中顾问的角色,用于在 AI 模型调用前后添加逻辑以增强 AI 的能力。它允许开发者在不修改核心代码的情况下,对AI请求进行预处理或后处理,例如日志记录、对话记忆、RAG等。
- 前置增强:调用 AI 前改写一下 Prompt 提示词、检查一下提示词是否安全
- 后置增强:调用 AI 后记录一下日志、处理一下返回的结果
比如对话记忆顾问 MessageChatMemoryAdvisor
可以帮助我们实现多轮对话能力,省去了自己维护对话列表的麻烦。
var chatClient = ChatClient.builder(chatModel).defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), // 对话记忆 advisornew QuestionAnswerAdvisor(vectorStore) // RAG 检索增强 advisor).build();String response = this.chatClient.prompt()// 对话时动态设定拦截器参数,比如指定对话记忆的 id 和长度.advisors(advisor -> advisor.param("chat_memory_conversation_id", "678").param("chat_memory_response_size", 100)).user(userText).call().content();
Advisors 的原理图如下:
实际开发中,往往我们会用到多个拦截器,组合在一起相当于一条拦截器链条(责任链模式的设计思想)。每个拦截器是有顺序的,通过 getOrder() 方法获取到顺序,得到的值越低,越优先执行。
想要实现对话记忆功能,可以使用 Spring AI 的 ChatMemoryAdvisor,它主要有几种内置的实现方式:
- MessageChatMemoryAdvisor:从记忆中检索历史对话,并将其作为消息集合添加到提示词中
- PromptChatMemoryAdvisor:从记忆中检索历史对话,并将其添加到提示词的系统文本中
- VectorStoreChatMemoryAdvisor:可以用向量数据库来存储检索历史对话
MessageChatMemoryAdvisor 和 PromptChatMemoryAdvisor 用法类似,但是略有一些区别:
MessageChatMemoryAdvisor
将对话历史作为一系列独立的消息添加到提示中,保留原始对话的完整结构,包括每条消息的角色标识(用户、助手、系统)
[{"role": "user", "content": "你好"},{"role": "assistant", "content": "你好!有什么我能帮助你的吗?"},{"role": "user", "content": "讲个笑话"}
]
PromptChatMemoryAdvisor
将对话历史添加到提示词的系统文本部分,因此可能会失去原始的消息边界。
以下是之前的对话历史:
用户: 你好
助手: 你好!有什么我能帮助你的吗?
用户: 讲个笑话
现在请继续回答用户的问题。
一般情况下,更建议使用 MessageChatMemoryAdvisor 更符合大多数现代 LLM 的对话模型设计,能更好地保持上下文连贯性。
使用 Chat Memory
构造ChatMemoryAdvisor
上述 ChatMemoryAdvisor 都依赖 Chat Memory 进行构造,Chat Memory 负责历史对话的存储,定义了保存消息、查询消息、清空消息历史的方法。
Spring AI 内置了几种 Chat Memory,可以将对话保存到不同的数据源中,比如:
- InMemoryChatMemory:内存存储
- CassandraChatMemory:在 Cassandra 中带有过期时间的持久化存储
- Neo4jChatMemory:在 Neo4j 中没有过期时间限制的持久化存储
- JdbcChatMemory:在 JDBC 中没有过期时间限制的持久化存储
- 自定义数据源存储
三、SpringAI会话记忆、Advisor实战
1. 使用 InMemoryChatMemory:内存存储 实现对话记忆
- 首先初始化 ChatClient 对象。使用 Spring 的构造器注入方式来注入阿里大模型dashscopeChatModel 对象,并使用该对象来初始化 ChatClient。初始化时指定默认的系统 Prompt 和基于内存的对话记忆 Advisor。代码如下:
@Component
@Slf4j
public class App {private final ChatClient chatClient;private static final String SYSTEM_PROMPT = "your system prompt";public LoveApp(ChatModel dashscopeChatModel) {// 初始化基于内存的对话记忆ChatMemory chatMemory = new InMemoryChatMemory();chatClient = ChatClient.builder(dashscopeChatModel).defaultSystem(SYSTEM_PROMPT).defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)).build();}
}
- 编写对话方法,调用 chatClient 对象,传入用户 Prompt,并且给 advisor 指定对话 id 和对话记忆大小。代码如下:
public String doChat(String message, String chatId) {ChatResponse response = chatClient.prompt().user(message).advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)).call().chatResponse();String content = response.getResult().getOutput().getText();log.info("content: {}", content);return content;
}
- 编写单元测试,测试多轮对话:
@SpringBootTest
class AppTest {@Resourceprivate App app;@Testvoid testChat() {String chatId = UUID.randomUUID().toString();// 第一轮String message = "你好,我是wangdong.";String answer = app.doChat(message, chatId);Assertions.assertNotNull(answer);// 第二轮message = "我想学习springAI!!!";answer = app.doChat(message, chatId);Assertions.assertNotNull(answer);// 第三轮message = "你记得我叫啥吗?";answer = app.doChat(message, chatId);Assertions.assertNotNull(answer);}
}
第一轮:
第二轮:
第三轮:
这样我们就完成了基于内存的会话记忆,但是这样还是有弊端,一旦程序重启,我们的会话还是会丢失,还需要别的方法来优化会话记忆
2. 自定义Advisor的使用
自定义Adcisor 和 Spring AOP 比较相似,我们可以通过编写拦截器或切面对请求和响应进行处理,比如记录请求响应日志、鉴权等。
Spring AI 的 Advisor 就可以理解为拦截器,可以对调用 AI 的请求进行增强,比如调用 AI 前鉴权、调用 AI 后记录日志。
官方已经提供了一些 Advisor,但可能无法满足我们实际的业务需求,这时我们可以使用官方提供的 自定义 Advisor 功能。按照下列步骤操作即可。
- 自定义的Advisor要实现这两个接口
CallAroundAdvisor
StreamAroundAdvisor
public class CustomAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {//实现方法...
}
- 实现核心方法
对于非流式处理 (CallAroundAdvisor),实现 aroundCall 方法:
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {// 1. 处理请求(前置处理)AdvisedRequest modifiedRequest = processRequest(advisedRequest);// 2. 调用链中的下一个AdvisorAdvisedResponse response = chain.nextAroundCall(modifiedRequest);// 3. 处理响应(后置处理)return processResponse(response);
}
对于流式处理 (StreamAroundAdvisor),实现 aroundStream 方法:
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {// 1. 处理请求AdvisedRequest modifiedRequest = processRequest(advisedRequest);// 2. 调用链中的下一个Advisor并处理流式响应return chain.nextAroundStream(modifiedRequest).map(response -> processResponse(response));
}
- 设置执行顺序
通过实现getOrder()方法指定 Advisor 在链中的执行顺序。值越小优先级越高,越先执行:
@Override
public int getOrder() {// 值越小优先级越高,越先执行return 100;
}
- 提供唯一名称
为每个 Advisor 提供一个唯一标识符:
@Override
public String getName() {return "鱼皮自定义的 Advisor";
}
3. 使用自定义 Advisor 实现自定义日志 Advisor
虽然 Spring AI 已经内置了 SimpleLoggerAdvisor 日志拦截器,但是以 Debug 级别输出日志,而默认 Spring Boot 项目的日志级别是 Info,所以看不到打印的日志信息。
虽然上述方式可行,但如果为了更灵活地打印指定的日志,建议自己实现一个日志 Advisor。
我们可以同时参考 官方文档 和内置的 SimpleLoggerAdvisor 源码,结合 2 者并略做修改,开发一个更精简的、可自定义级别的日志记录器。默认打印 info 级别日志、并且只输出单次用户提示词和 AI 回复的文本。
/*** 自定义日志 Advisor* 打印 info 级别日志、只输出单次用户提示词和 AI 回复的文本*/
@Slf4j
public class MyLoggerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {@Overridepublic String getName() {return this.getClass().getSimpleName();}@Overridepublic int getOrder() {return 0;}private AdvisedRequest before(AdvisedRequest request) {log.info("AI Request: {}", request.userText());return request;}private void observeAfter(AdvisedResponse advisedResponse) {log.info("AI Response: {}", 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);return (new MessageAggregator()).aggregateAdvisedResponse(advisedResponses, this::observeAfter);}
}
上述代码中值得关注的是 aroundStream 方法的返回,通过 MessageAggregator 工具类将 Flux 响应聚合成单个 AdvisedResponse。这对于日志记录或其他需要观察整个响应而非流中各个独立项的处理非常有用。注意,不能在 MessageAggregator 中修改响应,因为它是一个只读操作。
在 App 中应用自定义的日志 Advisor:
chatClient = ChatClient.builder(dashscopeChatModel).defaultSystem(SYSTEM_PROMPT).defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory),// 自定义日志 Advisor,可按需开启new MyLoggerAdvisor(),).build();
四、结语(会不断补充,可以收藏一下)
本章介绍了 Spring AI 调用大模型、提示词工程、对话记忆、Advisor 的基本使用,对于对话记忆的自定义和jdbc存储,也会在这段时间加入,我会在学习中不断整理笔记,巩固自己学到的东西。