96SEO 2026-04-20 12:29 1
构建一个属于自己的 AI 聊天机器人Yi经不再是遥不可及的梦想。无论是为了提升工作效率,还是单纯为了探索技术的边界,React 与 Node.js 的组合无疑是全栈开发者的“黄金搭档”。今天我们就来一场硬核的实战演练,不搞那些虚头巴脑的理论,直接上手,kankan如何从零开始,搭建一个支持流式响应、多模型切换的现代化 AI 聊天应用。

在敲下第一行代码之前,我们得先搞清楚整个系统的运作逻辑。传统的 HTTP 请求-响应模式在 AI 对话场景下显得有些笨重——你发个请求,然后干等十几秒,Zui后才收到一大段文字。这种体验,说实话,有点像回到了拨号上网时代。
为了解决这个问题,我们引入了 Server-Sent Events 技术。简单来说就是后端像挤牙膏一样,把 AI 生成的文字一点一点“流”向前端。这样一来用户就Nengkan到 AI 正在“思考”和“打字”的过程,交互感瞬间拉满。
我们的技术选型非常明确:
前端React 18 + Vite。
后端Node.js + Express,负责对接大模型 API并转发流式数据。
部署前端扔给 Vercel,后端Ke以考虑阿里云函数计算或者 ECS,主打一个性价比。
后端实现:Node.js 作为智Neng中枢后端的核心任务非常单纯:接收前端的消息,转发给 LLM,然后把模型的流式输出原封不动地转手给前端。但别小kan这个“转发”,里面有不少门道。
我们需要搭建一个 Express 服务器。为了支持多模型切换,我们需要在后端Zuo一个简单的配置映射。
环境配置与依赖安装新建一个 `server` 目录,初始化项目并安装必要的依赖。这里我们需要 `express` 来起服务,`openai` 库来调用接口,以及 `cors` 和 `dotenv` 来处理跨域和环境变量。
mkdir ai-chat && cd ai-chat
mkdir server && cd server
npm init -y
npm install express openai cors dotenv
别忘了在 `package.json` 里加上 `"type": "module"`,毕竟现在是 ES Modules 的天下了。创建一个 `.env` 文件,把你的 API Key 妥善藏好:
DEEPSEEK_API_KEY=sk-xxx
DASHSCOPE_API_KEY=sk-xxx
核心路由与流式处理
接下来是重头戏。我们需要编写 `server/index.js`。这里的关键在于如何正确处理 SSE 流。我们要设置特定的 HTTP 头部,告诉前端“我要开始发流了别断开”。
import express from 'express';
import cors from 'cors';
import OpenAI from 'openai';
import 'dotenv/config';
const app = express;
app.use);
app.use);
// 多模型配置:key 是前端传来的 modelId
const MODEL_CONFIGS = {
'deepseek-chat': {
client: new OpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: 'https://api.deepseek.com/v1',
}),
model: 'deepseek-chat',
},
'deepseek-r1': {
client: new OpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: 'https://api.deepseek.com/v1',
}),
model: 'deepseek-reasoner',
},
'qwen-max': {
client: new OpenAI({
apiKey: process.env.DASHSCOPE_API_KEY,
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
}),
model: 'qwen-max',
},
'qwen-turbo': {
client: new OpenAI({
apiKey: process.env.DASHSCOPE_API_KEY,
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
}),
model: 'qwen-turbo',
},
};
// POST /api/chat — 返回 SSE 流式响应
app.post => {
const { messages, modelId = 'deepseek-chat' } = req.body;
const config = MODEL_CONFIGS;
if {
return res.status.json;
}
// 设置 SSE headers
res.setHeader;
res.setHeader;
res.setHeader;
res.flushHeaders;
try {
const stream = await config.client.chat.completions.create({
model: config.model,
messages,
stream: true,
max_tokens: 2000,
});
for await {
const delta = chunk.choices?.delta?.content;
if {
// SSE 格式:data:
res.write}
`);
}
}
res.write;
} catch {
console.error;
res.write}
`);
} finally {
res.end;
}
});
app.listen => console.log);
前端实现:React 打造丝滑交互
后端搞定后前端的工作就是把这些数据漂亮地展示出来。我们使用 Vite 来创建 React 项目,因为它比 Create React App 快太多了。
cd .. && npm create vite@latest client -- --template react
cd client
npm install
核心 Hook:useChat.js
这是整个前端的大脑。我们需要管理消息列表、处理流式数据的接收、以及自动滚动到底部。这里有个坑:网络传输的数据包可Neng会把一行 SSE 数据切断,或者把多行数据粘在一起。所以我们必须维护一个 `buffer` 来处理这种“分包”现象。
import { useState, useCallback, useRef } from 'react';
const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:3000';
export function useChat {
const = useState => {
// localStorage 持久化:加载历史对话
try {
const saved = localStorage.getItem;
return saved ? JSON.parse : ;
} catch {
return ;
}
});
const = useState;
const = useState;
const abortRef = useRef;
// 持久化到 localStorage
const saveHistory = => {
try {
// 只保存Zui近 20 条,防止 localStorage 爆满
localStorage.setItem));
} catch {}
};
const sendMessage = useCallback => {
if || streaming) return;
const userMsg = { role: 'user', content: userText };
const newMessages = ;
setMessages;
// 添加一条空的 assistant 消息,稍后流式填充
const assistantMsg = { role: 'assistant', content: '' };
setMessages;
setStreaming;
const controller = new AbortController;
abortRef.current = controller;
try {
const res = await fetch(`${API_BASE}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify,
signal: controller.signal,
});
if throw new Error;
const reader = res.body.getReader;
const decoder = new TextDecoder;
let buffer = ''; // 关键:处理分包的 buffer
let accumulated = '';
while {
const { done, value } = await reader.read;
if break;
buffer += decoder.decode;
const lines = buffer.split;
buffer = lines.pop; // Zui后一行可Neng不完整,留到下次
for {
if ) continue;
const payload = line.slice;
if break;
try {
const { content, error } = JSON.parse;
if throw new Error;
if {
accumulated += content;
// 实时geng新Zui后一条 assistant 消息
setMessages => {
const updated = ;
updated = {
role: 'assistant',
content: accumulated,
};
return updated;
});
}
} catch {
console.error;
}
}
}
// 流结束后持久化
const finalMessages = ;
saveHistory;
} catch {
if {
setMessages => {
const updated = ;
updated = {
role: 'assistant',
content: `请求失败:${err.message}`,
};
return updated;
});
}
} finally {
setStreaming;
}
}, );
const stopStreaming = => abortRef.current?.abort;
const clearHistory = => {
setMessages;
localStorage.removeItem;
};
return { messages, streaming, modelId, setModelId, sendMessage, stopStreaming, clearHistory };
}
UI 组件组装
有了 Hook,剩下的就是拼积木了。我们需要一个模型选择器、一个消息展示窗口,以及一个输入框。
这里强烈推荐使用 `react-markdown` 和 `react-syntax-highlighter`,不然 AI 输出的代码块会是一团乱麻,可读性极差。
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
export function MessageItem {
const isUser = role === 'user';
return (
{isUser ? '你' : 'AI'}
{isUser ? (
{content}
) : (
{String.replace}
) : (
{children}
);
},
}}
>
{content || '▋'} {/* 流式未完成时显示光标 */}
)}
);
}
至于 `App.jsx`,就是把 `useChat` 的状态和这些组件连起来加上一个自动滚动到底部的 `useEffect` 就大功告成了。
部署方案:从本地到云端代码写完了总不Neng只在 localhost 上跑吧?
前端:VercelVercel 部署 React 项目简直是傻瓜式的。在项目根目录运行:
cd client
npm run build
npx vercel --prod
记得在 Vercel 的环境变量里配置好 `VITE_API_BASE`,指向你后端的地址。
后端:阿里云函数计算Ru果你不想一直开着个 ECS 服务器烧钱,阿里云函数计算是个绝佳选择。按量付费,平时没请求基本不花钱。
你需要写一个 `template.yml` 来定义服务:
ROSTemplateFormatVersion: '2015-09-01'
Transform: 'Aliyun::Serverless-2018-04-03'
Resources:
ai-chat-service:
Type: 'Aliyun::Serverless::Service'
Properties:
Description: AI Chat Backend
ai-chat-function:
Type: 'Aliyun::Serverless::Function'
Properties:
Handler: index.handler
Runtime: nodejs18
Timeout: 60 # 流式响应需要较长超时
EnvironmentVariables:
DEEPSEEK_API_KEY: !Ref DEEPSEEK_API_KEY
注意:函数计算的流式响应需要开启 "HTTP 触发器" 并配置 `responseType: stream`。Ru果觉得 FC 配置太繁琐,直接用个便宜的 ECS + PM2 部署也是极好的,几十块钱一个月,图个安稳。
几个容易踩的坑开发过程中,总有一些让人抓狂的瞬间。这里几个血泪经验,希望Neng帮你少掉几根头发。
1. React StrictMode 的双重渲染在开发环境下React 18 的 StrictMode 会让 `useEffect` 执行两次。这通常没问题,但Ru果你的 `useEffect` 里直接触发了 API 请求,你就会发现消息莫名其妙发两次。虽然 `useCallback` 包裹的函数只有依赖变化才重建,但Zui好还是检查一下是否有未清理的副作用,或者干脆在生产环境构建测试。
2. 上下文窗口爆炸随着对话越来越长,`messages` 数组会无限增长,Zui终超出模型的 Token 限制,导致报错。Zui简单的策略是“截断”:只保留Zui近的 N 轮对话,或者把Zui早的旧消息存到数据库里只给模型发“摘要”。
// 发送前裁剪:保留 system prompt + Zui近 N 轮
const MAX_TURNS = 10;
const contextMessages = ;
3. SSE 分包问题
这是Zui常见的新手坑。你以为 SSE 是一行一行发的,但网络层可Neng会把两行拼成一个 chunk 发过来或者把一行切成两半。Ru果不处理 `buffer`,你的 JSON 解析就会报错,前端界面就会卡住不动。一定要像上面 `useChat.js` 里那样,用 `buffer` 变量存一下不完整的行。
构建一个 AI 聊天应用,kan似复杂,实则只要理顺了数据流向——从用户输入,到 React 状态geng新,再到 Node.js 的流式转发,Zui后回到前端的实时渲染——一切dou会变得井井有条。这不仅仅是一次编程练习,geng是对现代 Web 全栈开发Neng力的一次综合体检。希望这篇文章Neng给你带来一些启发,别光kan不练,赶紧动手试试吧!有问题欢迎评论区交流,觉得有帮助的话点个赞 👍。
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback