ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

React 19.2 集成 DeepSeek 构建流式 AI 问答系统实战指南

React 19.2 集成 DeepSeek 构建流式 AI 问答系统实战指南 最近在尝试将最新的 React 19.2 与前沿的 DeepSeek 模型结合构建一个现代化的网页端 AI 问答系统时发现相关的整合资料比较零散尤其是在处理流式响应、状态管理和错误处理等环节容易遇到各种“坑”。本文将分享一套从零到一、可直接部署的完整实战方案涵盖前端 React 应用搭建、DeepSeek API 调用、流式对话实现以及生产级优化。无论你是想学习 React 19 新特性还是希望为自己的项目集成一个智能对话助手都能从本文中找到清晰的步骤和可复用的代码。1. 项目概述与技术选型1.1 什么是 Web AI 问答系统Web AI 问答系统是一个运行在浏览器中的应用程序它允许用户通过自然语言与后端的大型语言模型如 DeepSeek进行交互。用户在前端界面输入问题系统将问题发送到 AI 模型的 API接收并实时展示模型生成的回答。这种系统广泛应用于智能客服、学习助手、内容创作等场景。1.2 为什么选择 React 19.2 与 DeepSeekReact 19.2作为当前流行的前端框架的最新稳定版本之一它带来了性能优化、更好的开发体验以及对现代 Web 特性的支持。其组件化思想和丰富的生态系统如状态管理、路由非常适合构建复杂的交互式应用。DeepSeek这是一个性能强大、支持长上下文且具有优秀中文理解能力的开源大语言模型。通过其提供的 API开发者可以便捷地集成先进的 AI 对话能力而无需从头训练模型。1.3 核心功能与最终效果通过本教程你将构建一个具有以下功能的系统美观的聊天界面类似主流聊天软件的消息列表区分用户和 AI 消息。流式对话实现打字机效果实时逐字显示 AI 的回答提升用户体验。完整的对话管理支持多轮对话历史记录持久化。健壮的错误处理网络错误、API 限制等情况的友好提示。基础配置允许用户前端配置模型参数如温度、最大生成长度。2. 环境准备与项目初始化2.1 开发环境要求确保你的开发环境满足以下条件Node.js: 版本 18.0 或更高。推荐使用 LTS 版本。包管理器: npm 或 yarn 或 pnpm。本文示例使用npm。代码编辑器: VS Code 或其他现代 IDE。DeepSeek API Key: 你需要一个 DeepSeek 平台的账户并获取有效的 API Key。请妥善保管不要在前端代码中硬编码。2.2 使用 Vite 创建 React 项目我们将使用 Vite 作为构建工具它比传统的 Create React App 更快、更轻量。打开终端执行以下命令# 使用 npm 创建项目 npm create vitelatest web-ai-chat -- --template react # 进入项目目录 cd web-ai-chat # 安装依赖 npm install创建完成后项目结构大致如下web-ai-chat/ ├── node_modules/ ├── public/ ├── src/ │ ├── App.css │ ├── App.jsx │ ├── assets/ │ ├── index.css │ └── main.jsx ├── index.html ├── package.json └── vite.config.js2.3 安装必要的依赖库我们需要安装一些额外的库来增强功能。# 安装 UI 组件库 (以 Ant Design 为例你也可以选择其他如 MUI, Chakra UI) npm install antd ant-design/icons # 安装用于处理流式响应的库 npm install eventsource-parser # 安装日期时间格式化库用于消息时间戳 npm install dayjs # 开发依赖用于环境变量类型提示可选但推荐 npm install -D types/node2.4 配置环境变量为了安全地管理 API Key我们需要使用环境变量。在项目根目录创建两个文件.env.development(用于开发环境)VITE_DEEPSEEK_API_KEYyour_development_api_key_here VITE_DEEPSEEK_API_BASEhttps://api.deepseek.com.env.production(用于生产环境)VITE_DEEPSEEK_API_KEYyour_production_api_key_here VITE_DEEPSEEK_API_BASEhttps://api.deepseek.com重要请将your_*_api_key_here替换为你实际的 DeepSeek API Key。确保.env.*文件已被添加到.gitignore中避免密钥泄露。在vite.config.js中确保环境变量能被正确加载Vite 默认支持VITE_前缀。3. 核心组件设计与实现3.1 应用状态与类型定义首先我们在src目录下创建一个types.ts文件来定义 TypeScript 类型如果使用 JS可创建constants.js。// src/types.ts export interface Message { id: string; content: string; role: user | assistant; timestamp: number; // 使用时间戳 } export interface Chat { id: string; title: string; // 对话标题通常取第一条用户消息 messages: Message[]; createdAt: number; } export interface DeepSeekAPIRequest { model: string; // 例如 deepseek-chat messages: Array{ role: user | assistant | system; content: string; }; stream?: boolean; max_tokens?: number; temperature?: number; } export interface DeepSeekAPIResponse { id: string; object: string; created: number; model: string; choices: Array{ index: number; message: { role: string; content: string; }; finish_reason: string; }; usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number; }; }3.2 实现 API 服务层创建src/services/deepseekApi.ts文件封装所有与 DeepSeek API 交互的逻辑。// src/services/deepseekApi.ts import { DeepSeekAPIRequest } from ../types; const API_BASE import.meta.env.VITE_DEEPSEEK_API_BASE; const API_KEY import.meta.env.VITE_DEEPSEEK_API_KEY; if (!API_KEY) { console.error(DeepSeek API Key is not configured. Please check your .env file.); } /** * 发送普通请求非流式 */ export const sendChatRequest async (requestData: DeepSeekAPIRequest) { const response await fetch(${API_BASE}/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${API_KEY}, }, body: JSON.stringify(requestData), }); if (!response.ok) { const errorText await response.text(); throw new Error(API Request Failed: ${response.status} - ${errorText}); } return await response.json(); }; /** * 发送流式请求并处理 Server-Sent Events (SSE) * param requestData 请求数据 * param onChunk 接收到数据块时的回调 * param onDone 流式传输完成时的回调 * param onError 发生错误时的回调 */ export const sendStreamChatRequest async ( requestData: DeepSeekAPIRequest, onChunk: (content: string) void, onDone: () void, onError: (error: Error) void ) { // 确保请求是流式的 const payload { ...requestData, stream: true }; try { const response await fetch(${API_BASE}/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${API_KEY}, }, body: JSON.stringify(payload), }); if (!response.ok) { const errorText await response.text(); throw new Error(Stream Request Failed: ${response.status} - ${errorText}); } const reader response.body?.getReader(); const decoder new TextDecoder(utf-8); if (!reader) { throw new Error(Response body is not readable); } let buffer ; while (true) { const { done, value } await reader.read(); if (done) { onDone(); break; } buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop() || ; // 最后一行可能不完整放回缓冲区 for (const line of lines) { if (line.trim() ) continue; if (line.startsWith(data: )) { const data line.slice(6); // 去掉 data: 前缀 if (data [DONE]) { onDone(); return; } try { const parsed JSON.parse(data); const content parsed.choices[0]?.delta?.content || ; if (content) { onChunk(content); } } catch (e) { console.error(Failed to parse SSE data:, e, Raw data:, data); } } } } } catch (error) { onError(error instanceof Error ? error : new Error(Unknown stream error)); } };3.3 构建聊天界面组件创建src/components/ChatInterface.tsx这是应用的核心 UI。// src/components/ChatInterface.tsx import React, { useState, useRef, useEffect } from react; import { Input, Button, List, Avatar, Typography, Card, Spin, Alert, Form, Slider, Select } from antd; import { SendOutlined, UserOutlined, RobotOutlined } from ant-design/icons; import dayjs from dayjs; import { Message } from ../types; import { sendStreamChatRequest } from ../services/deepseekApi; import ./ChatInterface.css; // 我们将创建这个CSS文件 const { TextArea } Input; const { Text } Typography; const ChatInterface: React.FC () { const [messages, setMessages] useStateMessage[]([ { id: 1, content: 你好我是DeepSeek AI助手有什么可以帮你的, role: assistant, timestamp: Date.now() }, ]); const [inputText, setInputText] useState(); const [loading, setLoading] useState(false); const [error, setError] useStatestring | null(null); const messagesEndRef useRefHTMLDivElement(null); const [currentStreamingMessage, setCurrentStreamingMessage] useStatestring(); // 模型配置状态 const [modelConfig, setModelConfig] useState({ model: deepseek-chat, temperature: 0.7, max_tokens: 2048, }); // 滚动到底部 const scrollToBottom () { messagesEndRef.current?.scrollIntoView({ behavior: smooth }); }; useEffect(() { scrollToBottom(); }, [messages, currentStreamingMessage]); const handleSendMessage async () { if (!inputText.trim() || loading) return; const userMessage: Message { id: Date.now().toString(), content: inputText.trim(), role: user, timestamp: Date.now(), }; // 添加用户消息 setMessages(prev [...prev, userMessage]); setInputText(); setLoading(true); setError(null); setCurrentStreamingMessage(); // 清空当前的流式消息 // 构建请求消息历史 const requestMessages [ { role: system, content: You are a helpful assistant. }, ...messages.map(msg ({ role: msg.role, content: msg.content })), { role: user, content: userMessage.content }, ]; let fullResponse ; try { await sendStreamChatRequest( { model: modelConfig.model, messages: requestMessages, stream: true, max_tokens: modelConfig.max_tokens, temperature: modelConfig.temperature, }, (chunk) { fullResponse chunk; setCurrentStreamingMessage(fullResponse); // 更新流式消息 }, () { // 流式传输完成将最终消息添加到列表 const assistantMessage: Message { id: (Date.now() 1).toString(), content: fullResponse, role: assistant, timestamp: Date.now(), }; setMessages(prev [...prev, assistantMessage]); setCurrentStreamingMessage(); // 清空流式消息 setLoading(false); }, (err) { setError(请求失败: ${err.message}); setLoading(false); setCurrentStreamingMessage(); } ); } catch (err) { setError(发送请求时出错: ${err instanceof Error ? err.message : 未知错误}); setLoading(false); } }; const handleKeyPress (e: React.KeyboardEvent) { if (e.key Enter !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }; return ( div classNamechat-container Card title{ div classNamechat-header RobotOutlined style{{ marginRight: 8 }} / spanDeepSeek AI 对话助手/span {loading Spin sizesmall style{{ marginLeft: 12 }} /} /div } extra{ Form layoutinline sizesmall Form.Item label模型 Select value{modelConfig.model} onChange{(value) setModelConfig(prev ({ ...prev, model: value }))} style{{ width: 120 }} options{[ { value: deepseek-chat, label: DeepSeek-Chat }, { value: deepseek-coder, label: DeepSeek-Coder }, ]} / /Form.Item Form.Item label{温度: ${modelConfig.temperature}} Slider min{0} max{2} step{0.1} value{modelConfig.temperature} onChange{(value) setModelConfig(prev ({ ...prev, temperature: value }))} style{{ width: 100 }} / /Form.Item /Form } classNamechat-card {error ( Alert message错误 description{error} typeerror showIcon closable onClose{() setError(null)} style{{ marginBottom: 16 }} / )} div classNamemessages-list List dataSource{messages} renderItem{(msg) ( List.Item className{message-item ${msg.role}} List.Item.Meta avatar{ Avatar icon{msg.role user ? UserOutlined / : RobotOutlined /} style{{ backgroundColor: msg.role user ? #1890ff : #52c41a }} / } title{ div classNamemessage-header Text strong{msg.role user ? 你 : AI 助手}/Text Text typesecondary style{{ fontSize: 0.8em, marginLeft: 8 }} {dayjs(msg.timestamp).format(HH:mm:ss)} /Text /div } description{ div classNamemessage-content {msg.content.split(\n).map((line, idx) ( React.Fragment key{idx} {line} br / /React.Fragment ))} /div } / /List.Item )} / {/* 显示正在流式接收的消息 */} {currentStreamingMessage ( List.Item classNamemessage-item assistant List.Item.Meta avatar{ Avatar icon{RobotOutlined /} style{{ backgroundColor: #52c41a }} / } title{ div classNamemessage-header Text strongAI 助手/Text Text typesecondary style{{ fontSize: 0.8em, marginLeft: 8 }} 正在输入... /Text /div } description{ div classNamemessage-content streaming {currentStreamingMessage} span classNamecursor▋/span /div } / /List.Item )} div ref{messagesEndRef} / /div div classNameinput-area TextArea value{inputText} onChange{(e) setInputText(e.target.value)} onKeyDown{handleKeyPress} placeholder输入您的问题... (ShiftEnter 换行Enter 发送) autoSize{{ minRows: 2, maxRows: 6 }} disabled{loading} / Button typeprimary icon{SendOutlined /} onClick{handleSendMessage} loading{loading} disabled{!inputText.trim()} style{{ marginTop: 12, alignSelf: flex-end }} 发送 /Button /div /Card /div ); }; export default ChatInterface;3.4 添加样式文件创建src/components/ChatInterface.css来美化我们的聊天界面。/* src/components/ChatInterface.css */ .chat-container { height: 100vh; display: flex; justify-content: center; align-items: center; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); padding: 20px; } .chat-card { width: 100%; max-width: 900px; height: 90vh; display: flex; flex-direction: column; } .chat-header { display: flex; align-items: center; font-size: 1.2em; } .messages-list { flex: 1; overflow-y: auto; padding: 16px; background-color: #fafafa; border-radius: 8px; margin-bottom: 16px; } .message-item { padding: 12px 0; border-bottom: 1px solid #f0f0f0 !important; } .message-item.user { background-color: #e6f7ff; border-radius: 8px; padding-left: 12px; margin-bottom: 8px; } .message-item.assistant { background-color: #f6ffed; border-radius: 8px; padding-left: 12px; margin-bottom: 8px; } .message-header { display: flex; align-items: center; margin-bottom: 4px; } .message-content { white-space: pre-wrap; word-break: break-word; line-height: 1.6; } .message-content.streaming { font-family: Monaco, Menlo, Ubuntu Mono, monospace; } .cursor { display: inline-block; width: 8px; height: 1.2em; background-color: #1890ff; margin-left: 2px; animation: blink 1s infinite; vertical-align: middle; } keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } .input-area { display: flex; flex-direction: column; border-top: 1px solid #f0f0f0; padding-top: 16px; }3.5 修改主应用组件更新src/App.jsx或src/App.tsx引入我们的聊天组件。// src/App.tsx import React from react; import { ConfigProvider } from antd; import zhCN from antd/locale/zh_CN; import ChatInterface from ./components/ChatInterface; import ./App.css; const App: React.FC () { return ( ConfigProvider locale{zhCN} div classNameApp ChatInterface / /div /ConfigProvider ); }; export default App;同时可以简化src/App.css/* src/App.css */ .App { width: 100%; height: 100vh; } * { box-sizing: border-box; } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }4. 运行与测试4.1 启动开发服务器在项目根目录运行npm run devVite 会启动开发服务器通常在http://localhost:5173。打开浏览器访问该地址。4.2 功能测试基础对话在输入框输入“你好”点击发送或按 Enter 键。你应该能看到 AI 的回复实时地、逐字显示出来。流式效果问一个需要较长回答的问题例如“请用 JavaScript 写一个快速排序函数”观察打字机效果。多轮对话基于上一个回答继续提问例如“能解释一下代码的逻辑吗”系统应能结合上下文回答。配置调整尝试在右上角调整“温度”滑块控制回答的随机性或切换模型感受不同参数的效果。错误模拟可以临时断开网络或修改.env文件中的 API Key 为一个错误的值查看前端的错误提示是否友好。5. 核心原理与进阶实现解析5.1 流式响应SSE的实现机制本项目的核心亮点之一是实现了流式响应。其原理如下请求阶段在调用 DeepSeek API 时设置stream: true。这告诉 API 以 Server-Sent Events (SSE) 格式返回数据而不是等待生成完整文本后一次性返回。接收阶段前端使用fetchAPI 获取响应体response.body这是一个ReadableStream。读取阶段通过reader.read()方法循环读取流中的数据块。解析阶段每个数据块是遵循特定格式的文本行。我们寻找以data:开头的行其后的 JSON 数据中包含choices[0].delta.content字段这就是模型新生成的一小段文本。渲染阶段每解析出一段内容就立即通过 React 的状态更新到 UI 上形成“逐字打出”的效果。当收到data: [DONE]行时表示流式传输结束。5.2 React 状态管理策略消息列表 (messages)存储所有已完成的对话消息Message[]。这是对话历史的权威来源。流式消息 (currentStreamingMessage)一个独立的状态专门用于存储当前正在接收的、尚未完成的 AI 回复。这避免了频繁更新messages数组可能导致性能问题并实现了更流畅的 UI 更新。当流式传输完成时才将完整内容作为一个新的assistant消息加入messages。加载状态 (loading)控制发送按钮的禁用状态和显示加载指示器。错误状态 (error)集中管理 API 调用或网络错误并通过 Alert 组件友好地展示给用户。5.3 对话上下文构建为了进行多轮对话AI 模型需要知道之前的对话历史。我们在每次请求的messages数组中不仅包含当前用户的问题还包含之前所有轮次的user和assistant消息。通常还会在开头添加一个system消息来设定 AI 的角色和行为。这模拟了 ChatGPT 等产品的对话记忆功能。6. 常见问题与排查思路在开发和使用过程中你可能会遇到以下问题问题现象可能原因排查与解决思路页面打开空白控制台报错1. 依赖未正确安装。2. 环境变量未加载。3. TypeScript/React 语法错误。1. 删除node_modules和package-lock.json重新运行npm install。2. 检查.env.development文件是否存在且格式正确变量名是否以VITE_开头。3. 检查浏览器控制台和终端的错误信息根据提示修改代码。发送消息后无反应无错误提示1. API Key 错误或未设置。2. 网络问题如跨域、防火墙。3. DeepSeek API 服务暂时不可用。1. 确认.env文件中的VITE_DEEPSEEK_API_KEY值正确并在代码中通过console.log(import.meta.env.VITE_DEEPSEEK_API_KEY)检查是否成功读取注意不要在生产环境这样做。2. 打开浏览器开发者工具的Network标签页查看请求是否发出、状态码和响应内容。如果是 CORS 错误需确认 API 服务端是否允许你的前端域名。注意纯前端调用第三方 API 极易遇到 CORS 问题生产环境强烈建议通过自己的后端服务代理请求。3. 查看 DeepSeek 官方状态页或社区。流式响应不显示或一次性全部显示1. SSE 数据解析逻辑错误。2. React 状态更新未触发渲染。3. API 请求未设置stream: true。1. 在sendStreamChatRequest函数的onChunk回调中添加console.log(‘Received chunk:’, chunk)检查是否按预期收到数据块。2. 确认setCurrentStreamingMessage被正确调用。3. 检查发送给 API 的请求体确保包含”stream”: true。界面卡顿滚动不流畅1. 消息列表过长每次渲染开销大。2.useEffect依赖项设置不当导致频繁重渲染。1. 考虑对历史消息进行分页或虚拟滚动。可以引入react-window或react-virtualized库。2. 使用React.memo优化MessageItem组件避免不必要的重渲染。检查scrollToBottom的依赖项。生产环境构建失败1. TypeScript 类型错误。2. 引用了未定义的变量或路径。1. 运行npm run build前先运行npm run type-check(如果配置了) 或tsc --noEmit检查类型。2. 确保所有导入路径正确环境变量在生产构建时可用。7. 生产环境部署与优化建议7.1 安全加固使用后端代理当前实现有一个重大安全隐患API Key 暴露在前端。任何用户都可以通过浏览器开发者工具查看网络请求窃取你的 API Key。生产环境必须使用后端服务进行代理。解决方案以 Node.js Express 为例创建后端服务新建一个 Node.js 项目安装express,cors,dotenv。编写代理接口// server.js (后端) const express require(express); const cors require(cors); const fetch (...args) import(node-fetch).then(({default: fetch}) fetch(...args)); require(dotenv).config(); const app express(); app.use(cors()); app.use(express.json()); const DEEPSEEK_API_KEY process.env.DEEPSEEK_API_KEY; const DEEPSEEK_API_BASE process.env.DEEPSEEK_API_BASE || https://api.deepseek.com; app.post(/api/chat, async (req, res) { try { const response await fetch(${DEEPSEEK_API_BASE}/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${DEEPSEEK_API_KEY}, }, body: JSON.stringify(req.body), }); // 将 DeepSeek API 的响应包括流式响应头直接转发给前端 res.setHeader(Content-Type, response.headers.get(Content-Type) || application/json); response.body.pipe(res); } catch (error) { console.error(Proxy error:, error); res.status(500).json({ error: Internal Server Error }); } }); const PORT process.env.PORT || 3001; app.listen(PORT, () console.log(Proxy server running on port ${PORT}));修改前端请求将前端deepseekApi.ts中的API_BASE指向你的后端代理地址例如http://localhost:3001/api。环境隔离在后端的.env文件中设置DEEPSEEK_API_KEY确保其永远不会发送到客户端。7.2 性能优化消息列表虚拟化当对话历史很长时渲染所有 DOM 节点会严重影响性能。使用react-window或react-virtualized只渲染可视区域内的消息。API 请求防抖/节流对频繁触发的操作如自动保存对话标题进行防抖处理。代码分割与懒加载使用 React.lazy 和 Suspense 对非首屏必需的组件如设置页面、历史记录面板进行懒加载。PWA 支持考虑添加 Service Worker 和 Manifest使应用可以离线使用、安装到桌面并提升重复访问的加载速度。7.3 功能增强方向对话历史管理实现侧边栏支持创建新对话、重命名对话、删除对话并将历史记录持久化到localStorage或 IndexedDB。Markdown 渲染AI 的回答常常包含代码块、列表等。集成如react-markdown和remark-gfm库来美化渲染。代码高亮配合 Markdown 渲染使用prism.js或highlight.js为代码块添加语法高亮。停止生成功能在流式响应过程中提供一个按钮来中断当前的 AI 生成。复制与分享为每条消息添加“复制”按钮或支持分享整个对话。语音输入/输出集成 Web Speech API实现语音提问和语音播报回答。7.4 部署上线前端构建运行npm run build生成静态文件到dist目录。选择托管平台Vercel / Netlify最适合 React SPA关联 Git 仓库即可自动部署。静态服务器将dist目录上传到 Nginx、Apache 或任何静态文件托管服务。Docker 容器化创建 Dockerfile将构建好的静态文件和服务端代理如果有一起容器化部署到云服务器或 Kubernetes 集群。配置域名与 HTTPS为你的服务绑定域名并申请 SSL 证书启用 HTTPS这是使用某些浏览器 API如语音和提升安全性的必要条件。构建一个完整的 Web AI 应用涉及前端交互、网络通信、状态管理和生产部署等多个环节。本文提供的方案是一个坚实的起点你可以在此基础上根据具体业务需求进行扩展和深化。关键在于理解流式通信的原理、安全地处理敏感信息并设计出良好的用户体验。动手将代码跑起来再逐步添加你想要的功能是掌握这项技术的最佳途径。如果在实践中遇到新的问题欢迎在社区交流探讨。
返回列表