news 2026/5/1 6:11:00

全栈实现流式 AI 聊天机器人:从后端代理到前端实时渲染

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
全栈实现流式 AI 聊天机器人:从后端代理到前端实时渲染

全栈 AI Chatbot 实战:从后端流式转发到前端即时响应

在当今 AI 驱动的应用开发中,构建一个类似 ChatGPT 的对话界面已成为“Hello World”级别的必修课。然而,要实现一个丝滑的、具备打字机效果的聊天机器人,并非简单的 API 调用,而是涉及 前后端流式数据传输(Streaming) 、 服务端代理(Proxying) 以及 前端状态管理 的综合工程。

本文将通过模块化的方式,拆解一个全栈 AI Chatbot 的核心实现流程,带你深入理解数据是如何从 DeepSeek 大模型流向用户屏幕的。


后端部分(Mock 模拟)

后端的职责非常明确:它是一个“流式代理中间件”
它需要同时维护两条连接:

  • 左手与前端保持长连接,
  • 右手与大模型保持流式连接,
    并在中间进行数据的清洗和格式转换。

定义路由以及处理请求

当用户向 Chatbot 发送请求时,后端需要接收前端的请求体(包含用户消息),并将其转发给大模型。

import { config } from 'dotenv' config(); export default [ { url: '/api/ai/chat', method: 'post', rawResponse: async (req, res) => { let body = ''; req.on('data', (chunk) => { body += chunk }) req.on('end', async () => { try { const { messages } = JSON.parse(body); // ... 后续逻辑 } catch (err) { res.end(); } }) } } ]
核心代码与逻辑:
  • rawResponse
    使用rawResponse就像是从“自动挡”切换到了“手动挡”。
    虽然你需要自己处理req.on('data')res.end()等底层琐事,但它给了你控制时间的能力——让你能够决定什么时候发送数据,以及分多少次发送数据。这正是实现打字机效果的唯一途径。

  • req.on()
    这就是一个监听事件:

    • 'data'事件:当请求体有数据到达时触发。这里每当有数据传输就进行拼接。
    • 'end'事件:当请求体数据接收完毕时触发。表示用户的问题已经接收完全,可以开始调用大模型了。
  • res.end()
    因为我们使用rawResponse获得了绝对的控制权,所以当大模型返回完所有数据后,需要手动调用res.end()来结束响应。

通过字符串拼接,将二进制数据转化为字符串,因此可以通过JSON.parse(body)将字符串解析为 JSON 对象。


流式调用大模型 API

这里负责调用大模型的 API,获取流式响应。

// ... 在 req.on('end') 内部 res.setHeader('Content-Type', 'text/plain;charset=utf-8') res.setHeader('Transfer-Encoding', 'chunked') 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 }) });
核心代码与逻辑:
  • 三个res.setHeader

    1. Content-Type: text/plain;charset=utf-8

      • 作用:声明货物类型。
      • 解释:告诉浏览器“我发给你的是纯文本,不是 HTML 网页,也不是图片”。如果不写charset=utf-8,中文可能会变成乱码。
    2. Transfer-Encoding: chunked

      • 作用:声明发货方式(分批次)。
      • 解释:这是流式传输的开关。它告诉浏览器:“这个包裹太大(或者我还没生产完),我无法在发货前告诉你总重量(Content-Length)。我会切成一块一块地发给你,直到我说发完了为止。”
      • 后果:如果不写这个,浏览器可能会一直等到服务器把所有数据都准备好(res.end)才开始显示内容,那就没有打字机效果了。
    3. x-vercel-ai-data-stream: v1

      • 作用:对暗号。
      • 解释:这是前端使用的 Vercel AI SDK(useChat)特有的自定义协议头。前端 SDK 收到响应时,会检查有没有这个头。如果有,它就知道:“哦!这是自家兄弟发的流式数据,格式我懂(0:"xxx"),我可以自动解析它。”
      • 后果:如果不写,前端 SDK 可能会认为这是一个普通的文本响应,导致无法正确解析流内容或者报错。
  • const response = await fetch(...)
    调用大模型 API。


SSE 数据清洗与实时转发

将大模型回复生成的数据进行流式的清洗与转发。

// ... 接上文 const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); 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) {} } } }
核心代码与逻辑:
  • const reader = response.body.getReader();
    用于流式读取数据的对象。

  • const decoder = new TextDecoder();
    因为大模型返回的是二进制数据,所以需要使用TextDecoder来将其解码为文本。

  • const { done, value } = await reader.read();
    每次读取数据块,done表示是否读取完毕,value表示读取到的二进制数据。

  • const chunk = decoder.decode(value);
    const lines = chunk.split('\n');
    将每次读取的数据块进行解码与分块。

  • for(){...}—— 数据的清洗
    通过解码我们已经能够拿到数据,但是要想像聊天一样获得纯文本,就必须经过数据清洗:

    • if (line.startsWith('data: ') && line !== 'data: [DONE]')
      大模型返回的每个数据块以data:开头代表的就是我们需要的数据;当输出[data: [DONE]]时,代表大模型已经返回完所有数据。通过条件判断当前数据块是我们的有效数据。
    • const data = JSON.parse(line.slice(6));
      切割掉每条数据前的data:,将剩余的字符串解析为 JSON 对象。
    • const content = data.choices[0]?.delta?.content || '';
      拿到每次新增的内容字段(delta.content)。
    • res.write(0:${JSON.stringify(content)}\n)
      将新增的字段内容转发给前端。

前端部分

这里负责接收后端转发的流式数据,并将其显示在前端的聊天框中。


useChat

useChat是 Vercel AI SDK 提供的一个 React 钩子函数,用于在 React 组件中调用大模型 API 并处理流式响应。

import { useChat } from '@ai-sdk/react' export const useChatbot = () => { return useChat({ api: '/api/ai/chat', onError: (err) => { console.error("Chat Error:", err); } }) }

它带来的便利主要体现在以下三个方面:


1. 自动化的状态管理(省去了手动维护数组)
  • 痛点
    如果不使用useChat,你需要自己创建一个messages数组状态。每当用户发消息,你要手动push进去;每当 AI 回复,你又要手动更新最后一条消息。

  • 便利
    useChat自动维护messages

    • 用户发消息 → 自动追加到列表。
    • AI 流式回复 → 自动拼接到最后一条消息里,无需你写任何拼接逻辑。

2. 封装了复杂的流式网络请求(省去了手写fetchreader
  • 痛点
    手动处理流式响应非常麻烦。你需要写fetch,获取reader,写while循环读取流,用TextDecoder解码,还要处理 JSON 解析和错误。

  • 便利
    useChat内部已经写好了全套的流处理逻辑。

    • 你只需要配置一个api: '/api/ai/chat'
    • 它会自动发起请求,自动监听数据流,自动解析后端发来的0:"xxx"格式。

3. 开箱即用的 UI 交互逻辑(省去了写受控组件)
  • 痛点
    你需要自己处理输入框的onChange,自己处理表单的onSubmit,还要防止用户在生成过程中重复点击发送。

  • 便利

    • 提供inputhandleInputChange:直接绑定到输入框,实现双向绑定。
    • 提供handleSubmit:直接绑定到表单,自动处理提交。
    • 提供isLoading:自动告诉你 AI 是否正在生成,方便你禁用按钮或显示 Loading 动画。

UI 交互与渲染

通过 Vercel AI SDK 提供的useChat钩子函数,我们可以很方便地实现聊天界面的交互与渲染。我们只需要关心页面设计,其他的都交给useChat打理。

import { useChatbot } from '@/hooks/useChatBot'; // ... UI 组件导入 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="..."> {/* 消息列表区域 */} <ScrollArea className="..."> {messages.map((m, idx) => ( <div key={idx} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}> <div className={`... ${m.role === 'user' ? 'bg-primary' : 'bg-muted'}`}> {m.content} </div> </div> ))} </ScrollArea> {/* 输入区域 */} <form onSubmit={onSubmit} className="flex gap-2"> <Input value={input} onChange={handleInputChange} disabled={isLoading} /> <Button type="submit" disabled={isLoading}>Send</Button> </form> </div> ) }
核心代码和逻辑:
  • Optimistic UI(乐观更新)
    当你调用handleSubmit的瞬间,你的消息就会立即出现在messages列表中,无需等待服务器响应。这提供了极佳的流畅感。
  • 数据驱动视图
    注意看{m.content}这一行。我们没有写任何定时器或手动 DOM 操作来实现打字机效果。一切都是响应式的:
    后端推来一个字 → SDK 更新messages状态 → React 检测到状态变化 → 重新渲染组件 → 界面上多出一个字。
  • 受控组件
    <Input>valueonChange直接绑定了 SDK 提供的inputhandleInputChange。这意味着输入框的状态管理权也完全移交给了 SDK,减少了我们自己写useState的冗余代码。

总结:数据流的全景图

构建这个 AI Chatbot,本质上是搭建了一条从用户到 AI 再回到用户的高速数据管道

  1. 触发:用户点击发送,useChat携带历史消息发起请求。
  2. 中转:后端req.on接收数据,向 DeepSeek 发起stream: true的请求。
  3. 生成:DeepSeek 逐个生成 Token。
  4. 清洗:后端reader截获 Token,去除 SSE 外壳。
  5. 推送:后端res.write将 Token 实时推回前端。
  6. 渲染:前端 SDK 接收 Token,更新状态,React 刷新界面。

通过这种模块化、分层的设计,我们不仅实现了酷炫的打字机效果,更保证了系统的可维护性和扩展性。

学习资源推荐

如果你想更深入地学习大模型,以下是一些非常有价值的学习资源,这些资源将帮助你从不同角度学习大模型,提升你的实践能力。

一、全套AGI大模型学习路线

AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!​

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取

二、640套AI大模型报告合集

这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示

​因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取

三、AI大模型经典PDF籍

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取

四、AI大模型商业化落地方案

作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量。

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

Telegram APP技术架构分析报告

Telegram APP技术架构分析报告 目录 一、核心结论二、应用格式与模块结构三、原生架构分析四、网络层与MTProto五、媒体处理技术栈六、安全与加密七、业务逻辑与UI架构八、数据存储九、第三方与外部依赖十、技术架构总结十一、总结 一、核心结论 Telegram Android 采用纯原生…

作者头像 李华
网站建设 2026/4/22 20:09:11

32岁,代码还没写完,人没了

文章目录 「管理者要陪团队一起扛压力」心电图正常≠没事工伤认定&#xff1f;写给还在卷的兄弟们最后说两句 前几天刷到这条新闻的时候&#xff0c;我正在公司改一个线上bug&#xff0c;改着改着手就停了。 广州&#xff0c;32岁&#xff0c;程序员&#xff0c;猝死。 这几个…

作者头像 李华
网站建设 2026/4/17 23:28:56

SCI写作避坑|从大纲到查重全流程干货,新手也能快速上手✨

作为常年和SCI打交道的科研人&#xff0c;今天不聊虚的&#xff0c;纯纯干货分享——毕竟谁没在SCI写作里踩过坑呢&#xff1f; 说真的&#xff0c;写SCI最磨人的不是实验数据不够&#xff0c;而是从一开始的大纲搭建就卡壳&#xff0c;好不容易搭完大纲&#xff0c;绘图制表、…

作者头像 李华
网站建设 2026/4/29 0:17:15

【开题答辩过程】以《基于Spring Boot的疗养院理疗管理系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看

个人简介慕婉学姐精通Java、PHP、微信小程序、Python、Golang和安卓开发等语言&#xff0c;擅长开发大数据、深度学习、网站、小程序、安卓应用和算法项目。平时从事项目定制开发、代码讲解、答辩教学和文档编写&#xff0c;也掌握一些降重技巧。感谢大家的持续关注&#xff01…

作者头像 李华
网站建设 2026/4/25 9:43:16

学术“降重革命”:书匠策AI如何用“语义显微镜”重塑论文查重新范式

在学术写作的江湖里&#xff0c;“查重”二字如同一把悬在头顶的达摩克利斯之剑。无论是本科生为毕业论文焦头烂额&#xff0c;还是硕博生为期刊投稿殚精竭虑&#xff0c;查重率始终是绕不开的“生死线”。传统查重工具像一台笨重的“文字扫描仪”&#xff0c;只能机械比对字符…

作者头像 李华