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 是怎么切的?评论区交流下抽象的尺度。

相关推荐
金伟API10243 小时前
如何从零打造一个极简的DeepSeek-R1大模型
人工智能·ai
江畔柳前堤3 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Shockang3 小时前
智能体环路工程实战
人工智能
美摄科技3 小时前
美摄美颜特效SDK Skill:以AI赋能智能视音频新视界
人工智能
Web3_Daisy3 小时前
Pump.fun 与 FOMO 竞争背后的 Meme 市场变局
大数据·人工智能·金融·web3·区块链
AI创界者4 小时前
MiniMax-H3 本地一键部署整合包:8G 显存玩转文图生视频、视频参考、角色替换与超分补帧全流程
人工智能·深度学习
江畔柳前堤4 小时前
HBM:大语言模型时代的「算力血液」——从内存墙到带宽革命的深度拆解
服务器·人工智能·windows·目标检测·语言模型·自然语言处理·软件工程
美摄科技4 小时前
视频一键成片SDK Skill:AI智能分析与语义理解
人工智能
fthux5 小时前
装闭 RenoPit 源码解析(05):FastAPI与Celery如何执行AI装修分析
人工智能·ai·开源·github·open source·renopit
格数致用5 小时前
数据库设计与表结构详解|信息化项目全流程管理系统源码逐行精讲(五)
人工智能·政务·数据库设计·外键约束