天机学堂AI助手智能体 - 购买课程与知识库

参考视频:黑马程序员SpringAI+DeepSeek大模型应用开发实战视频教程

1. 查询课程

1.1. 需求分析

在天机AI助手中,学生可以查询课程信息,比如:提供了课程id进行查询:

img

1.2. 课程微服务

在原有系统中,有课程微服务,提供了课程相关的接口服务:

img

其中,提供了通过课程id查询课程信息的接口,我不需要关心具体的实现,只需要知道有这样的接口就行:

img

他所对应的Feign client是在这里:

img

有了FeignClient就可以访问微服务了。

1.3. 实现流程

img

1.4. 代码实现

1.4.1. 定义结果类

下面定义,Tools返回的结果数据:

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

}

1.4.2. 定义常量类

java
package com.tianji.aigc.constants;

public interface Constant {

    interface Tools {
        String QUERY_COURSE_BY_ID = "根据课程id查询课程详细信息";
    }

    interface ToolParams {
        String COURSE_ID = "课程id";
    }

}

1.4.3. 编写Tool代码

java
package com.tianji.aigc.tools;

import com.tianji.aigc.constants.Constant;
import com.tianji.aigc.tools.result.CourseInfo;
import com.tianji.api.client.course.CourseClient;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
@RequiredArgsConstructor
public class CourseTools {

    private final CourseClient courseClient;

    /**
     * 根据课程id查询课程信息
     *
     * @param courseId 课程id
     * @return 课程信息
     */
    @Tool(description = Constant.Tools.QUERY_COURSE_BY_ID)
    public CourseInfo queryCourseById(@ToolParam(description = Constant.ToolParams.COURSE_ID) Long courseId) {
        return Optional.ofNullable(courseId)
                .map(id -> CourseInfo.of(this.courseClient.baseInfo(id, true)))
                .orElse(null);
    }
}

1.4.4. 注册Tool

java
    /**
     * 配置 ChatClient
     */
    @Bean
    public ChatClient chatClient(ChatClient.Builder chatClientBuilder,
                                 Advisor loggerAdvisor,
                                 Advisor messageChatMemoryAdvisor,
                                 CourseTools courseTools // 课程工具
    ) {  // 日志记录器
        return chatClientBuilder
                .defaultAdvisors(loggerAdvisor, messageChatMemoryAdvisor) //添加 Advisor 功能增强
                .defaultTools(courseTools) //添加默认工具
                .build();
    }

1.4.5. 测试

输入内容:查询课程,课程id为:1589905661084430337, 可以看到,已经调用工具查询到课程数据了。

在tool代码处添加断点:

img

可以看到,传入课程参数了:

img

效果:

img

1.5. 课程卡片

1.5.1. 功能说明

img

需要在AI助手中,如果是查询课程,或推荐课程,需要展示出课程的卡片信息,其中,要显示课程的价格、名称等信息,还可以,点击卡片,跳转到课程详情页面。

1.5.2. 实现分析

如何实现展示卡片需求呢?

分析

要想实现这个效果,就必须给前端返回相应的参数数据,前端才能展示卡片,但是,上述的内容,都是大模型返回的,都是些文字数据,而我们需要给前端的是格式化的数据,例如json数据,该怎么做呢?

实际上,就是在Flux输出流的最后,做判断,如果调用了工具,拿到工具的结果,追加到输出流的结束标签之前即可。像这样:

img

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

json
{
    "eventData": {
        "courseInfo_1589905661084430337": {
            "id": "1589905661084430337",
            "name": "可能是史上最全的微服务技术栈课程",
            "price": 199.0,
            "validDuration": 9999,
            "usePeople": "有一定的Java开发基础,熟练使用了SpringBoot、MyBatis等基础框架",
            "detail": "可能是史上最全的微服务技术栈课程,由黑马名师授课,你值的拥有"
        }
    },
    "eventType": 1003
}

这里有一件很重要的事情还没搞清楚,就是,Tool执行的结果已经给了大模型,我们在Flux输出时如何获取到呢?(这个问题很重要)

要想解决这个问题,就必须在全局有个容器,工具执行完后,将结果放入容器,流输出的最后进行判断,判断这个容器中是否有数据,如果有,就添加到流中,反之,就不需要添加。这样就可以解决问题了。

仔细想想,其实还有一个问题,就是存入这个容器的数据,怎么确保是这次请求的结果数据呢?能不能和sessionId关联?这其实是不可以的,因为同一个sessionid也可能有并发的情况,所以不能使用sessionId,那就需要重新生成一个requestId,这个请求id,每次发起大模型时都会生成一个新的id,用这个请求id和容器的数据关联起来,问题就解决了。

基本的流程如下:

img

1.5.3. 代码实现

1.5.3.1. 生成请求id与传递

com.tianji.aigc.service.impl.ChatServiceImpl#chat方法中,需要生成requestId,并且传递到工具中,这里就需要使用SpringAItoolContext进行传递了。

java
package com.tianji.aigc.constants;

public interface Constant {

    String REQUEST_ID = "requestId";

    interface Tools {
        String QUERY_COURSE_BY_ID = "根据课程id查询课程详细信息";
    }

    interface ToolParams {
        String COURSE_ID = "课程id";
    }

}
java
    @Override
    public Flux<ChatEventVO> chat(String question, String sessionId) {
        // 获取对话id
        var conversationId = ChatService.getConversationId(sessionId);
        // 大模型输出内容的缓存器,用于在输出中断后的数据存储
        StringBuilder outputBuilder = new StringBuilder();
        // 生成请求id
        var requestId = IdUtil.fastSimpleUUID();

        return this.chatClient.prompt()
                .system(promptSystem -> promptSystem
                        .text(this.systemPromptConfig.getChatSystemMessage().get()) // 设置系统提示语
                        .param("now", DateUtil.now()) // 设置当前时间的参数
                )
                .advisors(advisor -> advisor.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId))
                .toolContext(Map.of(Constant.REQUEST_ID, requestId)) //通过工具上下文传递参数
                .user(question)
                .stream()
                .chatResponse()
                .doFirst(() -> { //输出开始,标记正在输出
                    GENERATE_STATUS.put(sessionId, true);
                })
                .doOnComplete(() -> { //输出结束,清除标记
                    GENERATE_STATUS.remove(sessionId);
                })
                .doOnError(throwable -> GENERATE_STATUS.remove(sessionId)) // 错误时清除标记
                .doOnCancel(() -> {
                    // 当输出被取消时,保存输出的内容到历史记录中
                    this.saveStopHistoryRecord(conversationId, outputBuilder.toString());
                })
                // 输出过程中,判断是否正在输出,如果正在输出,则继续输出,否则结束输出
                .takeWhile(s -> Optional.ofNullable(GENERATE_STATUS.get(sessionId)).orElse(false))
                .map(chatResponse -> {
                    // 获取大模型的输出的内容
                    String text = chatResponse.getResult().getOutput().getText();
                    // 追加到输出内容中
                    outputBuilder.append(text);
                    // 封装响应对象
                    return ChatEventVO.builder()
                            .eventData(text)
                            .eventType(ChatEventTypeEnum.DATA.getValue())
                            .build();
                })
                .concatWith(Flux.just(ChatEventVO.builder()
                        .eventType(ChatEventTypeEnum.STOP.getValue())
                        .build())
                );
    }
1.5.3.2. 工具结果保持器
java
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);
    }

}
1.5.3.3. 工具保存数据

CourseTools中,需要将查询的课程数据存储到ToolResultHolder中,并且要获取ToolContext到传来的requestId

java
package com.tianji.aigc.tools;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.tianji.aigc.config.ToolResultHolder;
import com.tianji.aigc.constants.Constant;
import com.tianji.aigc.tools.result.CourseInfo;
import com.tianji.api.client.course.CourseClient;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
@RequiredArgsConstructor
public class CourseTools {

    private final CourseClient courseClient;

    private static final String FIELD_NAME_FORMAT = "{}_{}";  // 提取格式字符串常量

    /**
     * 根据课程id查询课程信息
     *
     * @param courseId 课程id
     * @return 课程信息
     */
    @Tool(description = Constant.Tools.QUERY_COURSE_BY_ID)
    public CourseInfo queryCourseById(@ToolParam(description = Constant.ToolParams.COURSE_ID) Long courseId, ToolContext toolContext) {
        return Optional.ofNullable(courseId)
                .map(id -> CourseInfo.of(this.courseClient.baseInfo(id, true)))
                .map(courseInfo -> {
                    // 存储数据的字段名
                    String field = StrUtil.format(FIELD_NAME_FORMAT,
                            StrUtil.lowerFirst(CourseInfo.class.getSimpleName()),
                            courseInfo.getId());
                    // 存储的key
                    var requestId = Convert.toStr(toolContext.getContext().get(Constant.REQUEST_ID));
                    ToolResultHolder.put(requestId, field, courseInfo);
                    return courseInfo;
                })
                .orElse(null);
    }
}
1.5.3.4. 输出流中添加结果

com.tianji.aigc.service.impl.ChatServiceImpl#chat方法中,需要进行判断,如果当前请求中有工具生成的数据(也就是工具被调用了),就加到流中,反之不添加。

java
    // 输出结束的标记
    private static final ChatEventVO STOP_EVENT = ChatEventVO.builder().eventType(ChatEventTypeEnum.STOP.getValue()).build();

    @Override
    public Flux<ChatEventVO> chat(String question, String sessionId) {
        // 获取对话id
        var conversationId = ChatService.getConversationId(sessionId);
        // 大模型输出内容的缓存器,用于在输出中断后的数据存储
        StringBuilder outputBuilder = new StringBuilder();
        // 生成请求id
        var requestId = IdUtil.fastSimpleUUID();

        return this.chatClient.prompt()
                .system(promptSystem -> promptSystem
                        .text(this.systemPromptConfig.getChatSystemMessage().get()) // 设置系统提示语
                        .param("now", DateUtil.now()) // 设置当前时间的参数
                )
                .advisors(advisor -> advisor.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId))
                .toolContext(Map.of(Constant.REQUEST_ID, requestId)) //通过工具上下文传递参数
                .user(question)
                .stream()
                .chatResponse()
                .doFirst(() -> { //输出开始,标记正在输出
                    GENERATE_STATUS.put(sessionId, true);
                })
                .doOnComplete(() -> { //输出结束,清除标记
                    GENERATE_STATUS.remove(sessionId);
                })
                .doOnError(throwable -> GENERATE_STATUS.remove(sessionId)) // 错误时清除标记
                .doOnCancel(() -> {
                    // 当输出被取消时,保存输出的内容到历史记录中
                    this.saveStopHistoryRecord(conversationId, outputBuilder.toString());
                })
                // 输出过程中,判断是否正在输出,如果正在输出,则继续输出,否则结束输出
                .takeWhile(s -> Optional.ofNullable(GENERATE_STATUS.get(sessionId)).orElse(false))
                .map(chatResponse -> {
                    // 获取大模型的输出的内容
                    String text = chatResponse.getResult().getOutput().getText();
                    // 追加到输出内容中
                    outputBuilder.append(text);
                    // 封装响应对象
                    return ChatEventVO.builder()
                            .eventData(text)
                            .eventType(ChatEventTypeEnum.DATA.getValue())
                            .build();
                })
                .concatWith(Flux.defer(() -> {
                    // 通过请求id获取到参数列表,如果不为空,就将其追加到返回结果中
                    var map = ToolResultHolder.get(requestId);
                    if (CollUtil.isNotEmpty(map)) {
                        ToolResultHolder.remove(requestId); // 清除参数列表

                        // 响应给前端的参数数据
                        var chatEventVO = ChatEventVO.builder()
                                .eventData(map)
                                .eventType(ChatEventTypeEnum.PARAM.getValue())
                                .build();
                        return Flux.just(chatEventVO, STOP_EVENT);
                    }
                    return Flux.just(STOP_EVENT);
                }));
    }

1.5.4. 功能测试

输入内容进行测试:查询课程,Id为:1880529463279169537

img

可以看到,课程卡片的效果实现了。

1.6. 小结

在查询课程这个功能中,主要学习了:

  • 理解项目中原有的课程微服务,要知道怎么调用它

  • Tool的定义、注册方法,以及代码优化手段,比如:字符串要写到常量类中

  • 在课程卡片功能中的知识点

    • 给前端传递结构化的数据,不是大模型返回的
    • 通过toolContext传递数据到tool中
    • ToolResultHolder结构的设计
    • tool中的结果数据,存储到容器,再传回输出流中

2. 预下单

预下单,这个功能的实现与前面的课程查询实现思路基本一致。

2.1. 需求分析

在推荐课程的结果中,或者直接指定课程id,进行下单操作,同样要显示下单的卡片,点击【立即下单】即可完成下单功能:

img
img

2.2. 实现说明

  • 定义OrderTools工具,调用TradeClient中的prePlaceOrder方法进行预下单操作
  • 调用TradeClient中的prePlaceOrder方法时,需要在UserContext中设置当前用户id,这个用户id,需要通过ToolContext传递过来
  • 需要在SpringAIConfig中注册OrderTools工具到ChatClient
  • 工具名、参数描述,需要定义在常量类中的

响应给前端的数据结构:

json
{
    "eventData": {
        "prePlaceOrder": {
            "count": 1,
            "totalAmount": 199.0,
            "discountAmount": 6.0,
            "couponName": "叠加6券:【优惠6.0元】",
            "payAmount": 193.0,
            "courseIds": [
                "1589905661084430337"
            ],
            "orderId": "1904459544722419714",
            "couponId": "1901825343409999874"
        }
    },
    "eventType": 1003
}
img

结果对象不需要自己编写,直接使用给出的代码即可:

java
package com.tianji.aigc.tools.result;

import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.tianji.api.dto.promotion.CouponDiscountDTO;
import com.tianji.api.dto.promotion.OrderCourseDTO;
import com.tianji.api.dto.trade.OrderConfirmVO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.Optional;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PrePlaceOrder {

    @JsonPropertyDescription("课程数量")
    private int count;

    @JsonPropertyDescription("订单总金额")
    private double totalAmount;

    @JsonPropertyDescription("最大优惠金额")
    private double discountAmount;

    @JsonPropertyDescription("优惠券名称")
    private String couponName;

    @JsonPropertyDescription("实付金额")
    private double payAmount;

    @JsonPropertyDescription("课程id列表")
    private List<Long> courseIds;

    @JsonPropertyDescription("订单id")
    private Long orderId;

    @JsonPropertyDescription("优惠券id")
    private Long couponId;

    public static PrePlaceOrder of(OrderConfirmVO orderConfirmVO) {
        // 订单总金额
        double totalAmount = Optional.ofNullable(orderConfirmVO.getTotalAmount())
                .map(num -> num.doubleValue() / 100d)
                .map(num -> NumberUtil.round(num, 2).doubleValue())
                .orElse(0.0d);

        //最大优惠金额
        double discountAmount = Optional.ofNullable(CollUtil.getFirst(orderConfirmVO.getDiscounts()))
                .map(CouponDiscountDTO::getDiscountAmount)
                .map(num -> num.doubleValue() / 100d)
                .map(num -> NumberUtil.round(num, 2).doubleValue())
                .orElse(0.0d);

        //优惠券名称
        String couponName = Optional.ofNullable(CollUtil.getFirst(orderConfirmVO.getDiscounts()))
                .map(couponDiscountDTO -> {
                    List<String> rules = couponDiscountDTO.getRules();
                    int size = CollUtil.size(rules);
                    return size >= 2
                            ? StrUtil.format("叠加{}券:【优惠{}元】", size, discountAmount)
                            : StrUtil.format("单券:【{}】", CollUtil.getFirst(rules));
                })
                .orElse("");

        // 实付金额,保留小数点后两位
        double payAmount = NumberUtil.round(totalAmount - discountAmount, 2).doubleValue();

        // 课程id列表
        List<Long> courseIds = CollStreamUtil.toList(orderConfirmVO.getCourses(), OrderCourseDTO::getId);

        // 优惠券id
        Long couponId = Optional.ofNullable(CollUtil.getFirst(orderConfirmVO.getDiscounts()))
                .map(CouponDiscountDTO::getIds)
                .map(CollUtil::getFirst)
                .orElse(null);

        return PrePlaceOrder.builder()
                .count(CollUtil.size(orderConfirmVO.getCourses()))
                .totalAmount(totalAmount)
                .discountAmount(discountAmount)
                .couponName(couponName)
                .payAmount(payAmount)
                .courseIds(courseIds)
                .orderId(orderConfirmVO.getOrderId())
                .couponId(couponId)
                .build();
    }
}

2.3. 代码实现

2.3.1. OrderTools

java
package com.tianji.aigc.tools;

import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.tianji.aigc.config.ToolResultHolder;
import com.tianji.aigc.constants.Constant;
import com.tianji.aigc.tools.result.PrePlaceOrder;
import com.tianji.api.client.trade.TradeClient;
import com.tianji.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Optional;

@Component
@RequiredArgsConstructor
public class OrderTools {

    private final TradeClient tradeClient;

    @Tool(description = Constant.Tools.PRE_PLACE_ORDER)
    public PrePlaceOrder prePlaceOrder(@ToolParam(description = Constant.ToolParams.COURSE_IDS) List<Number> ids,
                                       ToolContext toolContext) {
        // 设置用户ID,用于身份验证,否在在Feign调用时会出现401错误
        UserContext.setUser(Convert.toLong(toolContext.getContext().get(Constant.USER_ID)));
        // 大模型传入的ids,可能是int类型,所以转化为long类型,再调用Feign
        var orderConfirmVO = this.tradeClient.prePlaceOrder(CollStreamUtil.toList(ids, Number::longValue));

        return Optional.ofNullable(orderConfirmVO)
                .map(PrePlaceOrder::of)
                .map(prePlaceOrder -> {
                    var field = StrUtil.lowerFirst(prePlaceOrder.getClass().getSimpleName());
                    var requestId = Convert.toStr(toolContext.getContext().get(Constant.REQUEST_ID));
                    ToolResultHolder.put(requestId, field, prePlaceOrder);
                    return prePlaceOrder;
                })
                .orElse(null);
    }
}

2.3.2. Constant

java
package com.tianji.aigc.constants;

public interface Constant {

    String USER_ID = "userId";
    String REQUEST_ID = "requestId";

    interface Tools {
        String QUERY_COURSE_BY_ID = "根据课程id查询课程详细信息";
        String PRE_PLACE_ORDER = "购买课程预下单操作";
    }

    interface ToolParams {
        String COURSE_ID = "课程id";
        String COURSE_IDS = "课程id列表";
    }

}

2.3.3. SpringAIConfig

java
    /**
     * 配置 ChatClient
     */
    @Bean
    public ChatClient chatClient(ChatClient.Builder chatClientBuilder,
                                 Advisor loggerAdvisor,
                                 Advisor messageChatMemoryAdvisor,
                                 CourseTools courseTools, // 课程工具
                                 OrderTools orderTools // 预下单工具
    ) {  // 日志记录器
        return chatClientBuilder
                .defaultAdvisors(loggerAdvisor, messageChatMemoryAdvisor) //添加 Advisor 功能增强
                .defaultTools(courseTools, orderTools) //添加默认工具
                .build();
    }

2.3.4. ChatServiceImpl

java
    @Override
    public Flux<ChatEventVO> chat(String question, String sessionId) {
        // 获取对话id
        var conversationId = ChatService.getConversationId(sessionId);
        // 大模型输出内容的缓存器,用于在输出中断后的数据存储
        StringBuilder outputBuilder = new StringBuilder();
        // 生成请求id
        var requestId = IdUtil.fastSimpleUUID();
        // 获取用户id
        var userId = UserContext.getUser();

        return this.chatClient.prompt()
                .system(promptSystem -> promptSystem
                        .text(this.systemPromptConfig.getChatSystemMessage().get()) // 设置系统提示语
                        .param("now", DateUtil.now()) // 设置当前时间的参数
                )
                .advisors(advisor -> advisor.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId))
                .toolContext(Map.of(Constant.REQUEST_ID, requestId, Constant.USER_ID, userId)) //通过工具上下文传递参数
                .user(question)
                .stream()
                .chatResponse()
                .doFirst(() -> { //输出开始,标记正在输出
                    GENERATE_STATUS.put(sessionId, true);
                })
                .doOnComplete(() -> { //输出结束,清除标记
                    GENERATE_STATUS.remove(sessionId);
                })
                .doOnError(throwable -> GENERATE_STATUS.remove(sessionId)) // 错误时清除标记
                .doOnCancel(() -> {
                    // 当输出被取消时,保存输出的内容到历史记录中
                    this.saveStopHistoryRecord(conversationId, outputBuilder.toString());
                })
                // 输出过程中,判断是否正在输出,如果正在输出,则继续输出,否则结束输出
                .takeWhile(s -> Optional.ofNullable(GENERATE_STATUS.get(sessionId)).orElse(false))
                .map(chatResponse -> {
                    // 获取大模型的输出的内容
                    String text = chatResponse.getResult().getOutput().getText();
                    // 追加到输出内容中
                    outputBuilder.append(text);
                    // 封装响应对象
                    return ChatEventVO.builder()
                            .eventData(text)
                            .eventType(ChatEventTypeEnum.DATA.getValue())
                            .build();
                })
                .concatWith(Flux.defer(() -> {
                    // 通过请求id获取到参数列表,如果不为空,就将其追加到返回结果中
                    var map = ToolResultHolder.get(requestId);
                    if (CollUtil.isNotEmpty(map)) {
                        ToolResultHolder.remove(requestId); // 清除参数列表

                        // 响应给前端的参数数据
                        ChatEventVO chatEventVO = ChatEventVO.builder()
                                .eventData(map)
                                .eventType(ChatEventTypeEnum.PARAM.getValue())
                                .build();
                        return Flux.just(chatEventVO, STOP_EVENT);
                    }
                    return Flux.just(STOP_EVENT);
                }));
    }

3. bug解决

3.1. bug描述

课程查询和预下单功能,给前端返回的数据中,包含了eventType1003的数据,这个叫作额外数据,给前端提供,前端是不会显示到页面的,正常对话是没问题的,但是,数据存储到Redis是没有保存进去的,如下:

img
img

所以,我们就需要来解决这个bug,在保存聊天记录到redis的同时,也将params保存进去。

3.2. bug解决

这个bug的解决思路就是,在RedisChatMemoryRepository中保存数据时,获取到ToolResultHolder中的数据,将数据保存到params中即可。

但是,ToolResultHolder中的数据,是与requestId关联的,requestId是我们自己生成的,在RedisChatMemory中是没有的,所以,这个问题的关键就是如何获取到requestId了,只要有了requestId就可以获取到数据,进行保存了。

如何传递requestId

其实,同样也是可以借助于ToolResultHolder来完成,我们可以把ToolResultHolder看作是一个通用的容器,可以放Tool的结果,也可以放其他的内容,只要及时的删除即可。

有了这个思路,问题就好解决了,解决代码如下:

java
    @Override
    public Flux<ChatEventVO> chat(String question, String sessionId) {
        // 获取对话id
        var conversationId = ChatService.getConversationId(sessionId);
        // 大模型输出内容的缓存器,用于在输出中断后的数据存储
        StringBuilder outputBuilder = new StringBuilder();
        // 生成请求id
        var requestId = IdUtil.fastSimpleUUID();
        // 获取用户id
        var userId = UserContext.getUser();

        return this.chatClient.prompt()
                .system(promptSystem -> promptSystem
                        .text(this.systemPromptConfig.getChatSystemMessage().get()) // 设置系统提示语
                        .param("now", DateUtil.now()) // 设置当前时间的参数
                )
                .advisors(advisor -> advisor.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId))
                .toolContext(MapUtil.<String, Object>builder() // 设置tool列表
                        .put(Constant.REQUEST_ID, requestId) // 设置请求id参数
                        .put(Constant.USER_ID, userId) // 设置用户id参数
                        .build()
                )
                .user(question)
                .stream()
                .chatResponse()
                .doFirst(() -> { //输出开始,标记正在输出
                    GENERATE_STATUS.put(sessionId, true);
                })
                .doOnComplete(() -> { //输出结束,清除标记
                    GENERATE_STATUS.remove(sessionId);
                })
                .doOnError(throwable -> GENERATE_STATUS.remove(sessionId)) // 错误时清除标记
                .doOnCancel(() -> {
                    // 当输出被取消时,保存输出的内容到历史记录中
                    this.saveStopHistoryRecord(conversationId, outputBuilder.toString());
                })
                // 输出过程中,判断是否正在输出,如果正在输出,则继续输出,否则结束输出
                .takeWhile(s -> Optional.ofNullable(GENERATE_STATUS.get(sessionId)).orElse(false))
                .map(chatResponse -> {
                    // 对于响应结果进行处理,如果是最后一条数据,就把此次消息id放到内存中
                    // 主要用于存储消息数据到 redis中,可以根据消息di获取的请求id,再通过请求id就可以获取到参数列表了
                    // 从而解决,在历史聊天记录中没有外参数的问题
                    var finishReason = chatResponse.getResult().getMetadata().getFinishReason();
                    if (StrUtil.equals(Constant.STOP, finishReason)) {
                        var messageId = response.getMetadata().getId();
                        ToolResultHolder.put(messageId, Constant.REQUEST_ID, requestId);
                    }
                    // 获取大模型的输出的内容
                    String text = chatResponse.getResult().getOutput().getText();
                    // 追加到输出内容中
                    outputBuilder.append(text);
                    // 封装响应对象
                    return ChatEventVO.builder()
                            .eventData(text)
                            .eventType(ChatEventTypeEnum.DATA.getValue())
                            .build();
                })
                .concatWith(Flux.defer(() -> {
                    // 通过请求id获取到参数列表,如果不为空,就将其追加到返回结果中
                    var map = ToolResultHolder.get(requestId);
                    if (CollUtil.isNotEmpty(map)) {
                        ToolResultHolder.remove(requestId); // 清除参数列表

                        // 响应给前端的参数数据
                        ChatEventVO chatEventVO = ChatEventVO.builder()
                                .eventData(map)
                                .eventType(ChatEventTypeEnum.PARAM.getValue())
                                .build();
                        return Flux.just(chatEventVO, STOP_EVENT);
                    }
                    return Flux.just(STOP_EVENT);
                }));
    }

常量类中增加常量值:

java
package com.tianji.aigc.constants;

public interface Constant {

    String USER_ID = "userId";
    String REQUEST_ID = "requestId";
    String STOP = "STOP";
    String ID = "id";

    interface Tools {
        String QUERY_COURSE_BY_ID = "根据课程id查询课程详细信息";
        String PRE_PLACE_ORDER = "购买课程预下单操作";
    }

    interface ToolParams {
        String COURSE_ID = "课程id";
        String COURSE_IDS = "课程id列表";
    }

}

MessageUtil中增加params参数的处理:

java
    /**
     * 将Message对象转换为Redis存储格式的JSON字符串
     *
     * @param message 需要转换的原始消息对象
     * @return 符合Redis存储规范的JSON字符串
     */
    public static String toJson(Message message) {
        var redisMessage = BeanUtil.toBean(message, RedisMessage.class);
        // 设置消息内容
        redisMessage.setTextContent(message.getText());
        if (message instanceof AssistantMessage assistantMessage) {
            redisMessage.setToolCalls(assistantMessage.getToolCalls());

            // 通过 messageId 获取 requestId,再通过 requestId 获取参数列表,如果有,就存储起来
            // 最后,删除 messageId 对应的数据
            var messageId = Convert.toStr(assistantMessage.getMetadata().get(Constant.ID));
            var requestId = Convert.toStr(ToolResultHolder.get(messageId, Constant.REQUEST_ID));
            var params = ToolResultHolder.get(requestId);
            if (ObjectUtil.isNotEmpty(params)) {
                myMessage.setParams(params);
            }
            ToolResultHolder.remove(messageId);
        }

        if (message instanceof ToolResponseMessage toolResponseMessage) {
            redisMessage.setToolResponses(toolResponseMessage.getResponses());
        }

        return JSONUtil.toJsonStr(redisMessage);
    }

在反序列化Message对象时,无法给AssistantMessage设置params属性,所以需要继承AssistantMessage来扩展params属性:

java
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;

@Setter
@Getter
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;
    }

}
java
    /**
     * 将存储的JSON字符串反序列化为对应的Message对象
     *
     * @param json JSON格式消息数据
     * @return 对应类型的Message对象
     * @throws RuntimeException 当无法识别的消息类型时抛出异常
     */
    public static Message toMessage(String json) {
        var myMessage = JSONUtil.toBean(json, MyMessage.class);
        var messageType = MessageType.valueOf(myMessage.getMessageType());
        switch (messageType) {
            case SYSTEM -> {
                return new SystemMessage(myMessage.getTextContent());
            }
            case USER -> {
                return new UserMessage(myMessage.getTextContent(), myMessage.getMedia(), myMessage.getMetadata());
            }
            case ASSISTANT -> {
                return new MyAssistantMessage(myMessage.getTextContent(), myMessage.getMetadata(), myMessage.getToolCalls(), myMessage.getMedia(), myMessage.getParams());
                // return new AssistantMessage(myMessage.getTextContent(), myMessage.getMetadata(), myMessage.getToolCalls());
            }
            case TOOL -> {
                return new ToolResponseMessage(myMessage.getToolResponses(), myMessage.getMetadata());
            }
        }

        throw new RuntimeException("Message data conversion failed.");
    }

根据sessionId查询对话详情时,也需要对params参数做处理:

java
    @Override
    public List<MessageVO> queryBySessionId(String sessionId) {
        // 根据会话ID获取对话ID
        String conversationId = ChatService.getConversationId(sessionId);
        // 从Redis中获取历史消息
        List<Message> messageList = this.chatMemory.get(conversationId, HISTORY_MESSAGE_COUNT);
        // 过滤并转换消息列表
        return StreamUtil.of(messageList)
                // 过滤掉非用户消息和助手消息
                .filter(message -> message.getMessageType() == MessageType.ASSISTANT || message.getMessageType() == MessageType.USER)
                // 转换为MessageVO对象
                .map(message -> {
                    if (message instanceof MyAssistantMessage) {
                        return MessageVO.builder()
                                .content(message.getText())
                                .type(MessageTypeEnum.valueOf(message.getMessageType().name()))
                                .params(((MyAssistantMessage) message).getParams())
                                .build();
                    }
                    return MessageVO.builder()
                            .content(message.getText())
                            .type(MessageTypeEnum.valueOf(message.getMessageType().name()))
                            .build();
                })
                .toList();
    }

3.3. 测试

img
img

可以看到,问题已经解决了。

4. 知识库

4.1. 为什么要用知识库

之所以要使用知识库,是因为我们在做课程推荐时,需要先从知识库匹配到课程,再通过课程id查询课程信息进行推荐,如果没有知识库,就无法根据学生的需求进行推荐,所以必须要用到知识库了。

实现流程如下:

img

前面我们学习过SpringAI的RAG,前面是使用内存向量库,实际上,在项目中是不可能用内存存储的,所以,在我们项目中,选择使用ElasticSearch作为向量库存储数据。

https://docs.spring.io/spring-ai/reference/1.0/api/vectordbs/elasticsearch.html

4.2. RAG基本原理

下面这张图是来源于Spring AI官网文档,说明了RAG整体实现流程。👉 RAG :: Spring AI Reference

img

📚 这张图展示了文档摄取(ETL)和检索增强生成(RAG)两个核心流程,具体可分为以下两部分:

  1. 文档摄取(ETL)流程(离线处理)

    a. 数据读取:从数据源(如文档、数据库)读取原始文档。

    b. 分割文档:通过分割模块(<<Split>>)将文档切分为更小的数据块(chunks)。

    c. 转换数据:通过转换模块(Transformer)处理数据块(如向量化、添加元数据)。

    d. 写入存储:将处理后的数据块写入向量数据库(Vector Store),为后续检索做准备。

    e. 核心目标:将非结构化文档转化为结构化、可检索的向量数据。

  2. 检索增强生成(RAG)流程(实时处理)

    a. 用户查询:接收用户提问(Chat Request)。

    b. 检索相关块:从向量库中检索与查询最相关(相似度高)的数据块(<<Retrieve>>)。

    c. 增强查询:将检索到的上下文信息(Context information)与用户问题结合,生成增强后的提示(<<Augment>>)。

    d. 生成响应:通过聊天模型(Chat Model)生成回答。

    e. 核心目标:通过外部知识库提升生成结果的准确性,解决了大模型信息缺失或滞后的问题。

4.3. 部署ES

在我们的虚拟机中,已经有一个ES服务了,这个ES是用作课程搜索的,并且版本也比较低,所以我们不直接使用这个,而是重新搭建一个ES服务,用来做向量库。

bash
#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/

img

说明,ES已经部署好了。

4.4. 项目集成ES

在tj-aigc项目中,导入SpringAI集成ES的依赖:

xml
<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文件中进行配置:

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  #向量维度维度

重启启动服务。

4.5. 部署kibana

部署kibana的目的是用于查看ES中是否已经创建了索引库。

bash
#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

img

spring-ai-document-index这个是默认的索引库的名称,在官方文档中已经有说明: 👉 Elasticsearch

img

索引库相关操作:

bash
# 查询索引库
GET /spring-ai-document-index

# 搜索数据
GET /spring-ai-document-index/_search

#删除索引库
DELETE /spring-ai-document-index

4.6. 写入数据

4.6.1. 准备数据

在知识库中,需要写入一些数据,以供推荐课程使用。具体数据查看:知识库-课程数据

4.6.2. 编写代码

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

}

测试:

img

验证是否写入成功:

img

可以看到已经有数据写入成功了。

接下来就可以把全部的数据导进去了。

4.7. 集成到chatClient

java
    private final VectorStore vectorStore;

    @Override
    public Flux<ChatEventVO> chat(String question, String sessionId) {
        // 获取对话id
        var conversationId = ChatService.getConversationId(sessionId);
        // 大模型输出内容的缓存器,用于在输出中断后的数据存储
        StringBuilder outputBuilder = new StringBuilder();
        // 生成请求id
        var requestId = IdUtil.fastSimpleUUID();
        // 获取用户id
        var userId = UserContext.getUser();

        // 创建RAG增强
        var qaAdvisor = QuestionAnswerAdvisor.builder(this.vectorStore)
                .searchRequest(SearchRequest.builder().similarityThreshold(0.6d).topK(6).build())
                .build();

        return this.chatClient.prompt()
                .system(promptSystem -> promptSystem
                        .text(this.systemPromptConfig.getChatSystemMessage().get()) // 设置系统提示语
                        .param("now", DateUtil.now()) // 设置当前时间的参数
                )
                .advisors(advisor -> advisor
                        // 设置RAG增强
                        .advisors(qaAdvisor)
                        .param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)
                )
                .toolContext(MapUtil.<String, Object>builder() // 设置tool列表
                        .put(Constant.REQUEST_ID, requestId) // 设置请求id参数
                        .put(Constant.USER_ID, userId) // 设置用户id参数
                        .build()
                )
                .user(question)
                .stream()
                .chatResponse()
                .doFirst(() -> { //输出开始,标记正在输出
                    GENERATE_STATUS.put(sessionId, true);
                })
                .doOnComplete(() -> { //输出结束,清除标记
                    GENERATE_STATUS.remove(sessionId);
                })
                .doOnError(throwable -> GENERATE_STATUS.remove(sessionId)) // 错误时清除标记
                .doOnCancel(() -> {
                    // 当输出被取消时,保存输出的内容到历史记录中
                    this.saveStopHistoryRecord(conversationId, outputBuilder.toString());
                })
                // 输出过程中,判断是否正在输出,如果正在输出,则继续输出,否则结束输出
                .takeWhile(s -> Optional.ofNullable(GENERATE_STATUS.get(sessionId)).orElse(false))
                .map(chatResponse -> {
                    // 对于响应结果进行处理,如果是最后一条数据,就把此次消息id放到内存中
                    // 主要用于存储消息数据到 redis中,可以根据消息di获取的请求id,再通过请求id就可以获取到参数列表了
                    // 从而解决,在历史聊天记录中没有外参数的问题
                    var finishReason = chatResponse.getResult().getMetadata().getFinishReason();
                    if (StrUtil.equals(Constant.STOP, finishReason)) {
                        var messageId = ((ChatResponseMetadata) ReflectUtil.getFieldValue(chatResponse, Constant.Chats.CHAT_RESPONSE_METADATA)).getId();
                        ToolResultHolder.put(messageId, Constant.REQUEST_ID, requestId);
                    }
                    // 获取大模型的输出的内容
                    String text = chatResponse.getResult().getOutput().getText();
                    // 追加到输出内容中
                    outputBuilder.append(text);
                    // 封装响应对象
                    return ChatEventVO.builder()
                            .eventData(text)
                            .eventType(ChatEventTypeEnum.DATA.getValue())
                            .build();
                })
                .concatWith(Flux.defer(() -> {
                    // 通过请求id获取到参数列表,如果不为空,就将其追加到返回结果中
                    var map = ToolResultHolder.get(requestId);
                    if (CollUtil.isNotEmpty(map)) {
                        ToolResultHolder.remove(requestId); // 清除参数列表

                        // 响应给前端的参数数据
                        ChatEventVO chatEventVO = ChatEventVO.builder()
                                .eventData(map)
                                .eventType(ChatEventTypeEnum.PARAM.getValue())
                                .build();
                        return Flux.just(chatEventVO, STOP_EVENT);
                    }
                    return Flux.just(STOP_EVENT);
                }));
    }

4.8. 测试课程推荐

img
img
img
img
img

4.9. 练习1

4.9.1. 说明

前面我们只实现了向量库的新增操作,除了新增操作外,我们还可以实现如下的几个接口:

img

实现了这几个接口后,我们就不需要在kibana中操作了。这几个作为练习部分由学员自主完成。

4.9.2. 参考代码

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

}

4.10. 练习2

  • 把所有提供的测试数据导入到向量库中,进行推荐课程的测试。知识库-课程数据

  • 按照SpringAI官方文档,实现使用Redis作为向量库存储,替换课程中的ES部分。👉 Redis :: Spring AI Reference

    • 实现提示:使用Redis实现向量库,需要使用到redis-stack

      📚 Redis Stack 是 Redis 官方推出的一个扩展工具集,基于 Redis 构建,整合了多个高性能模块和工具,旨在简化开发并增强 Redis 的核心功能。它特别适合需要多模型数据处理的场景,如搜索、时间序列、图数据等。


Q.E.D.
天机学堂AI助手智能体 - 多智能体协同工作
天机学堂AI助手智能体 - 基本对话与课程咨询