ai应用开发的基础知识点
1,实现以下接口 CallAroundAdvisor(处理非流式的请求和响应) StreamAroundAdvisor(用于处理流式请求和响应)// 实现方法...2,实现核心方法对于非流式处理 (CallAroundAdvisor),实现 aroundCall 方法:@Override// 1. 处理请求(前置处理)// 2. 调用链中的下一个Advisor// 3. 处理响应(后置处理)
ai应用开发的基础
Prompt
Prompt 工程(Prompt Engineering)又叫提示词工程,简单来说,就是输入给 AI 的指令。
高质量的 Prompt 可以显著提升 AI 输出的质量
ChatClient
ChatClient 是实现 “人与 AI 对话” 的工具或组件
可通过ChatClient
封装调用ai
public String doChat(String message, String chatId) { // 构建对话请求:包含用户消息、会话ID(用于记忆查询)、历史消息读取数量 ChatResponse chatResponse = chatClient .prompt() .user(message) // 用户输入消息 .advisors(spec -> spec .param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) // 指定会话ID .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) // 读取最近10条历史消息 .call() // 调用AI模型 .chatResponse(); // 提取AI回复内容并返回 return chatResponse.getResult().getOutput().getText(); }
.call()
调用的 AI 模型由配置文件中指定
spring: ai: dashscope: api-key: 你的API密钥 chat: options: model: qwen-plus # 这里指定了具体模型
通过ChatClient
调用与上次文章提到的SpringAiInvoke
的调用的差别
springAiInvoke`的调用ai
package com.yupi.aitest01mysql.invoke; import jakarta.annotation.Resource; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class SpringAiInvoke implements CommandLineRunner { @Resource private ChatModel dashscopeChatModel; @Override public void run(String... args) throws Exception { AssistantMessage assistantMessage = dashscopeChatModel.call(new Prompt("我是鼠鼠")) .getResult() .getOutput(); System.out.println(assistantMessage.getText()); } }
1,SpringAiInvoke的调用方式:直接调用
ChatModel(基础层)
2,ChatClient是
ChatModel的上层封装,通过
ChatClient`封装调用(高级层),这是 Spring AI 提供的高级封装调用
3,两者本质都是通过 Spring AI 框架调用 AI 模型,但:
-
ChatModel
是底层接口,负责与模型服务直接通信 -
ChatClient
是业务层工具,在通信基础上增加了对话管理、参数配置等功能
这两种调用方式本质都是Spring AI 框架中调用ai的方式
Advisors
Spring AI 使用 Advisors(拦截器)机制来增强 AI 的能力,在调用 AI 前和调用 AI 后可以执行一些额外的操作
-
比如调用 AI 前,想统一给所有请求加个前缀提示(比如 “请用中文回答”),Advisors 可以在发送请求前自动加上;
-
比如 AI 返回结果后,想过滤掉敏感内容,Advisors 可以在结果返回给用户前先 “过一遍筛子”;
-
再比如想记录每次 AI 调用的耗时、请求内容,Advisors 可以悄悄把这些信息记下来,不影响主流程。
保持单一职责:每个 Advisor 应专注于一项特定任务
用法——可以直接为 ChatClient 指定默认拦截器——在.defaultAdvisors(中指定用的拦截器 )
chatClient chatClient = ChatClient.builder(chatModel) //传入要使用的大模型 .defaultAdvisors( new MessageChatMemoryAdvisor(chatMemory), // 对话记忆 advisor new QuestionAnswerAdvisor(vectorStore) // RAG 检索增强 advisor ) .build();
拦截器的配置方式
用 defaultAdvisors()
配置拦截器的场景:全局通用逻辑,适合在初始化 ChatClient
时通过 defaultAdvisors()
配置。
用 advisors()
配置拦截器的场景:单个请求的特殊逻辑,当某个增强器只需要对当前请求生效,适合在构建具体请求时用 advisors()
配置。
// 全局默认增强器(所有请求都生效) ChatClient chatClient = ChatClient.builder(aiClient) .defaultAdvisors(new MyLoggerAdvisor()) // 全局日志 .build(); // 第一个请求:需要 RAG 增强(仅当前请求生效) chatClient.prompt()//开始构建一个对话请求(创建一个提示信息构建器) .user("恋爱中如何道歉?") .advisors(new QuestionAnswerAdvisor(loveAppVectorStore)) // 仅这个请求用 RAG .call(); // 第二个请求:不需要 RAG(不添加额外增强器,仅用全局默认的日志) chatClient.prompt() .user("今天天气如何?") .call();
==.prompt()开启单次对话请求== 每次调用
.prompt(),都会基于全局
ChatClient创建一个**独立的对话请求对象** **
ChatClient` 是全局客户端**
一些常见的Advisors
Spring AI 的 ChatMemoryAdvisor——实现对话记忆功能
-
MessageChatMemoryAdvisor:从记忆中检索历史对话,并将其作为消息集合添加到提示词中(首选)
-
PromptChatMemoryAdvisor:从记忆中检索历史对话,并将其添加到提示词的系统文本中
-
VectorStoreChatMemoryAdvisor:可以用向量数据库来存储检索历史对话
1)MessageChatMemoryAdvisor 将对话历史作为一系列独立的消息添加到提示中,保留原始对话的完整结构,包括每条消息的角色标识(用户、助手、系统)。
[ {"role": "user", "content": "你好"}, {"role": "assistant", "content": "你好!有什么我能帮助你的吗?"}, {"role": "user", "content": "讲个笑话"} ]
2)PromptChatMemoryAdvisor 将对话历史添加到提示词的系统文本部分,因此可能会失去原始的消息边界。
以下是之前的对话历史: 用户: 你好 助手: 你好!有什么我能帮助你的吗? 用户: 讲个笑话 现在请继续回答用户的问题。
自定义 Advisor
1,实现以下接口 CallAroundAdvisor(处理非流式的请求和响应) StreamAroundAdvisor(用于处理流式请求和响应)
public class MyCustomAdvisor implements CallAroundAdvisor, StreamAroundAdvisor { // 实现方法... }
2,实现核心方法
对于非流式处理 (CallAroundAdvisor),实现 aroundCall 方法:
@Override public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) { // 1. 处理请求(前置处理) AdvisedRequest modifiedRequest = processRequest(advisedRequest); // 2. 调用链中的下一个Advisor AdvisedResponse 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)); }
3)设置执行顺序
通过实现getOrder()
方法指定 Advisor 在链中的执行顺序。值越小优先级越高,越先执行
@Override public int getOrder() { // 值越小优先级越高,越先执行 return 100; }
4)提供唯一名称
为每个 Advisor 提供一个唯一标识符:
@Override public String getName() { return "自定义的 Advisor"; }
例子
自己实现一个日志 Advisor
/** * 自定义日志 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: {}", 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); } }
想用该拦截类的化只需要开启即可
自定义 Re-Reading Advisor
/** * 自定义 Re2 Advisor * 可提高大型语言模型的推理能力 */ public class ReReadingAdvisor implements CallAroundAdvisor, StreamAroundAdvisor { private AdvisedRequest before(AdvisedRequest advisedRequest) { Map<String, Object> advisedUserParams = new HashMap<>(advisedRequest.userParams()); advisedUserParams.put("re2_input_query", advisedRequest.userText()); return AdvisedRequest.from(advisedRequest) .userText(""" {re2_input_query} Read the question again: {re2_input_query} """) .userParams(advisedUserParams) .build(); } @Override public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) { return chain.nextAroundCall(this.before(advisedRequest)); } @Override public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) { return chain.nextAroundStream(this.before(advisedRequest)); } @Override public int getOrder() { return 0; } @Override public String getName() { return this.getClass().getSimpleName(); } }
想用该拦截类的化只需要开启即可
对话记忆的实现
Chat Memory
ChatMemoryAdvisor 都依赖 Chat Memory 进行构造,Chat Memory 负责历史对话的存储,定义了保存消息、查询消息、清空消息历史的方法。
Spring AI 内置了几种 Chat Memory,可以将对话保存到不同的数据源中,比如:
-
InMemoryChatMemory:内存存储
-
CassandraChatMemory:在 Cassandra 中带有过期时间的持久化存储
-
Neo4jChatMemory:在 Neo4j 中没有过期时间限制的持久化存储
-
JdbcChatMemory:在 JDBC 中没有过期时间限制的持久化存储
配置对话记忆参数-----需要让大语言模型 “拥有上下文记忆能力” 的场景中使用
.advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)//CHAT_MEMORY_CONVERSATTON_ID_KEY指定的为上下文关联的id .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))//CHAT_MEMORY_RETRIEVE_SIZE_KEY指定的为上下文关联的条数
自定义实现对话记忆持久化 ChatMemory
使用基于内存的对话记忆来保存对话上下文,但是服务器一旦重启了,对话记忆就会丢失。有时,我们可能希望将对话记忆持久化,保存到文件、数据库、Redis 或者其他对象存储中
官方目前提供的对话记忆持久化实现的方式JdbcChatMemory不太好用,所以我们用自定义的方式去实现。
如果使用 JSON 来序列化会存在很多报错。所以此处我们选择高性能的 Kryo 序列化库
实现起来最主要的问题是 消息和文本的转换。我们在保存消息时,要将消息从 Message 对象转为文件内的文本;读取消息时,要将文件内的文本转换为 Message 对象。 也就是对象的序列化和反序列化。
步骤
下面我们以将对话记忆保存在文本中为例,实现对话记忆的持久化
1)引入依赖:0FYFudbzgXxtRtTyE+eGgfzI+cTUgjyCrBoWq1T+rhQ=
<dependency> <groupId>com.esotericsoftware</groupId> <artifactId>kryo</artifactId> <version>5.6.2</version> </dependency>
2)在根包下新建 chatmemory
包,编写基于文件持久化的对话记忆 FileBasedChatMemory,代码如下:
package com.yupi.aiagent.chatmemory; import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; import org.objenesis.strategy.StdInstantiatorStrategy; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.messages.Message; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.util.ArrayList; import java.util.List; /** * 把消息和文件内容实现互转 */ public class FileBasedChatMemory implements ChatMemory { //指定默认的存储路径 private final String BASE_DIR; //指定kryo对象 //对 Kryo 进行了初始化配置,确保其能正常处理对象的序列化 / 反序列化: private static final Kryo kryo=new Kryo(); static { kryo.setRegistrationRequired(false); //false表示不要手动注册 //设置标准的实例化策略 kryo.setInstantiatorStrategy((new StdInstantiatorStrategy())); } //创建构造函数,指定文件保存的目录 public FileBasedChatMemory(String dir) { BASE_DIR=dir; File baseDir=new File(dir);//创建文件对象 //如果文件对象不存在先创造出来防止报错 if(!baseDir.exists()){ baseDir.mkdirs(); } } //增——保存单条消息 @Override public void add(String conversationId, Message message) { saveConversation(conversationId,List.of(message)); } //增——保存多条消息 @Override public void add(String conversationId, List<Message> messages) { List<Message> messageList=getOrCreateConversation(conversationId); messageList.addAll(messages); saveConversation(conversationId,messageList); } //查——读取消息——取跳过特定条数的消息 @Override public List<Message> get(String conversationId, int lastN) { List<Message> messageList=getOrCreateConversation(conversationId); return messageList.stream() .skip(Math.max(0, messageList.size()-lastN)) .toList(); } //删——根据id拿到文件,之后删除文件 @Override public void clear(String conversationId) { File file=getConversationFile(conversationId); if(file.exists()){ file.delete(); } } /** * 获取或创建会话消息列表-getOrCreateConversation 方法,通过 Kryo 的 readObject 实现文件内容到对象的反序列化: * @param conversationId * @return */ private List<Message> getOrCreateConversation(String conversationId) { File file=getConversationFile(conversationId);//根据会话id得到会话文件 List<Message> messages=new ArrayList<>(); //从文件中读取kryo的信息 if(file.exists()){ try(Input input=new Input(new FileInputStream(file))){ messages = kryo.readObject(input, ArrayList.class);//把输入的信息转化成消息列表信息 }catch(Exception e){ e.printStackTrace(); } } return messages; } /** * 保存会话信息 * @param conversationId-对应 saveConversation 方法,通过 Kryo 的 writeObject 实现对象到文件的序列化: * @param 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(Exception e){ e.printStackTrace(); } } /** * 每个会话文件单独保存 * @param conversationId * @return */ private File getConversationFile(String conversationId) { return new File(BASE_DIR+conversationId+"kryo");//指定文件名 } }
虽然上述代码看起来复杂,但大多数代码都是文件和 Message 对象的转换 3)修改 LoveApp 的构造函数,使用基于文件的对话记忆:
public LoveApp(ChatModel dashscopeChatModel) { //初始化基于文件的对话记忆——定义对话记忆数据的存储路径 String fileDir = System.getProperty("user.dir") + "/chat-memory";//把项目的临时文件都保存在了.gitignore中的tmp中 ChatMemory chatMemory = new FileBasedChatMemory(fileDir); // ChatMemory chatMemory =new InMemoryChatMemory(); 之前使用InMemoryChatMemory把对话记忆存在内存中的方法 chatClient = ChatClient.builder(dashscopeChatModel) .defaultSystem(SYSTEM_PROMPT) .defaultAdvisors( new MessageChatMemoryAdvisor(chatMemory) ) .build(); }
System.getProperty("user.dir")`用于获取当前程序的运行目录(即项目的根目录)
/tmp/chat-memory
在当前项目根目录后拼接子路径,最终得到完整的存储目录:
结构化输出
结构化输出转换器(Structured Output Converter)是 Spring AI 提供的一种实用机制,用于将大语言模型返回的文本输出转换为结构化数据格式,如 JSON、XML 或 Java 类
原理
-
调用前:转换器会在提示词后面附加格式指令,明确告诉模型应该生成何种结构的输出,引导模型生成符合指定格式的响应。
-
调用后:转换器将模型的文本输出转换为结构化类型的实例,比如将原始文本映射为 JSON、XML 或特定的数据结构。
结构化输出转换器 StructuredOutputConverter
接口允许开发者获取结构化输出,例如将输出映射到 Java 类或值数组。接口定义如下:
public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider { }
它集成了 2 个关键接口:
-
FormatProvider
接口:提供特定的格式指令给 AI 模型 -
Spring 的
Converter<String, T>
接口:负责将模型的文本输出转换为指定的目标类型T
public interface FormatProvider { String getFormat(); }
1)在调用大模型之前,FormatProvider
为 AI 模型提供特定的格式指令,使其可通过 Converter
转换为目标类型的文本。
FormatProvider转换器的格式指令组件会将格式指令附加到提示词中
使用 PromptTemplate
模板 将格式指令附加到用户输入的末尾
StructuredOutputConverter outputConverter = ...//结构化输出转化器 String userInputTemplate = """ ... 用户文本输入 .... {format}//把原本用户的输入定义一个模板 """; // 用户输入,包含一个“format”占位符。 //模板 Prompt prompt = new Prompt( new PromptTemplate( this.userInputTemplate, Map.of(..., "format", outputConverter.getFormat()) // 用转换器的格式替换“format”占位符 ).createMessage());
使用示例
// 定义一个记录类——record记录的语法,快速定义类 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);//BeanOutputConverter 示例:AI 输出转换为自定义 Java 类:
同理
.entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});用 //BeanOutputConverter 示例:ParameterizedTypeReference` 构造函数来指定更复杂的目标类结构,比如自定义对象列表: .entity(new ParameterizedTypeReference<Map<String, Object>>() {});)//MapOutputConverter 示例,将模型输出转换为包含数字列表的 Map:
实现的步骤
1.引入支持结构化输出的依赖
<dependency> <groupId>com.github.victools</groupId> <artifactId>jsonschema-generator</artifactId> <version>4.38.0</version> </dependency>
2,定义要输出的格式(使用record记录的语法快速创类)
//定义恋爱报告的格式,可以用类去定义,也可以用record记录的语法 record LoveReport(String title, List<String> suggestions){}//用record记录的语法,快速定义变量,可以理解为 LoveReport类中有title和suggestions字段
3.结构化输出
/** * Ai恋爱报告功能 * @param message * @param chatId * @return */ public LoveReport doChatReport(String message, String chatId) {//允许用户输入对话的内容和对话的id(多个对话id得到的内容是相对隔离的,不会混在一起) LoveReport loveReport = chatClient .prompt() .system(SYSTEM_PROMPT+"每一次对话都要生成恋爱结果,标题为{用户名}的恋爱报告,内容为建议列表")//指定输出的恋爱报告中有哪些信息 .user(message) //指定当前关联的上下文是哪一个对话的 .advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)//CHAT_MEMORY_CONVERSATTON_ID_KEY指定的为上下文关联的id .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))//CHAT_MEMORY_RETRIEVE_SIZE_KEY指定的为上下文关联的条数 .call() .entity(LoveReport.class);//使用这个类,用这个类的类型去输出 log.info("content: {}",loveReport); return loveReport; }
system
提示是给模型 “定规则”,告诉它应该输出什么格式的内容; 而.entity(...)
是 “解析规则”,告诉程序如何把模型输出的内容转换成目标结构。 两者配合才能确保结构化输出有效 —— 前者保证模型 “按格式输出”,后者保证程序 “能正确解析”。
SYSTEM_PROMPT
(预设的系统提示内容)与后面的字符串拼接,形成完整的系统指令。
PromptTemplate模板
PromptTemplate 支持从外部文件加载模板内容,很适合管理复杂的提示词。Spring AI 利用 Spring 的 Resource 对象来从指定路径加载模板文件:
// 从类路径资源加载系统提示模板 @Value("classpath:/prompts/system-message.st") private Resource systemResource; // 直接使用资源创建模板 SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
这种方式让你可以
-
将复杂的提示词放在单独的文件中管理
-
在不修改代码的情况下调整提示词
-
为不同场景准备多套提示词模板
像写配置文件,有点儿前后端分离的感觉
更多推荐
所有评论(0)