ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

React全栈实战:用DeepSeek API构建AI写作平台

2026/9/19 11:37:46 拓冰建站 浏览量
React全栈实战:用DeepSeek API构建AI写作平台 简介面向有编程基础、希望快速上手全栈AI应用开发的读者这份PDF系统讲解了如何结合DeepSeek生成API与React从零构建一个可运行的AI写作平台。文档由浅入深先介绍DeepSeek的概念与API申请流程再逐一说明prompt、max_tokens、temperature、top_p等生成参数的使用要点随后过渡到React前端环境搭建、函数组件与状态管理以及Flask后端集成DeepSeek接口、错误处理与性能优化最后补充前后端跨域通信、Nginx部署、域名与SSL配置等上线实操内容形成完整开发闭环。压缩包共1个PDF文件大小2.04MB内容完整附有清晰目录便于按章节查阅。目前已有96人学习适合希望掌握生成式API工程化落地、并想从零完成一个全栈项目的开发者作为参考手册。1. AI 写作平台真正要写的代码是什么React 与 DeepSeek API 的边界你打开任何一个 AI 写作编辑器输入主题后文章逐字出现这不是前端动画而是后端把大模型生成的增量内容实时推给了界面。拆开看就是三条链路DeepSeek 生成 API 提供文本能力React 负责输入和流式内容渲染中间一层服务端负责保管密钥、转发请求、控制参数。把这套链路从零搭起来就是接下来所有章节要做的事。整个项目不涉及模型训练、不需要高配置服务器核心投入在 API 调用参数、流式接收和全栈联调上。适合前端转全栈后想完整跑通一个 AI 应用的人也适合要交付写作类工具的前端工程师。如果你正把它排进自己的 AI 全栈学习路线这个项目的难度阶梯刚好卡在“能调通接口”和“能做出产品”之间。2. DeepSeek 生成 API 的调用方式鉴权、参数与 400 报错排查2.1 生成 API 为什么是 OpenAI 兼容格式DeepSeek 的 HTTP 接口遵循 OpenAI 的 Chat Completions 约定请求体里放一个 messages 数组数组元素带 role 和 content服务端通过一次或多次返回生成内容。采用这种协议意味着市面上大量基于 OpenAI 接口写的调用代码、SDK 封装和错误排查思路都可以直接平移到 DeepSeek 上理解成本被压到最低。调用前需要去开放平台创建 API Key密钥以 sk- 开头且只在创建时完整展示一次遗失就必须重新生成。调用入口是 POST https://api.deepseek.com/chat/completionsNode 18 以上自带全局 fetch不需要为了调 HTTP 额外安装请求库。鉴权通过请求头完成Authorization: Bearer sk-xxxx注意这个头的三个细节必须带 Bearer 前缀且前缀后有一个空格Key 里不能混入换行符否则服务端按整个字符串匹配就失败不要把 Key 放到 URL 查询参数里代理层和访问日志会把它明文记下来。2.2 用 curl 验证最小请求写业务代码前先用 curl 验证连通性把网络问题和鉴权问题从代码问题里剥离出来。下面是最小可用的请求体curl https://api.deepseek.com/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $DEEPSEEK_API_KEY \ -d { model: deepseek-chat, messages: [ {role: system, content: 你是一个中文技术写作助手。}, {role: user, content: 用三句话介绍 React 的虚拟 DOM。} ], temperature: 0.7, max_tokens: 500 }一次成功的响应会在 choices[0].message.content 里返回完整正文。如果返回 401先检查 Authorization 头是否带了 Bearer 前缀以及环境变量里有没有残留空格。如果返回 400最常见的两类原因都在请求体里model 字段名称与你账号开通的模型不匹配或 messages 最后一条不是 user 角色。许多网关的报错文本会直接列出它支持的模型名形式类似 the supported api model names are ...把你控制台里开通的那个名字填进 model问题就解决了。不要在任何前端代码里硬编码模型名后面会讲为什么。2.3 请求参数与响应字段说明请求体里最值得关注的字段如下表写作平台场景下每个参数都有明确调优方向参数类型写作场景建议modelstring填写控制台开通的模型名区分 chat 和推理模型messagesarraysystem 设角色和文风user 放选题历史消息按顺序放回temperaturenumber0.7 适合说明文大纲生成用 0.4小说发散用 1.0max_tokensint控制单次生成长度估算时预留 system 和输入占用的 tokenstreamboolean写作编辑器固定 true非流式只能拿到整段结果响应体里的字段则各有用途id 对应一次请求的唯一轨迹排错时拿它去平台查费用和日志choices[0].message.content 是核心生成文本choices[0].finish_reason 为 stop 表示正常结束length 表示内容被 max_tokens 截断usage.prompt_tokens 与 completion_tokens 拆分计算输入和输出成本做配额监控时这两个字段是唯一数据源。2.4 接入 SDK 还是直接用 fetch对只有一个补全接口的写作平台直接用 fetch 就够了。SDK 的价值在重试机制、自动限流和更方便的流式处理但这些在单实例服务里都可以用几十行代码实现。多数生产项目两种写法共存网关层用 SDK 做统一封装业务层用 fetch 保证行为透明排错时能直接看到请求和响应。本文统一使用 fetch原因只有一个它的每一步都能在 Network 面板里复现这是初学者把链路跑通的最短路径。3. 用 Node.js 写全栈后端把 DeepSeek 密钥留在服务端3.1 前端直连 API 的两个硬伤浏览器直连 DeepSeek 接口把 sk- 密钥写进 React 代码里任何一个打开 DevTools 的人都能把它拷走。个人学习项目也许无伤大雅一旦部署成多人使用的平台泄露的密钥就会被别人拿去刷你的余额。任何正规的全栈方案里生成 API 都只允许从服务端访问密钥永不进入浏览器。CORS 是第二个问题。DeepSeek 接口默认不会对浏览器开放跨域许可虽然可以去平台申请加白名单域名但更干净的做法是让同域的后端代发请求从根上把 CORS 绕开。两种方案的成本差异很大白名单需要每周维护域名列表而代理层几乎零成本还能在里面加鉴权、限流和日志记录。3.2 初始化服务端项目使用 Express 搭一个最小服务端Node.js 版本要求 18 以上mkdir ai-writer-server cd ai-writer-server npm init -y npm install express dotenv touch index.js根目录下创建 .env 文件存放密钥并确认它已写进 .gitignoreDEEPSEEK_API_KEYsk-xxxx DEEPSEEK_MODELdeepseek-chat PORT3001入口文件 index.js 负责加载环境变量、注册中间件和启动服务import express from express; import dotenv from dotenv; import { generate } from ./routes/generate.js; dotenv.config(); const app express(); app.use(express.json()); app.use(/api, generate); app.listen(process.env.PORT || 3001, () { console.log(server run at http://localhost:${process.env.PORT || 3001}); });这里的 dotenv.config() 必须在读 process.env 之前执行否则 DEEPSEEK_API_KEY 会是 undefinedexpress.json() 用来解析前端发来的 JSON 请求体缺少它会一直拿不到 prompt 字段。3.3 转发请求到 DeepSeek 的最小实现routes/generate.js 里写一个 POST 路由把前端传进来的 prompt 和 system 拼入消息数组再转发给 DeepSeekimport { Router } from express; const router Router(); router.post(/generate, async (req, res) { const { prompt, system 你是一个中文写作助手。, temperature 0.7, max_tokens 2048, } req.body; const body { model: process.env.DEEPSEEK_MODEL, messages: [ { role: system, content: system }, { role: user, content: prompt }, ], temperature, max_tokens, stream: true, }; const upstream await fetch(https://api.deepseek.com/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.DEEPSEEK_API_KEY}, }, body: JSON.stringify(body), }); if (!upstream.ok) { const errText await upstream.text(); res.status(upstream.status).json({ error: errText }); return; } res.setHeader(Content-Type, text/event-stream; charsetutf-8); res.setHeader(Cache-Control, no-cache); res.setHeader(Connection, keep-alive); upstream.body.pipe(res); }); export { router as generate };这段代码里最关键的是三处。第一密钥只从环境变量读取前端代码和网络请求面里都不存在明文。第二stream 设为 true上游响应会持续返回增量内容再通过 upstream.body.pipe(res) 把 Node 的流直接接到 Express 响应上省去手动解析 SSE 的工作。第三响应头显式声明 text/event-stream浏览器和代理层才会把它当作长连接处理而不是等待完整响应体。3.4 流式与非流式的取舍如果去掉 stream 字段接口会等待全文生成完毕才返回前端表现为转圈几秒后整段出现流式模式则让第一个字在几百毫秒内到达后续内容逐段追加。写作平台的逐字输出效果必须靠 stream: true 实现代价是前端处理逻辑复杂一层需要自己拼接持续到达的文本并在连接意外断开时保留已生成的部分。另外要关注超时。Node 对上游请求默认没有超时限制长文本生成可能持续几十秒连接一旦挂起会长期占用服务端 socket。下面用 AbortController 把超时控制在 120 秒const controller new AbortController(); const timer setTimeout(() controller.abort(), 120000); const upstream await fetch(apiUrl, { method: POST, headers: headers, body: JSON.stringify(body), signal: controller.signal, }); clearTimeout(timer);3.5 错误状态码与排查方向把上游错误原样抛给前端不便于定位这里整理一份常见状态码映射服务端可以把错误码翻译成前端可读的消息状态码返回体关键词排查方向400model 名称不支持或 messages 不合法核对控制台模型名与消息角色顺序401Invalid API Key or token密钥失效、缺 Bearer 前缀402Insufficient Balance账户欠费生成接口被停用429Rate limit reached并发过高需要加退避重试5xxInternal/Server error上游故障间隔重试 2 到 3 次4. React 前端消费 DeepSeek 流式接口从 fetch 到编辑器4.1 技术选型React 与 Vue 的取舍Vue 的模板语法上手快React 的函数式写法在处理持续变化的状态时更直接流式文本就是一个字符串视图层根据状态重渲染不需要与模板指令做额外同步。对准备前端转全栈的人来说React 的生态覆盖面和岗位需求量更大这也是本方案选它做主界面的原因。项目骨架用 Vite 初始化保留 JavaScript 模板把示例代码清空后按职责拆分组件组件名文件路径职责Writersrc/components/Writer.jsx输入框、生成按钮、加载状态OutputPanelsrc/components/OutputPanel.jsx展示生成结果并允许修改SettingsBarsrc/components/SettingsBar.jsxtemperature 与 max_tokens 滑块Appsrc/App.jsx状态集中管理并调用 generate这种划分下App 是唯一发起请求的地方子组件只负责把用户输入向上传再把生成结果向下渲染。4.2 第一次接流式接口直接拼接文本前端用 fetch 调用后端转发接口把响应体当作可读流逐块读取const generate async (prompt) { setLoading(true); setOutput(); try { const res await fetch(/api/generate, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ prompt }), }); if (!res.ok) { const err await res.json(); setError(err.error || HTTP ${res.status}); return; } const reader res.body.getReader(); const decoder new TextDecoder(utf-8); let text ; while (true) { const { done, value } await reader.read(); if (done) break; text decoder.decode(value, { stream: true }); setOutput(text); } } finally { setLoading(false); } };这套写法没有解析 SSE 里的 data: 前缀而是把二进制流直接解码成字符串后追加。原因很实际流式字节可能在任何位置断开半行 JSON 无法直接 JSON.parse与其引入状态管理处理碎片不如先让文本稳定滚出来。String 在 JavaScript 拼接性能足够高中文按字符追加几千字的内容并不会卡顿。4.3 升级为正规 SSE 解析当后续需求增加 token 计数时再升级为按行解析的版本。SSE 的标准格式是每个事件由 data: 开头事件之间用空行分隔流结束时发送 data: [DONE]let buffer ; let text ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop(); for (const line of lines) { const trimmed line.trim(); if (!trimmed.startsWith(data:)) continue; const payload trimmed.slice(5).trim(); if (payload [DONE]) return; try { const json JSON.parse(payload); const delta json.choices?.[0]?.delta?.content; if (delta) { text delta; setOutput(text); } } catch (e) { console.warn(sse parse skip, e); } } }buffer 变量保留了不完整的行避免数据被 TCP 分包切断时丢内容JSON.parse 只处理完成的行消费接口返回的增量字段。需要记录 token 用量时也可以在每个 data 事件里累加 usage.completion_tokens。4.4 把生成结果变成可编辑文本生成之后要让用户能改文章。OutputPanel 里用一个受控 textarea 展示 output用户既能手动修正也可以选中内容后再次让模型续写。受控组件意味着所有修改都先进入 state再回流到界面后续“保存”“导出”功能可以直接基于这份 state 实现。const OutputPanel ({ value, onChange }) ( textarea value{value} onChange{(e) onChange(e.target.value)} rows{24} classNameoutput-panel / );加载状态做成独立按钮文案生成期间禁用提交按钮避免用户连点触发多次计费请求。生成结束后把 finish_reason 为 length 的情况翻译成“内容已达到长度上限请调大 max_tokens 或分段生成”提示文案有实际业务含义不是简单透传状态码。5. 全栈联调清单与 3 个必调参数5.1 本地联调命令与代理设置分别启动服务端与前端两个进程cd ai-writer-server npm run dev cd ai-writer-web npm run devVite 默认端口 5173服务端在 3001跨域问题交给 Vite 代理在 vite.config.js 里配置export default defineConfig({ server: { proxy: { /api: http://localhost:3001, }, }, });打开浏览器 F12 的 Network 面板请求链路是 React 发出 POST /api/generateVite 转发到 3001服务端再向上游发起请求。如果一直看不到流式响应先确认服务端响应头 Content-Type 是不是 text/event-stream部分代理或压缩插件会吞掉它。5.2 生产部署的 3 个必调参数参数位置推荐设置作用model 名称服务端环境变量注入前端不出现以控制台开通列表为准避免前端传参被篡改导致 400 报错请求超时服务端 fetch 的 AbortController120000 ms长文本生成超过 2 分钟主动断开SSE 缓冲Nginx 反代配置proxy_buffering off让增量数据实时转发到浏览器第三项在本地开发感知不到部署在 Nginx 后会出现“等半天突然整段出现”的现象原因是 Nginx 默认缓冲了响应。对应配置如下location /api/ { proxy_pass http://127.0.0.1:3001; proxy_buffering off; proxy_read_timeout 120s; }5.3 验证流式生效的一个小技巧联调时有个快速判断流式是否真正工作的办法在服务端日志里观察请求耗时。如果整个请求在 10 到 30 秒后才返回说明流被中间层缓存如果请求发出后立即有内容持续秒级抵达说明链路是通畅的。也可以用 curl 直接验证后端接口curl -N http://localhost:3001/api/generate \ -H Content-Type: application/json \ -d {prompt:写一段关于 React 的简介}-N 参数禁用 curl 的输出缓冲看到逐行冒出的文字代表服务端流式转发正常这时再回到前端排查 React 侧代码即可不用把问题发散到整个链路。本文还有配套的精品资源点击获取