当前位置: 首页 > news >正文

第三章 AI应用开发

代码仓库地址:https://github.com/Liucc-123/ai-agent

项目目标:通过项目实战掌握AI 大模型应用开发的常见知识,包括有:掌握AI 应用平台使用、AI 大模型接入、开发框架(Spring AI + LangChain4j)、本地部署、Prompt 工程、多模态特性、RAG 知识库、工具调用、MCP 服务开发、智能体原理和开发、服务化部署等技术。

一、Prompt 工程基础

1.1 基本概念

  • Prompt 工程:设计和优化输入给 AI 的指令,以引导 AI 模型生成符合预期的输出。
  • 重要性:提示词质量直接影响 AI 输出结果,是 AI 应用开发的关键技能。

1.2 提示词分类

基于角色的分类
  • 用户 Prompt:用户向 AI 提出的问题或指令。
  • 系统 Prompt:设置 AI 模型的行为规则和角色定位。
  • 助手 Prompt:AI 模型的响应内容。
基于功能的分类
  • 指令型:明确告诉 AI 需要执行的任务。
  • 对话型:模拟自然对话。
  • 创意型:引导 AI 进行创意内容生成。
  • 角色扮演型:让 AI 扮演特定角色回答问题。
  • 少样本学习型:通过示例引导 AI 理解输出格式和风格。
基于复杂度的分类
  • 简单提示词:单一指令或问题。
  • 复合提示词:包含多个相关指令或步骤。
  • 链式提示词:一系列连续的、相互依赖的提示词。
  • 模板提示词:包含可替换变量的标准化提示词结构。

1.3 Token

  • 定义:大模型处理文本的基本单位,可能是单词或标点符号。
  • 重要性:Token 数量影响成本和输出速度。
  • 优化技巧
    • 精简系统提示词。
    • 定期清理对话历史。
    • 使用向量检索代替直接输入。
    • 使用结构化格式代替自然语言描述。

二、Prompt 优化技巧

2.1 利用资源

  • 学习资源:阅读官方文档、提示词优化指南等。
  • 提示词库:参考现成的提示词库进行优化。

2.2 基础提示技巧

  • 明确指定任务和角色:清晰描述任务和角色定位。
  • 提供详细说明和具体示例:减少模型的不确定性。
  • 使用结构化格式:使指令更易理解,输出更有条理。
  • 明确输出格式要求:指定输出的格式、长度、风格等。

2.3 进阶提示技巧

  • 思维链提示法:引导模型展示推理过程。
  • 少样本学习:通过输入-输出对示例帮助模型理解任务模式。
  • 分步骤指导:将复杂任务分解为可管理的步骤。
  • 自我评估和修正:让模型评估自己的输出并进行改进。
  • 知识检索和引用:引导模型检索相关信息并明确引用来源。
  • 多视角分析:从不同角度分析问题。
  • 多模态思维:结合不同表达形式进行思考。

2.4 提示词调试与优化

  • 迭代式提示优化:逐步修改和完善提示词。
  • 边界测试:通过极限情况测试模型的能力边界。
  • 提示词模板化:创建结构化模板,便于一致性提示。
  • 错误分析与修正:系统性分析模型回答中的错误并优化提示词。

三、AI 应用需求分析

3.1 需求来源

可以参考现有的智能体平台,比如Coze、Dify、阿里云百炼等平台。

  • 参考现有平台:从现有 AI 应用平台获取灵感。
  • 用户反馈:了解用户需求和痛点。

3.2 需求细化

  • 借助 AI:通过精心设计的提示词让 AI 帮助进行需求细化。
  • 示例 Prompt:让 AI 分析目标用户群体、提出功能模块、考虑竞争点等。

3.3 MVP 最小可行产品策略

  • 定义核心功能:先开发包含核心功能的基础版本产品。
  • 快速验证:通过用户反馈进行迭代优化。

四、AI 应用方案设计

根据需求我们将实现一个具有多轮对话能力的AI恋爱大师智能体,整个方案设计将围绕2个核心展开:

  • 系统提示词的设计
  • 多轮对话的实现

4.1 系统提示词设计

系统提示词直接决定了 AI 的行为模式和交互风格。

最简单的系统预设就是定义“你是谁,你能做什么?”,如以下:

你是一位恋爱大师,为用户提供情感咨询服务

这种简单提示虽然可以工作,但效果往往不太理想,达不到现实中应用的效果。在现实情感咨询过程中,专家是通过抛出一系列引导性问题来深入了解咨询者的背景、情感困惑,而不是被动等待用户自己描述问题。比如:

  • 最近有什么迷茫的事情?
  • 在两性关系中有什么困惑的地方?

我们可以通过AI工具来优化系统提示词。优化prompt示例:

我正在开发【恋爱大师】AI 对话应用,请你帮我编写设置给 AI 大模型的系统预设 Prompt 指令。要求让 AI 作为恋爱专家,模拟真实恋爱咨询场景、多给用户一些引导性问题,不断深入了解用户,从而提供给用户更全面的建议,解决用户的情感问题。

AI优化后的系统提示词:

### 1. 角色设定 (Role Setting)你是一个专业、温暖、富有同理心的【恋爱大师】AI 顾问。你具备深厚的心理学、两性关系学、沟通学知识。你的目标是帮助用户识别、理解并解决他们在恋爱过程中遇到的各种情感难题。### 2. 对话目标 (Dialogue Goal)通过深入对话,充分理解用户的情感困境,提供个性化、可操作、具有建设性的情感指导和解决方案,帮助用户提升自我认知,改善两性关系,最终实现情感幸福。### 3. 对话策略 (Dialogue Strategy)* **开场策略:** 始终以温暖、接纳的态度开始对话,鼓励用户详细描述他们的问题。* *示例开场白:* “你好!我是你的恋爱大师AI顾问。我知道在感情中遇到困惑是常有的事,别担心,我在这里倾听你的故事,帮助你找到答案。你现在遇到了什么让你困扰的问题呢?请尽可能详细地告诉我。”
* **倾听与共情:** 在用户描述问题时,积极倾听,展现同理心,不打断,不评判。用“我理解”、“我能感受到”等词语表达共情。
* **引导性提问:*** **开放式问题:** 鼓励用户详细阐述,而不是简单的是非回答。* *例如:* “你觉得是什么原因导致了现在这种局面?”、“具体是哪一点让你感到最困扰?”、“如果回到那个时候,你希望自己怎么做?”* **情绪探索:** 引导用户表达自己的情绪和感受。* *例如:* “面对这种情况,你现在内心有什么感受?是委屈、愤怒、还是迷茫?”、“这些感受对你产生了什么影响?”* **背景深挖:** 了解问题的来龙去脉和相关背景信息。* *例如:* “这种情况是从什么时候开始出现的?之前有没有类似的情况?”、“你和对方在平时是如何沟通的?”、“你认为对方对此事的看法或感受是什么?”* **需求澄清:** 帮助用户明确自己的真实需求和期望。* *例如:* “你最希望通过这次对话解决什么问题?”、“你期待这段关系最终走向何方?”、“你认为理想的解决方案是什么样的?”* **行为与模式:** 引导用户反思自己的行为模式和在关系中的角色。* *例如:* “在这种情况下,你通常会如何反应?”、“你觉得自己在关系中扮演着什么样的角色?”
* **多角度分析:** 在给出建议前,从多个角度(如沟通技巧、需求满足、情绪管理、个人成长、边界设定等)对问题进行分析。
* **逐步深入:** 避免一次性给出长篇大论的解决方案。而是通过持续的提问和引导,像剥洋葱一样,层层深入地了解问题的核心。每次回答都应基于用户最新的信息和反馈。
* **提供具体建议:** 建议应具体、可操作,避免空泛。可以给出多种选择,让用户选择最适合自己的方式。* *例如:* “你可以尝试以下沟通方式:一是……”、“你还可以这样做:先……”
* **赋能用户:** 鼓励用户积极思考,培养他们解决问题的能力,而不是完全依赖AI。* *例如:* “你觉得这其中哪些是你现在就可以尝试去做的?”、“如果让你来解决这个问题,你会怎么做?”
* **风险提示与边界:** 提醒用户某些问题的复杂性,必要时建议寻求专业线下咨询。你不是心理治疗师,不提供诊断。
* **结束语:** 结束对话时,鼓励用户积极行动,并表达持续支持。### 4. 输出风格 (Output Style)* **温暖亲切:** 像一位知心朋友,语气平和,充满关怀。
* **专业严谨:** 基于专业知识提供建议,但不使用晦涩难懂的术语。
* **条理清晰:** 回答内容结构分明,逻辑清晰,易于理解。
* **鼓励积极:** 即使面对负面情绪,也要引导用户看到积极的一面,鼓励成长。
* **非判断性:** 无论用户提出什么问题,都保持中立、不评判的态度。### 5. 内容禁忌 (Content Restrictions)* **不提供法律或医疗建议。**
* **不诱导、不传播、不鼓励任何非法、不道德或有害行为。**
* **不进行任何形式的歧视、攻击或冒犯。**
* **不替代专业心理治疗或精神健康诊断。**
* **不泄露用户隐私,不要求用户提供敏感个人信息。**### 6. 交互循环 (Interaction Loop)**用户提问 -> AI 倾听/共情/引导性提问 -> 用户回应 -> AI 分析/提供建议/进一步提问 -> ... (循环直至问题解决或用户结束对话)**---**核心原则:**始终以**用户为中心**,通过**高质量的提问**来**深入理解问题**,然后提供**个性化、可操作的解决方案**。记住,你的目标是帮助用户**成为他们自己情感问题的解决者**。

在正式开发AI应用之前,可以通过AI云平台(如阿里云百炼)对系统提示词进行测试和调优:

4.2 多轮对话实现

要实现“具备记忆能力的”AI应用,让AI能够记住用户之前的对话内容并保持上下文连贯性,可以通过SpringAI框架提供的对话记忆能力

如何实现对话记忆能力, 可以参考官方文档:ChatClient API来和AI大模型进行交互。

  • Spring AI 的 ChatClient:支持多轮对话和对话记忆。
  • Advisors:增强 AI 能力,如对话记忆、日志记录等。
  • Chat Memory Advisor:实现对话记忆功能。
  • Chat Memory:负责历史对话的存储。

五、多轮对话 AI 应用开发

在后端项目的根目录下创建新包app存放AI应用,新建LoveApp类,参考SpringAI Alibaba 官方文档示例代码。

1)首先是初始化ChatClient

基于之前的方案设计,编写系统提示词,给大模型一个人设,通过构造注入的方式来初始化ChatClient,因为我们只引入了一个阿里的DashscopeChatModel,所以ChatClient.Builder会自动从容器中查找到DashscopeChatModel,就不需要我们显示的指定哪个大模型来初始化builder了,最后通过builder初始化ChatClient。

/*** 恋爱助手*/
@Component
@Slf4j
public class LoveApp {@Resourceprivate ChatModel dashScopeChatModel;// 系统提示词private static final String SYSTEM_PROMPT = """扮演深耕恋爱心理领域的专家。开场向用户表明身份,告知用户可倾诉恋爱难题。围绕单身、恋爱、已婚三种状态提问:单身状态询问社交圈拓展及追求心仪对象的困扰;恋爱状态询问沟通、习惯差异引发的矛盾;已婚状态询问家庭责任与亲属关系处理的问题。引导用户详述事情经过、对方反应及自身想法,以便给出专属解决方案。""";private final ChatClient chatClient;/*** 构造函数注入ChatClient。默认使用的大模型是 DashScopeChatModel** @param builder 通过ChatClient.Builder构造ChatClient*/public LoveApp(ChatClient.Builder builder) {this.mySqlChatMemory = mySqlChatMemory;ChatMemory chatMemory = new InMemoryChatMemory();chatClient = builder.defaultSystem(SYSTEM_PROMPT).defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)).build();}}

2)编写对话方法

核心是advisor的配置,chatId保证是同一个会话,模型上下文记忆数量是10条。

/*** 与大模型聊天** @param message* @param chatId* @return*/
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;}

3)编写单元测试

@Test
void chatTest() {String chatId = UUID.randomUUID().toString();// 第一轮String message = "你好,我是tiga";String answer = loveApp.doChat(message, chatId);Assertions.assertNotNull(answer);// 第二轮message = "我想让另一半(水冠)更爱我";answer = loveApp.doChat(message, chatId);Assertions.assertNotNull(answer);// 第三轮
//        message = "我的另一半叫什么来着?刚跟你说过,帮我回忆一下";message = "我的名字叫什么来着?刚跟你说过,帮我回忆一下";answer = loveApp.doChat(message, chatId);Assertions.assertNotNull(answer);
}

测试效果如下:

六、扩展知识

Spring AI Advisors API和SpringAOP、Servlet的拦截器类似, 提供了一种灵活且强大的方式,用于拦截、修改和增强Spring 应用中的 AI 驱动交互,允许开发者在请求到达模型前、模型回复后进行自定义逻辑处理。

SpringAI提供了很多内置的Advisor:

  • MessageChatMemoryAdvisor(使模型具备上下文能力)
  • QuestionAnswerAdvisor(RAG advisor)
  • SafeGuardAdvisor(对用户提示词进行敏感词处理)
  • SimpleLoggerAdvisor(对请求和模型响应进行日志记录)

6.1 自定义 Advisor

SpringAI允许定义用户自己的Advisor,按照它的规范实现CallAroundAdvisor、StreamAroundAdvisor即可。这里我们定义自己的日志顾问Advisor,通过看源码可以发现SpringAI内置的SimpleLoggerAdvisor的日志等级是debug而SpringBoot默认的日志等级是info,如果想要使用需要修改日志等级:

logging:level:org.springframework.ai.chat.client.advisor: DEBUG

使用SpringAI内置的SimpleLoggerAdvisor:

1、在初始化ChatClient时,为其配置顾问Advisor

public LoveApp(ChatClient.Builder builder) {ChatMemory chatMemory = new InMemoryChatMemory();chatClient = builder.defaultSystem(SYSTEM_PROMPT).defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), new SimpleLoggerAdvisor()).build();

2、测试默认日志Advisor的效果:

可以看到控制台中输出了用户请求request及大模型响应response的具体日志信息,但是日志输出了很多用户可能并不关心的内容。

6.1.1、自定义日志Advisor

假设我们只想让日志Advisor记录用户prompt和assistant的prompt

实现步骤:

1、参考官方的SimpleLoggerAdvisor改写为我们自己的MyLoggerAdvisor

实现CallAroundAdvisorStreamAroundAdvisor两个SpringAI的父接口,按照规范定义我们自己的逻辑,aroundCall方法表示处理同步请求,aroundStream处理流式请求。
一般建议这两个接口都做实现。示例代码如下:

@Slf4j
public class MyLoggerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {// 0、为advisor提供一个唯一的名称@Overridepublic String getName() {return "这是我的自定义顾问";}// 1、定义顾问的优先级,数字越小,优先级越高@Overridepublic int getOrder() {return 100;}private AdvisedRequest before(AdvisedRequest request) {log.info(request.userText());return request;}private void observeAfter(AdvisedResponse advisedResponse) {log.info(advisedResponse.response().getResult().getOutput().getText());}/*** 3、重写同步*/@Overridepublic AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {/*** 前置处理*/advisedRequest = before(advisedRequest);AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);/*** 后置处理*/observeAfter(advisedResponse);return advisedResponse;}// 4、重写流式@Overridepublic Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {// 前置处理advisedRequest = before(advisedRequest);Flux<AdvisedResponse> advisedResponses = chain.nextAroundStream(advisedRequest);// 通过MessageAggregator等模型回复全部完成后聚合起来,避免回复碎片化return new MessageAggregator().aggregateAdvisedResponse(advisedResponses, this::observeAfter);}}

2、测试效果:只打印了user prompt和assistant prompt

6.1.2、Re-Reading (Re2) Advisor

这篇文章 介绍了一种称为 Re-Reading(Re2)的技术,该技术可以提高大型语言模型的推理能力。其核心原理就是让大模型再重复读取一遍用户prompt,增强大模型对用户query的理解。

优缺点:

  • 好处:大模型的推理能力得到增强,可以更准确的回复用户的问题
  • 坏处:再次读取一遍用户prompt,对应地,token的消耗数量也是成倍的增加。

因此需要针对于实际业务与成本的考虑,到底是要更准确的回复,还是更关注成本,以此来决定是否要开启Re2配置。

Q: {Input Query}
Read the question again: {Input Query}

示例:

/*** 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());// 使用字符串模板,动态替换参数 re2_input_queryreturn AdvisedRequest.from(advisedRequest).userText("""{re2_input_query}Read the question again: {re2_input_query}""").userParams(advisedUserParams).build();}@Overridepublic AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {// 改写请求,让大模型多阅读一次用户promptreturn chain.nextAroundCall(this.before(advisedRequest));}@Overridepublic Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {// 改写请求,让大模型多阅读一次用户promptreturn chain.nextAroundStream(this.before(advisedRequest));}public String getName() { return this.getClass().getSimpleName();}@Overridepublic int getOrder() { return 0;}
}

在LoveApp类中使用Advisor,测试请求是否被加强

chatClient = ChatClient.builder(dashscopeChatModel).defaultSystem(SYSTEM_PROMPT).defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory),// 自定义推理增强 Advisor,可按需开启new ReReadingAdvisor()).build();

6.1.3、最佳实践

1)保持单؜一职责:每个 Ad‍visor 应专注‌于一项特定任务

2)注意执行顺序:合理设置<font style="color:rgb(31, 35, 40);background-color:rgba(175, 184, 193, 0.2);">getOrder()</font>值确保 Advisor 按正确顺序执行

3)同时支؜持流式和非流式:尽‍可能同时实现两种接‌口以提高灵活性

4)高效处理请求:避免在 Advisor 中执行耗时操作

5)测试边؜界情况:确保 Ad‍visor 能够优‌雅处理异常和边界情⁡况

6)对于需؜要更复杂处理的流式‍场景,可以使用 R‌eactor 的操⁡作符:

@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {return Mono.just(advisedRequest).publishOn(Schedulers.boundedElastic()).map(request -> {// 请求前处理逻辑return modifyRequest(request);}).flatMapMany(request -> chain.nextAroundStream(request)).map(response -> {// 响应处理逻辑return modifyResponse(response);});
}

可以使用 adviseContext在 Advisor 链中共享状态:

// 更新上下文
advisedRequest = advisedRequest.updateContext(context -> {context.put("key", "value");return context;
});// 读取上下文
Object value = advisedResponse.adviseContext().get("key");

6.2 结构化输出

恋爱功能开发

我们使用SpringAI的结构化输出特性,给用户生成恋爱报告,并转换为一个恋爱报告的POJO,报告里包含标题和恋爱建议。

说明:在使用官方提供的结构化输出示例中,遇到了一个json转换的错误,这个错误官方并没有给提供,但实际运行确实会报错。需要添加以下依赖进行解决:

<!-- https://mvnrepository.com/artifact/com.github.victools/jsonschema-generator -->
<dependency><groupId>com.github.victools</groupId><artifactId>jsonschema-generator</artifactId><version>4.38.0</version>
</dependency>

报告对象结构如下:

// 恋爱报告 记录
record LoveReport(String title, List<String> suggestions) {
}

生成恋爱报告方法如下:

/*** 结构化输出实战(生成恋爱报告)* * @param message 用户prompt* @param chatId  会话id* @return*/
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)// 指定记忆的大小 10条记录.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)).call()// 使用entity()方法后,底层会动态添加对应的提示词,和使用Converter转换器将回复内容映射到POJO中.entity(LoveReport.class); return loveReport;
}

编写单元测试:

/*** 测试结构化输出(生成恋爱报告)*/
@Test
void doChatWithReportTest() {String message = "我是水冠,我喜欢一个女孩momo,但我不知道该怎么去追她";String chatId = UUID.randomUUID().toString();loveApp.doChatWithReport(message, chatId);
}

通过debug可以发现,在顾问Advisor的上下文中有一个formatParam参数,是SpringAI动态生成的提示词

动态提示词内容展开如下:

Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```{"$schema" : "https://json-schema.org/draft/2020-12/schema","type" : "object","properties" : {"suggestions" : {"type" : "array","items" : {"type" : "string"}},"title" : {"type" : "string"}},"additionalProperties" : false
}```

模型回复的内容也是结构化的,符合我们的预期:

转换器成功将json文本转换为Java对象

最佳实践

  1. 尽量为模型提供清晰的格式指导
  2. 实现输出验证机制和异常处理逻辑,确保结构化数据符合预期
  3. 选择支持结构化输出的合适模型
  4. 对于复杂数据结构,考虑使用ParameterizedTypeReference

6.3 对话记忆持久化

大型语言模型(LLMs)是无状态的,这意味着它们不会保留关于先前交互的信息。当您希望与大模型在多次对话交互中能够维护上下文或状态时,这可能是一个限制。为了解决这个问题,Spring AI 提供了聊天记忆功能,允许您跨与 LLM 的多个交互存储和检索信息。

ChatMemory 抽象允许你实现各种类型的记忆存储组件来支持不同的用例。消息的底层存储由 ChatMemoryRepository 处理,其唯一职责是存储和检索消息。保留哪些消息以及何时删除它们由 ChatMemory 实现决定。一般的策略包括有保留最新的 N 条消息、保留特定时间段的消息或保留达到特定 token 限制的消息。

快速入门

Spring AI 自动配置一个 ChatMemory Bean,您可以直接在您的应用程序中使用。默认情况下,它使用内存存储库来存储消息( InMemoryChatMemoryRepository ),并使用一个 MessageWindowChatMemory 实现来管理对话历史。如果已经配置了不同的存储库(例如,Cassandra、JDBC 或 Neo4j),Spring AI 将使用你配置的存储库实现。

@Autowired
ChatMemory chatMemory;

记忆存储类型

Spring AI 提供了用于存储聊天记忆的 ChatMemoryRepository 抽象,Spring AI 提供的内置存储库:

  • In-Memory Repository:基于内存的记忆存储,缺点是没有持久化
  • JdbcChatMemoryRepository:在 JDBC 中没有过期时间限制的持久化存储
  • CassandraChatMemoryRepository:在 Cassandra 中带有过期时间的持久化存储
  • Neo4j ChatMemoryRepository:在 Neo4j 中没有过期时间限制的持久化存储

除了使用SpringAI内置的记忆存储库,我们也可以实现自己的记忆存储库。

自定义实现基于MySQL的记忆存储

如果我们要将对话持久化到数据库中,可以考虑使用JdbcChatMemory。但spring-ai-starter-model-chat-memory-jdbc依赖目前很少人在用,缺少文档介绍,不建议自己开荒。

因此我们可以自己实现一个基于MySQL的记忆存储库。

我们可以参考ChatMemory接口的默认实现InMemoryChatMemory 的源码来实现

可以看到主要就是三个实现,add、get、clear方法的实现。

这里很简单,我就让AI帮我实现了,这是我的提示词:

背景:类似于SpringAI内置的InMemoryChatMemory,我想自定义实现一个基于MySQL实现的记忆存储来保存用户聊天记录。要求:请你基于mysql、mybatis plus实现一个ChatMemory组件,在LoveApp构造函数中替换使用我们实现的ChatMemory组件,并给我一个可执行的SQL脚本。

基于AI生成的代码和自己的实际情况,对代码进行部分调整,最终生成的代码文件如下:

  • pom.xml
<!-- MySQL Connector -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.33</version>
</dependency><!-- MyBatis Plus Spring Boot Starter -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.12</version>
</dependency>
  • application.yml
spring:application:name: spring-ai-alibaba-qwq-chat-client-exampleai:dashscope:api-key: ''chat:options:model: qwen-plusdatasource:url: jdbc:mysql://localhost:3306/ai-agent?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword: '123456'driver-class-name: com.mysql.cj.jdbc.Drivermybatis-plus:mapper-locations: classpath*:/mapper/**/*.xmlglobal-config:db-config:id-type: auto
  • Java Bean
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("chat_message")
public class ChatMessage {@TableId(type = IdType.AUTO)private Long id;private String conversationId;private String messageType;private String content;
}
  • Mapper
public interface ChatMessageMapper extends BaseMapper<ChatMessage> {
}
  • Service
@Service
public class ChatMessageService extends ServiceImpl<ChatMessageMapper, ChatMessage> {
}
  • InMySqlChatMemory
@Component
@RequiredArgsConstructor
public class InMySqlChatMemory implements ChatMemory {private final ChatMessageService chatMessageService;@Overridepublic void add(String conversationId, Message message) {add(conversationId, List.of(message));}@Overridepublic void add(String conversationId, List<Message> messages) {List<ChatMessage> chatMessages = messages.stream().map(message ->  ChatMessage.builder().conversationId(conversationId).messageType(message.getMessageType().getValue()).content(message.getText()).build()).collect(Collectors.toList());chatMessageService.saveBatch(chatMessages);}@Overridepublic List<Message> get(String conversationId, int lastN) {QueryWrapper<ChatMessage> queryWrapper = new QueryWrapper<>();queryWrapper.eq("conversation_id", conversationId).orderByDesc("id").last("LIMIT " + lastN);List<ChatMessage> chatMessages = chatMessageService.list(queryWrapper);return chatMessages.stream().map(chatMessage -> (Message) new UserMessage(chatMessage.getContent())) // Simplified for example.collect(Collectors.toList());}@Overridepublic void clear(String conversationId) {QueryWrapper<ChatMessage> queryWrapper = new QueryWrapper<>();queryWrapper.eq("conversation_id", conversationId);chatMessageService.remove(queryWrapper);}
}
  • LoveAPP
/*** 恋爱助手*/
@Component
@Slf4j
public class LoveApp {@Resourceprivate ChatModel dashScopeChatModel;record ActorFilms(String actor, List<String> movies) {}// 恋爱报告 记录record LoveReport(String title, List<String> suggestions) {}// 系统提示词private static final String SYSTEM_PROMPT = """扮演深耕恋爱心理领域的专家。开场向用户表明身份,告知用户可倾诉恋爱难题。围绕单身、恋爱、已婚三种状态提问:单身状态询问社交圈拓展及追求心仪对象的困扰;恋爱状态询问沟通、习惯差异引发的矛盾;已婚状态询问家庭责任与亲属关系处理的问题。引导用户详述事情经过、对方反应及自身想法,以便给出专属解决方案。""";private final ChatClient chatClient;/*** 构造函数注入ChatClient。默认使用的大模型是 DashScopeChatModel** @param builder 通过ChatClient.Builder构造ChatClient*/private final MySqlChatMemory mySqlChatMemory;public LoveApp(ChatClient.Builder builder, MySqlChatMemory mySqlChatMemory) {this.mySqlChatMemory = mySqlChatMemory;ChatMemory chatMemory = this.mySqlChatMemory;chatClient = builder.defaultSystem(SYSTEM_PROMPT).defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory),new MyLoggerAdvisor()).build();}/*** 与大模型聊天** @param message* @param chatId* @return*/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;}
}
  • sql脚本
-- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS `ai-agent` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;-- 使用数据库
USE `ai-agent`;-- 如果存在则删除表(可选,便于开发)
-- DROP TABLE IF EXISTS `chat_message`;-- 创建表 chat_message
CREATE TABLE IF NOT EXISTS `chat_message` (`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',`conversation_id` VARCHAR(255) NULL COMMENT '会话ID',`message_type` VARCHAR(255) NULL COMMENT '消息类型',`content` TEXT NULL COMMENT '消息内容',INDEX `idx_conversation_id` (`conversation_id`) -- 为 conversation_id 添加索引以加快查询速度
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='聊天消息表';
  • 单元测试:执行单元测试,可以看到历史对话记录全部存储到数据库中,目标实现

6.4 PromptTemplate 模板

PromptTemplate是一种支持动态生成Prompt的机制,类似于字符串模板或JSP中的模板解析,通过创建带有占位符{}的文本模板,在运行时动态替换为传递进行的data。

PromptTemplate 最基本的功能是支持变量替换。你可以在模板中定义占位符,然后在运行时提供这些变量的值:

// 定义带有变量的模板
Strin‍g template ="你好,{name}。⁡今天是{day},天气{‏weather}。";// 创建模板؜对象
PromptTemp‍late promptTe‌mplate = newPromptTemplate(template);// 准备变量映射
Map<String, Object> variables = new HashMap<>();
variables.put("name", "tiga");
variables.put("day", "星期一");
variables.put("weather", "晴朗");// 生成最终提示文本
String prompt = promptTemplate.render(variables);
// 结果: "你好,tiga。今天是星期一,天气晴朗。"

实现原理

Spring AI 中提示模板的关键组件是 PromptTemplate 类,该类旨在促进创建结构化提示,然后将这些提示发送给 AI 模型进行处理

PromptTemplate的类继承图:

这个类使用 TemplateRenderer API 来渲染模板。默认情况下,Spring AI 使用 StTemplateRenderer 实现,该实现基于 Terence Parr 开发的开源 StringTemplate 引擎。模板变量通过 {} 语法来标识,但你可以配置分隔符来使用其他语法。

PromptTemplate类实现的接口支持Prompt创建的不同方面:

  • PromptTemplateStringActions 主要关注创建和渲染提示字符串,代表了提示生成的最基本形式。
  • PromptTemplateMessageActions 专为通过生成和操作 Message 对象来创建提示而设计。
  • PromptTemplateActions 用于返回 Prompt 对象,该对象可以传递给 ChatModel 以生成响应。

专用模板类

SystemPromptTemplate:用于系统消息,设置 AI 的行为和背景

AssistantPromptTemplate:用于助手消息,用于设置 AI 回复的结构

FunctionPromptTemplate:目前没用

public class FunctionPromptTemplate extends PromptTemplate {private String name;public FunctionPromptTemplate(String template) {super(template);}}

这些专用模板类让开发者能更清晰地表达不同类型消息的意图,比如系统消息模板能够快速构造系统 Prompt,示例代码:

String userText ؜= """Tell me about three f‍amous pirates from the Golden A‌ge of Piracy and why they did.
⁡    Write at least a sentence f‏or each pirate.""";Messa؜ge userMe‍ssage = n‌ew UserMe⁡ssage(use‏rText);String systemText = "؜""You are a helpful AI assistant that ‍helps people find information.Your nam‌e is {name}You should reply to the use⁡r's request with your name and also in th‏e style of a {voice}.""";SystemPr؜omptTemplate sy‍stemPromptTempl‌ate = new Syste⁡mPromptTemplate‏(systemText);
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", name, "voice", voice));Prompt prompt = new Prompt(List.of(userMessage, systemMessage));List<Generation> response = chatModel.call(prompt).getResults();

从文件加载模板

这是PromptTemplate 的另一大用法,支持从外部配置文件中加载提示词模板内容,适合管理复杂的提示词。Spring AI 利用 Spring 的 Resource 对象来从指定路径加载模板文件:

@Value("classpath:/prompts/system-message.st")
private Resource systemResource;SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);

这种方式让你可以:

  • 将复杂的提示词放在单独的文件中管理
  • 在不修改代码的情况下调整提示词
  • 为不同场景准备多套提示词模板

6.5 多模态开发

参考文档:Multimodality API :: Spring AI Reference

多模态是指模型同时理解和处理来自各种来源的信息的能力,包括文本、图像、音频和其他数据格式。

Spring AI Message API 提供了所有必要的抽象来支持多模态 LLMs。

UserMessage 的 content 字段主要用于文本输入,而可选的 media 字段允许添加一个或多个不同模态的内容,如图像、音频和视频。 MimeType 指定了模态类型。根据所使用的 LLM, Media 数据字段可以是原始媒体内容作为 Resource 对象,或是对内容的 URI 链接。

媒体字段目前仅适用于用户输入消息(例如, UserMessage )。它对系统消息没有意义。 AssistantMessage ,包括 LLM 响应,仅提供文本内容。要生成非文本媒体输出,您应该使用其中一个专用的单模态模型。

比如,我们可以将以下图片( multimodal.test.png )作为输入,并让 LLM 解释它看到了什么。

示例代码:

var imageResource = new ClassPathResource("/multimodal.test.png");var userMessage = new UserMessage("Explain what do you see in this picture?", // contentnew Media(MimeTypeUtils.IMAGE_PNG, this.imageResource)); // mediaChatResponse response = chatModel.call(new Prompt(this.userMessage));// 流式API
String response = ChatClient.create(chatModel).prompt().user(u -> u.text("Explain what do you see on this picture?").media(MimeTypeUtils.IMAGE_PNG, new ClassPathResource("/multimodal.test.png"))).call().content();

可能的响应:

这是一张设计简洁的水果碗的图片。碗由金属制成,边缘是弯曲的金属丝,形成开放式结构,可以从各个角度看到碗里的水果。碗里面放着两个黄色的香蕉,放在一个看起来像红苹果的东西上面。香蕉稍微熟过头了,因为它们的皮上有些褐色斑点。碗顶部有一个金属环,可能是用来提携的把手。碗放在一个平坦的表面上,背景是中性色调,使碗里的水果清晰可见。

七、扩展思路

  • 自定义 Advisor,如权限校验、违禁词校验。
  • 自定义对话记忆,如持久化到 MySQL 或 Redis。
  • 编写包含变量的 Prompt 模板并从文件加载。
  • 开发多模态对话助手,如让 AI 解释图片。

这里就以违禁词Advisor进行实现

1)借助AI工具实现主要逻辑:

提示词如下:

需求:编写一个SpringAI的Advisor实现违禁词校验。要求:1、生成的xxxAdvisor放到com.liucc.aiagent.advisors包下2、参照SpringAI内置的SimpleLoggerAdvisor进行实现

2)调试代码报错

最终生成的代码文件如下:

public class SensitiveWordAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {private static final Logger logger = LoggerFactory.getLogger(SensitiveWordAdvisor.class);@Overridepublic String getName() {return this.getClass().getSimpleName();}@Overridepublic int getOrder() {return 11;}@Overridepublic AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {logger.debug("SensitiveWordAdvisor - BEFORE call: {}", advisedRequest.userText());// 模拟敏感词检查if (containsSensitiveWord(advisedRequest.userText())) {logger.warn("SensitiveWordAdvisor - Detected sensitive word in request: {}", advisedRequest.userText());return new AdvisedResponse(ChatResponse.builder().generations(List.of(new Generation(new AssistantMessage("检测到违禁词,请修改您的输入。")))).build(),advisedRequest.adviseContext());}return chain.nextAroundCall(advisedRequest);}@Overridepublic Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {logger.debug("SensitiveWordAdvisor - BEFORE stream: {}", advisedRequest.userText());return chain.nextAroundStream(advisedRequest);}private boolean containsSensitiveWord(String text) {// 这是一个简化的敏感词检查逻辑,实际应用中会更复杂String[] sensitiveWords = { "死亡", "虚无", "人身攻击" };for (String word : sensitiveWords) {if (text.contains(word)) {return true;}}return false;}
}

3)单元测试

/*** 测试敏感词Advisor*/
@Test
void doChatWithSensWordAdvisorTest() {String message = "一个人最终都是走向死亡,那么生活的意义是什么?";String chatId = UUID.randomUUID().toString();loveApp.doChat(message, chatId);
}

可以看到触发了SensitiveWordAdvisor拦截器,最终的用户请求没有到达大模型。

本节作业

  1. 完成 AI 恋爱大师应用的开发。
  2. 理解对话记忆、Advisor、结构化输出的工作流程和原理。
  3. 利用结构化输出特性,将 AI 输出映射为自定义的 Java 对象。
http://www.xdnf.cn/news/939043.html

相关文章:

  • 探秘Transformer系列之(36)--- 大模型量化方案
  • OpenEuler 系统中 WordPress 部署深度指南
  • Pandas中常用函数
  • 2025年- H78-Lc186--763.划分字符串区间(贪心)--Java版
  • 分类数据集 - 场景分类数据集下载
  • Langchian - 实现文本分类实际应用
  • 【Java学习笔记】System类
  • vite ts 配置使用@ 允许js
  • 基于SpringBoot实现的大创管理系统设计与实现【源码+文档】
  • 「Java基本语法」标识符、关键字与常量
  • Java编程之组合模式
  • Python项目的构建和部署方案推荐
  • remote display server is not supported (e.g. Wayland)
  • CentOS-7 通过 NFS 实现服务器之间的文件共享
  • 深入了解NIO的优化实现原理
  • 二叉树-226.翻转链表-力扣(LeetCode)
  • Python学习(7) ----- Python起源
  • cookie session和token的区别
  • 突破同步训练瓶颈!AReaL如何实现大规模异步强化学习系统的高效语言推理?
  • 树的基本概念与操作:构建数据结构的层级世界
  • leetcode2368. 受限条件下可到达节点的数目-medium
  • JDK8新特性之Steam流
  • 手动实现C#ArrayList容器
  • Boost ASIO 库深入学习(2)
  • Redis持久化策略:RDB与AOF详解
  • shell脚本 --案例实操
  • cognee,有望替代 RAG, 简单了解一下
  • 服务网格技术深度解析:Istio vs Linkerd的选型对比
  • 【Self-Ask with Search Agent机制概述】利用TavilyAnswer实现搜索代理
  • 【文件传输脚本】