🚩第一章:需求分析——AI聊天各家打字机效果统一解 - stream流式传输
效果拆解
ai回复时文字规模过大,需要一个方法来让已经生成的文字提前显示在页面上,制造出正在逐渐打字出来的效果
✈️第二章:流式输出介绍
纵观所有ai对话应用,使用打字机效果的,都使用了流式传输的方法,即contentType为text/event-stream的http请求
注意:这种方式与EventSource Api不同,虽然后端都是sse,但是EventSource Api无法传递参数,而使用text/event-stream的http请求,请求方式与普通请求无异,只在接受响应时需要特殊处理。
传统HTTP请求遵循请求-响应全量交付模式(图1),服务器必须生成完整响应后才能返回数据。这种模式在AI长文本生成场景中面临两大瓶颈:
- 高延迟:用户需等待全部内容生成完成(如30秒生成1000字)
- 资源浪费:大文本传输易受网络波动影响,重传成本极高
而流式传输通过分块传送实现技术突围:
[客户端] --提问--> [服务器]
│
├── chunk1 (200ms) → 即时渲染
├── chunk2 (400ms) → 增量更新
└── chunkN (...) → 持续拼接
具体实现方式如下
xhr:
const xhrApi = () => {
const xhr = new XMLHttpRequest();
// 修改:正确使用 open 方法
xhr.open('post', 'http://dify.wujialin.top/v1/chat-messages', true);
// 修改:使用 setRequestHeader 设置请求头
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer app-7vsaRmKs8yG87fNQ4KDB1ZBE');
const requestBody = JSON.stringify({
"inputs": {
"agent_id": '100407',
"language": "en"
},
"query": "What are the specs of the iPhone 13 Pro Max?",
"response_mode": "streaming",
"conversation_id": "",
"user": "abc-123",
});
xhr.responseType = 'text';
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.OPENED) {
// 当请求已打开时,可在此处进行一些初始化操作
}
if (xhr.readyState === XMLHttpRequest.LOADING) {
// 当数据正在接收时,处理接收到的部分数据
const chunk = xhr.responseText;
console.log(chunk);
}
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
// 请求成功完成
console.log('Request completed successfully');
} else {
// 请求失败
console.error('Request failed with status:', xhr.status);
}
}
};
// 修改:使用 send 方法发送请求体
xhr.send(requestBody);
return () => {
xhr.abort();
};
}
fetch:
const fetchApi = async () => {
const controller = new AbortController();
const signal = controller.signal;
try {
// 修改:添加请求参数
const response = await fetch('http://dify.wujialin.top/v1/chat-messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer app-7vsaRmKs8yG87fNQ4KDB1ZBE'
},
body: JSON.stringify({
"inputs": {
"agent_id": '100407',
"language": "en"
},
"query": "What are the specs of the iPhone 13 Pro Max?",
"response_mode": "streaming",
"conversation_id": "",
"user": "abc-123",
}),
signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader?.read() as ReadableStreamReadResult<Uint8Array>;
if (done) break;
const chunk = decoder.decode(value);
console.log(chunk);
}
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', error);
}
}
return () => {
controller.abort();
};
}
这边主要介绍一下fetch的流式传输,fetch会响应一个类型为ReadableStream的可读流,该流通过分块(chunk)逐步传输数据,避免一次性加载全部内容到内存。这种机制通过getReader()方法获取流读取器,逐块读取数据并异步处理。但是需要注意的是,流的分块可能与业务逻辑的数据单元(如JSON对象)不一致,需设计缓冲区或使用分隔符解析。这边给出一个示例缓冲写法
const decoder = new TextDecoder('utf-8')
const reader = response.getReader()
let buffer = ''
let bufferObj: Record<string, any>
const read = () => {
let hasError = false
reader
?.read()
.then((result: any) => {
if (result.done) {
console.log('done')
return
}
buffer += decoder.decode(result.value, { stream: true })
const lines = buffer.split('\n')
// console.log(lines, 'line')
try {
lines.forEach((message) => {
if (message.startsWith('data: ')) {
// check if it starts with data:
try {
bufferObj = JSON.parse(message.substring(6)) as Record<string, any> // remove data: and parse as json
} catch (e) {
// mute handle message cut off
// console.warn(e)
return
}
console.log(bufferObj)
}
})
buffer = lines[lines.length - 1]
} catch (e: any) {
hasError = true
console.warn(e)
return
}
if (!hasError) read()
})
.catch((err) => {
console.warn(err)
})
}
read()
利用每一块之间的换行符进行分割,分割后若有多余块信息存进buffer
🏁第三章 总结
技术收获
对比一下三种长连接、类长连接的方式:
| 协议 | 特点 | 适用场景 |
|---|---|---|
| SSE(Event Source Api) | 基于HTTP长连接,服务端单向推送,轻量级 | 文本流(如聊天消息、AI回复) |
| WebSocket | 双向全双工通信,支持二进制数据,协议复杂 | 实时游戏、协同编辑、音视频流 |
| HTTP Chunked(text/event-stream) | 原生分块传输,无需额外协议,兼容性强 | 简单文本/JSON流(如API逐段输出) |
流式传输的核心是数据分块、按序推送、实时拼接,需权衡延迟、吞吐与可靠性。SSE适合轻量级文本流,WebSocket用于复杂交互,HTTP Chunked可作为兜底方案。关键难点在于异常处理与性能优化,需结合场景针对性设计。
📢📢📢预告
全新的文章系列——「前线业务开发」。这个系列将聚焦于笔者自己真实业务中的真实需求,分享从需求分析、技术选型到最终实现的完整过程。每一篇文章都将包含:
- 业务场景还原:还原真实需求背景,明确技术挑战。
- 技术方案探索:从多个角度分析可能的解决方案,记录踩坑与优化过程。
- 代码实现与优化:提供可直接复用的代码,并分享性能优化技巧。
- 总结与思考:提炼通用解法,为类似场景提供参考。
当然,笔者还是小牛马,更新时间不定请谅解🫡🫡🫡🫡🫡大家有想分享的解决的没解决的需求,也可以私信我一起探讨