ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用WebSocket把DeepSeek接进聊天框:全链路实践解析

用WebSocket把DeepSeek接进聊天框:全链路实践解析 简介面向希望快速集成DeepSeek大模型的开发者这份资源以WebSocket流式聊天为示例完整还原从聊天界面搭建到模型实时响应的前后端协作过程适合用于智能助手、实时对话类产品的前期原型验证。压缩包共13个文件、约30KB内部由React前端组件与样式、Python后端脚本、Vite构建及依赖管理配置、README说明文档等构成目录结构清晰便于按模块阅读。项目演示了前端如何通过WebSocket建立长连接并接收流式消息同时给出服务端示例脚本覆盖DeepSeek API对接、密钥安全保存、流式数据解析与异常处理等关键细节前端部分重点展示了消息列表、输入框与滚动交互的实现思路后端脚本则提供WebSocket服务与模型请求转发的参考写法。该资源已有468人学习适合有一定前端或后端开发经验、想了解大模型落地链路的中高级开发者参考。1. 用 WebSocket 把 DeepSeek 接进聊天框先搞清楚这三段链路再动手做聊天应用最难受的不是调不通接口而是“接口通了一刷新又断”。DeepSeek 这类大模型本身给的是 HTTP 流式接口你用 fetch 等它全部生成完再渲染用户得盯着一两秒空白用 SSE 可以省事但想加停止生成、主动推送、心跳维护WebSocket 才是能把 DeepSeek 大模型聊天做成“落地系统”的那个底座。我这次拆的 antdx 项目就是把前端 React 聊天界面、后端 Python 代理、DeepSeek 流式接口这三段串在一起前端不用管 Key 安全后端不用管 UI 状态。适合正打算把 DeepSeek 接进自己产品的开发者或者课程设计里要交“实时聊天”作业的人——你能直接对照这套代码跑通全链路而不是只看一个孤立的前端 demo。2. 流式聊天链路设计DeepSeek 的 token 是怎么“一个字一个字”到浏览器的2.1 为什么聊天场景选 WebSocket 而不是 fetch 轮询或 SSE大模型聊天的实时性要求很特殊不需要 20ms 级延迟但不能让用户对着空白页面等。fetch 轮询的做法是定时去拉结果消息不是流式送达用户看到的是一段一段跳出来的文字而且频繁轮询对 DeepSeek API 配额也是浪费。SSEServer-Sent Events倒是天然支持流式但它只是服务端单向推送聊天场景里你要做“停止生成”、后端主动通知前端会话过期、多人协同时刻推送这些操作都需要客户端往后端发指令SSE 就力不从心了。WebSocket 全双工通信刚好覆盖聊天场景的所有诉求。客户端往后端发一条 “stop”后端可以通过同一条连接告诉 DeepSeek 那边取消请求再把取消结果推回给前端。我自己做过的聊天项目里还遇到过后端要主动通知前端“左侧会话列表有更新”的情况这种推送用 WebSocket 写起来顺手很多。SSE 能做的流式文本渲染WebSocket 也能做只是需要自己在消息协议里区分“增量内容”“结束标记”“错误信息”三类事件。这个项目的前端 antdx/src/App.jsx 里就定义了一套简单的消息协议后端 index.py 往 WS 连接里推的是 JSON 字符串前端再按 type 字段区分处理。底层传输是 WebSocket 还是 HTTP 长连接对上层用户完全透明但协议层不设计好后面加一个“停止生成”都会改到痛哭。2.2 后端代理层把 DeepSeek 的 HTTP 流式响应转成 WS 帧DeepSeek 官方的 API 走的是 HTTP SSE 流式返回不能直接用 WS 连过去所以中间必须有一层代理前端连我们自己的 WebSocket 服务后端代理去请求 DeepSeek API拿到增量就通过 WS 推给前端。这套架构最大的好处是 DeepSeek 的 API Key 只存在后端不会被打包进前端资源。后端我用 Python 实现核心逻辑就是一个 WebSocket 处理器收前端来的消息文本组装大模型请求参数然后一边读 DeepSeek 的流式响应一边把内容片段推给前端。下面是去掉业务逻辑后的核心骨架直接对应本项目 index.py 里的工作方式import json from fastapi import WebSocket import httpx DEEPSEEK_API_URL https://api.deepseek.com/chat/completions # 实际项目中从环境变量读取不要写死在代码里 DEEPSEEK_API_KEY os.getenv(DEEPSEEK_API_KEY) async def handle_chat_ws(websocket: WebSocket): await websocket.accept() # 维护连接状态前端可以随时发消息过来 while True: raw await websocket.receive_text() request_data json.loads(raw) user_message request_data.get(content, ) # 把用户的聊天文本转成 DeepSeek 的请求体 payload { model: deepseek-chat, messages: [{role: user, content: user_message}], stream: True, # 开启流式返回 temperature: 0.7, } headers { Authorization: fBearer {DEEPSEEK_API_KEY}, Content-Type: application/json, } # 用 httpx 异步流式请求 DeepSeek 接口 async with httpx.AsyncClient(timeout60) as client: async with client.stream( POST, DEEPSEEK_API_URL, jsonpayload, headersheaders ) as response: # 逐行读取 SSE 返回把增量推给前端 async for line in response.aiter_lines(): if not line.startswith(data:) or line data: [DONE]: continue json_str line[5:] chunk json.loads(json_str) delta chunk[choices][0][delta].get(content, ) if not delta: continue # 推给前端的消息带上事件类型 await websocket.send_text(json.dumps({ type: delta, content: delta, }, ensure_asciiFalse)) # 流式结束推送结束标记前端据此恢复输入框状态 await websocket.send_text(json.dumps({ type: done, }, ensure_asciiFalse))逻辑说明receive_text()等前端的消息client.stream()发起 HTTP 流式请求aiter_lines()逐行读取返回。SSE 格式里每行以data:开头最终以[DONE]结束这里用条件判断过滤掉空行和结束行。每次从delta取到内容片段就转成 WS 消息推出去ensure_asciiFalse是为了保证中文内容以原文传输不做 ASCII 转义。参数说明temperature控制随机性聊天场景我一般取 0.7timeout60不只是单次等待时间还包括整体流式读取期限如果大模型思考时间比较久建议放宽到 120 秒。实际项目里还需要把连接对象挂到一个连接管理器里方便后端主动关闭超时连接这部分 index.py 里有对应实现稍后章节细拆。2.3 前端消费增量从“拿到一坨字符串”到“逐字渲染”前端 App.jsx 里的核心不是 UI 组件而是对 WS 消息事件的分发处理。很多新手直接拿e.data当文本拼进聊天记录结果渲染出一大段 JSON。正确做法是先把消息解析成对象再按type分路处理。useEffect(() { const ws new WebSocket(ws://${location.host}/ws/chat) ws.onmessage (e) { try { const msg JSON.parse(e.data) if (msg.type delta) { setStreamText(prev prev msg.content) // 增量拼接到当前回复 } if (msg.type done) { setIsStreaming(false) // 状态归位恢复输入框 saveToMessageList(streamText) // 把完整回复存入消息列表 } if (msg.type error) { setError(msg.content) setIsStreaming(false) } } catch (err) { console.error(解析 WS 消息失败, e.data) } } return () ws.close() }, [])逻辑说明json.parse(e.data)把 WS 消息还原成结构化对象delta事件是增量内容把它追加到streamText状态变量上React 检测到状态变化就会重新渲染效果上就是打字机done事件代表这一轮回复结束把完整文本写入消息列表error事件透传后端错误。这样 UI 和链路的状态机是分离的不会出现“回复还没结束输入框又可用”的问题。这里有个容易忽视的开关ws.onclose一定要重连。生产环境里 WebSocket 连接断开频率比想象中高得多服务器重启、网络波动、Nginx 空闲超时都会断开。项目里如果没处理onclose用户在聊天框敲了一行字连接已经静默死亡半天没反馈。3. 前端工程搭建Vite 配置、组件拆分与流式渲染交互3.1 Vite 配置让开发环境不跨域、让 WS 请求正确转发拿到 antdx 工程第一件事就是看 vite.config.js。聊天应用最典型的翻车点是前端fetch后端接口跨域后端起在 8000 端口前端 Vite 起在 5173浏览器直接拒绝。常见做法是配置 Vite 的代理把所有/api请求转发到后端同时把/ws的 WebSocket 请求也代理过去。// vite.config.js import { defineConfig } from vite import react from vitejs/plugin-react export default defineConfig({ plugins: [react()], server: { port: 5173, proxy: { /api: { target: http://localhost:8000, changeOrigin: true, }, /ws: { target: ws://localhost:8000, ws: true, // 关键开启 WebSocket 代理 changeOrigin: true, }, }, }, })参数说明changeOrigin: true让请求头里的 Host 变成目标地址很多后端校验 Host 的接口这个必须开ws: true是 WebSocket 代理的开关很多人配置了半天发现连接 404就是少了这一行。注意 target 里ws://localhost:8000写的是 ws 协议对应的后端 FastAPI 服务也监听这个端口。开发环境这样配生产环境就交给 Nginx 做/ws的升级转发原理一致。3.2 App.jsx 组件结构消息列表、输入区、流式状态管理antdx 的前端结构不算复杂main.jsx是入口App.jsx是单页核心App.css管样式。App.jsx里的状态我建议拆成四块消息列表messages、当前流式回复streamText、WebSocket 连接状态connected、是否正在流式输出isStreaming。function App() { const [messages, setMessages] useState([]) const [streamText, setStreamText] useState() const [isStreaming, setIsStreaming] useState(false) const [input, setInput] useState() const wsRef useRef(null) const sendMessage () { if (!input.trim() || isStreaming) return // 流式期间禁止连发 const userMsg { role: user, content: input.trim() } setMessages(prev [...prev, userMsg]) setInput() setStreamText() setIsStreaming(true) // 发送给后端后端再转发给 DeepSeek wsRef.current.send(JSON.stringify({ content: input.trim() })) } return ( div classNamechat-container div classNamemessage-list {messages.map((msg, i) ( div key{i} className{message ${msg.role}}{msg.content}/div ))} {isStreaming ( div classNamemessage assistant{streamText}/div // 流式区域 )} /div div classNameinput-area textarea value{input} onChange{e setInput(e.target.value)} onKeyDown{e { if (e.key Enter !e.shiftKey) { e.preventDefault() sendMessage() } }} / button onClick{sendMessage} disabled{isStreaming} {isStreaming ? 生成中... : 发送} /button /div /div ) }逻辑说明isStreaming状态贯穿整个组件发送时置为true收到done事件后置为false。流式区域渲染的是streamText它每次只追加新内容所以 UI 上是一点点变长。按钮在流式期间禁用防止用户连发导致上下文错乱。wsRef用useRef保存连接实例而不是useState避免组件重渲染时反复建立连接。这套结构里有一个细节值得照抄用户消息和助手消息分开渲染message-list里用 CSS 区分左右两侧但数据层不做嵌套结构。很多人把“用户回复”包成一组对象流式更新时一侧变了另一侧也要跟着重渲染浪费性能还容易出 bug。3.3 交互细节自动滚动、打字机光标、失败重发流式聊天页面最让人难受的是回复长了页面不跟着滚动用户盯着屏幕外的内容干等。我在message-list上监听滚动事件用scrollTop scrollHeight实现强制吸底但要做个条件判断用户主动向上翻看历史消息时不要强制拉回否则体验更差。const listRef useRef(null) useEffect(() { const el listRef.current if (el isStreaming) { // 只在下一条消息进入时自动吸底 el.scrollTop el.scrollHeight } }, [streamText, isStreaming])参数说明依赖数组里写[streamText, isStreaming]每次流式内容更新触发一次isStreaming变化时也会触发保证新会话开始时列表吸附到最新位置。打字机光标效果用 CSS 伪元素实现.streaming::after { content: |; animation: blink 1s infinite; }全凭个人偏好不影响数据链路。失败重发要做的不是简单弹窗而是把发送失败的消息留在输入框里让用户一键重发后端返回的 error 消息里带request_id方便排查。3.4 依赖管理说明工程里的package.json依赖主要是 React 和 ViteWebSocket 是浏览器原生 API不需要额外装库。加上antd风格的 UI 组件库需要看antdx目录命名推测但没有本质影响。如果后端要同时支持 HTTP 接口和 WSFastAPI 加上即可前端不用装 axios原生fetch配合 WS 足够。很多项目把socket.io-client引进来做 WebSocket 封装我不太推荐原生 WS 在聊天场景下可控性更高心跳、重连都可以自己写没必要引入一套它有自己协议格式的方案。4. 后端 Python 代理服务模型参数、密钥管理与流式转发4.1 FastAPI 工程骨架index.py 入口与模块划分antdx 项目里后端文件放在 py 目录下index.py 是入口。FastAPI 的写法非常直观定义一个/ws/chat端点处理 WebSocket 连接再定义一个/api/health给前端探活。from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware import os app FastAPI() app.add_middleware( CORSMiddleware, allow_origins[*], # 开发环境放开生产必须收紧 allow_credentialsTrue, allow_methods[*], allow_headers[*], ) app.websocket(/ws/chat) async def chat_endpoint(websocket: WebSocket): await handle_chat_ws(websocket)参数说明allow_origins在开发环境设*方便调试但部署生产一定要改成具体域名否则任何网站都能往你的后端发请求烧你的 DeepSeek 额度。WebSocketDisconnect异常要捕获前端突然刷新、断网时后端接收异常如果不处理会打印一堆报错。实际部署时用 uvicorn 启动uvicorn index:app --host 0.0.0.0 --port 8000生产环境建议加--workers 4但要注意 WebSocket 连接会分散到多个 worker 进程连接状态不能存在进程内存里。4.2 DeepSeek 请求参数整理调用 DeepSeek API 的参数和 OpenAI 兼容格式一致但有些细节值得单独拉出来参数取值建议说明modeldeepseek-chatDeepSeek-V3 聊天模型temperature0.6 - 0.8聊天取 0.7代码生成建议 0.2max_tokens2048单次回复上限超长文档场景调到 4096streamtrue流式开关聊天场景必须为 truemessagesrole/content 数组需要把完整对话历史传给模型presence_penalty0 - 1控制话题跳跃默认 0我用 FastAPI httpx 实现的流式消息体是这样的async def forward_to_deepseek(websocket, messages): payload { model: deepseek-chat, messages: messages, stream: True, temperature: 0.7, max_tokens: 2048, } headers { Authorization: fBearer {os.getenv(DEEPSEEK_API_KEY)}, Accept: application/json, } async with httpx.AsyncClient(timeout120) as client: async with client.stream( POST, https://api.deepseek.com/chat/completions, jsonpayload, headersheaders, ) as response: async for line in response.aiter_lines(): if not line.startswith(data: ): continue line_data line[6:] # 去掉 data: 前缀 if line_data [DONE]: break chunk_json json.loads(line_data) delta_content chunk_json[choices][0][delta].get(content, ) if delta_content: await websocket.send_text(json.dumps({ type: delta, content: delta_content, }, ensure_asciiFalse))逻辑说明messages参数需要把整个对话历史拿过来因为大模型本身无状态每轮都是完整上下文。有些新手只发最后一句话模型答几句就忘了前面内容这是聊天上下文断裂的常见原因。line[6:]去掉data:前缀[DONE]表示流式结束。注意这里break而不是continue因为[DONE]之后不会有有效数据了。4.3 密钥管理API Key 不落前端、不上 Git这个项目里最容易出安全事故的地方就是密钥。前端资源是公开展示的把 key 写进 App.jsx别人看源码就能扒走。正确做法是后端从环境变量读 key前端连的永远只有自己的 WS 服务。存放方式我用的是一个.env文件内容形如DEEPSEEK_API_KEYsk-xxxx.gitignore里必须排除.env。antdx 工程里有现成的.gitignore默认会忽略.env和node_modules但如果你自己开新项目这个文件是最容易忘的。提交代码前用git status复查一遍防止不小心把 key 推到远端。另外要尽量在后端做错误信息脱敏。DeepSeek API 返回 401 时不要把响应体原样透传给前端应该统一包装成{type: error, content: 服务鉴权失败}。否则后端日志、前端控制台都会暴露密钥相关信息。我见过一个上线项目因为错误透传用户拿错误信息里的请求 ID 去刷后端日志虽然没拿到 key但也算信息泄露。4.4 并发与连接管理聊天的并发场景要注意 WS 连接数的管理。本地用单机跑没什么压力但一旦部署到公网每个用户占用一个 TCP 连接后端要有连接管理器统一回收class ConnectionManager: def __init__(self): self.active_connections: dict[str, WebSocket] {} async def connect(self, user_id: str, websocket: WebSocket): await websocket.accept() self.active_connections[user_id] websocket def disconnect(self, user_id: str): self.active_connections.pop(user_id, None) async def broadcast(self, message: dict): for user_id, conn in self.active_connections.items(): await conn.send_text(json.dumps(message, ensure_asciiFalse))逻辑说明用字典以user_id为键管理连接断开时删除。broadcast支持向所有在线用户推送消息这个在后续做“会话列表同步”“系统通知”时直接复用。单机内存存储够用多机部署需要换成 Redis pub/sub但项目骨架是一样的。5. 避坑WebSocket 流式聊天最容易翻车的 6 个问题5.1 前端收不到消息后端日志却在正常打印现象后端控制台能看到 DeepSeek 返回的 delta 内容但浏览器聊天框一直空白。原因后端推给前端的消息是 JSON 字符串前端onmessage里直接把它当文本追加到消息列表了渲染出来是一大段带花括号的 JSON。还有一种是消息协议没对齐后端推的是{type: delta, ...}前端判断的是msg.type message。解决前端强制先JSON.parse再按 type 分发。后端在推送前也打一条日志确认websocket.send_text()没有被异常吞掉给每个消息加个seq序号前端发现序号不连续可以主动报错远比看半天日志高效。5.2 回复到一半 WebSocket 连接断掉现象大模型回复生成长文本时输出到一半浏览器控制台报WebSocket is closed before the connection is established或者后端报发送异常。原因一是不论是 Nginx 还是云服务器对空闲连接有默认超时二是流式接口耗时太久中间没有任何心跳数据连接被判定为死连接回收。三是 uvicorn 单 worker 处理能力到达瓶颈吞掉了部分发送任务。解决前端每隔 25 秒向后端发一个心跳帧{type: ping}后端收到后回{type: pong}这样连接每 25 秒都有一次活跃数据不会被断开。同时在后端给发消息加保护逻辑发送失败时记录断点位置重连后从断点续传。5.3 中文乱码或最后一个字重复现象后端返回的 delta 内容拼接后偶尔出现乱码或者结尾多了个一模一样的字符。原因上游接口是 SSE 流式返回 UTF-8 字节某些情况下一个中文汉字被拆到两次 delta 里前端拿到半截字节直接渲染就乱码凑完整后又渲染一次。这类问题通常和客户端解码方式有关后端推送时自己消费的内容没问题但前端Blob方式读事件时容易踩编码坑。解决前端统一走e.data文本方式接收不要用FileReader读 WebSocket 二进制帧。后端可以做一层缓冲攒够一段完整文本再推送简单粗暴的方案是让前端拿到 delta 后不立即渲染而是推入一个 30ms 的合并节流器合并后再拼接到消息体。这个 30ms 延迟人眼感知不到但能过滤掉大量半字问题。5.4 “停止生成”按钮无效消息还在继续滚动现象点击停止按钮没有任何反应前端代码也调了ws.close()但消息还在输出。原因ws.close()只是关闭了客户端到后端的连接后端的aiter_lines()还在跑DeepSeek 那边的 HTTP 请求没有取消token 消费也不会停止。解决停止按钮不应该直接关 WS而是发送一条{type: stop}消息给后端。后端收到后取消当前的 httpx 流式请求再推{type: done}告诉前端流式结束。Python 侧用asyncio.Task.cancel()或给client.stream()传timeout参数控制。核心思想是停止动作一定要贯穿整条链路而不只是断掉浏览器这条连接。5.5 Vite 代理配了但 WS 连接 404现象前端页面能正常打开new WebSocket(/ws/chat)一直报 404后端日志没收到任何 connect 事件。原因忘了在vite.config.js的 proxy 里加ws: true。这行配置在文档里不显眼但缺了它Vite 只把 HTTP 请求转发到后端WebSocket 握手请求直接在 5173 端口被吞掉。解决把ws: true加上同时检查 target 写的是ws://localhost:8000而不是http://localhost:8000。如果用了 Nginx 部署还要在配置里显式增加proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection upgrade;否则 Nginx 默认不会转发连接协议升级请求。5.6 密匙泄漏进 Git 历史现象项目推送到远程仓库后发现.env文件或硬编码的 API Key 已经进了 Git 历史删掉也没用。原因.gitignore只对未跟踪文件生效已经提交过的文件删掉后仍在历史记录里。解决如果不涉及敏感资金最干净的是重置历史如果仓库已经公开直接换 key 比删历史更稳妥。从那以后我新建项目第一件事就是把.env写进.gitignore在提交信息里再加一次git diff复查。这个习惯治好了我所有“密钥裸奔”的焦虑。6. 给这套 WebSocket 流式链路上三道保险心跳、重连和停止生成心跳、重连、停止生成这三个能力不是可有可无的优化而是生产环境里聊天系统的底线。先说心跳前端开一个 25 秒定时器发 ping后端收到后回 pong。如果连续三次没有 pong 响应可以判定连接已死主动触发重连。为什么要用这种主动模拟而不是依赖浏览器 WS 状态因为 ws 的状态在断网时可能仍然显示OPEN实际链路已经断了只有真实往返才能确认。let heartbeatTimer null const HEARTBEAT_INTERVAL 25000 function startHeartbeat(ws) { clearInterval(heartbeatTimer) heartbeatTimer setInterval(() { if (ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ type: ping })) if (missedPong 3) { ws.close() // 连续三次未收到 pong主动断开让重连逻辑接管 } } }, HEARTBEAT_INTERVAL) }参数说明25 秒是经验值要小于 Nginx 默认的 60 秒超时又不要太频繁 以免浪费后端资源。missedPong计数在每次收到 pong 时清零。然后是重连。重连算法用指数退避第一次失败等 1 秒第二次 2 秒第三次 4 秒最多等 30 秒避免服务器刚重启时所有客户端同时涌进来。function connectWithRetry(maxDelay 30000) { let delay 1000 const attempt () { const ws new WebSocket(ws://${location.host}/ws/chat) ws.onopen () { delay 1000; missedPong 0; } ws.onclose () { setTimeout(attempt, delay) delay Math.min(delay * 2, maxDelay) } } attempt() }最后是停止生成。后端收到 stop 消息后用asyncio取消当前转发任务一般做法是把 httpx 流式请求封装成一个 Taskstop 消息到达时直接task.cancel()然后把连接恢复为可接收下一条消息的状态。做完这三件事这套从 DeepSeek 大模型到 React 聊天框的 WebSocket 流式链路才算真正“能扛事”。从那以后我每次接大模型聊天都会强制把心跳、重连、停止这三项做成模板默认配置而不是等到线上出问题再补。希望这份拆解帮到你直接拿 antdx 这份工程对照着改能少走不少弯路。本文还有配套的精品资源点击获取
返回列表