AI对话页的流式处理架构:基于Web Streams+Fetch API的实践

AI对话页的流式处理架构:基于Web Streams+Fetch API的实践

引言

        当前AI浪潮下,基于各大agent平台,我们可以在几分钟内就搭建出一个具备页面交互的智能体,从问答输出到页面交互,这个过程中的数据流转、UI实现被统一封装以降低模型搭建复杂度。为了探索这个过程的底层实现,我们采用“生产者-消费者模式”的流式处理架构,将网络IO、数据解码、文本解析与UI渲染解耦,实现实时流式响应、UI增量渲染。

使用框架如下:

  • 前端框架:Vue 3 + TypeScript + Vite
  • UI组件库:Ant Design Vue、Ant Design X Vue
  • 流处理:Web Streams API + Fetch API

        从请求发送到UI渲染,流程如下:

流式响应处理

请求管理

  1. 采用 AbortControllerReadableStreamDefaultReader 实现“上游网络请求中止”和“下游字节流读取控制”,共同实现一次会话的可取消、可停止的流式处理。
  2. AbortController:管理当前请求的“上游网络中止”句柄,用于在开始新的提问前中止上一轮未完成的请求,或用户点击取消时终止本次请求。
  3. ReadableStreamDefaultReader:管理“下游传输层字节流”的读取器句柄,用来驱动上游生产者向管道入队字节块,以及在用户点击取消时终止字节读取。
// 流控制相关句柄 let abortController: AbortController | null = null; let currentReader: ReadableStreamDefaultReader<Uint8Array> | null = null;

流处理管道

        建立 Web Streams 流式解析管线:生产者 → 解码 → 按行拆分 → 消费者。整体处理流程如下所示:

生产者流

        负责把上游 reader 的chunk字节块统一按流的背压节奏入队,供下游统一消费,实现“读-推送”连续循环。并将下游或外部触发的取消信号正确传播到上游,终止读取链路。当外部状态指示停止或上游耗尽时,关闭控制器并复位响应状态,保证资源释放与状态一致性。

实现原理可参考文档:https://developer.mozilla.org/zh-CN/docs/Web/API/ReadableStream

 const producerStream = new ReadableStream<Uint8Array>({ start(controller) { function pump() { if (!isResponding.value) { controller.close(); return; } currentReader?.read().then(({ done, value }) => { if (done) { isResponding.value = false; controller.close(); return; } controller.enqueue(value); // 推送字节块 pump(); }); } pump(); }, });

转换流

        当上游将生产的字节块入队后,我们构建一条流式处理管道,兼容粘包/半包:先将上游的二进制字节流解码为字符串流,再定义转换流(TransformStream类型)按行拆分并过滤空行,确保下游以“完整且非空的文本行”为单位消费数据。并在流结束的 flush() 钩子中,再次冲刷缓冲区,以处理可能残留的最后一行,避免丢失收尾数据,最后得到的是由一对可读流和可写流组成的TransformStream

const textStream = producerStream.pipeThrough(new TextDecoderStream() as unknown as TransformStream<Uint8Array, string>); let; 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 终止读取,cancel() 方法返回一个 Promise,这个Promise 在流被取消时兑现,消费者在流中调用该方法发出取消流的信号。

const sseStream = textStream.pipeThrough(lineSplitter); // ReadableStream const downstreamReader = sseStream.getReader(); // ReadableStreamDefaultReader function consume() { downstreamReader.read().then(({ done, value }) => { if (done) { isResponding.value = false; downstreamReader.releaseLock(); return; } const message = value.replace(/^data:\s*/, ''); if (message === '[DONE]') { …… } 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通信层

发送请求

        以qwen-plus模型为例,其接入方式如下:

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, }); }

中止请求

  1. 重置 UI 响应状态:将会话状态标记为非响应中,避免界面继续显示“正在响应”等状态提示
  2. 终止流式读取:如果存在当前的流读取器,则主动取消读取,防止继续从流中消费数据,并清理引用
  3. 终止网络请求:如果存在未完成的流,则触发终止,并清理引用
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 VueAnt Design X Vue组件库,其中 Ant Design X Vue 专注于Vue生态的先进AI组件库,旨在简化对话式AI应用的开发,同时支持tstsx

消息展示

  1. 功能:展示对话历史,支持多角色渲染,支持Markdown渲染。
  2. 处理 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) }), });

3. 角色映射配置:

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;

4.  适配 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, // 绑定role,对应rolesAsObject中的配置项 placement: rolesAsObject[roleKey].placement, avatar: rolesAsObject[roleKey].avatar, content: m.content, }; }) );

5. 最后将 bubbleItems 和 bubbleListRoles 传入 BubbleList 组件
 

// ChatBubble组件 <template> <BubbleList :items="bubbleItems" :roles="bubbleListRoles" /> </template> // 使用方法 <ChatBubble :chat-list="chatList" :md-render="false" />

消息发送

  1. 功能:支持消息输入、发送控制、状态展示
<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对话页中最基础也是必不可少的部分,基于业务背景和用户体验提升,我们还可以添加更多的交互配置,比如还可以使用 vue-clipboard3 库中的 toClipboard 方法实现一键复制功能,等等。

Read more

Java 大视界 -- Java+Spark MLlib 构建智能推荐系统:协同过滤算法实战与优化(441)

Java 大视界 -- Java+Spark MLlib 构建智能推荐系统:协同过滤算法实战与优化(441)

Java 大视界 -- Java+Spark MLlib 构建智能推荐系统:协同过滤算法实战与优化(441) * 引言: * 正文: * 一、 推荐系统整体架构设计:从业务场景出发,搭建高可用架构 * 1.1 架构设计核心原则:贴合业务,兼顾性能与可扩展性 * 1.2 全链路架构图:纵向布局,清晰呈现核心模块 * 1.3 核心模块职责:分工明确,形成闭环 * 1.3.1 数据采集层 * 1.3.2 数据处理层 * 1.3.3 模型层 * 1.3.4 推荐生成层 * 1.3.5 存储层

By Ne0inhk
【Linux系统编程】(四十)线程控制终极指南:从资源共享到实战操控,带你吃透线程全生命周期

【Linux系统编程】(四十)线程控制终极指南:从资源共享到实战操控,带你吃透线程全生命周期

前言         在 Linux 多线程开发中,“线程控制” 是贯穿始终的核心技能 —— 从线程的创建、终止,到等待、分离,每一步操作都直接影响程序的性能、稳定性和资源利用率。而要熟练掌握线程控制,首先必须理清一个关键问题:进程和线程究竟哪些资源共享、哪些资源独占?这是理解线程控制逻辑的底层基石。         很多开发者在编写多线程程序时,常会陷入这样的困境:明明调用了pthread_create却创建失败,线程退出后出现资源泄漏,用pthread_join等待线程却始终阻塞,甚至因误操作导致整个进程崩溃。这些问题的根源,往往是对线程与进程的资源关系理解不深,或是对 POSIX 线程库的控制接口使用不当。         本文将从 “进程与线程的资源划分” 入手,层层递进讲解 Linux 线程的完整控制流程 —— 包括 POSIX 线程库的使用、线程创建、终止、等待、分离等核心操作,全程结合实战代码和底层原理,用通俗的语言拆解复杂概念,让你不仅 “会用” 线程控制接口,更能 “懂原理”

By Ne0inhk
内网安全部署:Java + OpenClaw 本地大模型私有化方案

内网安全部署:Java + OpenClaw 本地大模型私有化方案

文章目录 * 前言 * 一、开篇:你的数据正在裸奔吗? * 二、技术栈选型:为什么选这三兄弟? * 2.1 本地大模型:Ollama是傻瓜相机 * 2.2 OpenClaw:AI界的机械臂 * 2.3 Java:老当益壮的底盘 * 三、架构设计:三层铁桶怎么搭? * 3.1 数据流向图(脑补版) * 3.2 安全边界划分 * 四、环境搭建:从0到1手摸手 * 4.1 本地模型部署(Ollama) * 4.2 OpenClaw安装与配置 * 4.3 Java项目准备 * 五、Java集成实战:代码说话 * 5.1 基础对话接口 * 5.

By Ne0inhk
Java 手写 AI Agent:ZenoAgent 实战笔记

Java 手写 AI Agent:ZenoAgent 实战笔记

摘要:作为一个长期使用 Java 的后端开发者,我对 AI Agent 的内部运作机制充满了好奇。为了深入理解 Agent 的工作原理,我决定动手写一个简单的 Agent 系统 —— ZenoAgent。本文记录了我在这个过程中的学习心得与技术实践,包括如何手写 ReAct 循环、在分布式环境下实现 Human-in-the-loop、尝试复刻类 o1 的流式思考以及探索错误处理机制。希望这些踩坑经验能给同样想探索 AI 的 Java 开发者一些参考。 👀 在线体验:项目已部署上线,欢迎试玩:线上部署地址 (注:受限于服务器资源,线上本地部署了 Qwen3:8B 模型(参见另一篇博文华为云服务器本地部署大模型实战),虽不如商业模型聪明,但足以演示 Agent 的核心能力) 💡 写在前面:我的学习初衷 市面上已经有了像 LangChain 和 AutoGen

By Ne0inhk