常见的AI对话场景和特殊情况

常见的AI对话场景

大部分AI对话场景都与chatGPT很相似

session_id可以放在url中 并根据其变化情况 为会话组件设置不同的key 以规避"重置state"的情况

会话区域有三种状态:

1.无会话(无session_id)

2.刚创建的新会话(有session_id)

3.老会话(有session_id)

仅当从状态1->2时 虽然session_id变化 但是会话组件不能重载.此时session_id会从空变为另一个值

不过 从1->3时 用户在无会话时点击历史记录 session_id也会从无到有 因此需要为这种情况设置一个特殊入口

代码如下

tsx 复制代码
import { FC } from 'react'

export const AIChat: FC = () => {
  /** 从url中取到当前会话的id */
  const { id } = useParams<{ id?: string }>()
  const session_id = useMemo(() => {
    const _id = parseInt(id!)
    return Number.isInteger(_id) ? _id : null
  }, [id])

  /** 用key使会话组件重载 */
  const keyRef = useRef(0)
  /** 创建新会话时 不重置组件 */
  const prevSessionId = useRef(session_id)
  if (prevSessionId.current && prevSessionId.current !== session_id) {
    ++keyRef.current
  }
  /** 点击历史记录时 必定重置组件 */
  const nextUnstableSessionId = useRef<number>()
  if (nextUnstableSessionId.current === session_id) {
    ++keyRef.current
    nextUnstableSessionId.current = undefined
  }
  prevSessionId.current = session_id

  return (
    <div className='flex '>
      <ChatHistory
        session_id={session_id}
        setNextSessionId={(val) => (nextUnstableSessionId.current = val)}
      />
      <Dialog key={keyRef.current} session_id={session_id} />
    </div>
  )
}

/** 历史记录 */
declare const ChatHistory: FC<{
  session_id: null | number
  setNextSessionId: (val: number) => void
}>
/** 会话区域 */
declare const Dialog: FC<{ session_id: null | number }>

nextUnstableSessionId是为空会话点击历史记录的情形提供的 只要session_id变为该id 就重置组件

tsx 复制代码
// ChatHistory中
<Link to={`/qa/${session_id}`} onClick={() => setNextSessionId(session_id)}>
  历史会话{session_id}
</Link>

在Dialog组件中 由于使用key进行了重置 因此仅需要根据第一次传入session_id的判断当前所处的状态

ts 复制代码
// Dialog组件 控制对话的hook
export const useDialog = (session_id: number | null) => {
  const [dialog, setDialog] = useImmer<(UserDialog | AIDialog)[]>([])
  const [status, setStatus] = useState<'loading' | 'answering' | 'ready'>(
    'loading',
  )
  const acceptAnswer = async (stream: ReadableStream, index: number) => {
    const reader = stream.getReader()
    const decoder = new TextDecoder()
    while (true) {
      const { done, value } = await reader.read()
      const text = decoder.decode(value, { stream: true })
      setDialog((draft) => {
        draft[index].content += text
      })
      if (done) {
        setStatus('ready')
        break
      }
    }
  }
  useMount(async () => {
    if (!session_id) {
      setStatus('ready')
      return
    }
    const historyDialog = await getSessionHistory({ session_id })
    setDialog(historyDialog)
    // 还可以继续上一次被中断的会话
    setStatus('ready')
  })
  const navigate = useNavigate()
  const startQA = useMemoizedFn(async (content: string) => {
    setStatus('answering')
    let answerIndex: number
    setDialog((draft) => {
      draft.push(
        {
          role: 'question',
          content,
        },
        {
          role: 'answer',
          content: '',
        },
      )
      answerIndex = draft.length - 1
    })
    let currentSessionId = session_id
    if (!currentSessionId) {
      // 无会话时 先创建会话
      const { id } = await createSession()
      currentSessionId = id
      navigate(`/qa/${id}`)
      // 可以向历史记录中添加当前的session
    }
    const { body } = await startSessionQA({
      session_id: currentSessionId,
      content,
    })
    acceptAnswer(body, answerIndex!)
  })
  return {
    dialog,
    status,
    startQA,
    // 也可以有stopQA等函数
  }
}

如果不能重置Dialog组件

我所遇到的业务场景是:

1.用户可以创建会话组

2.如果当前的组无会话 则展示常规情况的大输入框

3.如果有会话,则展示这样的内容

在开发时 我将2、3种场景视作同一组件

tsx 复制代码
const SessionGroup: FC = () => {
  const { data, loading } = useSessionGroupInfo(group_id)
  const { dialog } = useDialog()
  if (loading) return <Skeleton active className='p-4' />
  if (!data) return <EmptyGroup />
  return (
    <div className='flex'>
      <Dialog />
      <GroupChart />
    </div>
  )
}

但这就带来一个问题 如果使用key重置了SessionGroup组件 那么与会话无关的GroupChart一定也会重置 但这些统计内容是一定不能重置的 因为包含了echarts、入场动画、用户滚动等

因此我对useDialog进行了改造 使之具有了"重置state"的能力

ts 复制代码
// useDialog 中
const resetKey = useRef(0)
function withResetKey<Args extends any[], R>(fn: (...args: Args) => R) {
  const currentKey = resetKey.current
  const fnWithResetKey = (...args: Args) => {
    if (currentKey !== resetKey.current) {
      throw new Error('resetKey落后')
    }
    return fn(...args)
  }
  return fnWithResetKey
}

const [dialog, _setDialog] = useImmer<(UserDialog | AIDialog)[]>([])
const setDialog = withResetKey(_setDialog)

const [status, setStatus] = useState<'loading' | 'answering' | 'ready'>(
  'loading',
)
const setStatus = withResetKey(_setStatus)

const resetAllState = ()=> {
 ++ resetKey.current
 // 重置dialog、status
}

对每个setState函数都使用withResetKey包裹 确保在state重置前的回调函数不会干扰到重置后的state.需要注意的是 withResetKey是基于闭包进行的 因此不能和useMemoizedFn配合使用

此外 由于历史记录和无会话展示在同一个区域 url中的session_id都为空 无法区分 因此必须手动维护一个sessionStatus 用于判断当前组件的状态.此外 session_id还需要和sessionStatus同步更新 session_id也需要手动维护

ts 复制代码
const [sessionStatus, setSessionStatus] = useState<
  'history' | 'new' | 'creating' | 'created'
>('new')

const { id } = useParams<{ id?: string }>()
const [session_id, _setSessionId] = useState(() => {
  const _id = parseInt(id!)
  return Number.isInteger(_id) ? _id : null
})
const navigate = useNavigate()
const setSessionId = useMemoizedFn((val?: number | null) => {
  navigate(`/session-group/${group_id}` + (val ? `/${val}` : ''))
  _setSessionId(val ?? null)
})

每当变更session_id时(包括使用<Link>组件跳转路由) 需要调用setSessionId函数 并用setSessionStatus更新session的状态

反思和优化

我在实际开发中 发现"重置state"和"手动维护session_id和sessionStatus"非常繁琐

后者是由于历史记录和会话在同一个位置 是不可避免的 但重置state完全可以通过合理的设计进行规避 优化方案如下:

  • 用css控制GroupChart的位置 使之在空会话时隐藏
  • 将GroupChart组件挂载在html的其他位置 通过绝对定位控制其位置(即KeepAlive)
  • 从逻辑上 将无、有会话的组件拆分为两个组件.在其公共父组件维护一个state 包含创建会话的信息.无会话组件提问后 设置该state 并将其输入给有会话组件.有会话组件就可以用key重置组件内部的Dialog组件
相关推荐
sophie旭3 小时前
一道面试题,开始性能优化之旅(5)-- 浏览器和性能
前端·面试·性能优化
lypzcgf3 小时前
Coze源码分析-资源库-编辑知识库-前端源码-核心组件
前端·知识库·coze·coze源码分析·智能体平台·ai应用平台·agent平台
小墨宝3 小时前
web前端学习 langchain
前端·学习·langchain
北城以北88883 小时前
Vue--Vue基础(一)
前端·javascript·vue.js
IT_陈寒4 小时前
Python 3.12新特性实战:5个让你的代码提速30%的性能优化技巧
前端·人工智能·后端
sniper_fandc4 小时前
Vue Router路由
前端·javascript·vue.js
excel4 小时前
为什么 Vue 组件中的 data 必须是一个函数?(含 Vue2/3 对比)
前端
妄小闲4 小时前
成品网站模板源码 网站源码模板 html源码下载
前端·html
知识分享小能手12 小时前
微信小程序入门学习教程,从入门到精通,微信小程序常用API(上)——知识点详解 + 案例实战(4)
前端·javascript·学习·微信小程序·小程序·html5·微信开放平台