在AI聊天应用开发中,流式输出 已成为提升用户体验的关键特性。传统的一次性响应往往导致用户等待时间过长,而流式输出允许模型生成的文本逐 token 实时传输,像打字机一样逐步显示内容。这不仅使交互更自然,还能让用户感知到应用的响应速度更快。本文将基于一个实际项目,分享如何使用 React 前端结合自定义 Hook ,以及 Mock 后端集成DeepSeek模型,实现支持流式输出的聊天机器人。项目强调前后端分离、响应式设计和HTTP协议优化,适用于开发者快速构建原型或学习AI集成。
流式输出的本质在于LLM(Large Language Model)生成token的过程:模型通过自回归方式,基于已有序列预测下一个token,而不是等待全部内容生成后再返回。在AI场景中,这意味着前端可以边接收边渲染,提升互动性。技术栈上,前端采用React与shadcn/ui组件库,实现优雅UI;后端使用Mock.js模拟API,调用DeepSeek的chat completions接口,并处理SSE(Server-Sent Events)流。
通过这个项目,读者可以掌握HTTP chunked传输、Node.js原生请求体解析,以及React Hook在聊天业务中的应用。接下来,我们逐步剖析架构、代码和知识点。
项目架构概述
项目采用前后端分离模式,前端负责UI渲染和用户交互,后端(Mock)处理AI调用和流式响应。
- 前端:React应用,使用自定义Hook如useChatBot封装聊天逻辑,包括消息管理、输入处理和提交。UI组件来自shadcn/ui,如ScrollArea优化滚动体验,Input和Button构建表单。
- 后端:通过Mock.js模拟API路由 /api/ai/chat,接收POST请求,调用DeepSeek API(stream: true),并转发流式数据。使用dotenv管理API密钥,确保安全。
- AI集成:DeepSeek-chat模型,提供chat completions接口,支持流式输出。响应通过SSE格式处理,逐chunk发送token。
这种架构便于调试:前端可独立运行,后端Mock模拟真实服务。实际部署时,可替换Mock为Express或Next.js API路由。
前端实现详解
前端的核心是Chat组件,构建一个全屏聊天界面,支持消息显示、输入和加载动画。使用React Hook剥离业务逻辑,确保组件专注UI。
chat.tsx:聊天组件
组件代码如下:
typescriptreact
javascript
import {
useEffect,
} from 'react';
import Header from '@/components/Header'
import {
useChatBot
} from '@/hooks/useChatBot'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
export default function Chat() {
const {
messages,
input,
handleInputChange,
handleSubmit,
isLoading,
} = useChatBot();
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
handleSubmit(e);
}
return (
<div className="flex flex-col h-screen max-w-4xl mx-auto p-4 pb-2">
<Header title="DeepSeek Chat" showBackBtn={true} />
{/* html 原生滚动条不太好看,体验不好
shadcn ScrollArea 样式和体验上优化
*/}
<ScrollArea className="flex-1 border rounded-lg p-4 mb-4 bg-background">
{
messages.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
Start a conversation with DeepSeek
</div>
) : (
<div className="space-y-4">
{
messages.map((m, idx) => (
<div
key={idx}
className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[80%] rounded-lg px-4 py-2 ${m.role === 'user'
? 'bg-primary text-primary-foreground'
: 'bg-muted'
}`}
>
{m.content}
</div>
</div>
))
}
{isLoading && (
<div className="flex justify-start">
<div className="bg-muted rounded-lg px-4 py-2">
<span className="animate-pulse">...</span>
</div>
</div>
)}
</div>
)
}
</ScrollArea>
<form onSubmit={onSubmit} className="flex gap-2">
<Input value={input} onChange={handleInputChange}
placeholder="Type your message..."
disabled={isLoading}
className="flex-1 "
/>
<Button type="submit" disabled={isLoading || !input.trim()}>
Send
</Button>
</form>
</div>
)
}
组件使用useChatBot Hook获取状态:messages(消息数组)、input(输入值)、handleInputChange(输入变更)、handleSubmit(提交处理)、isLoading(加载中)。onSubmit函数防止空输入提交。
UI设计:Header提供标题和返回按钮。ScrollArea包裹消息区,优化滚动条样式和体验。如果无消息,显示提示;否则,映射messages渲染泡泡式对话,用户消息右对齐,AI左对齐。isLoading时,显示动画"..."表示思考中。
表单部分:Input绑定input和handleInputChange,禁用加载中;Button提交,禁用无输入或加载。整体布局flex-col h-screen,确保全屏适配,max-w-4xl居中。
这种设计提升用户体验:实时反馈、禁用防止重复提交,shadcn组件确保美观一致。
useChatBot Hook:业务逻辑封装
虽然代码未提供完整Hook,但从组件可见,它管理聊天状态。典型实现中使用useState存储messages和input,useEffect可能监听流式更新。handleSubmit发送请求到 /api/ai/chat,追加用户消息,然后处理流式响应,逐token追加AI消息。isLoading控制UI状态。
Hook的优势:剥离响应式业务(如API调用、状态更新),让组件单一。类似vercel ai-sdk/react的useChat,但自定义更灵活,可集成流式监听。
后端(Mock)实现详解
后端使用Mock.js定义路由,支持流式输出。代码处理原始HTTP响应,实现chunked传输。
mock/chat.js:流式API模拟
代码如下:
JavaScript
javascript
// 流式输出本质是变算(llm token 生成)边给,而不是等全部结果生成再一次性返回
// AI场景中,模型生成文本是逐个token 产生的(模型每次基于已生成的token 序列)
// 通过自回归方式预测下一个最可能的方式预测下一个最可能的token
// streaming:true
// http chunked 数据块来传 不用res.end()
// res.write(chunk)
// SSE 服务器发送事件(Server-Sent Events)
// text/event-stream 模式去发送token
import { config } from 'dotenv';
config();
export default [
{
url: "/api/ai/chat",
method: "post",
// rawResponse 用于自定义原始的 HTTP 响应(如流式输出)
// 而 response 通常指封装后的结构化响应
rawResponse: async (req, res) => {
// node 原生地去拿到请求体
// console.log("/////[][][]/////");
// chunk 数据块(buffer)
// tcp/ip tcp:可靠的传输协议
// 按顺序组装,失败重传 html
// on data
let body = '';
// chunk 二进制流 buffer
// 最后把 buffer 转成字符串
req.on('data', (chunk) => { body += chunk })
// 数据接收完成
req.on('end', async () => {
// 都到位了
console.log(body);
try {
const { messages } = JSON.parse(body);
// console.log(messages);
res.setHeader('Content-Type', 'text/plain;charset=utf-8');
// 响应头先告诉浏览器 这是流式的 数据会分块传输
res.setHeader('Transfer-Encoding', 'chunked');
// vercel ai sdk 特制头
res.setHeader('x-vercel-ai-data-stream', 'v1');
const response = await fetch("https://api.deepseek.com/v1/chat/completions", {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.VITE_DEEPSEEK_API_KEY}`
},
body:JSON.stringify({
model: "deepseek-chat",
messages: messages,
stream: true // 流式输出
})
})
// console.log(process.env.VITE_DEEPSEEK_API_KEY, "[][][]{}{}{}{[][][]")
if (!response.body) throw new Error("No response body");
// SSE:二进制流 有个reader 对象 接根管子一样
const reader = response.body.getReader();
// 用于将ArrayBuffer 或 TypedArray(如 Uint8Array) 转换为字符串
const decoder = new TextDecoder();
while(true) {
const { done, value } = await reader.read(0);
// console.log(done, value, '--------------');
if (done) break;
const chunk = decoder.decode(value);
// console.log(chunk, "------")
const lines = chunk.split('\n');
for (let line of lines) {
if (line.startsWith('data:') && line !== 'data: [DONE]') {
try {
const data = JSON.parse(line.slice(6));
const content = data.choices[0]?.delta?.content || '';
if (content) {
res.write(`0:${JSON.stringify(content)}\n`);
}
} catch (err) {
}
}
}
}
res.end();
} catch (err) {
}
})
}
}
]
使用dotenv加载环境变量,如API密钥。rawResponse异步处理原始响应:通过req.on('data')和req.on('end')原生解析请求体(body),避免中间件依赖。
解析后,提取messages,设置响应头:Content-Type为text/plain,Transfer-Encoding: chunked表示分块传输,x-vercel-ai-data-stream兼容SDK。
fetch调用DeepSeek API,stream: true启用流。获取response.body的ReadableStream,使用getReader()读取chunk。TextDecoder解码二进制为字符串,拆分行,过滤data:开头,解析JSON提取delta.content,res.write发送格式化chunk(如"0:"content"\n")。
循环直到done,res.end()结束响应。这种方式实现SSE-like流,支持前端事件监听。
关键知识点:流式输出与HTTP协议
流式输出的核心是LLM token生成:模型自回归预测下一个token,前端边收边显,提升感知速度。
HTTP方面:使用Keep-Alive连接持久化,Transfer-Encoding: chunked分块传输,无需Content-Length。res.write(chunk)逐块发送,不用一次性end。
SSE(Server-Sent Events):text/event-stream模式,浏览器EventSource监听message事件。代码中虽用chunked,但格式兼容SSE(data: JSON),前端可解析。
TCP/IP基础:请求体chunk是Buffer,按序组装,确保可靠传输。
前端集成:useChatBot可能用EventSource或fetch stream监听,逐token追加messages。
关键知识点:Mock.js与AI SDK
Mock.js的rawResponse自定义响应,适合模拟流式API,便于开发测试。
vercel ai-sdk/react提供useChat等Hook,封装流式处理,但自定义实现更理解底层:如reader.read()循环处理chunk,忽略[DONE]结束信号。
安全考虑:dotenv管理密钥,避免硬编码。try-catch容错,防止崩溃。
结语
通过这个支持流式输出的AI聊天应用,我们看到React Hook和HTTP chunked的强大结合。流式设计不仅优化体验,还深化对AI生成过程的理解。