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

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


文章目录

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

前言

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

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


效果展示

【GPT前端实用系列】流式请求+渲染内容兼容deepseek返回think思考标签(保姆级别教程)_vue think标签
【GPT前端实用系列】流式请求+渲染内容兼容deepseek返回think思考标签(保姆级别教程)_vue 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<TRequest = any, TResponse = any> { url: string body: TRequest onMessage: (msg: { status: SSEStatus data: TResponse | string | null }) => void autoStart?: boolean}export function usePostSSE<TRequest = any, TResponse = any>({ url, body, onMessage, autoStart = true,}: UsePostSSEParams<TRequest, TResponse>) { const controllerRef = useRef<AbortController | null>(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\' // 你封装的 hookinterface 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<SSEStatus>(\'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 ( <div className=\"w-screen h-screen flex\"> <div className=\"flex-1 flex flex-col h-screen items-center\"> <div className=\"p-4 border rounded min-h-[100px]\"> {gptReply || \'等待响应...\'} </div> <p className=\"text-sm mt-2 text-gray-500\">状态:{status}</p> <button className=\"mt-4 px-4 py-2 rounded bg-red-500 text-white hover:bg-red-600\" onClick={stop} > 停止生成 </button> </div> </div> )}

基本是用方法

import React, { useState } from \'react\'import { usePostSSE, SSEStatus } from \'@/hooks/usePostSSE\' // 你封装的 hookinterface 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<SSEStatus>(\'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 ( <div className=\"w-screen h-screen flex\"> <div className=\"flex-1 flex flex-col h-screen items-center\"> <h2 className=\"text-xl font-semibold mb-2\">GPT 的对话</h2> <div className=\"p-4 border rounded min-h-[100px]\"> {gptReply || \'等待响应...\'} </div> <p className=\"text-sm mt-2 text-gray-500\">状态:{status}</p> <button className=\"mt-4 px-4 py-2 rounded bg-red-500 text-white hover:bg-red-600\" onClick={stop} > 停止生成 </button> </div> </div> )}

四、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 ( <ReactMarkdown remarkPlugins={[remarkDirective]} rehypePlugins={[rehypeHighlight]} > {content} </ReactMarkdown> )}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: OpenAIlet 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 = 1const 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<string, string | number | boolean> data?: Data}

那么我的实现方式就是这样

import { Node, Data } from \'unist\'import { Root, Content } from \'mdast\'// 扩展 Directive 节点类型type Directive = Content & { type: \'textDirective\' | \'leafDirective\' | \'containerDirective\' name: string attributes?: Record<string, string | number | boolean> data?: Data}const remarkCustomDirectives = () => { return (tree: Root) => { visit<Root, Directive>(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 }) => ( <details open className=\"mb-4 max-w-full break-words\"> <summary className=\"text-sm text-gray-400 cursor-default ml-2\"> 思考中 </summary> <div className=\"mt-1 text-sm text-gray-500 border-l-2 border-gray-300 pl-3 ml-3\"> {children} </div> </details>)

注册

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<string, string | number | boolean> data?: Data}const remarkCustomDirectives = () => { return (tree: Root) => { visit<Root, Directive>(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 }) => ( <details open className=\"mb-4 max-w-full break-words\"> <summary className=\"text-sm text-gray-400 cursor-default ml-2\"> 思考中 </summary> <div className=\"mt-1 text-sm text-gray-500 border-l-2 border-gray-300 pl-3 ml-3\"> {children} </div> </details>)const Markdown: FC<{ content: string }> = ({ content }) => { return ( <ReactMarkdown remarkPlugins={[remarkDirective, remarkCustomDirectives]} rehypePlugins={[rehypeHighlight]} components={ { ThinkBlock, } as Components } > {content} </ReactMarkdown> )}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<string, string | number | boolean> data?: Data}// 扩展 Code 节点类型interface CodeNode extends Node { lang?: string meta?: string data?: Data & { meta?: string }}const remarkCustomDirectives = () => { return (tree: Root) => { visit<Root, Directive>(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 }) => ( <details open className=\"mb-4 max-w-full break-words\"> <summary className=\"text-sm text-gray-400 cursor-default ml-2\"> 思考中 </summary> <div className=\"mt-1 text-sm text-gray-500 border-l-2 border-gray-300 pl-3 ml-3\"> {children} </div> </details>)const Markdown: FC<{ content: string }> = ({ content }) => { return ( <ReactMarkdown remarkPlugins={[remarkCustomDirectives, remarkDirective]} rehypePlugins={[rehypeHighlight]} components={ { ThinkBlock, pre: ({ children }) => <pre className=\"not-prose\">{children}</pre>, 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 ( <div className=\"not-prose rounded-mdoverflow-x-auto\">  <div className=\"flex h-12 items-center justify-between bg-zinc-100 px-4 dark:bg-zinc-900\">  <div className=\"flex items-center gap-2\"><Terminal size={18} /><p className=\"text-sm text-zinc-600 dark:text-zinc-400\"> {codeNode?.data?.meta || lang}</p>  </div>  <CopyButton id={id} />  </div>  <div className=\"overflow-x-auto w-10/12 box-border\">  <div id={id} className=\"p-4\"><code className={className} {...props}> {children}</code>  </div>  </div> </div>  ) } return (  <code {...props} className=\"not-prose rounded bg-gray-100 px-1 dark:bg-zinc-900\"  > {children}  </code> ) }, } as Components } > {content} </ReactMarkdown> )}export default Markdown

五、页面完成示例

\'use client\'import React, { useState } from \'react\'import { usePostSSE, SSEStatus } from \'@/hooks/usePostSSE\' // 你封装的 hookimport 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<SSEStatus>(\'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 ( <div className=\"w-screen h-screen flex\"> <div className=\"flex-1 flex flex-col h-screen items-center\"> <h2 className=\"text-xl font-semibold mb-2\">GPT 的对话</h2> <div className=\"w-10/12 flex-1 overflow-y-auto flex flex-col custom-scrollbar\"> <Markdown content={gptReply}></Markdown> </div> {/* 
{gptReply || \'等待响应...\'}
*/
} <p className=\"text-sm mt-2 text-gray-500\">状态:{status}</p> <button className=\"mt-4 px-4 py-2 rounded bg-red-500 text-white hover:bg-red-600\" onClick={stop} > 停止生成 </button> </div> </div> )}

git地址

教学地址:gitee地址
https://gitee.com/dabao1214/csdn-gpt
内涵next后端不会请参看
本地搭建属于自己的GPT(保姆级别教程)

总结

不懂可以评论,制作不易。请大佬们动动发财的小手点点关注