📊 流式输出实现总结

📊 流式输出实现总结

通过 Server-Sent Events (SSE) 协议实现了 AI 回复的流式输出,整个流程如下:

1️⃣ 发起流式请求 (ChatInput.vue - fetchMessage 函数)

php 复制代码
typescript
const response = await fetch("/api/chat/stream", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${token}`,
  },
  body: JSON.stringify({
    message,
    session_id: sessionId,
  }),
});
  • 使用原生 fetch API 而不是 Axios(因为需要读取流)
  • 向后端 /api/chat/stream 接口发送 POST 请求
  • 携带用户消息和会话 ID

2️⃣ 获取可读流

ini 复制代码
typescript
const reader = response.body?.getReader();
const decoder = new TextDecoder();
  • 通过 response.body.getReader() 获取一个可读流阅读器
  • 创建 TextDecoder 用于将二进制数据解码为文本

3️⃣ 初始化 AI 消息占位符

ini 复制代码
typescript
messageStore.addAIMessage("");  // 添加一个空的 AI 消息
aiMessageIndex = messageStore.message.length - 1;  // 记录该消息的索引
  • 在消息列表中先添加一个空的 AI 消息对象
  • 保存其索引位置,后续用于实时更新内容

4️⃣ 循环读取流数据

arduino 复制代码
typescript
while (true) {
  const { done, value } = await reader.read();
  if (done) break;  // 流结束时退出循环
  
  const text = decoder.decode(value, { stream: true });
  buffer += text;
  
  // 按换行符分割处理
  const lines = buffer.split("\n"); //分割成数组
  buffer = lines.pop() || "";  // 保留不完整的最后一行,pop()删除数组最后一个元素,并且返回他
  
  for (let line of lines) {
    if (line.startsWith("data: ")) {
      const jsonStr = line.slice(6); //字符串方法,从字符串索引6开始切割到末尾
      const data = JSON.parse(jsonStr); //jsonStr是{"content": "..."}这样的结构
      if (data.content) {
        fullContent += data.content;
        messageStore.updateAIMessageContent(aiMessageIndex, fullContent);// 更新状态仓库数组
      }
    }
  }
}

核心逻辑:

  • 使用 while(true) 循环持续读取数据块
  • 每次读取后检查 done 标志,判断是否完成
  • 将二进制数据解码为文本并追加到缓冲区
  • 按换行符 \n 分割,处理完整的 SSE 数据行
  • 保留最后一行(可能是不完整的数据片段)到下一次循环

5️⃣ 解析 SSE 格式数据

后端返回的数据格式遵循 SSE (Server-Sent Events) 规范:

css 复制代码
data: {"content": "你"}
data: {"content": "好"}
data: {"content": ","}
data: {"content": "我"}
...

前端解析步骤:

  1. 检测行是否以 data: 开头
  2. 提取 data: 后面的 JSON 字符串
  3. 解析 JSON 获取 content 字段
  4. 累加到 fullContent 变量中

6️⃣ 实时更新 UI (message.ts - updateAIMessageContent)

scss 复制代码
typescript
function updateAIMessageContent(index: number, content: string) {
  const msgs = [...message.value];  // 创建新数组(不可变更新)
  if (msgs[index]) {
    msgs[index] = { ...msgs[index], content };  // 创建新对象
    message.value = msgs;
    triggerRef(message);  // 手动触发响应式更新
  }
}

关键点:

  • 采用不可变数据更新策略:创建新数组和新对象
  • 使用 triggerRef 确保 shallowRef 的响应式更新被触发
  • Vue 检测到变化后自动重新渲染组件

7️⃣ 视图渲染 (MessageItem.vue)

ini 复制代码
vue
<div v-else class="m-6 px-4 py-6">
  <div class="max-w-4xl mx-auto">
    <AIMarkdown :content="content" />
  </div>
</div>
  • AI 消息通过 AIMarkdown 组件渲染
  • 支持 Markdown 格式的实时显示
  • 每次 content 更新时,组件自动重新渲染

8️⃣ 处理残留数据

kotlin 复制代码
typescript
if (buffer.trim().startsWith("data: ")) {
  try {
    const data = JSON.parse(buffer.slice(6));
    if (data.content) {
      fullContent += data.content;
      messageStore.updateAIMessageContent(aiMessageIndex, fullContent);
    }
  } catch (e) {
    console.debug("解析最后一行失败");
  }
}
  • 循环结束后,检查缓冲区是否还有未处理的数据
  • 防止最后一行数据丢失

9️⃣ 释放资源

csharp 复制代码
typescript
finally {
  reader.releaseLock();  // 释放阅读器锁
}

🎯 技术要点总结

技术点 说明
SSE 协议 服务器推送事件,单向流式数据传输
ReadableStream Web API,用于读取流式响应
TextDecoder 将二进制数据解码为 UTF-8 文本
缓冲区管理 处理不完整的数据片段,确保正确解析
不可变更新 创建新数组/对象而非修改原数据,确保响应式追踪
triggerRef 手动触发 shallowRef 的响应式更新
Vue 响应式 数据变化自动触发视图重新渲染

🔄 完整流程图

bash 复制代码
用户发送消息
    ↓
调用 fetchMessage()
    ↓
发起 POST 请求到 /api/chat/stream
    ↓
获取 ReadableStream Reader
    ↓
添加空 AI 消息占位符
    ↓
┌─→ 循环读取数据块 ──────────────┐
│   ↓                            │
│   解码二进制数据为文本          │
│   ↓                            │
│   按 \n 分割行                  │
│   ↓                            │
│   解析 "data: {...}" 格式      │
│   ↓                            │
│   累加 content 到 fullContent  │
│   ↓                            │
│   调用 updateAIMessageContent  │
│   ↓                            │
│   触发 Vue 响应式更新          │
│   ↓                            │
│   UI 自动重新渲染               │
│   ↓                            │
└── 直到 done === true ←─────────┘
    ↓
释放 Reader 锁
    ↓
完成
相关推荐
IT_陈寒2 小时前
Java集合的这个坑,我调试了整整3小时才爬出来
前端·人工智能·后端
前端老石人3 小时前
前端网站换肤功能的 3 种实现方案
开发语言·前端·css·html
冴羽yayujs3 小时前
2026 年的 JavaScript 已经不是你认识的 JavaScript 了
前端·javascript
小灰灰搞电子3 小时前
PyQt QWebChannel详解-C++与Web页面的无缝双向通信
前端·pyqt
M ? A3 小时前
你的 Vue v-for,VuReact 会编译成什么样的 React 代码?
前端·javascript·vue.js·经验分享·react.js·面试·vureact
午安~婉3 小时前
Electron桌面应用(续3)
前端·javascript·electron·重构通用模型·异步可迭代对象
W.A委员会3 小时前
伪类与伪元素
前端·javascript·css
午安~婉3 小时前
Electron桌面应用(续2)
前端·javascript·electron·路由守卫·优化llm返回的内容
eEKI DAND3 小时前
一个比 Nginx 还简单的 Web 服务器
服务器·前端·nginx