Vue3封装可复用AI对话组件:一次抽象复盘

公司三个页面都要塞 AI 对话框:客服页、文档问答页、后台运营页。我先是复制粘贴了两遍,第三遍实在受不了------改个流式逻辑要三处同步,漏一处就出 bug。狠下心抽了个可复用的 <AiChat> 组件,过程里对「该抽什么、不该抽什么」有了新认识。复盘一下。

第一版抽错了:把 UI 焊死了

我的第一版把消息气泡的样式、头像、布局全写死在组件里,参数只暴露一个 apiUrl。结果客服页要圆角气泡、文档页要纯文本流、后台要紧凑表格风,三个设计稿一来,我的「通用组件」当场报废,又开始堆 props.theme === 'xxx' 的分支,越堆越烂。

教训:UI 是最不该硬抽的部分。第二版我把渲染权用 slot 交还给调用方,组件只管「对话这件事的逻辑」:

xml 复制代码
<!-- AiChat.vue 暴露 slot,UI 由外部决定 -->
<template>
  <div class="ai-chat">
    <slot name="message" v-for="m in messages" :key="m.id" :message="m" />
    <slot name="input" :send="send" :stop="stop" :loading="isStreaming" />
  </div>
</template>

调用方爱怎么画怎么画,组件不插手长相。这一刀切对了之后,三个页面复用率才真上来。

逻辑用 composable,组件只是壳

更进一步,我发现连那个「壳」组件都不是必须的------真正可复用的是对话逻辑本身。把它抽成 composable,组件想要就用,不要也能裸用:

ini 复制代码
// useAiChat.ts
export function useAiChat(opts: { endpoint: string }) {
  const messages = ref<Message[]>([]);
  const isStreaming = ref(false);
  let ctrl: AbortController | null = null;

  async function send(text: string) {
    messages.value.push({ id: uid(), role: 'user', content: text });
    const assistant = reactive<Message>({ id: uid(), role: 'assistant', content: '' });
    messages.value.push(assistant);
    isStreaming.value = true;
    ctrl = new AbortController();
    try {
      for await (const delta of streamChat(opts.endpoint, text, ctrl.signal)) {
        assistant.content += delta; // reactive 对象,直接改属性触发更新
      }
    } finally {
      isStreaming.value = false;
    }
  }

  function stop() { ctrl?.abort(); }

  return { messages, isStreaming, send, stop };
}

这里有个 Vue3 的小细节坑:往 messages.value 里 push 普通对象,然后流式改它的 content,更新有时不触发。得保证那个 assistant 消息是 reactive 的(或整个数组的元素都经过响应式包装),直接改属性才会被追踪。我在这上面卡了快一个小时,盯着「数据变了视图不动」一脸问号。

别把 fetch 逻辑也焊进 composable

第二个取舍:要不要把请求实现写死在 useAiChat 里?我最后没有,把 streamChat 作为可替换的传输层注入。因为有的页面走 SSE、有的走 WebSocket,还有的本地 mock 联调。写死传输层,等于又造了个换皮就废的组件。

php 复制代码
useAiChat({
  endpoint: '/api/chat',
  transport: customWsTransport, // 可选,默认 SSE
});

一个没做到的理想态

我本想把「消息持久化」也一并封进去,结果发现三个页面的存储诉求差太多------客服要存后端、文档问答只要 session 内存、后台要本地草稿。硬塞进组件只会让它变臃肿。最后我把持久化彻底踢出去,组件只 emit 消息变更事件,存哪由调用方接。组件因此「不够开箱即用」,每个页面还得自己写几行存储------这是我为了保住它的纯粹故意留的口子,算个清醒的妥协。

模型和工具编排那层我没自己搭,对话能力直接挂讯飞这种 MaaS 的 API 上,省掉自建算力的负担,我专心把组件的边界划清楚。

你们的 AI 对话组件,slot 和 composable 是怎么切的?评论区交流下抽象的尺度。

相关推荐
怕浪猫1 小时前
哪些软件对 Chrome DevTools Protocol 频繁使用
人工智能·架构·前端框架
leo在掘金3 小时前
从DeepSeek 510亿融资到GitHub 33K Star开源项目:这周的技术生态发生了什么?
人工智能
小姜前线技术4 小时前
AI流式渲染打字机效果抖动?节流方案踩坑实录
人工智能
用户018349301694 小时前
AI对话状态管理:useReducer还是XState
人工智能
先锋部队4 小时前
给AI对话加「停止生成」按钮:abort SSE实战
人工智能
新新技术迷4 小时前
移动端H5接AI对话的坑:键盘顶起与滚动到底
人工智能
aqi007 小时前
15天学会AI应用开发(七)有了大模型为什么还要引入RAG
人工智能·python·大模型·ai编程·ai应用