Skip to main content

Spring ai 1.1 智能体开发全实战笔记 基础学习AGENT+RAG+FC+MCP

2026快速 Java选手快速从qwen3转到qwen3.5避坑自用版

环境:

  1. Java 21
  2. dashscope 2.22.11
  3. 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为例

image-20260316171657743

可以从今天的官方文档中 看出啊 即使是3.5模型点进去的示例代码 仍然是以qwen3-plus 为主的 Generation.call的实例

这里如果你使用这套示例 一定会报错的 因为

image-20260316171840585

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

image-20260316174728752

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

image-20260316174747627

这里就是使用多模态的对话方案 可以看到请求的信息从普通的生成式message变成了

一个含有Map的列表 以key作为分类

包含 textimage

image-20260316174915882

提示词

提示词工程是 使用AI 智能体 或者一些ai文字输入ai工具必备的技术

复习一下文本生成模型的信息概念

文本生成模型的输入为提示词(Prompt),它由一个或多个消息(Message)对象构成。每条消息由角色(Role)和内容(Content)组成,具体为:

  • 系统消息(System Message):设定模型扮演的角色或遵循的指令。若不指定,默认为"You are a helpful assistant"。
  • 用户消息(User Message):用户向模型提出的问题或输入的指令。
  • 助手消息(Assistant Message):模型的回复内容。

我们往往需要精确 多轮的提示词来让ai帮助我们完成需求

这里给开发 ai智能体的部分提示词做一个分类

[核心] 基于角色分类

如基于 角色分类:

  1. 专业工程师
  2. 心理咨询师
  3. 算命先生

基于功能分类

  1. 指令性(最常见): 明确告知AI需要执行的任务 和各个步骤

    系统提示词: 你是一个经验丰富的Java教师 擅长解决初学者的问题

    用户: 请给我演示一下 在java中的集合如何使用

  2. 对话形 (最常见): 模拟自然对话 以问答的形式和AI交互

    比如 你觉得人工智能会代替 程序员吗

  3. 创意性提示词 : 引导 ai 进行模型交互 创意内容生成 比如 图片 诗词 广告文案

    写一个关于火星撞地球的科幻故事

  4. 角色扮演提示词 : 让大模型扮演特定的角色 来进行回答

    你是爱迪生 你能和我说说你是如何发明灯泡的吗

  5. 少量样本的学习提示词 : 基于一些案例 让大模型理解并且按照案例输出

    比如 原句 我是一个大宝剑

    帮我改写这段文字 更加有欧洲王者风范

基于复杂度分类

  1. 简单提示词 : 一句话 没有任何复杂背景和条件的问题

  2. 复杂提示词: 包含一些步骤 或者 相关指令的提示词

  3. 链式提示词(比较高效率): 将一系列连续相互依赖的提示词 发送给大模型 循序渐进分好步骤

    比如

    1. 生成一个番茄炒蛋攻略
    2. 根据不同的地域 进行口味创新
    3. 如何让蛋保持滑嫩口感不散
  4. 模版提示词: 包含一些可以替换的变量的提示词 可以动态的更换提示词

    比如 你是一个 职业 ${厨师}

提示词优化

高质量的提示词可以显著的提升AI输出的质量

这里推荐区 openAi和 spring提供的提示词 多参考

网上还有提示词库

  1. 文本对话 authropic

基础优化

  1. 明确确定角色定位和需要做到的任务 帮助模型理解背景和期望
  2. 提供详细的说明和具体的实例,比如指定多个条件 和规定示例格式等
  3. 使用结构化思维 引导大模型
  4. 明确输出格式要求 (核心)

如: 写一片关于气候变化的科普文章 要求:

  1. 使用适合高中生的语言
  2. 包含七个小标题 每个标题下三段文字
  3. 总字数控制在800字

进阶技巧

  1. CoT 思维链提示法

    比如 先帮我计算出这段公式的用法

    然后帮我带入各个参数 计算出结果

    根据带入结果 验算

  2. 少样本学习

    通过几个输入和输出的实例 让大模型理解我们的需求模式和期望输出

  3. 分步骤指导

    将复杂的任务 拆分成可管理的步骤 需要确保模型完成每一个环节

  4. 让AI 自我评估并修正 让ai自己做验算

  5. 知识检索和引用 : 引导大模型指出明确的信息来源 作为参考避免出错

  6. 让AI 作为多个不同的立场和角度来分析问题 提供不同见解的理解

  7. 多模态思维 : 比如文字 + 图片+音频 多方面的去作参考和实例 去得到结果

提示词调试

  1. 逐步的去完善和修改相同问题的提示词 比如 逐渐缩小专业范围 如

    1. ai的影响
    2. ai在某领域的影响
    3. ai在这个领域的某个工作的影响
  2. 边界测试 通过极限情况来测试模型 找到优化空间和上限

  3. 提示词模板化 : 将条件相同的步骤拆分成模板 来实现相同步骤不同内容的达成效果的提示词

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);
}

结果

image-20260318192445950

自定义 advisor

advisor 的执行流程

节选自 spring AI 官方文档

Advisors API Flow

  1. Spring AI 框架将用户的 Prompt 封装为 AdvisedRequest 对象,并创建空的 AdvisorContext 上下文。
  2. 链中每个 Advisor 依次处理请求并可进行修改,也可选择阻断请求(不调用下一实体)。若选择阻断,该 Advisor 需负责填充响应内容。
  3. 框架提供的最终 Advisor 将请求发送至聊天模型。
  4. 聊天模型的响应会逆向传回 Advisor 链,被转换为包含共享 AdvisorContext 实例的 AdvisedResponse 对象。
  5. 每个 Advisor 均可处理或修改该响应。
  6. 通过提取 ChatCompletion 内容,最终生成的 AdvisedResponse 将返回给客户端。

最佳实践 (官方文档)

  1. 保持 Advisor 功能单一化以提升模块性。
  2. 必要时通过 adviseContext 在 Advisor 间共享状态。
  3. 同时实现流式与非流式版本以获得最佳灵活性。
  4. 谨慎规划 Advisor 链顺序以确保数据流正确。

观察型自定义advisor log

我们可以像在spring aop 一样 自己来定义切面 , 实现想要的结果 , 让日志输出我们想要的内容

在调用链中下一 Advisor 前后,分别记录 AdvisedRequestAdvisedResponse。该实现仅观察请求与响应而不修改,同时支持非流式与流式场景。

自定义日志拦截器

这里我们直接复制 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理解用户的问题

  1. before 方法通过应用重读技术增强用户输入查询。
  2. aroundCall 方法拦截非流式请求并应用重读技术。
  3. aroundStream 方法拦截流式请求并应用重读技术。
  4. 通过设置 order 值控制执行顺序 — 数值越小优先级越高。
  5. 为 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

记得使用之前一定要确认 大模型支不支持结构化输出

image-20260318191408874

PS:StructuredOutputConverter 会尽力将模型输出转换为结构化格式,但 AI 模型并不保证按请求返回结构化输出(可能无法理解提示或生成所需结构)。建议实现验证机制以确保模型输出符合预期。

public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {

}

FormatProvider 向 AI 模型提供特定格式指南,使其生成可被 Converter 转换为目标类型 T 的文本输出

image-20260318191551501

代码 在 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();
}

效果

image-20260318204327535

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")
priva⁠te Resouce systemResource;
SystemPromptemplate systemPromptTemplate = new SystemPromptTemplae(systemResource);

这种方式让你可以:

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

推荐大家使用 这种方式来管理自己的 提示词模板

多模态

AI 多模态 可以同时处理图片 视频 音频 文字等多种信息途径的大模型 , 现在比如 qwen3.5 是原生多模态大模型 将多种不同信息源一起训练 这样的设计可以让大模型可以捕获更多跨模态特征之间的复杂关系

检索增强生成 RAG

基础知识

检索增强生成(Retrieval Augmented Generation, RAG)是一种有用的技术,用于克服大型语言模型在长篇内容、事实准确性和上下文感知方面的局限性。

通过RAG 增强过后 以从外部知识库检索提供给大模型的方式,提升AI回答专业特定内容问题和给出更准确的建议的能力

如果不给 ⁠AI 提供特定领域‌的知识库,AI 可能会面临这些问题:

  1. 知识有限:AI 不知道你的最新课程和内容
  2. 编故事:当 AI 不知道答案时,它可能会 “自圆其说” 编造内容
  3. 无法个性化:不了解你的特色服务和回答风格
  4. 不会推销:不知道该在什么时候推荐你的付费课程和服务S

我们将 RAG技术主要分为四个核心步骤:

  • 文档收集和切割

  • 文档收集:从各种来源(网页、PDF、数据库等)收集原始文档

  • 文档预处理:清洗、标准化文本格式

  • 文档切割:⁠将长文档分割成适当‌大小的片段

    • 基于固定大小(如 512 个 token)
    • 基于语义边界(如段落、章节)
    • 基于递归分割策略(如递归字符 n-gram 切割)

    image-20260323152622713

  • 向量转换和存储

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

    image-20260323152613405

  • 文档过滤和检索

    • 查询处理:将用户问题也转换为向量表示

    • 过滤机制:基于元数据、关键词或自定义规则进行过滤

    • 相似度搜索⁠:在向量数据库中查‌找与问题向量最相似的文档块,常用的相‎似度搜索算法有余弦‌相似度、欧氏距离等

    • 排序后 根据模型再筛选出更贴合问题的 topk 最后选出最高的N项 加入到大模型的提示词

    • 上下文组装:将检索到的多个文档块组装成连贯上下文

      image-20260323152755635

  • 查询增强和关联

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

完整的工作流程

image-20260323153217366

Embedding模型

Embeddin⁠g 嵌入是将高维离散数据(如文‌字、图片)转换为低维连续向量的过程。这些向量能在数学空间中表‎示原始数据的语义特征,使计算机‌能够理解数据间的相似性。

Embedding 模型是⁠执行这种转换算法的机器学习模型,如 Word2Ve‌c(文本)、ResNet(图像)等。不同的 Embedding 模型产生的向量表示和维度数不同,一般‎维度越高表达能力更强,可以捕获更丰富的语义信息和更‌细微的差别,但同样占用更多存储空间

向量数据库

向量数据库⁠是专门存储和检索向量‌数据的数据库系统。通过高效索引算法实现快‎速相似性搜索,支持 ‌K 近邻查询等操作。

image-20260323153716797

召回

召回是信息检索中的第一阶段,目标是从大规模数据集中快速筛选出可能相关的候选项子集。强调速度和广度,而非精确度。

精排和 Rank 模型

精排(精确排⁠序)是搜索 / 推荐系统‌的最后阶段,使用计算复杂度更高的算法,考虑更多特‎征和业务规则,对少量候选‌项进行更复杂、精细的排序。

比如,短视频推荐⁠先通过召回获取数万个可能相关视频‌,再通过粗排缩减至数百条,最后精排阶段会考虑用户最近的互动、视频‎热度、内容多样性等复杂因素,确定‌最终展示的 10 个视频及顺序。

Rank ⁠模型(排序模型)负‌责对召回阶段筛选出的候选集进行精确排‎序,考虑多种特征评‌估相关性。

image-20260323153650750

混合检索策略(常用)

混合检索策⁠略结合多种检索方法‌的优势,提高搜索效果。常见组合包括关‎键词检索、语义检索、知‌识图谱等。

在使用费混合检索策略的时候 推荐将语义的权重 设置为 0.7 左右 更加的通用

Spring AI RAG+本地知识库+云知识库

Spring AI 使用 Advisor API 为常见的 RAG 流提供开箱即用的支持

制作一些简单的md文件 存到 类路径下

image-20260323172723101

ETL

提取、转换和加载 (ETL) 框架是检索增强生成 (RAG) 用例中数据处理的支柱。

ETL 管道协调从原始数据源到结构化向量存储的数据流,确保数据以最优格式供 AI 模型检索。

RAG 用例通过从数据主体中检索相关信息来增强生成模型的能力,从而提高生成输出的质量和相关性。

工作流程

image-20260323173146446

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

SimpleVe⁠ctorStore 实现了 Ve‌ctorStore 接口,而 VectorStore 接口集成了‎ DocumentWriter,‌所以具备文档写入能力

image-20260323173834615

/**
* @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);
}

效果 可以看出 我们的文本增强已经依据数据库实现了

image-20260323211700672

云知识库

在百炼云平台 创建一个 云知识库

使用云知识库 快速的集成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;
}

效果

image-20260323215423352

可以看出 在切片和命中还有提示词方面 云知识库和本地知识库还是有区别的

RAG 进阶

extract 提取

Sprin⁠g AI 通过 D‌ocumentReader 组件实现‎文档抽取,也就是把‌文档加载到内存中。

看下源码,DocumentReader 接口实现了 Supplier<List<Document>> 接口,主要负责从各种数据源读取数据并转换为 Document 对象集合。

spring ai 和 alibaba 提供了很多可以参考的提取类案例 有需要可以直接去官网查看和学习

public interface DocumentReader extends Supplier<List<Document>> {
default List<Document> read() {
return get();
}
}

Transformer 转换

Sprin⁠g AI 通过 D‌ocumentTransformer‎ 组件实现文档转换‌。

看下源码,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);
}
}

文档转换是保证 R⁠AG 效果的核心步骤,也就是如何将大‌文档合理拆分为便于检索的知识碎片,Spring AI 提供了多种 Doc‎umentTransformer 实‌现类,可以简单分为 3 类。

  1. 文本分割
  2. 元数据增强(常用)
  3. 内容格式化工具

Load加载

Sprin⁠g AI 通过 D‌ocumentWriter 组件实现‎文档加载(写入)。

DocumentWriter 接口实现了 Consumer<List<Document>> 接口,负责将处理后的文档写入到目标存储中:

public interface DocumentWriter extends Consumer<List<Document>> {
default void write(List<Document> documents) {
accept(documents);
}
}

Sprin⁠g AI 提供了 ‌2 种内置的 DocumentWri‎ter 实现:

1)Fil⁠eDocument‌Writer:将文档写入到文件系统

@Component
class MyDocumentWriter {
public void writeDocuments(List<Document> documents) {
FileDocumentWriter writer = new FileDocumentWriter("output.txt", true, MetadataMode.ALL, false);
writer.accept(documents);
}
}

2)Vec⁠torStoreW‌riter:将文档写入到向量数据库

@Component
class MyVectorStoreWriter {
private final VectorStore vectorStore;

MyVectorStoreWriter(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}

public void storeDocuments(List<Document> documents) {
vectorStore.accept(documents);
}
}

当然,你也⁠可以同时将文档写入‌多个存储,只需要创建多个 Write‎r 或者自定义 W‌riter 即可。