跳到主要内容
AI 对话页流式处理架构:Web Streams 与 Fetch API 实践 | 极客日志
TypeScript AI 大前端
AI 对话页流式处理架构:Web Streams 与 Fetch API 实践 AI 对话应用中,流式响应能显著提升用户体验。通过 Web Streams 和 Fetch API 构建生产者 - 消费者模型,可实现网络 IO、数据解码与 UI 渲染的解耦。核心在于利用 AbortController 管理请求中止,TransformStream 处理粘包半包及行拆分,并结合 SSE 协议解析增量内容。配合 Ant Design X 组件库封装消息展示与发送交互,确保实时性与稳定性。
beaabea 发布于 2026/3/16 更新于 2026/4/27 3 浏览引言
在构建 AI 智能体应用时,为了降低模型搭建复杂度,通常会将数据流转与 UI 实现统一封装。但若要深入理解底层机制,采用'生产者 - 消费者模式'的流式处理架构是更优解。这种架构将网络 IO、数据解码、文本解析与 UI 渲染解耦,从而实现实时流式响应和 UI 增量渲染。
本文基于 Vue 3 + TypeScript + Vite 技术栈,结合 Ant Design Vue 及 Ant Design X Vue 组件库,探讨如何利用 Web Streams API 与 Fetch API 实现这一流程。
从请求发送到 UI 渲染的整体链路如下:
![流式响应整体链路示意图]
流式响应处理
请求管理
我们需要同时控制上游的网络请求和下游的字节流读取,以实现会话的可取消与可停止。
AbortController :作为上游网络中止句柄。在发起新提问前中止旧请求,或用户点击取消时立即终止当前请求。
ReadableStreamDefaultReader :作为下游传输层字节流的读取器句柄。它驱动上游生产者向管道入队字节块,并在用户取消时终止读取。
let abortController : AbortController | null = null ;
let currentReader : ReadableStreamDefaultReader <Uint8Array > | null = null ;
流处理管道
建立一条 Web Streams 解析管线:生产者 → 解码 → 按行拆分 → 消费者。整体处理流程包含以下三个核心环节。
生产者流
负责将上游 reader 获取的 chunk 字节块按背压节奏入队,供下游消费,实现'读 - 推送'循环。同时需将外部触发的取消信号传播到上游,终止读取链路。当状态指示停止或上游耗尽时,关闭控制器并复位响应状态,确保资源释放。
参考文档:ReadableStream
const producerStream = new ReadableStream <Uint8Array >({
start (controller ) {
function pump ( ) {
if (!isResponding.value ) {
controller.close ();
return ;
}
currentReader?.read ().then ( {
(done) {
isResponding. = ;
controller. ();
;
}
controller. (value);
();
});
}
();
},
});
({ done, value } ) =>
if
value
false
close
return
enqueue
pump
pump
转换流 上游产生的二进制字节流需要兼容粘包/半包场景。我们先将字节流解码为字符串流,再定义 TransformStream 按行拆分并过滤空行,确保下游以'完整且非空的文本行'为单位消费。在流结束的 flush() 钩子中冲刷缓冲区,避免丢失收尾数据。
const textStream = producerStream.pipeThrough (
new TextDecoderStream () as unknown as TransformStream <Uint8Array , string >
);
const lineSplitter = new TransformStream <string , string >({
transform (chunk, controller ) {
buffer += chunk;
const lines = buffer.split ('\n' );
buffer = lines.pop () || '' ;
for (const line of lines) {
const trimmed = line.trim ();
if (trimmed) controller.enqueue (trimmed);
}
},
flush (controller ) {
const trimmed = buffer.trim ();
if (trimmed) controller.enqueue (trimmed);
},
});
SSE 解析流 构建 SSE 消费者读取器并驱动 UI 增量渲染。若 done = true,表示传输层流真正结束,调用 releaseLock 释放锁;若读取到 message === '[DONE]',表示应用层结束,业务上不再需要继续读取,此时主动调用 cancel 终止读取,防止连接残留。
const sseStream = textStream.pipeThrough (lineSplitter);
const downstreamReader = sseStream.getReader ();
function consume ( ) {
downstreamReader.read ().then (({ done, value } ) => {
if (done) {
isResponding.value = false ;
downstreamReader.releaseLock ();
return ;
}
const message = value.replace (/^data:\s*/ , '' );
if (message === '[DONE]' ) {
downstreamReader.cancel ('stream finished' );
return ;
}
try {
const parsed = JSON .parse (message);
const content = parsed?.choices ?.[0 ]?.delta ?.content ;
if (content) {
streamReply += content;
if (chatList.value .length < index + 1 ) {
const newReply : ChatItem = {
key : index,
role : 'assistant' ,
content : streamReply,
};
chatList.value .push (newReply);
} else {
chatList.value [index]!.content = streamReply;
}
}
} catch (e) {
console .warn ('SSE parse error:' , e);
}
consume ();
});
}
consume ();
API 通信层
发送请求 async function fetchReply (list : ChatItem [], signal ?: AbortSignal ) {
const messages = list.map (({ role, content } ) => ({ role, content }));
return fetch ('https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions' , {
method : 'POST' ,
headers : {
Authorization : `Bearer ${import .meta.env.VITE_ALIYUN_API_KEY} ` ,
'Content-Type' : 'application/json' ,
},
body : JSON .stringify ({
model : 'qwen-plus' ,
messages,
stream : true ,
}),
signal,
});
}
中止请求
重置 UI 响应状态 :标记为非响应中,避免界面显示'正在响应'。
终止流式读取 :若存在读取器,主动取消读取并清理引用。
终止网络请求 :触发中止并清理引用。
const handleCancel = ( ) => {
isResponding.value = false ;
if (currentReader) {
try {
currentReader.cancel ('user canceled' );
} catch (e) {
console .error (e);
}
currentReader = null ;
}
if (abortController) {
try {
abortController.abort ('user cancellation' );
} catch (e) {
console .error (e);
}
abortController = null ;
}
message.error ('已取消发送' );
};
基础 UI 交互组件封装 UI 设计主要基于 Ant Design Vue 和 Ant Design X Vue。后者专注于 Vue 生态的 AI 组件库,支持 TS 和 TSX,能简化对话式 AI 应用的开发。
消息展示
功能 :展示对话历史,支持多角色渲染及 Markdown 渲染。
Markdown 渲染处理 :
import { h } from 'vue' ;
import { type BubbleProps } from 'ant-design-x-vue' ;
import { Typography } from 'ant-design-vue' ;
import markdownit from 'markdown-it' ;
const md = new markdownit ({
html : false ,
breaks : true ,
linkify : true ,
typographer : true ,
});
const renderMarkdown : BubbleProps ['messageRender' ] = (content ) =>
h (Typography , null , {
default : () => h ('div' , { innerHTML : md.render (content) }),
});
import { UserOutlined } from '@ant-design/icons-vue' ;
import { h } from 'vue' ;
const rolesAsObject = {
assistant : {
placement : 'start' ,
avatar : {
icon : h (UserOutlined ),
style : { background : '#fde3cf' },
},
typing : { step : 5 , interval : 20 },
styles : { maxWidth : '600px' },
messageRender : renderMarkdown,
},
user : {
placement : 'end' ,
avatar : {
icon : h (UserOutlined ),
style : { background : '#87d068' },
},
},
system : {
placement : 'start' ,
avatar : {
icon : h (UserOutlined ),
style : { background : '#d9d9d9' },
},
styles : { maxWidth : '600px' },
messageRender : renderMarkdown,
},
} as const ;
适配 BubbleList 组件的 roles 类型处理 :
const bubbleListRoles = rolesAsObject as NonNullable <BubbleListProps ['roles' ]>;
const bubbleItems = computed (() =>
props.chatList .map ((m, idx ) => {
type RoleKey = keyof typeof rolesAsObject;
const roleKey = (m.role in rolesAsObject ? m.role : 'assistant' ) as RoleKey ;
return {
key : m.key ?? idx,
role : m.role ,
placement : rolesAsObject[roleKey].placement ,
avatar : rolesAsObject[roleKey].avatar ,
content : m.content ,
};
})
);
<!-- ChatBubble 组件 -->
<template>
<BubbleList :items="bubbleItems" :roles="bubbleListRoles" />
</template>
<!-- 使用方法 -->
<ChatBubble :chat-list="chatList" :md-render="false" />
消息发送 <template>
<Sender
:value="props.inputText"
@update:value="onUpdateValue"
:loading="props.isResponding"
:auto-size="{ minRows: 2, maxRows: 6 }"
:onSubmit="handleSubmit"
:onCancel="handleCancel"
/>
</template>
<script setup lang="ts">
import { Sender } from 'ant-design-x-vue';
const props = defineProps({
inputText: String,
isResponding: Boolean,
});
const emit = defineEmits(['submit', 'cancel', 'update:inputText']);
const onUpdateValue = (val: string) => {
emit('update:inputText', val);
};
const handleSubmit = () => {
emit('submit', props.inputText);
};
const handleCancel = () => {
emit('cancel');
};
</script>
以上是 AI 对话页中最基础也是必不可少的部分。在实际业务场景中,还可以根据需求扩展更多交互配置,例如集成剪贴板工具实现一键复制功能等。
相关免费在线工具 RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
随机西班牙地址生成器 随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online