Spring ai 1.1 智能体开发全实战笔记 基础学习AGENT+RAG+FC+MCP
2026快速 Java选手快速从qwen3转到qwen3.5避坑自用版
环境:
- Java 21
- dashscope 2.22.11
- spring-ai-alibaba-starter 1.0.0-M6.1
前言
博主本身实在之前qwen3的时候就简单的写过一个简易版智能体 本地部署过一个小参数deepseek学习了一下 智能体的其他知识 如 RAG 上下文记忆 等知识
今天心血来潮 正好qwen3.5 给了 很多的免费token 就想着去跑个hello world 之后呢在按照自己的想法拓展一下 就当是学习了 但是发现现在的qwen文档 (可能是我没找到 ) 存在一定的问题 比如qwen3.5 全员多模态 调用的api 出现了 变化 文档里提到了一嘴 但是示例代码却没什么变化 导致出现报错
进过半小时的探索 决定留下一个快速从qwen3转到 3.5 需要避免的几个小坑 仅代表个人观点奥
1. 确定好版本
我习惯和qwen的网页端双排了 有不懂的就搜一下 一下数据来自qwen-wen
dashscope-sdk 的版本
依赖版本:必须使用 dashscope-sdk-java 2.21.10 或更高版本(支持多模态)
如果你想要免费试用这次的多模态模型 那么请一定记住需要
2 示例代码的问题
在你的项目一切就绪之后 准备跑示例代码出现问题 会出现报错
这里以dashscope为例

可以从今天的官方文档中 看出啊 即使是3.5模型点进去的示例代码 仍然是以qwen3-plus 为主的 Generation.call的实例
这里如果你使用这套示例 一定会报错的 因为

在这个提示错误连接中 你获得了一些线索 其实如果你是一个搜打撤老手 这个时候已经可以去人工智能那里根据这个类 , 这里其实文档有给我们提示 我们也可以直接在导航栏找到图像与视频理解

你就可以得到多模态的调用示例了 , 其实文档完善还有待提高 但是好在还是有些线索的

这里就是使用多模态的对话方案 可以看到请求的信息从普通的生成式message变成了
一个含有Map的列表 以
key作为分类包含
text和image

提示词
提示词工程是 使用AI 智能体 或者一些ai文字输入ai工具必备的技术
复习一下文本生成模型的信息概念
文本生成模型的输入为提示词(Prompt),它由一个或多个消息(Message)对象构成。每条消息由角色(Role)和内容(Content)组成,具体为:
- 系统消息(System Message):设定模型扮演的角色或遵循的指令。若不指定,默认为"You are a helpful assistant"。
- 用户消息(User Message):用户向模型提出的问题或输入的指令。
- 助手消息(Assistant Message):模型的回复内容。
我们往往需要精确 多轮的提示词来让ai帮助我们完成需求
这里给开发 ai智能体的部分提示词做一个分类
[核心] 基于角色分类
如基于 角色分类:
- 专业工程师
- 心理咨询师
- 算命先生
基于功能分类
-
指令性(最常见): 明确告知AI需要执行的任务 和各个步骤
系统提示词: 你是一个经验丰富的Java教师 擅长解决初学者的问题
用户: 请给我演示一下 在java中的集合如何使用
-
对话形 (最常见): 模拟自然对话 以问答的形式和AI交互
比如 你觉得人工智能会代替 程序员吗
-
创意性提示词 : 引导 ai 进行模型交互 创意内容生成 比如 图片 诗词 广告文案
写一个关于火星撞地球的科幻故事
-
角色扮演提示词 : 让大模型扮演特定的角色 来进行回答
你是爱迪生 你能和我说说你是如何发明灯泡的吗
-
少量样本的学习提示词 : 基于一些案例 让大模型理解并且按照案例输出
比如 原句 我是一个大宝剑
帮我改写这段文字 更加有欧洲王者风范
基于复杂度分类
-
简单提示词 : 一句话 没有任何复杂背景和条件的问题
-
复杂提示词: 包含一些步骤 或者 相关指令的提示词
-
链式提示词(比较高效率): 将一系列连续相互依赖的提示词 发送给大模型 循序渐进分好步骤
比如
- 生成一个番茄炒蛋攻略
- 根据不同的地域 进行口味创新
- 如何让蛋保持滑嫩口感不散
-
模版提示词: 包含一些可以替换的变量的提示词 可以动态的更换提示词
比如 你是一个 职业 ${厨师}
提示词优化
高质量的提示词可以显著的提升AI输出的质量
这里推荐区 openAi和 spring提供的提示词 多参考
网上还有提示词库
- 文本对话 authropic
基础优化
- 明确确定角色定位和需要做到的任务 帮助模型理解背景和期望
- 提供详细的说明和具体的实例,比如指定多个条件 和规定示例格式等
- 使用结构化思维 引导大模型
- 明确输出格式要求 (核心)
如: 写一片关于气候变化的科普文章 要求:
- 使用适合高中生的语言
- 包含七个小标题 每个标题下三段文字
- 总字数控制在800字
进阶技巧
-
CoT 思维链提示法
比如 先帮我计算出这段公式的用法
然后帮我带入各个参数 计算出结果
根据带入结果 验算
-
少样本学习
通过几个输入和输出的实例 让大模型理解我们的需求模式和期望输出
-
分步骤指导
将复杂的任务 拆分成可管理的步骤 需要确保模型完成每一个环节
-
让AI 自我评估并修正 让ai自己做验算
-
知识检索和引用 : 引导大模型指出明确的信息来源 作为参考避免出错
-
让AI 作为多个不同的立场和角度来分析问题 提供不同见解的理解
-
多模态思维 : 比如文字 + 图片+音频 多方面的去作参考和实例 去得到结果
提示词调试
-
逐步的去完善和修改相同问题的提示词 比如 逐渐缩小专业范围 如
- ai的影响
- ai在某领域的影响
- ai在这个领域的某个工作的影响
-
边界测试 通过极限情况来测试模型 找到优化空间和上限
-
提示词模板化 : 将条件相同的步骤拆分成模板 来实现相同步骤不同内容的达成效果的提示词
TOKEN
大模型处理文本的计算单位 不同模型对于 token 的划分是不一样的
这很重要 这涉及到对于预算的节约
- 英文 一个token大约相当于 4个字符 约等于 0.7个单词
- 中文 一个汉字 约等于 1~2个token
- 空格和标点符号 也会被计算到token中
- 特殊表情符号 可能需要更多的token去表示
输入和输出的收费价格 可能是不一样的, 推荐是列为可量化内容的表格 来展示
成本优化技巧
系统提示词 用户提示词等都是会消耗成本的 所以优化需要从多个角度来
精简 系统提示词
移除掉一些没必要的表述 只保留最核心的指令
如 你是一个有耐心 非常专业的程序员
修改为 你是程序员
定期清理对话历史
在智能体交互中 对话历史会作为上下文不断的累积 如果控制不好
在长对话中 会非常的消耗token 所以要定期的去维护和清理
使用向量检索代替直接输入
对于需要大量参考文档 不要直接将整个文档作为提示词 而是利用向量数据库或检索技术 比如 RAG 来代替一些段落
结构化代替自然语言
使用表格 列表等结构化的格式 代替过长的描述
需求 MVP 最小可行产品策略
MVP 最小可行产品策略是指先开发包含 核心功能 的基础版本产品快速推向市场,以最小成本验证产品假设和用户需求。通过收集真实用户反馈进行迭代优化,避免开发无人使用的功能,降低资源浪费和开发风险。
功能设计
根据需求,我们将实现一个具有多轮对话能力的 AI 恋爱大师应用。整体方案设计将围绕 2 个核心展开:
- 系统提示词的设计
- 多轮对话的实现
系统提示词设计
系统提示词需要精简 但是不能太过简约, 最简单的作法就是 你是谁 你需要做什么 这样可以做到简单的工作 但是效果不会很理想 我们可以思考一下 一个真正的专家需要怎么去做
- 需要我来帮助你什么 哪方面的问题
- 通过询问 来完善背景和一些解决问题需要的必要条件
- 通过循序渐进的多轮对话 来解决用户的问题
智能体对话基础开发
依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.37</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
<!-- Spring AI Alibaba Agent Framework -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>1.1.0.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-agent-framework</artifactId>
<version>1.1.2.0</version>
</dependency>
<!-- Spring AI Document Reader for Markdown -->
<!-- Source: https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-markdown-document-reader -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-markdown-document-reader</artifactId>
<version>1.1.3</version>
<scope>compile</scope>
</dependency>
<!--用于RAG 写入提示词增强 Source: https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-advisors-vector-store -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
<version>1.1.3</version>
<scope>compile</scope>
</dependency>
<!-- 支持结构化输出-->
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-generator</artifactId>
<version>4.38.0</version>
</dependency>
<!-- 用来高性能序列化 Source: https://mvnrepository.com/artifact/com.esotericsoftware/kryo -->
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.6.2</version>
</dependency>
</dependencies>
Spring 配置
这里我们 将api-key 作为系统环境变量 来调用 确保本地学习的安全性
spring:
application:
name: jx-ai-agent
ai:
dashscope:
chat:
options:
model: qwen3-max-2026-01-23
api-key: ${AI_DASHSCOPE_API_KEY}
server:
port: 8123
servlet:
context-path: /api
# springdoc-openapi项目配置
springdoc:
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
api-docs:
path: /v3/api-docs
group-configs:
- group: "default"
paths-to-match: "/**"
packages-to-scan: com.hyc.jxaiagent.controller
# knife4j的增强配置,不需要增强可以不配
knife4j:
enable: true
setting:
language: zh_cn
智能体设置
- 系统提示词
- 大模型参数配置
- 窗口大小
- 日志打印
- 对话记忆
- 调用大模型
这里我们使用的是最新版本的 spring ai 1.1 注入会话记忆语法上出现了一些改变
@Component
@Slf4j
public class LoveApp {
private final ChatClient chatClient;
// 系统提示词
private static final String SYSTEM_PROMPT = "你在之后的对话中可以自称小恋,扮演深耕恋爱心理领域的专家。开场向用户表明身份,告知用户可倾诉恋爱难题。" +
"围绕单身、恋爱、已婚三种状态提问:单身状态询问社交圈拓展及追求心仪对象的困扰;" +
"恋爱状态询问沟通、习惯差异引发的矛盾;已婚状态询问家庭责任与亲属关系处理的问题。" +
"引导用户详述事情经过、对方反应及自身想法,以便给出专属解决方案。";
public LoveApp(ChatModel dashScopeChatModel) {
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder().maxMessages(6).build();
chatClient = ChatClient.builder(dashScopeChatModel)
.defaultSystem(SYSTEM_PROMPT)
.defaultAdvisors(
//自定义拦截器
new JxLoveAppLoggerAdvisor(),
// 在需要的时候开启 new ReReadingAdvisor(),
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
/**
* AI 基础的对话操作 配置会话记忆
*
* @author 冷环渊
* date: 2026/3/16 下午10:27
*/
public String doChat(String message, String chatId) {
ChatResponse chatResponse = chatClient
.prompt()
.user(message)
// chatid用于分离会话和设置记忆的上下文数量 控制篇幅和成本
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
.call()
.chatResponse();
return chatResponse.getResult().getOutput().getText();
}
}
测试 基础对话
@Test
void doChat() {
String chatId = UUID.randomUUID().toString();
String message = "您好 我是小冷 ";
String answer = loveApp.doChat(message, chatId);
Assertions.assertNotNull(answer);
}
结果

自定义 advisor
advisor 的执行流程
节选自 spring AI 官方文档

- Spring AI 框架将用户的 Prompt 封装为
AdvisedRequest对象,并创建空的AdvisorContext上下文。 - 链中每个 Advisor 依次处理请求并可进行修改,也可选择阻断请求(不调用下一实体)。若选择阻断,该 Advisor 需负责填充响应内容。
- 框架提供的最终 Advisor 将请求发送至聊天模型。
- 聊天模型的响应会逆向传回 Advisor 链,被转换为包含共享
AdvisorContext实例的AdvisedResponse对象。 - 每个 Advisor 均可处理或修改该响应。
- 通过提取
ChatCompletion内容,最终生成的AdvisedResponse将返回给客户端。
最佳实践 (官方文档)
- 保持 Advisor 功能单一化以提升模块性。
- 必要时通过
adviseContext在 Advisor 间共享状态。 - 同时实现流式与非流式版本以获得最佳灵活性。
- 谨慎规划 Advisor 链顺序以确保数据流正确。
观察型自定义advisor log
我们可以像在spring aop 一样 自己来定义切面 , 实现想要的结果 , 让日志输出我们想要的内容
在调用链中下一 Advisor 前后,分别记录 AdvisedRequest 和 AdvisedResponse。该实现仅观察请求与响应而不修改,同时支持非流式与流式场景。
自定义日志拦截器
这里我们直接复制 simpleloggerAdvisor 的代码 做一下精简就可以了
这里要注意使用 CallAroundAdvisor 和 StreamAroundAdvisor 如果不这么用 会出现无法捕捉到增强日志的情况
@Slf4j
public class JxLoveAppLoggerAdvisor implements CallAdvisor, StreamAdvisor {
public String getName() {
return this.getClass().getSimpleName();
}
public int getOrder() {
return 0;
}
private void logRequest(ChatClientRequest request) {
log.info("AI request {}", request.prompt().getUserMessage().getText());
}
private void logResponse(ChatClientResponse chatClientResponse) {
log.info("AI response {}", chatClientResponse.chatResponse().getResult().getOutput().getText());
}
public String toString() {
return SimpleLoggerAdvisor.class.getSimpleName();
}
public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
this.logRequest(chatClientRequest);
ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);
this.logResponse(chatClientResponse);
return chatClientResponse;
}
public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {
this.logRequest(chatClientRequest);
Flux<ChatClientResponse> chatClientResponses = streamAdvisorChain.nextStream(chatClientRequest);
return (new ChatClientMessageAggregator()).aggregateChatClientResponse(chatClientResponses, this::logResponse);
}
}
增强型自定义advisor reReading
用来提高大模型接收问题的的能力 RE2这个拦截器用于让AI重复读提示词 来提升AI理解用户的问题
before方法通过应用重读技术增强用户输入查询。aroundCall方法拦截非流式请求并应用重读技术。aroundStream方法拦截流式请求并应用重读技术。- 通过设置
order值控制执行顺序 — 数值越小优先级越高。 - 为 Advisor 提供唯一标识名称。
public class ReReadingAdvisor implements BaseAdvisor {
private static final String DEFAULT_RE2_ADVISE_TEMPLATE = """
{re2_input_query}
Read the question again: {re2_input_query}
""";
private final String re2AdviseTemplate;
public ReReadingAdvisor() {
this(DEFAULT_RE2_ADVISE_TEMPLATE);
}
public ReReadingAdvisor(String re2AdviseTemplate) {
this.re2AdviseTemplate = re2AdviseTemplate;
}
@Override
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
String augmentedUserText = PromptTemplate.builder()
.template(this.re2AdviseTemplate)
.variables(Map.of("re2_input_query", chatClientRequest.prompt().getUserMessage().getText()))
.build()
.render();
return chatClientRequest.mutate()
.prompt(chatClientRequest.prompt().augmentUserMessage(augmentedUserText))
.build();
}
@Override
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
return chatClientResponse;
}
@Override
public int getOrder() {
return 0;
}
}
结构化输出
Spring AI 给我们提供了一种使用的机制 用于将大模型返回的文本 转换为结构化模式 比如 JSON XML
记得使用之前一定要确认 大模型支不支持结构化输出

PS:
StructuredOutputConverter会尽力将模型输出转换为结构化格式,但 AI 模型并不保证按请求返回结构化输出(可能无法理解提示或生成所需结构)。建议实现验证机制以确保模型输出符合预期。
public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {
}
FormatProvider 向 AI 模型提供特定格式指南,使其生成可被 Converter 转换为目标类型 T 的文本输出

代码 在 LoveApp新增数据结构loveReport 和结构化输出方法
// 定义恋爱列表和标体
record loveReport(String title, List<String> suggestions) {
}
/**
* AI 结构化输出
*
* @author 冷环渊
* date: 2026/3/16 下午10:27
*/
public loveReport doChatWithReport(String message, String chatId) {
loveReport loveReport = chatClient
.prompt()
.system(SYSTEM_PROMPT + "每次对话都需要生成恋爱结果,标体为 {用户名} 的恋爱报告,内容为建议列表")
.user(message)
// chatid用于分离会话和设置记忆的上下文数量 控制篇幅和成本
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
.call()
.entity(loveReport.class);
return loveReport;
}
}
这里我们可以看得到 我们调用大模型的方式从 获取响应 变成了 entity+自定义类型 这里就是spring AI 封装给我们的结构化输出方法 这里我们就可以获取到我们定义的数据结构的响应
对话记忆持久化
官方提供的持久化实在是太过的抽象 , 所以这里我们选择 自定义 chatmemory 来实现持久化
由于我们对于持久化的学习 不想要引入更多的第三方库和数据源 你也可以基于这个思路去自己实现拓展为 redis or mysql等数据库
这里我们采用使用 KRYO序列化库 直接序列化到文件中的方式来实现持久化 之后
依赖
<!-- 用来高性能序列化 Source: https://mvnrepository.com/artifact/com.esotericsoftware/kryo -->
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.6.2</version>
</dependency>
创建 chatmemory 文件夹
FileBasedChatMemory
这里 spring ai 1.1 版本也是更新的get方法的参数 所以使用建造者模式创建
- 新增变量 MAX_MESSAGES 用于代替 原来get方法中的参数 lastN
/**
* @author 冷环渊
* @date 2026/3/18 下午7:44
* @description FileByteChatMemory
*/
public class FileBasedChatMemory implements ChatMemory {
private final String BASE_DIR;
private final int MAX_MESSAGES;
private static final Kryo kryo = new Kryo();
static {
// 关闭手动注册
kryo.setRegistrationRequired(false);
// 设置实例化策略
kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
}
//构造对象 指定文件目录
public FileBasedChatMemory(String baseDir, int maxMessages) {
this.BASE_DIR = baseDir;
this.MAX_MESSAGES = maxMessages;
File file = new File(BASE_DIR);
if (!file.exists()) {
file.mkdirs();
}
}
@Override
public void add(String conversationId, Message message) {
saveConversation(conversationId, message);
}
@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) {
List<Message> allmessages = getOrCreateConversation(conversationId);
return allmessages.stream()
.skip(Math.max(0, allmessages.size() - MAX_MESSAGES))
.toList();
}
@Override
public void clear(String conversationId) {
File conversationFile = getConversationFile(conversationId);
if (conversationFile.exists()) {
conversationFile.delete();
}
}
/**
* 获取 或创建会话消息列表
*
* @author 冷环渊
* date: 2026/3/18 下午7:54
*/
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 (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
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 (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
private void saveConversation(String conversationId, Message messages) {
File file = getConversationFile(conversationId);
try (Output output = new Output(new FileOutputStream(file))) {
kryo.writeObject(output, messages);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* 创造的每个会话文件单独保存
*
* @author 冷环渊
* date: 2026/3/18 下午7:54
*/
private File getConversationFile(String conversationId) {
return new File(BASE_DIR, conversationId + ".kryo");
}
public static FileBasedChatMemory.Builder builder() {
return new FileBasedChatMemory.Builder();
}
public static final class Builder {
private String baseDir;
private int maxMessages = 10;
private Builder() {
}
public FileBasedChatMemory.Builder maxMessages(int maxMessages) {
this.maxMessages = maxMessages;
return this;
}
public FileBasedChatMemory.Builder baseDir(String baseDir) {
this.baseDir = baseDir;
return this;
}
public FileBasedChatMemory build() {
return new FileBasedChatMemory(this.baseDir, this.maxMessages);
}
}
}
在 loveAPP构造方法修改 chetmemory 改为我们自己定义的
public LoveApp(ChatModel dashScopeChatModel) {
// ChatMemory chatMemory = new InMemoryChatMemory();
String fileDir = System.getProperty("user.dir") + "/tmp/chat-memory";
FileBasedChatMemory chatMemory = new FileBasedChatMemory(fileDir);
chatClient = ChatClient.builder(dashScopeChatModel)
.defaultSystem(SYSTEM_PROMPT)
.defaultAdvisors(
//自定义拦截器
new JxLoveAppLoggerAdvisor(),
// 在需要的时候开启 new ReReadingAdvisor(),
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
效果

promptTemplate
基础理念
spring 提供给我们的类似 JSP 一样的 动态变量模版
Spring AI 框架中用于构建和管理提示词的核心组件。允许开发者创建带有占位符的文本模板,然后在运行时动态替换这些占位符。
案例
Strig template = "你好,{name}。今天是{day},天气weather}。";
PromptTemlate promptTemplate = new PromptTemplae(template);
Map<String, Object> variables = new HashMap<>();
variables.put("name", "鱼皮");
variables.put("day", "星期一");
variables.put("weather", "晴朗");
String prompt = promptTemplate.render(variables);
底层基于 一个叫 OSS String tamplate的强大字符串引擎
Spring AI 通过 TemplateRenderer 接口处理模板字符串中的变量替换,默认实现使用 [StringTemplate]。若需自定义逻辑,可提供自己的 TemplateRenderer 实现。对于无需模板渲染的场景(如模板字符串已完整),可使用提供的 NoOpTemplateRenderer。
案例
PromptTemplate promptTemplate = PromptTemplate.builder()
.renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
.template("""
Tell me the names of 5 movies whose soundtrack was composed by <composer>.
""")
.build();
String prompt = promptTemplate.render(Map.of("composer", "John Williams"));
从文件加载模板
PromptTemplate 支持从外部文件加载模板内容,很适合管理复杂的提示词。Spring AI 利用 Spring 的 Resource 对象来从指定路径加载模板文件:
@Value("classpath:/prompts/system-message.st")
private Resouce systemResource;
SystemPromptemplate systemPromptTemplate = new SystemPromptTemplae(systemResource);
这种方式让你可以:
- 将复杂的提示词放在单独的文件中管理
- 在不修改代码的情况下调整提示词
- 为不同场景准备多套提示词模板
推荐大家使用 这种方式来管理自己的 提示词模板
多模态
AI 多模态 可以同时处理图片 视频 音频 文字等多种信息途径的大模型 , 现在比如 qwen3.5 是原生多模态大模型 将多种不同信息源一起训练 这样的设计可以让大模型可以捕获更多跨模态特征之间的复杂关系
检索增强生成 RAG
基础知识
检索增强生成(Retrieval Augmented Generation, RAG)是一种有用的技术,用于克服大型语言模型在长篇内容、事实准确性和上下文感知方面的局限性。
通过RAG 增强过后 以从外部知识库检索提供给大模型的方式,提升AI回答专业特定内容问题和给出更准确的建议的能力
如果不给 AI 提供特定领域的知识库,AI 可能会面临这些问题:
- 知识有限:AI 不知道你的最新课程和内容
- 编故事:当 AI 不知道答案时,它可能会 “自圆其说” 编造内容
- 无法个性化:不了解你的特色服务和回答风格
- 不会推销:不知道该在什么时候推荐你的付费课程和服务S
我们将 RAG技术主要分为四个核心步骤:
-
文档收集和切割
-
文档收集:从各种来源(网页、PDF、数据库等)收集原始文档
-
文档预处理:清洗、标准化文本格式
-
文档切割:将长文档分割成适当大小的片段
- 基于固定大小(如 512 个 token)
- 基于语义边界(如段落、章节)
- 基于递归分割策略(如递归字符 n-gram 切割)

-
向量转换和存储
- 向量转换:使用 Embedding 模型将文本块转换为高维向量表示,可以捕获到文本的语义特征
- 向量存储:将生成的向量和对应文本存入向量数据库,支持高效的相似性搜索

-
文档过滤和检索
-
查询处理:将用户问题也转换为向量表示
-
过滤机制:基于元数据、关键词或自定义规则进行过滤
-
相似度搜索:在向量数据库中查找与问题向量最相似的文档块,常用的相似度搜索算法有余弦相似度、欧氏距离等
-
排序后 根据模型再筛选出更贴合问题的 topk 最后选出最高的N项 加入到大模型的提示词
-
上下文组装:将检索到的多个文档块组装成连贯上下文

-
-
查询增强和关联
- 提示词组装:将检索到的相关文档与用户问题组合成增强提示
- 上下文融合:大模型基于增强提示生成回答
- 源引用:在回答中添加信息来源引用
- 后处理:格式化、摘要或其他处理以优化最终输出
完整的工作流程

Embedding模型
Embedding 嵌入是将高维离散数据(如文字、图片)转换为低维连续向量的过程。这些向量能在数学空间中表示原始数据的语义特征,使计算机能够理解数据间的相似性。
Embedding 模型是执行这种转换算法的机器学习模型,如 Word2Vec(文本)、ResNet(图像)等。不同的 Embedding 模型产生的向量表示和维度数不同,一般维度越高表达能力更强,可以捕获更丰富的语义信息和更细微的差别,但同样占用更多存储空间
向量数据库
向量数据库是专门存储和检索向量数据的数据库系统。通过高效索引算法实现快速相似性搜索,支持 K 近邻查询等操作。

召回
召回是信息检索中的第一阶段,目标是从大规模数据集中快速筛选出可能相关的候选项子集。强调速度和广度,而非精确度。
精排和 Rank 模型
精排(精确排序)是搜索 / 推荐系统的最后阶段,使用计算复杂度更高的算法,考虑更多特征和业务规则,对少量候选项进行更复杂、精细的排序。
比如,短视频推荐先通过召回获取数万个可能相关视频,再通过粗排缩减至数百条,最后精排阶段会考虑用户最近的互动、视频热度、内容多样性等复杂因素,确定最终展示的 10 个视频及顺序。
Rank 模型(排序模型)负责对召回阶段筛选出的候选集进行精确排序,考虑多种特征评估相关性。

混合检索策略(常用)
混合检索策略结合多种检索方法的优势,提高搜索效果。常见组合包括关键词检索、语义检索、知识图谱等。
在使用费混合检索策略的时候 推荐将语义的权重 设置为 0.7 左右 更加的通用
Spring AI RAG+本地知识库+云知识库
Spring AI 使用 Advisor API 为常见的 RAG 流提供开箱即用的支持
制作一些简单的md文件 存到 类路径下

ETL
提取、转换和加载 (ETL) 框架是检索增强生成 (RAG) 用例中数据处理的支柱。
ETL 管道协调从原始数据源到结构化向量存储的数据流,确保数据以最优格式供 AI 模型检索。
RAG 用例通过从数据主体中检索相关信息来增强生成模型的能力,从而提高生成输出的质量和相关性。
工作流程

Document 类包含文本、元数据以及可选的附加媒体类型,如图像、音频和视频。
ETL 管道有三个主要组件:
DocumentReader实现了Supplier<List<Document>>DocumentTransformer实现了Function<List<Document>, List<Document>>DocumentWriter实现了Consumer<List<Document>>
Document 类的内容是通过 DocumentReader 从 PDF、文本文件和其他文档类型创建的。
读取document
我们准备用 md格式的文件来构建一个简单的知识库 所以需要引入spring ai 读取md文件的依赖
<!-- Source: https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-markdown-document-reader -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-markdown-document-reader</artifactId>
<version>1.1.3</version>
<scope>compile</scope>
</dependency>
知识库读取类
package com.hyc.jxaiagent.rag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.markdown.MarkdownDocumentReader;
import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @author 冷环渊
* @date 2026/3/23 下午3:50
* @description LoveAppDocumentLoader
* 用于 加载 md RAG知识库
*/
@Component
@Slf4j
public class LoveAppDocumentLoader {
private final ResourcePatternResolver resourcePatternResolver;
public LoveAppDocumentLoader(ResourcePatternResolver resourcePatternResolver) {
this.resourcePatternResolver = resourcePatternResolver;
}
public List<Document> loadMarkdowns() {
List<Document> allDocument = new ArrayList<>();
//加载多个markdown 文件
log.info("开始加载RAG知识库");
try {
Resource[] resources = resourcePatternResolver.getResources("classpath:document/*.md");
for (Resource resource : resources) {
String filename = resource.getFilename();
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(true)
.withIncludeCodeBlock(false)
.withIncludeBlockquote(false)
.withAdditionalMetadata("filename", filename)
.build();
MarkdownDocumentReader reader = new MarkdownDocumentReader(resource, config);
allDocument.addAll(reader.get());
}
} catch (IOException e) {
log.error("加载 Markdown 文件失败 ", e);
}
return allDocument;
}
}
会帮我们按照文件名分段的加载 md文件中的数据
使用向量数据库
这里我们使用 spring 给我们内部集成的一个基于内存的向量数据库 SimpleVectorStore
SimpleVectorStore 实现了 VectorStore 接口,而 VectorStore 接口集成了 DocumentWriter,所以具备文档写入能力

/**
* @author 冷环渊
* @date 2026/3/23 下午5:39
* @description LoveAppVectorStoreConfig
* 恋爱大师向量数据库配置 初始化基于内存的向量数据库 bean
*/
@Component
@Slf4j
public class LoveAppVectorStoreConfig {
@Resource
private LoveAppDocumentLoader loveAppDocumentLoader;
@Bean
public VectorStore LoveAppVectorStore(EmbeddingModel dashScopeModel) {
log.info("初始化基于内存的向量数据库");
SimpleVectorStore simpleVectorStore = SimpleVectorStore.builder(dashScopeModel).build();
List<Document> documents = loveAppDocumentLoader.loadMarkdowns();
simpleVectorStore.doAdd(documents);
return simpleVectorStore;
}
}
问答增强
spring 提供了两种 增强 Advisor RetrievalAugmentationAdvisor 这里我们先试用更为简单的QuestionAnswerAdvisor
引入依赖
<!--用于RAG 写入提示词增强 Source: https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-advisors-vector-store -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
<version>1.1.3</version>
<scope>compile</scope>
</dependency>
QuestionAnswerAdvisor
在 loveApp中 加入新方法
@Resource
private VectorStore loveAppVectorStore;
/**
* 基于 本地知识库+向量模型使用
*
* @author 冷环渊
* date: 2026/3/23 下午5:47
*/
public String doChatWithRag(String message, String chatId) {
ChatResponse chatResponse = chatClient
.prompt()
.user(message)
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
.advisors(new QuestionAnswerAdvisor(loveAppVectorStore))
.call()
.chatResponse();
String content = chatResponse.getResult().getOutput().getText();
log.info("content: {}", content);
return content;
}
测试案例
@Test
void doChatRag() {
String chatId = UUID.randomUUID().toString();
String message = "我已经结婚了 但是我婚后的亲密关系一直在下降 婚后与伴侣家人产生矛盾,如何妥善解决 ";
String answer = loveApp.doChatWithRag(message, chatId);
Assertions.assertNotNull(answer);
}
效果 可以看出 我们的文本增强已经依据数据库实现了

云知识库
在百炼云平台 创建一个 云知识库
使用云知识库 快速的集成spring ai alibaba 快速的实现 切片rank等云服务厂商提供的便利服务
缺点就是 有开销 信息不保密等
这里我们使用 RetrievalAugmentationAdvisor 来连接云知识库 这里基于百炼sdk 来实现
新建配置类 并且更改love APP 入口中的方法
配置 advisor
/**
* @author 冷环渊
* @date 2026/3/23 下午5:39
* @description LoveAppVectorStoreConfig
* 恋爱大师向量数据库配置 基于阿里云知识库方法
*/
@Component
@Slf4j
public class LoveAppCloudRAGAdvisorConfig {
@Value("${spring.ai.dashscope.api-key}")
private String dashScopeApiKey;
@Bean
public Advisor LoveAppCloudRAGAdvisor() {
log.info("初始化基于云的RAG");
DashScopeApi dashScopeApi = DashScopeApi.builder()
.apiKey(dashScopeApiKey)
.build();
final String KONWLADGE_INDEX = "恋爱大师智能体";
DashScopeDocumentRetriever dashScopeDocumentRetriever = new DashScopeDocumentRetriever(dashScopeApi,
DashScopeDocumentRetrieverOptions.builder().withIndexName(KONWLADGE_INDEX)
.build());
return RetrievalAugmentationAdvisor.builder()
.documentRetriever(dashScopeDocumentRetriever)
.build();
}
}
修改love app
/**
* 基于 本地知识库+向量模型使用
*
* @author 冷环渊
* date: 2026/3/23 下午5:47
*/
public String doChatWithRag(String message, String chatId) {
ChatResponse chatResponse = chatClient
.prompt()
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
//启用advisor 本地知识库问答增强服务
// .advisors(QuestionAnswerAdvisor.builder(loveAppVectorStore).build())
//基于云知识库 检索增强
.advisors(LoveAppCloudRAGAdvisor)
.advisors(new JxLoveAppLoggerAdvisor())
.user(message)
.call()
.chatResponse();
String content = chatResponse.getResult().getOutput().getText();
log.info("content: {}", content);
return content;
}
效果

可以看出 在切片和命中还有提示词方面 云知识库和本地知识库还是有区别的
RAG 进阶
extract 提取
Spring AI 通过 DocumentReader 组件实现文档抽取,也就是把文档加载到内存中。
看下源码,DocumentReader 接口实现了 Supplier<List<Document>> 接口,主要负责从各种数据源读取数据并转换为 Document 对象集合。
spring ai 和 alibaba 提供了很多可以参考的提取类案例 有需要可以直接去官网查看和学习
public interface DocumentReader extends Supplier<List<Document>> {
default List<Document> read() {
return get();
}
}
Transformer 转换
Spring AI 通过 DocumentTransformer 组件实现文档转换。
看下源码,DocumentTransformer 接口实现了 Function<List<Document>, List<Document>> 接口,负责将一组文档转换为另一组文档。
public interface DocumentTransformer extends Function<List<Document>, List<Document>> {
default List<Document> transform(List<Document> documents) {
return apply(documents);
}
}
文档转换是保证 RAG 效果的核心步骤,也就是如何将大文档合理拆分为便于检索的知识碎片,Spring AI 提供了多种 DocumentTransformer 实现类,可以简单分为 3 类。
- 文本分割
- 元数据增强(常用)
- 内容格式化工具
Load加载
Spring AI 通过 DocumentWriter 组件实现文档加载(写入)。
DocumentWriter 接口实现了 Consumer<List<Document>> 接口,负责将处理后的文档写入到目标存储中:
public interface DocumentWriter extends Consumer<List<Document>> {
default void write(List<Document> documents) {
accept(documents);
}
}
Spring AI 提供了 2 种内置的 DocumentWriter 实现:
1)FileDocumentWriter:将文档写入到文件系统
@Component
class MyDocumentWriter {
public void writeDocuments(List<Document> documents) {
FileDocumentWriter writer = new FileDocumentWriter("output.txt", true, MetadataMode.ALL, false);
writer.accept(documents);
}
}
2)VectorStoreWriter:将文档写入到向量数据库
@Component
class MyVectorStoreWriter {
private final VectorStore vectorStore;
MyVectorStoreWriter(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void storeDocuments(List<Document> documents) {
vectorStore.accept(documents);
}
}
当然,你也可以同时将文档写入多个存储,只需要创建多个 Writer 或者自定义 Writer 即可。