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

相关推荐
xywww1682 小时前
大模型 API 选型实战:GPT、Gemini、Claude 接入时该看哪些指标?
运维·服务器·人工智能·python·gpt·langchain
私人珍藏库7 小时前
[Android] PeakFinder AR v4.8.89 (山峰全景识别+增强现实山峰查看器)
android·人工智能·智能手机·ar·工具·软件
CS创新实验室8 小时前
算法、齿轮与硅基大脑:数值计算发展简史
人工智能·算法·数值计算
能有时光8 小时前
PyTorch KernelAgent 源码解读 ---(4)--- ExtractorAgent
人工智能·pytorch·python
fthux8 小时前
GitZip Pro 源码解析:一个 GitHub 文件/文件夹下载扩展是如何工作的(一)整体架构与扩展入口
人工智能·ai·开源·github·open source
aqi009 小时前
15天学会AI应用开发(十七)使用LangGraph实现会话记忆功能
人工智能·python·大模型·ai编程·ai应用
xixixi777779 小时前
三大 AI 安全里程碑:Akamai 高危风险预警、智能体水印强制落地、PQC 量子安全全产业链统一
大数据·人工智能·安全·ai·大模型·智能体·政策
AI小码9 小时前
LLM 应用的缓存工程:当每次 API 调用都在燃烧成本
java·人工智能·spring·计算机·llm·编程·api
code 小楊9 小时前
AI函数调用:Function Calling从理论到实战全解析
人工智能
delishcomcn10 小时前
智切未来:AI算法如何重塑不干胶标签分切机的精度与效率边界
人工智能·神经网络·计算机视觉