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

相关推荐
光锥智能41 分钟前
手机存量时代,vivo能用Harness让用户换机吗?
人工智能
比奥利奥还傲.2 小时前
哪吒监控面板实战:部署 Dashboard、接入 Agent、钉钉告警,再配置固定公网访问
网络·人工智能·分布式·1024程序员节
FII工业富联科技服务3 小时前
从人机共舞到工业精密操作:机器人如何突破动作控制与场景适应?
人工智能·机器人·机器翻译模型
serdes213 小时前
56G PAM4 SerDes RX 32 路时间交织 SAR ADC 顶层与采样前端
人工智能
小和尚同志8 小时前
小黑插图 Skill:从 11.7k star 的 Codex 专属,到 Claude Code 能用的平替
人工智能·aigc
高洁018 小时前
孪生不止在工厂:能源、医疗与农业
人工智能·深度学习·transformer·知识图谱·tornado
外域速览8 小时前
智谱50亿美元押注AI自训练
大数据·人工智能
人工智能培训9 小时前
孪生不止在工厂:能源、医疗与农业
大数据·人工智能
米小虾9 小时前
让模型说"我不知道",比让它答对更难:放弃文本生成能换来什么
人工智能
新新学长搞科研9 小时前
【SPIE出版】2026年人工智能、新材料与新能源国际学术会议(AINMNE 2026)
人工智能·新能源·新材料