news 2026/8/11 15:32:29

大模型对话前端第一版:先跑通流式响应和取消

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型对话前端第一版:先跑通流式响应和取消

大模型对话前端第一版:先跑通流式响应和取消

第一版大模型对话界面先验证一条主链路:接收流式响应、正确解码、可取消地渲染,并在异常时给出可恢复的提示。本文以流式 Markdown 的前端实现为例,讨论这条链路的最小边界。

fetchTextDecoder足以搭起原型,但还要处理不完整的 UTF-8 字节、被截断的事件以及高频更新带来的重复渲染。状态机、Worker 等机制应由测得的瓶颈决定,而不是一开始全部引入。

MVP 核心链路拆解

在第一版交付中,前端最核心的链路只有一条:接收服务端 Server-Sent Events (SSE) 字节流,逐块解码后通过平滑打字机队列推入状态,同时控制 Markdown DOM 树的渲染频率。

flowchart TD A[SSEResponse 字节流] --> B(ReadableStream DefaultReader) B --> C{TextDecoder Chunk 解码} C --> D[打字机缓冲区队列 Queue] D --> E{requestAnimationFrame 调度器} E --> F[更新 React/Vue State] F --> G[DOM 增量渲染与自动滚动] G --> H{用户触发 AbortController?} H -- 是 --> I[CancelToken 中断底层 HTTP 链接] H -- 否 --> J[流传输结束释放 Worker/Timer]

整个链路涉及三个主要瓶颈点:

  1. 网络层EventSourceAPI 不支持 POST 请求与自定义 Header,必须改用原生fetch结合ReadableStream
  2. 缓冲层:后端返回的 Chunk 尺寸不均,单次推送可能包含半个 UTF-8 字符或 incomplete JSON,直接赋值给 UI 会导致乱码闪烁。
  3. 渲染层:高频setState触发 React Re-render,导致 CPU 占用率飙升至 100%。

关键代码取舍与实现

为了在最小代码量下解决高频渲染卡顿,第一版不需要引入复杂的 Web Worker,而是采用基于requestAnimationFrame的缓冲队列(Buffer Queue)。

// sse_client.ts export interface SSEOptions { url: string; headers: Record<string, string>; body: object; onChunk: (text: string) => void; onError: (err: Error) => void; signal?: AbortSignal; } export async function fetchSSEResponse(options: SSEOptions): Promise<void> { const { url, headers, body, onChunk, onError, signal } = options; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers, }, body: JSON.stringify(body), signal, }); if (!response.ok || !response.body) { throw new Error(`HTTP 异常: ${response.status} ${response.statusText}`); } const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); // 保留未完成的最后一行 buffer = lines.pop() || ''; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith(':')) continue; if (trimmed === 'data: [DONE]') return; if (trimmed.startsWith('data: ')) { try { const parsed = JSON.parse(trimmed.slice(6)); const delta = parsed.choices?.[0]?.delta?.content || ''; if (delta) onChunk(delta); } catch (e) { // 避免单个解析失败阻断后续流处理 console.warn('Chunk 解析不完整:', trimmed); } } } } } catch (err: any) { if (err.name === 'AbortError') { console.log('请求已被用户主动中断'); } else { onError(err); } } }

在 UI 层,直接将onChunk的回调关联到组件 State 是灾难性的。按照 50ms 一次 Chunk 的频率,一秒钟触发 20 次组件树重绘。我们可以通过平滑渲染器截断频率。

// useSmoothStream.ts import { useState, useRef, useEffect } from 'react'; export function useSmoothStream() { const [displayText, setDisplayText] = useState(''); const queueRef = useRef<string[]>([]); const frameRef = useRef<number | null>(null); const pushChunk = (chunk: string) => { queueRef.current.push(...chunk.split('')); if (!frameRef.current) { scheduleFlush(); } }; const scheduleFlush = () => { frameRef.current = requestAnimationFrame(() => { if (queueRef.current.length > 0) { // 每次渲染取出最多 3 个字符,保持流畅打字感 const batch = queueRef.current.splice(0, 3).join(''); setDisplayText((prev) => prev + batch); scheduleFlush(); } else { frameRef.current = null; } }); }; const reset = () => { if (frameRef.current) cancelAnimationFrame(frameRef.current); frameRef.current = null; queueRef.current = []; setDisplayText(''); }; return { displayText, pushChunk, reset }; }

第一版中果断放弃的技术点:

  • 离线 VectorDB 本地检索:完全在服务端处理,前端只负责呈现。
  • 自定义 Markdown AST 增量解析引擎:直接选用轻量markedmarkdown-it加防抖,不写自定义 Parser。
  • 复杂的多会话并行 Worker:MVP 阶段单次窗口仅支持单条活跃 Request。

现场诊断与优化验证

线上验证时,不能凭感觉判断“卡不卡”。用 Chrome DevTools 的 Performance 面板录制流式输出过程中的 CPU Profile 才是标准动作。

打开 Chrome DevTools,切到Performance标签页,点击 Record,开始一次 1000 字的大模型文本生成,录制 15 秒后停止。

在没有优化前,Profile 显示大量的Recalculate StyleLayout频次高达 45 次/秒,Task列表中长任务(Long Task > 50ms)占比超过 35%。使用requestAnimationFrame批处理后:

  • Layout频次降至 16 次/秒以内。
  • JS 执行线程的火焰图中,setDisplayText导致的重绘耗时从 320ms 降至 42ms。
  • 内存占用在整个流式传输期间稳定在 38MB 左右,没有任何内存泄漏曲线。

做 MVP 的核心思维是:给网络层加终止器(AbortController),给渲染层加缓冲闸门(rAF),给异常加降级保护。三者具备,第一版就立住了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/11 15:29:54

AI搜索与知识图谱:构建GEO全链路技能库的架构与实践

1. 项目概述&#xff1a;当AI搜索遇上“GEO全链路技能库” 最近和几个做增长、做运营的朋友聊天&#xff0c;大家普遍有个感觉&#xff1a;现在找资料、学东西&#xff0c;用传统搜索引擎越来越“钝”了。你想了解一个新兴的营销玩法&#xff0c;比如“小红书蒲公英平台最新报备…

作者头像 李华
网站建设 2026/8/11 15:24:45

BOM核心概念与实战技巧:从原理到应用

1. BOM核心概念与实战价值 浏览器对象模型(BOM)是JavaScript与浏览器交互的核心接口&#xff0c;它提供了独立于页面内容的浏览器功能操作能力。与DOM操作文档结构不同&#xff0c;BOM让我们能够控制浏览器窗口行为、访问导航历史、获取屏幕信息等。在实际项目中&#xff0c;BO…

作者头像 李华
网站建设 2026/8/11 15:24:35

9种字重免费字体:Outfit字体库的终极选择指南

9种字重免费字体&#xff1a;Outfit字体库的终极选择指南 【免费下载链接】Outfit-Fonts The most on-brand typeface 项目地址: https://gitcode.com/gh_mirrors/ou/Outfit-Fonts 还在为设计项目寻找既专业又免费的字体解决方案吗&#xff1f;Outfit字体库就是您一直在…

作者头像 李华
网站建设 2026/8/11 15:21:39

如何用开源SRAM编译器实现高性能内存设计

如何用开源SRAM编译器实现高性能内存设计 【免费下载链接】OpenRAM An open-source static random access memory (SRAM) compiler. 项目地址: https://gitcode.com/gh_mirrors/op/OpenRAM 在ASIC设计中&#xff0c;SRAM性能瓶颈常常成为系统优化的最大障碍。传统手动设…

作者头像 李华