8. 综合演练聊天面板后端实现-SpringAI实战

8 篇文章 0 订阅

后端实现

后端实现会话和消息记录必然要先从建表开始,然后编写增删改查,最后实现一些功能。

会话

  • 会话保存接口
    • 会话保存dto
  • 会话列表接口
    • 获取当前登录用户id作为查询条件
    • 一对多关联获取会话内的消息
  • 会话删除接口
    • 根据会话id列表批量删除
    • 删除会话时,会话内的消息也一并级联删除
  • 会话id查询接口
    • 一对多关联获取会话内的消息

消息

  • 消息发送接口
    • 消息保存dto(接收前端发送的消息)
    • 消息实体内容设计,支持图片/语音/文字
    • MessageChatMemoryAdvisor历史消息增强和消息自动保存,调用AiMessageChatMemoryget方法获取会话的历史消息和add方法保存消息。
    • 使用ChatClient填写用户发送的消息(可能包含图片/语言),填写MessageChatMemoryAdvisor。最后发起流式请求,获取AI回复的消息。
  • 历史消息实现
    • add方法,保存用户发送的消息和AI回复的消息。因为需要填写创建人id,创建人id是在请求里面存储的。保存的时候是在异步的线程里面,要模拟请求环境才能插入数据到数据库。
    • get方法,获取会话历史消息
    • clear方法,清空会话的历史消息
    • 消息转换器,将消息实体转换为SpringAI的Message,将Message转换为消息实体。互相转换。

实体设计

消息实体设计

消息表

create table ai_message
(
    id            varchar(36) not null
        primary key,
    created_time  datetime(6) not null,
    edited_time   datetime(6) not null,
    creator_id    varchar(32) not null,
    editor_id     varchar(32) not null,
    type          varchar(32) not null comment '消息类型(用户/ 助手/ 系统)',
    text_content  text        not null comment '消息内容',
    medias        json        null comment '媒体内容如图片链接、语音链接',
    ai_session_id varchar(32) not null comment '会话id'
);

消息实体类

/**
 * 历史消息
 */ 
@Entity
public interface AiMessage extends BaseEntity {

    /**
     * 消息类型(用户/助手/系统)
     */
    MessageType type();

    /**
     * 消息内容
     */
    String textContent();

    @Serialized
    @Null
    List<Media> medias();

    @IdView
    String sessionId();

    /**
     * 会话
     */
    @ManyToOne
    @JoinColumn(name = "ai_session_id")
    @OnDissociate(DissociateAction.DELETE)
    AiSession session();

    @Data
    @AllArgsConstructor
    class Media {
        public String type;
        public String data;
    }
}

会话实体设计

会话表

create table ai_session
(
    id           varchar(36) not null
        primary key,
    created_time datetime(6) not null,
    edited_time  datetime(6) not null,
    creator_id   varchar(32) not null,
    editor_id    varchar(32) not null,
    name         varchar(32) not null comment '会话名称'
);

会话实体类

/**
 * 会话
 */
@Entity
public interface AiSession extends BaseEntity {

    /**
     * 会话名称
     */
    String name();

    /**
     * 一对多关联消息,按创建时间升序
     */

    @OneToMany(mappedBy = "session", orderedProps = @OrderedProp(value = "createdTime"))
    List<AiMessage> messages();
}

AiMessageChatMemory(数据库消息记录)

AiMessageChatMemory实现了ChatMemory接口,用于保存聊天记录到数据库。

@Service
@AllArgsConstructor
public class AiMessageChatMemory implements ChatMemory {
    private final AiMessageRepository messageRepository;
    private final AiSessionRepository sessionRepository;

    /**
     * 将用户发送的消息和AI回复的消息保存到数据库
     *
     * @param conversationId 会话id
     * @param messages       org.springframework.ai.chat.messages.Message 用户发送的消息和AI回复的消息
     */
    @Override
    public void add(String conversationId, List<Message> messages) {
        List<AiMessage> aiMessageList = messages
                .stream()
                .map(message -> toAiMessage(message, conversationId))
                .toList();
        // 当前的现场处于异步状态,非没有servletRequest无法获取当前登录的用户信息。
        // 模拟请求
        RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest()));
        // 设置当前的用id
        String userId = sessionRepository.findById(conversationId, AiSessionFetcher.$.creator()).orElseThrow().creator().id();
        StpUtil.switchTo(userId);
        // 保存到数据库,这样创建人和编辑人才有数据
        messageRepository.saveEntities(aiMessageList);
    }

    /**
     * 查询会话内的消息最新n条历史记录
     *
     * @param conversationId 会话id
     * @param lastN          最近n条
     * @return org.springframework.ai.chat.messages.Message格式的消息
     */
    @Override
    public List<Message> get(String conversationId, int lastN) {
        return messageRepository
                // 查询会话内的最新n条消息
                .findBySessionId(conversationId, lastN)
                .stream()
                // 转成Message对象
                .map(AiMessageChatMemory::toSpringAiMessage)
                .toList();
    }

    /**
     * 清除会话内的消息
     *
     * @param conversationId 会话id
     */
    @Override
    public void clear(String conversationId) {
        messageRepository.deleteBySessionId(conversationId);
    }
    /**
     * 忽略...
     */
}

可以把AiMessageChatMemory注入到MessageChatMemoryAdvisor中。

MessageChatMemoryAdvisor的作用有下面三个

  1. 用户消息发送给大模型之前,获取会话内的最新n条记录和用户的消息拼接在一起,形成历史消息记录。
  2. 拼接完历史消息之后把用户发送的消息保存到数据库。
  3. 大模型回复完消息之后,将回复的消息保存到数据库。
// 注入chatMemory
private final AiMessageChatMemory chatMemory;
// 传入chatMemory,会话id,查询最近n条历史消息
var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, input.getSessionId(), 10);

消息发送接口

通常来说发送消息给大模型只需要填写用户的消息就行,但是要支持历史会话需要获取历史消息。这个功能可以使用MessageChatMemoryAdvisor来实现。

@RequestMapping("message")
@RestController
@AllArgsConstructor
@Slf4j
public class AiMessageController {
    private final AiMessageChatMemory chatMemory;
    private final DashScopeAiChatModel dashScopeAiChatModel;

    /**
     *
     * @param input 消息包含文本信息,会话id,多媒体信息(图片语言)。参考src/main/dto/AiMessage.dto
     * @return SSE流
     */
    @PostMapping(value = "chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> chatStreamWithHistory(@RequestBody AiMessageInput input) {
        // MessageChatMemoryAdvisor的三个参数解释。
        // 1. 如果需要存储会话和消息到数据库,自己可以实现ChatMemory接口,这里使用自己实现的AiMessageChatMemory,数据库存储。
        // 2. 传入会话id,MessageChatMemoryAdvisor会根据会话id去查找消息。
        // 3. 只需要携带最近10条消息
        var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, input.getSessionId(), 10);
        return ChatClient.create(dashScopeAiChatModel).prompt()
                .user(promptUserSpec -> {
                    // AiMessageInput转成Message
                    Message message = AiMessageChatMemory.toSpringAiMessage(input.toEntity());
                    if (!CollectionUtils.isEmpty(message.getMedia())) {
                        // 用户发送的图片/语言
                        Media[] medias = new Media[message.getMedia().size()];
                        promptUserSpec.media(message.getMedia().toArray(medias));
                    }
                    // 用户发送的文本
                    promptUserSpec.text(message.getContent());
                })
                // MessageChatMemoryAdvisor会在消息发送给大模型之前,从ChatMemory中获取会话的历史消息,然后一起发送给大模型。
                .advisors(messageChatMemoryAdvisor)
                .stream()
                .content()
                .map(chatResponse -> ServerSentEvent.builder(chatResponse)
                        // 和前端监听的事件相对应
                        .event("message")
                        .build());
    }
}
  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值