Java 篇-项目实战-AI 天机学堂(从0到1)-day3
java 篇: 1.基础地基 2.设计原理 3.项目实战
购买课程与知识库-查询课程-实现分析:

调用这个 OpenFeign 接口:


实现流程:

因为如果直接调用返回的数据比较多,并不一定都需要,所以需要定义一个类。
定义 DTO:

package com.tianji.aigc.tools.result;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.NumberUtil;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.tianji.api.dto.course.CourseBaseInfoDTO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Optional;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CourseInfo {
@JsonPropertyDescription("课程id")
private Long id;
@JsonPropertyDescription("课程名称")
private String name;
@JsonPropertyDescription("课程价格,单位为元,货币为人民币")
private double price;
@JsonPropertyDescription("课程学习有效期,单位:月")
private Integer validDuration;
@JsonPropertyDescription("适用人群,例如:初学者")
private String usePeople;
@JsonPropertyDescription("课程详细介绍")
private String detail;
_/**_
_ * 将CourseBaseInfoDTO转换为CourseInfo对象_
_ *_
_ * @param courseBaseInfoDTO 课程基础信息数据传输对象(包含原始课程数据)_
_ * @return 转换后的课程信息实体对象(包含格式化后的价格和详情页URL)_
_ */_
_ _public static CourseInfo of(CourseBaseInfoDTO courseBaseInfoDTO) {
if (null == courseBaseInfoDTO) {
return null;
}
// 基础对象属性拷贝(忽略转换错误)
CourseInfo courseInfo = BeanUtil._toBeanIgnoreError_(courseBaseInfoDTO, CourseInfo.class);
// 价格格式化处理:分转元 -> 四舍五入保留两位小数 -> 默认值0.0
courseInfo.setPrice(Optional._ofNullable_(courseBaseInfoDTO.getPrice())
.map(num -> num.doubleValue() / 100d)
.map(num -> NumberUtil._round_(num, 2).doubleValue())
.orElse(0.0d));
return courseInfo;
}
}
![]()
- `public static`:工具方法,可以直接通过类名调用
- `CourseInfo`:返回类型是课程信息实体
- `of`:工厂方法命名惯例,表示"转换/创建"
- 参数:原始的课程基础信息 DTO
基础属性拷贝
![]()
- `BeanUtil.toBeanIgnoreError()`:Hutool 工具方法
- 作用:将 DTO 中同名的属性自动拷贝到 CourseInfo 对象中
- `IgnoreError`:遇到类型不匹配等错误时忽略,不中断执行
- 例如:DTO 的 `name` → CourseInfo 的 `name`,DTO 的 `description` → CourseInfo 的 `description`

价格格式化处理(核心逻辑)

逐步解析:
① `Optional.ofNullable(courseBaseInfoDTO.getPrice())`
- 将价格包装成 Optional,避免空指针
- 如果价格为 `null`,后面会走 `orElse(0.0d)`
② `.map(num -> num.doubleValue() / 100d)`
- 分转元:数据库通常存储分为单位(避免浮点精度问题)
- 例如:`1299 分` → `12.99 元`
- 除以 `100d` 得到元,`d` 是 Java 中表示 double 类型字面量 的后缀。
③ `.map(num -> NumberUtil.round(num, 2).doubleValue())`
- 四舍五入保留两位小数
- `NumberUtil.round()`:Hutool 的精确四舍五入
- 例如:`12.999` → `13.00`
④ `.orElse(0.0d)`
- 如果原始价格为 `null`,默认设置为 `0.0`
定义课程工具类:


工具描述和工具参数描述放到常量类当中
定义常量类:

package com.tianji.aigc.constants;
public interface Constant {
interface Tools {
String _QUERY_COURSE_BY_ID _= "根据课程id查询课程详细信息";
}
interface ToolParams {
String _COURSE_ID _= "课程id";
}
}

可以直接都罗列,但不优雅。


这里就用到了之前的 of 方法,写好之后,就需要在 SpringAIConfig 把它注入进来。

下面进行测试:
在工具里面打个断点:

输入内容:查询课程,课程 id 为:1589905661084430337
发现进到这个工具里面了,调用工具成功



购买课程与知识库-查询课程-课程卡片:

实现思路:
要想实现这个效果,就必须给前端返回相应的参数数据,前端才能展示卡片,但是,上述的内容,都是大模型返回的,都是些文字数据,而我们需要给前端的是格式化的数据,例如 json 数据,该怎么做呢?
实际上,就是在 `Flux` 输出流的最后,做判断,如果调用了工具,拿到工具的结果,追加到输出流的结束标签之前即可。像这样

这个参数数据结构是这样的:**(这个是和前端约定好的结构)**

这里有一件很重要的事情还没搞清楚,就是,Tool 执行的结果已经给了大模型,我们在 Flux 输出时如何获取到呢?**(这个问题很重要)**
要想解决这个问题,就必须在全局有个容器,工具执行完后,将结果放入容器,流输出的最后进行判断,判断这个容器中是否有数据,如果有,就添加到流中,反之,就不需要添加。这样就可以解决问题了。
仔细想想,其实还有一个问题,就是存入这个容器的数据,怎么确保是这次请求的结果数据呢?能不能和 sessionId 关联?这其实是不可以的,因为同一个 sessionid 也可能有并发的情况,所以不能使用 sessionId,那就需要重新生成一个 requestId,这个请求 id,每次发起大模型时都会生成一个新的 id,用这个请求 id 和容器的数据关联起来,问题就解决了。
基本的流程如下:


生成一个唯一的请求 ID,并传递给工具方法


`toolContext()` 方法

- 这是 `ChatClient` 或类似类的一个方法
- 作用:设置工具调用的上下文参数
- 参数:接收一个 `Map` 类型的参数
- 返回:返回自身对象,支持继续链式调用
`Map.of()` 静态方法
![]()
- Java 9+ 引入的静态工厂方法
- 作用:快速创建不可变的 Map(最多支持 10 对键值对)
- 语法:`Map.of(key1, value1, key2, value2, ...)`
在 CourseTools 添加接收这个参数:

定义一个工具结果保持器,也就是容器:

package com.tianji.aigc.config;
import cn.hutool.core.lang.Assert;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
_/**_
_ * 工具结果保持器,用来存储tools中得到的结果,请求id 作为key, value为键值对数据_
_ *_
_ * @author zzj_
_ * @version 1.0_
_ */_
public class ToolResultHolder {
private static final Map<String, Map<String, Object>> _HANDLER_MAP _= new ConcurrentHashMap<>();
_/**_
_ * 工具类,禁止实例化_
_ */_
_ _private ToolResultHolder() {
}
public static void put(String key, String field, Object result) {
Assert._notNull_(key, "key is not null!");
Assert._notNull_(field, "field is not null!");
_HANDLER_MAP_.computeIfAbsent(key, k -> new HashMap<>()).put(field, result);
}
public static Map<String, Object> get(String key) {
return key == null ? null : _HANDLER_MAP_.get(key);
}
public static Object get(String key, String field) {
Assert._notNull_(key, "key is not null!");
Assert._notNull_(field, "field is not null!");
return Optional._ofNullable_(_HANDLER_MAP_.get(key))
.map(map -> map.get(field))
.orElse(null);
}
public static void remove(String key) {
Assert._notNull_(key, "key is not null!");
_HANDLER_MAP_.remove(key);
}
}

前面的大 key 对应 request_id,而小 key 对应如下

`HANDLER_MAP.computeIfAbsent(key, k -> new HashMap<>()).put(field, result);`
获取指定 key 对应的 Map,如果不存在就创建一个新的,然后往这个 Map 中放入键值对。
工具保存数据:

整体功能:将查询到的课程信息缓存到 `ToolResultHolder` 中,以便在同一次请求的其他地方复用,避免重复查询。
常量定义
![]()
- 定义缓存 key 的格式模板
- 最终会生成类似 `courseInfo_12345` 这样的 key
核心逻辑(在 map 中执行)

`MapUtil.get(toolContext.getContext(), Constant.REQUEST_ID, String.class)`
从 `toolContext` 的上下文 Map 中,取出 `Constant.REQUEST_ID` 这个 key 对应的值,并且把这个值转为 `String` 类型返回。
`var field = StrUtil.format(FIELD_NAME_FORMAT, StrUtil.lowerFirst(CourseInfo.class.getSimpleName()), courseId);`
获取类名
![]()
| 项目 | 说明 |
| `CourseInfo.class` | 获取 `CourseInfo` 类的 Class 对象 |
| `getSimpleName()` | 获取类的简单名称(不包含包名) |
| 返回值 | `"CourseInfo"` |
首字母小写
![]()
| 项目 | 说明 |
| `StrUtil.lowerFirst()` | Hutool 工具,将字符串首字母转为小写 |
| 返回值 | `"courseInfo"` |
格式化字符串
![]()
| 项目 | 说明 |
| `StrUtil.format()` | Hutool 的字符串格式化工具 |
| 执行过程 | 将 `{}` 占位符依次替换为后面的参数 |
| 返回值 | `"courseInfo_12345"` |
`ToolResultHolder.` _put_ `(requestId, field, courseInfo);`
这段代码只是中间处理(副作用操作),对 `courseInfo` 对象本身没有任何实际修改。
输出流中添加结果


| 步骤 | 操作 | 目的 |
| 1 | `concatWith(Flux.defer(...))` | 在响应流结束后追加逻辑 |
| 2 | `ToolResultHolder.get(requestId)` | 获取缓存的工具调用结果 |
| 3 | `ToolResultHolder.remove(requestId)` | 清理缓存,释放内存 |
| 4 | 构建 `PARAM` 事件 | 将参数数据传给前端 |
| 5 | 发送 `STOP_EVENT` | 告诉前端流结束 |
![]()
作用:创建一个 Flux 流,依次发出指定的元素,然后自动结束。
`Flux.just(chatEventVO, STOP_EVENT)` 创建了一个响应式流,这个流会依次发出参数事件和结束事件,然后自动关闭。
测试一下:
还是输入:查询课程,课程 id 为:1589905661084430337

测试成功
购买课程与知识库-无法存储 params 的 bug 修复:

params 参数没有内容,查看历史记录,数据无法回显。
定位问题:


解决思路:
这个 bug 的解决思路就是,在 `RedisChatMemoryRepository` 中保存数据时,获取到 `ToolResultHolder` 中的数据,将数据保存到 `params` 中即可。
但是,`ToolResultHolder` 中的数据,是与 `requestId` 关联的,`requestId` 是我们自己生成的,在 `RedisChatMemory` 中是没有的,所以,这个问题的关键就是如何获取到 `requestId` 了,只要有了 `requestId` 就可以获取到数据,进行保存了。
如何传递 `requestId`?
其实,同样也是可以借助于 `ToolResultHolder` 来完成,我们可以把 `ToolResultHolder` 看作是一个**通用的容器**,可以放 Tool 的结果,也可以放其他的内容,只要及时的删除即可。
在 ChatServiceImpl 中将 messageId 和 requestId 关联起来

在 MessageUtil 当中添加:

测试一下:

params 有值了
购买课程与知识库-代码优化:

因为是流式输出,所以现在是每次都去获取消息 id,但是其实在要结束的时候去获取关联就行了。


获取结束原因
![]()
作用:获取 AI 响应的结束原因
数据流向:


购买课程与知识库-反序列化的处理:
因为 SpringAI 没有这个参数,所以我们需要定义一个类然后继承它来实现
MessageUtil 当中:

**MyAssistantMessage:**

package com.tianji.aigc.memory;
import lombok.Getter;
import lombok.Setter;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.content.Media;
import java.util.List;
import java.util.Map;
@Getter
@Setter
public class MyAssistantMessage extends AssistantMessage {
private Map<String, Object> params;
public MyAssistantMessage(String content, Map<String, Object> properties, List<ToolCall> toolCalls, List<Media> media, Map<String, Object> params) {
super(content, properties, toolCalls, media);
this.params = params;
}
}
要实现原来最多参数的那个方法。
MessageUtil 替换原来的 **AssistantMessage**

查询的时候也用到了,所以 ChatSessionServiceImpl 当中 queryBySessionId 的方法也得改。
测试一下:


测试通过。
购买课程与知识库-RAG 基本原理:
之所以要使用知识库,是因为我们在做课程推荐时,需要先从知识库匹配到课程,再通过课程 id 查询课程信息进行推荐,如果没有知识库,就无法根据学生的需求进行推荐,所以必须要用到知识库了。
实现流程如下:


部署 es:
#win平台
docker run -d \
--name es2 \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
-v es2-data:/usr/share/elasticsearch/data \
-v es2-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es2-net \
--restart=always \
-p 19200:9200 \
-p 19300:9300 \
registry.cn-beijing.aliyuncs.com/itcast/elasticsearch:8.13.4
---------------------------------------------------------------------------------
#M系列MAC虚拟机
docker run -d \
--name es2 \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
-v es2-data:/usr/share/elasticsearch/data \
-v es2-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es2-net \
--restart=always \
-p 19200:9200 \
-p 19300:9300 \
docker.1ms.run/elasticsearch:8.13.4
#如果容器已经存在,可以先删除,再创建
#删除
docker rm -f es2
#清理挂载目录中无用的数据
docker volume prune

之后访问:[http://192.168.150.101:19200/](http://192.168.150.101:19200/)
看到下面就成功了

然后在项目中集成 es:
在 tj-aigc 项目中,导入 SpringAI 集成 ES 的依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-elasticsearch</artifactId>
<exclusions>
<exclusion>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.15.5</version>
</dependency>
在 nacos 的配置中心的 `aigc-service.yaml` 文件中进行配置:
spring:
elasticsearch:
uris: http://192.168.150.101:19200
ai:
dashscope:
api-key: ${tj.ai.dashscope.key}
chat:
enabled: true
options:
model: qwen-plus
# model: qwen-plus
# model: qwen2.5-1.5b-instruct 免费模型
embedding:
enabled: true
options:
model: text-embedding-v3 #向量模型
dimensions: 1024 #向量维度维度
vectorstore: #向量库配置
elasticsearch: #使用ES作为向量库存储
initialize-schema: true #开启初始化向量库结构
dimensions: 1024 #向量维度维度
| 放 Nacos | 放 application.yml |
| ✅ 数据库/ES/Redis 连接地址 | ✅ 应用名称、端口 |
| ✅ API Key、密码、密钥 | ✅ 固定开关(enabled: true) |
| ✅ 环境差异配置 | ✅ 固定维度、模型名 |
| ✅ 动态开关(可实时修改) | ✅ 日志格式、编码 |
| ✅ 不同环境不同值 | ✅ 本地开发专用配置 |
核心原则:会变的放 Nacos,不变的放本地;敏感信息放 Nacos,框架配置放本地。当然也可以都在 nacos 当中配,但是不推荐。这里所有关于 spring-ai 的配置都在 nacos 当中配
部署 kibana
可视化界面,部署 `kibana` 的目的是用于查看 `ES` 中是否已经创建了索引库。
#win平台
docker run -d \
--name kibana2 \
-e ELASTICSEARCH_HOSTS=http://192.168.150.101:19200 \
-p 15601:5601 \
docker.elastic.co/kibana/kibana:8.13.4
#M系列MAC虚拟机
docker run -d \
--name kibana2 \
-e ELASTICSEARCH_HOSTS=http://192.168.150.101:19200 \
-p 15601:5601 \
docker.1ms.run/kibana:8.13.4
等待 `kibana` 启动好之后,访问地址:[http://192.168.150.101:15601/app/dev_tools#/console](http://192.168.150.101:15601/app/dev_tools#/console)
看到页面就成功了,现在是直接进入 dev_tools 开发工具

如果是首页,可以点击这里

或者直接搜索

默认索引库名字:

查下看下索引库在不在
在:

不在:

搜索索引库数据:

购买课程与知识库-写入数据到知识库:
在知识库中,需要写入一些数据,以供推荐课程使用

package com.tianji.aigc.controller;
import cn.hutool.core.collection.CollStreamUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/embedding")
@RequiredArgsConstructor
public class EmbeddingController {
private final VectorStore vectorStore;
@PostMapping
public void saveVectorStore(@RequestParam("messages") List<String> messages) {
_log_.info("保存到向量数据库中,消息数据:{}", messages);
//构建文档
List<Document> documents = CollStreamUtil._toList_(messages, message -> Document._builder_()
.text(message)
.build());
//存储到向量数据库中
this.vectorStore.add(documents);
_log_.info("保存到向量数据库成功, 数量:{}", messages.size());
}
}
将传入的文本消息转换为向量并存储到向量数据库,用于后续的相似度搜索和语义检索。
依赖注入

- `VectorStore`:向量数据库接口(可能是 Chroma、Pinecone、Milvus 等)
- `final` + `@RequiredArgsConstructor`:自动生成构造器注入
接口定义

| 注解 | 说明 |
| `@PostMapping` | 处理 POST 请求 |
| `@RequestParam("messages")` | 从请求参数中获取 `messages` |
请求示例:
![]()
构建文档对象

作用:将字符串消息转换为向量数据库的 Document 对象

`CollStreamUtil.toList()`:Hutool 工具,结合了 Collection 和 Stream

存储到向量数据库
![]()
- 将每个 Document 的 text 字段转换为向量(embedding vector)
- 存储向量和原始文本
- 建立索引,支持相似度搜索
完整数据流

准备写入数据:

注入的是 es



先用向量模型进行向量化

然后进行保存
现在就又多了 3 条数据

下面是 1024 维上的数值


购买课程与知识库-集成到 chatClient:

依赖注入
![]()
- `VectorStore`:向量数据库接口
- 存储了之前通过 `/embedding` 接口保存的文本向量
创建 RAG Advisor

作用:构建一个 RAG 增强器,配置检索参数
应用到 AI 对话

RAG 工作流程

参数详解
`similarityThreshold(0.6d)`
相似度阈值,只返回相似度 > 0.6 的结果
| 阈值 | 效果 |
| 0.9+ | 严格匹配,返回少但精准 |
| 0.6-0.7 | 平衡,推荐值 |
| 0.5- | 宽松,返回多但可能有噪音 |
`topK(6)`
最多返回 6 条最相似的结果
| topK | 效果 |
| 小(1-3) | 上下文少,回答简洁 |
| 中(4-6) | 平衡,推荐值 |
| 大(10+) | 上下文多,但可能引入噪音,消耗更多 token |
先在 apifox 上测下:

可以,然后前端测试

测试通过


这里是因为调用工具,但那个工具还没有实现。
有些模型看到下单购买,不会去看 ID,所以可以换下模型试下
Max 效果比 Plus 好

购买课程与知识库-实战任务:
练习 1:
前面我们只实现了向量库的新增操作,除了新增操作外,我们还可以实现如下的几个接口:

实现了这几个接口后,我们就不需要在 kibana 中操作了。
参考代码:
package com.tianji.aigc.controller;
import cn.hutool.core.collection.CollStreamUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/embedding")
@RequiredArgsConstructor
public class EmbeddingController {
private final VectorStore vectorStore;
private final EmbeddingModel embeddingModel;
@PostMapping
public void saveVectorStore(@RequestParam("messages") List<String> messages) {
log.info("保存到向量数据库中,消息数据:{}", messages);
//构建文档
List<Document> documents = CollStreamUtil.toList(messages, message -> Document.builder()
.text(message)
.build());
//存储到向量数据库中
this.vectorStore.add(documents);
log.info("保存到向量数据库成功, 数量:{}", messages.size());
}
@GetMapping
public EmbeddingResponse embed(@RequestParam("message") String message) {
return this.embeddingModel.embedForResponse(List.of(message));
}
@DeleteMapping
public void deleteVectorStore(@RequestParam("ids") List<String> ids) {
// 删除向量数据库中的数据
this.vectorStore.delete(ids);
}
@GetMapping("/search")
public List<Document> search(@RequestParam("message") String message) {
return this.vectorStore.similaritySearch(SearchRequest.builder().query(message).topK(5).build());
}
@GetMapping("/search/all")
public List<Document> searchAll() {
// 搜索全部数据
return this.vectorStore.similaritySearch(SearchRequest.builder().query("").topK(999).build());
}
}
练习 2:
-
按照SpringAI官方文档,实现使用Redis作为向量库存储,替换课程中的ES部分。👉 Redis :: Spring AI Reference
-
实现提示:使用Redis实现向量库,需要使用到redis-stack。
-
Docker 镜像网址:1ms.run
https://1ms.run/r/redis/redis-stack


copy 过来之后记得改下

要走镜像的话,加下

当然最好不要用 latest 最新版本,因为你不知道是哪个版本
docker run -d \
--name redis-stack \
-p 16379:6379 \
-p 18001:8001 \
-e REDIS_ARGS="--requirepass 123456" \
docker.lms.run/redis/redis-stack:7.2.0-v17
docker run -d \
--name redis-stack \
-p 16379:6379 \
-p 18001:8001 \
-e REDIS_ARGS="--requirepass 123456" \
redis/redis-stack:7.2.0-v17
第一个端口是 redis 的,第二个端口是 redisInsight,可视化界面的。下次运行的时候记得启动 redis-stack 服务.

执行一下,这里我有魔法,就不用这个镜像了


完整格式
![]()
逐部分解析

各部分组成
| 部分 | 值 | 说明 |
| 协议 | `redis://` | 使用 Redis 协议连接 |
| 密码 | `:123456` | 冒号后面跟密码,前面没有用户名 |
| 主机 | `192.168.150.101` | Redis 服务器的 IP 地址 |
| 端口 | `16379` | Redis 服务的端口号 |

http://192.168.150.101:18001/redis-stack/browser
这样就有个可视化界面了

项目集成 spring-ai-redis 向量存储
先注释掉 es 向量库,然后加入 redis 向量库依赖,vectorstore 才不会出现歧义

改下 application-local.yml 当中的配置

先把原先的配置注释掉,不然两个会覆盖,原先的只是提供基础的 redis 操作,而新的不仅还。

在 nacos 配置中把 es 的注释掉,加入 redis

`initialize-schema: true`
| 项目 | 说明 |
| 作用 | 应用启动时自动创建 Redis 索引结构 |
| 值 | `true` = 自动创建,`false` = 手动创建 |
| 首次使用 | 建议设为 `true`,让框架自动建索引 |
| 生产环境 | 可以设为 `false`,由 DBA 统一管理 |

`index-name: spring-ai-index`
| 项目 | 说明 |
| 作用 | Redis 索引的名称 |
| 值 | `spring-ai-index` |
| 用途 | 标识这个索引是 Spring AI 使用的 |
| 查询时 | 需要通过这个名字指定使用哪个索引 |
![]()
`prefix: "embedding:"`
| 项目 | 说明 |
| 作用 | Redis Key 的前缀 |
| 值 | `"embedding:"` |
| 用途 | 区分不同用途的数据 |
Redis 中实际存储的 Key:

这个时候启动还是会报错的,得在配置当中加上这个

`client-type: jedis` 用于指定 Spring Boot 使用 Jedis 作为 Redis 的客户端驱动。
Spring Boot 支持三种 Redis 客户端:
| client-type | 说明 | 特点 |
| jedis | Apache Jedis | 同步阻塞,线程不安全(需用连接池) |
| lettuce | Lettuce(默认) | 异步非阻塞,线程安全,基于 Netty |
| none | 禁用自动配置 | 手动配置 |
| 对比项 | Jedis | Lettuce |
| 线程安全 | ❌ 需要连接池 | ✅ 线程安全 |
| 连接池 | ✅ 必须 | ⚠️ 可选 |
| 异步支持 | ❌ | ✅ |
| 响应式支持 | ❌ | ✅ |
| 默认配置 | ❌ | ✅ (Spring Boot) |
| 适用场景 | 传统同步项目 | 现代项目推荐 |
`client-type: jedis` 指定使用 Jedis 作为 Redis 客户端驱动,但 Spring Boot 默认推荐 Lettuce,除非有特殊需求,否则可以省略此配置。但这里就是得用 jedis,不然不能生效。


测试一下:

测试成功:


前端测试通过:

如果对你有帮助的话,请点赞,关注,收藏。热爱可抵一切!👍 ❤️ 🔥
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐




所有评论(0)