SSE 实现 AI 对话中的流式输出

0 阅读3分钟

SSE 实现 AI 对话中的流式输出

日常使用 deepseek,经常看到聊天机器人的流式输出,感觉很赞,想尝试下自己实现一个类似效果

各个大模型平台,api 调用都支持流式输出,如果用 node.js 如何实现一个流式输出效果呢

整体内容包含服务端实现客户端实现

整体效果

steamOut.gif

SSE 技术原理

Server-Sent Events (SSE) 是一种基于 HTTP 的服务器向客户端推送数据的技术。与 WebSocket 不同,SSE 是单向的,仅支持服务器向客户端推送数据,但实现简单,且天然支持断线重连。

  • 单向通信,由服务器发送数据,客户端接收数据
  • 自动重连
  • 轻量级,相比 websocket,开销更小
  • 主要传输文本数据,适合 JSON 等结构化数据
  • sse 的消息结构
    • id: 事件 id
    • event: 事件名称 如果不传默认为 message,如果设置其他名称,前端也需要修改成对应的名称,进行消息接受
    • data: 事件数据
    • retry: 重连时间

服务端实现

  • node.js 的 koa2 框架

实现步骤

  1. 设置 SSE 响应头 响应内容类型,客户端不缓存,保持长连接
  2. 防止 koa 自动处理响应,手动设置响应状态码为 200
  3. 定时发送消息模拟推流,通过ctx.res.write(data: msg),也可以传入id, event补充消息内容。
  4. 通过ctx.req.on(eventName, callback)进行连接监听,处理连接关闭,连接失败
aiRouer.get("/stream", (ctx) => {
  console.log("进入stream");

  // 设置SSE响应头
  ctx.set({
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
    "Access-Control-Allow-Origin": "*",
  });

  // 关键修复:防止Koa自动处理响应
  ctx.respond = false;
  ctx.status = 200;

  // 发送初始连接成功消息
  ctx.res.write(`data: ${JSON.stringify({ message: "Connected to Koa2 SSE server" })}\n\n`);

  // 设置定时器,定期发送消息
  let counter = 0;
  const timer = setInterval(() => {
    counter++;
    const data = {
      message: `Server time: ${new Date().toLocaleTimeString()}`,
      counter: counter,
    };

    // 检查连接是否仍然有效
    if (ctx.res.writable) {
      // 按照SSE格式发送数据
      ctx.res.write(`id: ${counter}\n`);
      ctx.res.write(`event: message\n`);
      ctx.res.write(`data: ${JSON.stringify(data)}\n\n`);

      if (counter > 10) {
        ctx.res.end();
      }
    } else {
      // 如果连接已关闭,清理资源
      clearInterval(timer);
      console.log("SSE connection closed due to client disconnect");
    }
  }, 1000);

  // 处理连接关闭
  ctx.req.on("close", () => {
    clearInterval(timer);
    console.log("SSE connection closed");
  });

  // 处理错误
  ctx.req.on("error", (err) => {
    clearInterval(timer);
    console.error("SSE connection error:", err);
  });
});

客户端实现

  • vue.js

实现步骤

  1. 触发 SSE 推送,创建一个 EventSource 对象,并监听服务端的推送消息。
  2. 通过 inputStr 接受消息,设置连接关闭条件,并结束定时器。
  3. 定时取数据,输出到outStr
<template>
  <div class="steam">
    <h3>服务端回答的消息</h3>
    <div ref="outRef" class="out-textarea" v-if="outStr">{{ outStr }}</div>
    <div>
      <n-button type="info" @click="onSend">发送</n-button>
    </div>
  </div>
</template>
import { NInput, NButton } from "naive-ui";
import { reactive, toRefs } from "vue";

const state = reactive({
  outRef: null,
  outStr: "",
  inputStr: "",
  inputFinish: false,
});
const { outRef, outStr, inputStr, inputFinish } = toRefs(state);

/**
 * 流式输出
 */
function outStream() {
  let i = 0;
  let timer = setInterval(() => {
    // 一直等待输出,直到服务端停止输出
    if (inputFinish.value && i >= inputStr.value.length) {
      clearInterval(timer);
      timer = null;
      return;
    }
    if (i < inputStr.value.length) {
      state.outStr += inputStr.value[i];
      i++;
    }
  }, 100);
}
function onSend() {
  let eventSource = new EventSource("/api/ai/stream");
  inputStr.value = "";
  outStr.value = "";
  inputFinish.value = false;
  eventSource.onmessage = function (e) {
    const data = JSON.parse(e.data);
    inputStr.value += data.message;
    if (data.counter > 10) {
      eventSource.close();
      eventSource = null;
      inputFinish.value = true;
    }
  };
  outStream();
}
.out-textarea {
  display: inline-block;
  background-color: rgba(0, 0, 0, 0.06);
  padding: 12px 16px;
  border-radius: 8px;
  position: relative;
  box-sizing: border-box;
  min-width: 0;
  max-width: 100%;
  color: rgba(0, 0, 0, 0.88);
  font-size: 14px;
  line-height: 1.5714285714285714;
  min-height: 46px;
  word-break: break-word;
  margin-top: 24px;
  margin-bottom: 24px;
  scrollbar-color: rgba(0, 0, 0, 0.45) transparent;
}

ai 组件库

  • 目前大厂都有成熟的 ai 组件库,其中就包括对话流式输出组件
  • antd-design-x-vue 对话气泡框
  • RICH 设计范式思考
    • Role 【角色】以后产品和人交互,更像是一个人。可以通过角色外观,声音,情绪,专业领域知识
    • Intention 【意图】 以前收集用户需求,通过输入框,按钮,鼠标,触摸动作 以后 ai 会做的更多,比如通过对话、语音、结合少量原先图形化交互、更加准确和简单
    • Conversation 【对话】人与 ai 的会话规则 开始/追问/提示/确认/错误/结束
    • Hybrid UI 【混合界面】 Do 为主/Do + Chat 均衡/Chat 为主