【GPT前端实用系列】流式请求+渲染内容兼容deepseek返回think思考标签(保姆级别教程)
文章目录
- 前言
- 效果展示
- 一、所需插件
- 二、实现思路
- 三、搭建流式请求
- 基本是用方法
- 四、markDown渲染详细版本
- 1.搭建组件Markdown,及加载所需插件
- 2.定义内容替换函数
- 前端示例
- next后端示例
- 组件实现
- 节点替换处理
- 注册展开插件
- 五、页面完成示例
- git地址
- 总结
前言
需要本地环境及测试代码可以参考👇面项目搭建本地的gpt业务
本地搭建属于自己的GPT(保姆级别教程)
效果展示
提示:以下是本篇文章正文内容,下面案例可供参考
一、所需插件
lucide-react:图标库。(非必要库,展示代码需要可自信更改)
npm install react-markdown lucide-react unist-util-visit remark-directive
二、实现思路
我的思路核心是将think标签进行替换成:::think 内容 ::: 形式,使用remark-directive进行解析成标签,再使用unist-util-visit进行映射组件,在与react-markdown中 components定义组件进行实现
三、搭建流式请求
hook主要功能,展示当前状态、手动取消、实时接收回调消息、ts类型支持
不想看代码的兄弟直接看下面是用方法即可
import { useEffect, useRef } from 'react' export type SSEStatus = | 'idle' | 'connecting' | 'message' | 'error' | 'closed' | 'aborted' interface UsePostSSEParams { url: string body: TRequest onMessage: (msg: { status: SSEStatus data: TResponse | string | null }) => void autoStart?: boolean } export function usePostSSE({ url, body, onMessage, autoStart = true, }: UsePostSSEParams) { const controllerRef = useRef(null) const start = () => { const controller = new AbortController() controllerRef.current = controller fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream', }, body: JSON.stringify(body), signal: controller.signal, }) .then((response) => { if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`) const reader = response.body?.getReader() const decoder = new TextDecoder('utf-8') let buffer = '' const read = () => { reader ?.read() .then(({ done, value }) => { if (done) { onMessage({ status: 'closed', data: null }) return } buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() || '' for (let line of lines) { line = line.trim() if (line.startsWith('data:')) { const jsonData = line.slice(5).trim() try { const parsed = JSON.parse(jsonData) onMessage({ status: 'message', data: parsed }) } catch { onMessage({ status: 'error', data: jsonData }) } } } read() }) .catch((err) => { onMessage({ status: 'error', data: err.message }) }) } onMessage({ status: 'connecting', data: null }) read() }) .catch((err) => { onMessage({ status: 'error', data: err.message }) }) } const stop = () => { controllerRef.current?.abort() onMessage({ status: 'aborted', data: null }) } useEffect(() => { if (autoStart) start() return () => stop() // Clean up on unmount }, []) return { start, stop } }
'use client' import React, { useState } from 'react' import { usePostSSE, SSEStatus } from '@/hooks/usePostSSE' // 你封装的 hook interface GPTStreamResponse { sendUserInfo: { user_id: number message: string sender_type: 'user' } sender_type: 'gpt' message: string } export default function ChatSSE() { const [gptReply, setGptReply] = useState('') const [status, setStatus] = useState('idle') const { stop } = usePostSSE message: string }, GPTStreamResponse({ url: '/api/chat', body: { message: `帮我写个描述天气的好语句,50字`, }, // 用户输入的消息 onMessage: ({ status, data }) => { setStatus(status) if (status === 'message' && data && typeof data === 'object') { const gptData = data as GPTStreamResponse setGptReply((prev) => prev + gptData.message) } }, }) return (
{gptReply || '等待响应...'}状态:{status}
stop} 停止生成基本是用方法
import React, { useState } from 'react' import { usePostSSE, SSEStatus } from '@/hooks/usePostSSE' // 你封装的 hook interface GPTStreamResponse { sendUserInfo: { user_id: number message: string sender_type: 'user' } sender_type: 'gpt' message: string } export default function ChatSSE() { const [gptReply, setGptReply] = useState('') const [status, setStatus] = useState('idle') const { stop } = usePostSSE message: string }, GPTStreamResponse({ url: '/api/chat', body: { message: `帮我写个描天气的100字文案`, }, // 用户输入的消息 onMessage: ({ status, data }) => { setStatus(status) if (status === 'message' && data && typeof data === 'object') { const gptData = data as GPTStreamResponse setGptReply((prev) => prev + gptData.message) } }, }) return (
与 GPT 的对话
{gptReply || '等待响应...'}状态:{status}
stop} 停止生成四、markDown渲染详细版本
1.搭建组件Markdown,及加载所需插件
这里就简单实现一个组件
import { FC } from 'react' import ReactMarkdown from 'react-markdown' import rehypeHighlight from 'rehype-highlight' import 'highlight.js/styles/atom-one-dark.css' import remarkDirective from 'remark-directive' const Markdown: FC content: string } = ({ content }) => { return ( [remarkDirective]} rehypePlugins={[rehypeHighlight]} {content} ) } export default Markdown
2.定义内容替换函数
我的建议是让后端进行处理,因为deepseek的思考一般是不存入数据库的。同时think标签是直接返回的还是比较好处理的。如果不处理咱们前端也可以进行处理
const replaceThink = (str: string) => { try { return str .replace(/]*>/gi, '\n:::think\n') .replace(//gi, '\n:::\n') } catch (error) { console.error('Error replacing think:', error) return str } }
前端示例
const replaceThink = (str: string) => { try { return str .replace(/]*>/gi, '\n:::think\n') .replace(//gi, '\n:::\n') } catch (error) { console.error('Error replacing think:', error) return str } } const { stop } = usePostSSE message: string }, GPTStreamResponse({ url: '/api/chat', body: { message: `帮我写个描天气的100字文案`, }, // 用户输入的消息 onMessage: ({ status, data }) => { setStatus(status) if (status === 'message' && data && typeof data === 'object') { const gptData = data as GPTStreamResponse setGptReply((prev) => prev + replaceThink(gptData.message)) } }, })
next后端示例
import { NextRequest } from 'next/server' import OpenAI from 'openai' import { mysql, redis } from '@/utils/db' let openai: OpenAI let model: string = 'gpt-3.5-turbo' if (process.env.LOC_GPT_URL) { openai = new OpenAI({ baseURL: process.env.LOC_GPT_URL, }) model = 'deepseek-r1:1.5b' } else { openai = new OpenAI({ baseURL: process.env.OPENAI_BASE_URL || 'https://api.chatanywhere.tech', apiKey: process.env.OPENAI_API_KEY || '', }) } const pro = { role: 'system', content: '你是一个编程助手', } const userId = 1 const replaceThink = (str: string) => { try { return str .replace(/]*>/gi, '\n:::think\n') .replace(//gi, '\n:::\n') } catch (error) { console.error('Error replacing think:', error) return str } } async function getChatRedisHistory(key: string) { try { const redis_history = (await redis.get(key)) as string const list = JSON.parse(redis_history) return list } catch (e) { return [] } } export async function POST(request: NextRequest) { try { const body = await request.json() const message = body.message if (!message) { return new Response(JSON.stringify({ error: 'Message is required' }), { status: 400, headers: { 'Content-Type': 'application/json' }, }) } const redis_history = (await getChatRedisHistory(`user_${userId}_chatHistory`)) || [] redis_history.push({ role: 'user', content: message }) redis_history.unshift(pro) const res = await mysql.gpt_chat_history.create({ data: { user_id: 1, message, sender_type: 'user' }, }) const stream = new ReadableStream({ async start(controller) { try { const completionStream = await openai.chat.completions.create({ model, messages: redis_history, stream: true, }) let obj: any = { user_id: 1, sender_type: 'gpt', message: '', } // think 标签处理缓存 for await (const chunk of completionStream) { let content = chunk.choices[0]?.delta?.content || '' if (!content) continue console.log('content', content) const text =replaceThink(content) // 处理think标签:开始标签 obj.message += text // 非think标签内容,正常返回 controller.enqueue( new TextEncoder().encode( `data: ${JSON.stringify({ sendUserInfo: res, sender_type: 'gpt', message: text, })}\n\n` ) ) } await mysql.gpt_chat_history.create({ data: obj }) redis_history.push({ role: 'system', content: obj.message }) redis.set( 'user_1_chatHistory', JSON.stringify(redis_history.slice(-10)) ) } catch (error: any) { console.error('OpenAI API error:', error) controller.enqueue( new TextEncoder().encode( `data: ${JSON.stringify({ error: error.message })}\n\n` ) ) } finally { controller.close() } }, }) return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', 'Access-Control-Allow-Origin': '*', }, }) } catch (error: any) { return new Response(JSON.stringify({ error: error.message }), { status: 500, headers: { 'Content-Type': 'application/json' }, }) } }
组件实现
节点替换处理
上述操作把think转换成了:::think了,remarkDirective插件又能转换成标签,我们只需要是用visit访问根节点把Directive扩展类型进行处理替换即可
Directive数据类型结构
type Directive = Content & { type: 'textDirective' | 'leafDirective' | 'containerDirective' name: string attributes?: Record data?: Data }
那么我的实现方式就是这样
import { Node, Data } from 'unist' import { Root, Content } from 'mdast' // 扩展 Directive 节点类型 type Directive = Content & { type: 'textDirective' | 'leafDirective' | 'containerDirective' name: string attributes?: Record data?: Data } const remarkCustomDirectives = () => { return (tree: Root) => { visit(tree, (node: any) => { if ( node.type === 'textDirective' || node.type === 'leafDirective' || node.type === 'containerDirective' ) { if (node.name === 'think') { node.data = { ...node.data, hName: 'ThinkBlock', hProperties: {}, } } } }) } }
注册展开插件
展开组件
import { FC, ReactNode } from 'react' const ThinkBlock: FC children?: ReactNode } = ({ children }) => ( 思考中
{children})注册
import { FC, ReactNode } from 'react' import ReactMarkdown, { Components } from 'react-markdown' import rehypeHighlight from 'rehype-highlight' import 'highlight.js/styles/atom-one-dark.css' import remarkDirective from 'remark-directive' import { visit } from 'unist-util-visit' import { Node, Data } from 'unist' import { Root, Content } from 'mdast' // 扩展 Directive 节点类型 type Directive = Content & { type: 'textDirective' | 'leafDirective' | 'containerDirective' name: string attributes?: Record data?: Data } const remarkCustomDirectives = () => { return (tree: Root) => { visit(tree, (node: any) => { if ( node.type === 'textDirective' || node.type === 'leafDirective' || node.type === 'containerDirective' ) { if (node.name === 'think') { node.data = { ...node.data, hName: 'ThinkBlock', hProperties: {}, } } } }) } } const ThinkBlock: FC children?: ReactNode } = ({ children }) => ( 思考中
{children}) const Markdown: FC content: string } = ({ content }) => { return ( [remarkDirective, remarkCustomDirectives]} rehypePlugins={[rehypeHighlight]} components={ { ThinkBlock, } as Components } {content} ) } export default Markdown到此已经完成了
补充下code基本样式,随便gpt生成的样式丑勿怪
import { FC, ReactNode } from 'react' import ReactMarkdown, { Components } from 'react-markdown' import rehypeHighlight from 'rehype-highlight' import { Terminal } from 'lucide-react' import 'highlight.js/styles/atom-one-dark.css' import CopyButton from './CopyButton' import { visit } from 'unist-util-visit' import remarkDirective from 'remark-directive' import { Node, Data } from 'unist' import { Root, Content } from 'mdast' // 扩展 Directive 节点类型 type Directive = Content & { type: 'textDirective' | 'leafDirective' | 'containerDirective' name: string attributes?: Record data?: Data } // 扩展 Code 节点类型 interface CodeNode extends Node { lang?: string meta?: string data?: Data & { meta?: string } } const remarkCustomDirectives = () => { return (tree: Root) => { visit(tree, (node: any) => { if ( node.type === 'textDirective' || node.type === 'leafDirective' || node.type === 'containerDirective' ) { if (node.name === 'think') { node.data = { ...node.data, hName: 'ThinkBlock', hProperties: {}, } } } }) } } const ThinkBlock: FC children?: ReactNode } = ({ children }) => ( 思考中
{children}) const Markdown: FC content: string } = ({ content }) => { return ( [remarkCustomDirectives, remarkDirective]} rehypePlugins={[rehypeHighlight]} components={ { ThinkBlock, pre: ({ children }) ={children}
, code: ({ node, className, children, ...props }) => { const codeNode = node as CodeNode const match = /language-(\w+)/.exec(className || '') if (match) { const lang = match[1] const id = `code-${Math.random().toString(36).substr(2, 9)}` return (18} /id} /{codeNode?.data?.meta || lang}
id} className="p-4" className} {...props}> {children}五、页面完成示例
'use client' import React, { useState } from 'react' import { usePostSSE, SSEStatus } from '@/hooks/usePostSSE' // 你封装的 hook import Markdown from '@/app/components/markDown' interface GPTStreamResponse { sendUserInfo: { user_id: number message: string sender_type: 'user' } sender_type: 'gpt' message: string } export default function ChatSSE() { const [gptReply, setGptReply] = useState('') const [status, setStatus] = useState('idle') const replaceThink = (str: string) => { try { return str .replace(/]*>/gi, '\n:::think\n') .replace(//gi, '\n:::\n') } catch (error) { console.error('Error replacing think:', error) return str } } const { stop } = usePostSSE message: string }, GPTStreamResponse({ url: '/api/chat', body: { message: `帮我写个描天气的100字文案`, }, // 用户输入的消息 onMessage: ({ status, data }) => { setStatus(status) if (status === 'message' && data && typeof data === 'object') { const gptData = data as GPTStreamResponse setGptReply((prev) => prev + replaceThink(gptData.message)) } }, }) return (
与 GPT 的对话
gptReply}{/* {gptReply || '等待响应...'} */}状态:{status}
stop} 停止生成git地址
教学地址:gitee地址
https://gitee.com/dabao1214/csdn-gpt
内涵next后端不会请参看
本地搭建属于自己的GPT(保姆级别教程)
总结
不懂可以评论,制作不易。请大佬们动动发财的小手点点关注
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。