个人技术总结——AI辅助创作、流式问答的实现

222100222林致超 2024-06-05 00:58:29

技术概述

ChatUI

ChatUI是阿里巴巴开源的一套服务于对话领域的设计和开发体系,助力智能对话机器人的搭建。

特性如下:

  • 😎 最佳实践:基于阿里小蜜业务积累和打磨的对话式交互最佳实践
  • 🛡 TypeScript:使用 TypeScript 开发,提供完整的类型定义文件
  • 📱 响应式:响应式布局,在无线和 PC 端都可以友好展现
  • 障碍:支持无障碍,已通过深圳市无障碍研究会的认证
  • 🎨 主题:支持灵活的样式定制,以满足业务和品牌上多样化的视觉需求
  • 🌍 国际化:支持多语言和本土化特性

SSE

SSE(Server-Sent Events)是一种用于实现服务器主动向客户端推送数据的技术,也被称为“事件流”(Event Stream)。它基于 HTTP 协议,利用了其长连接特性,在客户端与服务器之间建立一条持久化连接,并通过这条连接实现服务器向客户端的实时数据推送。

技术详述

如何使用

Chat UI文档:https://chatui.io/sdk/getting-started

SSE API: https://developer.mozilla.org/zh-CN/docs/Web/API/EventSource

流程图

img

核心代码

如图所示,本项目使用了ChatUI + SSE实现了AI辅助创作的功能,核心代码框架如下:

img

当用户通过用户界面输入发送文本消息的时候,会触发send函数,send函数返回一个Promise,在这个Promise的状态确定之前,ChatUI会呈现出滚动的句号等待状态。

img

在Promise中,我们通过fetchEventSource(封装了原生SSE操作),向我们的AI后端发送请求,建立SSE连接。

连接建立成功后,我们会收到SSE事件源发送的流式响应,当收到第一条消息时,我们就可以resolve当前这个Promise了,此时Chat UI就会停止当前的等待状态,我们开始处理流式消息的拼接。

具体拼接的实现:

由于SSE返回的数据是一段一段的,我们通过DOM操作拿到当前最后一个回复的消息元素,在每次onmessage出发的时候,给这个DOM的innerHTML属性重新赋值,并且讲滚动条设置到最底部,在视觉上就会呈现出文字不断出现的拼接效果了。

onmessage: (event) => {
  // ctx.scrollToEnd()
  let jsonData = JSON.parse(event.data)
  if (jsonData.data) {
    resStr += jsonData.data
  }
  if (firstReceive) {
    resolve(' ')
    firstReceive = false
    return
  }
  // resolve(resStr)
  if (interval) return
  interval = setInterval(() => {
    let lastMsg = Array.from(
      document.querySelectorAll('.Knowledge-content'),
    ).pop()
    console.log('lastMsg', lastMsg)
    lastMsg.innerHTML = resStr
    var div = document.querySelector('.PullToRefresh')
    div.scrollTop = div.scrollHeight - div.clientHeight
    clearInterval(interval)
    interval = null
  }, SPEED)
},

遇到的问题与解决方案

问题一

ChatUI的文档十分简单,没有提供一个流式问答的完整案例,我们项目需要的效果需要较高的定制性,缺乏详细文档,实现流式问答困难

解决:

好在项目是开源的,直接阅读相关源码,结合Github Issue,来找到答案。(文档有时候不准确,代码永远不会骗你)

img

问题二

接口响应速度太快,导致onmessage频繁触发,频繁重新赋值,导致DOM渲染卡顿,视觉上出现空白的状态

解决:

我们加入一个定时器,来控制重新赋值的速度

  if (interval) return
  interval = setInterval(() => {
    let lastMsg = Array.from(
      document.querySelectorAll('.Knowledge-content'),
    ).pop()
    console.log('lastMsg', lastMsg)
    lastMsg.innerHTML = resStr
    var div = document.querySelector('.PullToRefresh')
    div.scrollTop = div.scrollHeight - div.clientHeight
    clearInterval(interval)
    interval = null
  }, SPEED)

问题三

如何与编辑器联动,实现选中提问,提问后一键引入到编辑器?

解决:

由于组件化的设计,我们需要在redux仓库中定义一个公共的状态selectedText,通过编写编辑器插件实现选中扩写、改写事件的触发,再在触发的事件中使用ChatUI声明式的发送消息

img

以扩写为例的核心代码:

  /* 监听是否有选中的内容(点击扩写会改变) */
  useEffect(() => {
    if (selectedText) {
      expendFlag = true
      ctx.postMessage({
        type: 'text',
        content: {
          text: selectedText,
        },
        position: 'right',
      })
    }
  }, [selectedText])

在根据ChatUI Pro提供的自定义卡片功能,实现一个特定回复类型卡片组件并引入,便可以完成导入编辑器

  /* 生成了需要添加的内容后,将其插入卡片 */
  useEffect(() => {
    if (!contentToAdd) return
    ctx.appendMessage({
      type: 'card',
      content: {
        code: 'adaptable-action-card', // 卡片code
        data: {
          title: selectedText.slice(0, 2) + '结果',
          content: contentToAdd,
          actionList: [
            {
              text: '应用',
              action: '',
              style: 'default',
              param: {
                /* url: 'https://www.taobao.com', */
              },
            },
            {
              text: '重新生成',
              action: 'sendText',
              style: 'default',
              param: {
                text: '重新生成中',
              },
            },
          ],
        }, // 卡片数据
      },
    })
  }, [contentToAdd])

绑定应用按钮的点击事件,完成引入功能

  /* 事件委托,绑定点击应用的事件 */
  useEffect(() => {
    const messageListDOM = document.querySelector('.MessageList')
    console.log('messageListDOM', messageListDOM)
    // 定义事件处理函数
    function handleClick(event) {
      if (
        event.target.classList.contains('Btn') &&
        event.target.textContent == '应用'
      ) {
        console.log('dispatch setEditorAddedContent', contentToAdd)
        /* 派发,告诉Editor添加 */
        dispatch(setEditorAddedContent({ contentToAdd }))
      }
    }

    // 在父元素上添加事件监听器,实现事件委托
    messageListDOM.addEventListener('click', handleClick)

    // 返回一个清理函数,在组件销毁时移除事件监听器
    return () => {
      messageListDOM.removeEventListener('click', handleClick)
    }
  }, [contentToAdd])

总结

最后还是顺利地用ChatUI和SSE完成了这个AI辅助创作的效果,当初使用Chat UI也是因为没有时间从头到尾再实现一个自己的Chat界面,还要支持大量的事件和组件,初看他的文档感觉功能应有尽有,可以满足我们项目的需求,但是实现的时候发现还是存在很多问题,好在最后都一一解决了。

刚刚点进官网发现Chat UI Pro又升级成了Chat SDK,可以直接接入阿里云智能客服,只需要通过简单的配置就能搭建出对话机器人;同时它强大易扩展,通过丰富的接口和自定义卡片满足各种定制化需求。

(为什么不早点出来。。。

参考

ChatUI Github仓库

SSE MDN介绍

FetchEventSource使用

...全文
732 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

122

社区成员

发帖
与我相关
我的任务
社区描述
FZU-SE
软件工程 高校
社区管理员
  • LinQF39
  • 助教-吴可仪
  • 一杯时间
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧