【GPT前端实用系列】流式请求+渲染内容兼容deepseek返回think思考标签(保姆级别教程)

06-01 1060阅读

文章目录

  • 前言
  • 效果展示
  • 一、所需插件
  • 二、实现思路
  • 三、搭建流式请求
      • 基本是用方法
      • 四、markDown渲染详细版本
        • 1.搭建组件Markdown,及加载所需插件
        • 2.定义内容替换函数
          • 前端示例
          • next后端示例
          • 组件实现
            • 节点替换处理
            • 注册展开插件
            • 五、页面完成示例
            • git地址
            • 总结

              前言

              需要本地环境及测试代码可以参考👇面项目搭建本地的gpt业务

              本地搭建属于自己的GPT(保姆级别教程)


              效果展示

              【GPT前端实用系列】流式请求+渲染内容兼容deepseek返回think思考标签(保姆级别教程)

              【GPT前端实用系列】流式请求+渲染内容兼容deepseek返回think思考标签(保姆级别教程)

              提示:以下是本篇文章正文内容,下面案例可供参考

              一、所需插件

              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} /

              {codeNode?.data?.meta || lang}

              id} /
              id} className="p-4" className} {...props}> {children}
              ) } return ( ...props} className="not-prose rounded bg-gray-100 px-1 dark:bg-zinc-900" > {children} ) }, } as Components } > {content} ) } export default Markdown

              五、页面完成示例

              '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,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

相关阅读

目录[+]

取消
微信二维码
微信二维码
支付宝二维码