第三章 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
实现CallAroundAdvisor
和StreamAroundAdvisor
两个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)保持单一职责:每个 Advisor 应专注于一项特定任务
2)注意执行顺序:合理设置<font style="color:rgb(31, 35, 40);background-color:rgba(175, 184, 193, 0.2);">getOrder()</font>
值确保 Advisor 按正确顺序执行
3)同时支持流式和非流式:尽可能同时实现两种接口以提高灵活性
4)高效处理请求:避免在 Advisor 中执行耗时操作
5)测试边界情况:确保 Advisor 能够优雅处理异常和边界情况
6)对于需要更复杂处理的流式场景,可以使用 Reactor 的操作符:
@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对象
最佳实践
- 尽量为模型提供清晰的格式指导
- 实现输出验证机制和异常处理逻辑,确保结构化数据符合预期
- 选择支持结构化输出的合适模型
- 对于复杂数据结构,考虑使用
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 最基本的功能是支持变量替换。你可以在模板中定义占位符,然后在运行时提供这些变量的值:
// 定义带有变量的模板
String template = "你好,{name}。今天是{day},天气{weather}。";// 创建模板对象
PromptTemplate promptTemplate = new PromptTemplate(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 famous pirates from the Golden Age of Piracy and why they did.
Write at least a sentence for each pirate.""";Message userMessage = new UserMessage(userText);String systemText = """You are a helpful AI assistant that helps people find information.Your name is {name}You should reply to the user's request with your name and also in the style of a {voice}.""";SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(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拦截器,最终的用户请求没有到达大模型。
本节作业
- 完成 AI 恋爱大师应用的开发。
- 理解对话记忆、Advisor、结构化输出的工作流程和原理。
- 利用结构化输出特性,将 AI 输出映射为自定义的 Java 对象。