【GPT前端实用系列】本地搭建属于自己的GPT,内涵next、java、react(保姆级别教程)

文章目录

  • 前言
  • 一、ollama本地搭建
  • 二、后端搭建
    • 数据库环境搭建
  • 二、next后端搭建
    • 创建项目
    • 基本环境配置
      • 创建.env本地文件管理本地环境
      • 创建mysql链接模型
    • 首次数据库同步
    • 搭建接口
      • 发送接口
      • 聊天记录查询接口
    • java版本流式接口
      • 安装包
      • 接口搭建
  • react前端搭建(未完成,后续有空更新,git仓库有一版本可以自行参考)
    • 封装发送hook
  • 总结


前言

技术栈:openai、ollama、next、react搭建本地gpt,代码基于openai接口规范处理。使用openaisdk进行对接。本教程分为两个版本,一个无需本地搭建后端环境,一个搭建
gitee仓库地址
注:前端小白,代码质量不高大佬勿喷。


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

一、ollama本地搭建

ollama官网地址根据地址选择符合自己系统进行安装,安装无需配置别的。直接下一步下一步即可


验证安装
cmd输入

ollama --version

成功则会展示
安装成功


安装模型
ollama模型地址自行选取使用的模型
懒人直接安装deepseek-r1,这里安装1.5b模型兼容大部分电脑,运行命令等待即可
安装1.5b模型

ollama run deepseek-r1:1.5b

等待安装完成,验证是否安装,
ollama list查看本地模型,安装成功即可看到
ollama run deepseek-r1:1.5b运行模型

ollama list
ollama run deepseek-r1:1.5b

安装成功示例
安装成功示例
查看api服务

ollama serve

在这里插入图片描述
127.0.0.1:11434 这个就是你的服务地址,到这里本地搭建最基本服务就已经完成了

二、后端搭建

数据库环境搭建


小白用小皮安装mysql、redis教程有环境跳过即可
小皮下载地址直接按提示安装即可
在这里插入图片描述
在这里插入图片描述
安装完成进去这两个目录去下载sql、redis,下载完启动。首页可以配置基本信息等
在这里插入图片描述
sql配置,直接vscode下载mysql进行链接
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
redis
在这里插入图片描述
按这个搭建即可


二、next后端搭建

创建项目

npx create-next-app@latest my-gpt

进入项目npm\yarn进行安装,

基本环境配置

使用prisma、ioredis链接mysql及redis,
考略到本地模型不方便演示为在线api,本地模型也会同步去写代码

创建.env本地文件管理本地环境

  DATABASE_URL="mysql://账号:密码@localhost:3306/库名?schema=public"
  REDIS_URL="redis://localhost:6379/第几个序号"
  LOC_GPT_URL:"http://127.0.0.1:11434/v1"
  OPENAI_BASE_URL=""
  OPENAI_API_KEY=""

创建mysql链接模型

安装依赖

npm install ioredis @prisma/client@6.5.0 prisma@6.5.0 axios openai

根目录创建prisma>schema.prisma文件,vscode有prisma代码提示插件

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL") // 获取环境变量DATABASE_URL
}

// 这个是一个gpt聊天记录模型根据这个模型就可以直接同步数据库
model gpt_chat_history {
  id          Int                          @id @default(autoincrement())
  user_id     Int
  sender_type gpt_chat_history_sender_type
  message     String                       @db.Text
  created_at  DateTime?                    @default(now()) @db.DateTime(3)
}

enum gpt_chat_history_sender_type {
  user
  gpt
}

app目录下创建mysql.ts

import { PrismaClient } from '@prisma/client'
const prismaClientSingleton = () => {
  return new PrismaClient()
}

type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClientSingleton | undefined
}

const prisma = globalForPrisma.prisma ?? prismaClientSingleton()

export default prisma

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

redis.ts

import Redis from 'ioredis'
// 可以写一个单列
const url = process.env.REDIS_URL as string
let redis = new Redis(url)

export default redis

db.ts 汇总导出

import mysql from './mysql'
import redis from './redis'
export { mysql, redis }

首次数据库同步

  1. 本地项目已经按上述配置好,数据库连接好
  2. yarn dev 启动项目(必须要先启动项目不然后续无法执行)
  3. 另起一个窗口npx prisma db push 你的数据库就可以同步进去gpt_chat_history 表
  4. npx prisma generate 重新生成下不然ts容易报错

搭建接口

发送接口

app目录下创建router进行统一api开发
创建app>api>chat>route.ts
标准版不需要数据库和redis,上下文需要后续处理

import { NextRequest } from 'next/server'
import OpenAI from 'openai'

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 || '', // 确保在环境变量中配置 API Key
  })
}
// 定义提示词
const pro = {
  role: 'system',
  content:
    '你是一个全栈开发助手,需遵守以下代码块处理规则:\n' +
    '1. 遇到非标准Markdown代码标记时(如`vue`/`jsx`/`txt`):\n' +
    '   - 自动转换为最接近的标准语法标记(Vue→javascript, JSX→javascript)\n' +
    '   - 保留原始代码内容不变\n' +
    '2. 对混合代码块(如Vue模板+JS)统一用`javascript`标记\n' +
    '3. 示例转换:\n' +
    '   ```vue → ```javascript\n' +
    '   <template>...</template> → 保持内容不变\n' +
    '4. 特殊情形处理:\n' +
    '   - 若含CSS/SCSS代码块则标记为`css`\n' +
    '   - 若含纯HTML则标记为`html`',
}

export function POST(request: NextRequest) {
  try {
    // 获取 URL 查询参数中的 message 参数
    const url = new URL(request.url)
    const message = url.searchParams.get('message') || 'hello'

    if (!message) {
      return new Response(JSON.stringify({ error: 'Message is required' }), {
        status: 400,
        headers: { 'Content-Type': 'application/json' },
      })
    }
    // 提示词+用户信息
    const redis_history: any[] = []
    redis_history.push({ role: 'user', content: message })
    redis_history.unshift(pro)

    // 创建 ReadableStream 进行流式响应
    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: '',
          }

          // 逐步读取 OpenAI 流
          for await (const chunk of completionStream) {
            const content = chunk.choices[0]?.delta?.content || ''
            if (content) {
              const data = {
                sendUserInfo: { user_id: 1, message, sender_type: 'user' },
                sender_type: 'gpt',
                message: content,
              }

              obj.message += content
              controller.enqueue(
                new TextEncoder().encode(`data: ${JSON.stringify(data)}\n\n`)
              )
            }
          }
        } 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) {
    // console.error('Request processing error:', error)
    return new Response(JSON.stringify({ error: error.message }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' },
    })
  }
}

携带数据库版

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 || '', // 确保在环境变量中配置 API Key
  })
}
// 定义提示词
const pro = {
  role: 'system',
  content:
    '你是一个全栈开发助手,需遵守以下代码块处理规则:\n' +
    '1. 遇到非标准Markdown代码标记时(如`vue`/`jsx`/`txt`):\n' +
    '   - 自动转换为最接近的标准语法标记(Vue→javascript, JSX→javascript)\n' +
    '   - 保留原始代码内容不变\n' +
    '2. 对混合代码块(如Vue模板+JS)统一用`javascript`标记\n' +
    '3. 示例转换:\n' +
    '   ```vue → ```javascript\n' +
    '   <template>...</template> → 保持内容不变\n' +
    '4. 特殊情形处理:\n' +
    '   - 若含CSS/SCSS代码块则标记为`css`\n' +
    '   - 若含纯HTML则标记为`html`',
}
// 模拟用户id
const userId = 1
/**
 *
 * @param key redis 用于读取上下文 格式 user_用户id_chatHistory  模拟数据直接用 user_1_chatHistory
 * @returns []
 */

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 {
    // 获取 URL 查询参数中的 message 参数
    const body = await request.json() // 解析 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`)) || []
    console.log('histort', redis_history)
    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' },
    })
    // 创建 ReadableStream 进行流式响应
    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: '',
          }

          // 逐步读取 OpenAI 流
          for await (const chunk of completionStream) {
            const content = chunk.choices[0]?.delta?.content || ''
            if (content) {
              const data = {
                sendUserInfo: { user_id: 1, message, sender_type: 'user' },
                sender_type: 'gpt',
                message: content,
              }

              obj.message += content
              controller.enqueue(
                new TextEncoder().encode(`data: ${JSON.stringify(data)}\n\n`)
              )
            }
          }
          // 完成数据库记录
          await mysql.gpt_chat_history.create({ data: obj })
          redis_history.push({ role: 'system', content: obj.message })
          // 更新 Redis 中的聊天记录 当作上下文
          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) {
    // console.error('Request processing error:', error)
    return new Response(JSON.stringify({ error: error.message }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' },
    })
  }
}

聊天记录查询接口

创建app>api>msg>route.ts

import { NextRequest, NextResponse } from 'next/server'
import { mysql, redis } from '@/utils/db'

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url)
    const page = parseInt(searchParams.get('page') || '1', 10)
    const pageSize = parseInt(searchParams.get('pageSize') || '20', 10)

    const skip = (page - 1) * pageSize
    const take = pageSize

    const total = await mysql.gpt_chat_history.count()
    const gpt_list = await mysql.gpt_chat_history.findMany({
      orderBy: {
        created_at: 'desc',
      },
      skip,
      take,
    })

    const data = gpt_list.reverse()

    // Redis 存储最近 10 条记录(第一页才设置)
    if (page === 1) {
      const recent = data.slice(-10).map((item) => ({
        role: item.sender_type === 'user' ? 'user' : 'system',
        content: item.message,
      }))
      await redis.set('user_1_chatHistory', JSON.stringify(recent))
      await redis.expire('user_1_chatHistory', 60 * 60 * 24)
    }

    return NextResponse.json(
      {
        data,
        meta: {
          total,
          page,
          pageSize,
          totalPages: Math.ceil(total / pageSize),
        },
      },
      { status: 200 }
    )
  } catch (error) {
    return NextResponse.json(
      {
        error: 'Failed to fetch chat history',
        details: error instanceof Error ? error.message : 'Unknown error',
      },
      { status: 500 }
    )
  }
}

java版本流式接口

安装包

 <dependency>
     <groupId>com.openai</groupId>
     <artifactId>openai-java</artifactId>
     <version>0.40.1</version>
  </dependency>

接口搭建

utils>ChatUtils.java

package com.example.gpt.utils;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.errors.OpenAIException;
import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionChunk;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.stream.Stream;

@Service
public class ChatUtils {
    @Value("${openai.api-key}")
    private String apiKey;

    @Value("${openai.api-host}")
    private String apiHost;

    @Value("${openai.api-model}")
    private String apiModel;


    private volatile OpenAIClient client;
    @PostConstruct
    public void init() {
        this.client  = OpenAIOkHttpClient.builder()
                .baseUrl(apiHost)
                .apiKey(apiKey)
                .build();
    }
    //    正常返回
    public ChatCompletion sendChatMessage(String message) throws OpenAIException {
        try {
            ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
                    .addUserMessage(message)
                    .model(apiModel)
                    .temperature(0.7)
                    .maxTokens(1000)
                    .build();

            return client.chat().completions().create(params);
        } finally {

        }
    }
    //    流式返回
    public Stream<ChatCompletionChunk> sendChatStreamMessage(String message) throws OpenAIException {
        try {
            ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
                    .addUserMessage(message)
                    .model(apiModel)
                    .temperature(0.7)
                    .maxTokens(1000)
                    .build();

            return  client.chat().completions().createStreaming(params).stream();
        } finally {

        }
    }



}

接口示例

package com.example.gpt.controller.gpt;

import com.example.gpt.utils.ChatUtils;
import com.openai.models.chat.completions.ChatCompletionChunk;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;

@RestController
public class TestGptController {
    @Autowired
    ChatUtils chatUtils;
    @GetMapping("/gpt")
    @CrossOrigin
    public SseEmitter gpt() {
        SseEmitter emitter = new SseEmitter();
        Stream<ChatCompletionChunk> chunkStream= chatUtils.sendChatStreamMessage("帮我写个js正则手机号");
        CompletableFuture.runAsync(()->{
            try {
                chunkStream.forEach(chunk  -> {
                    chunk.choices().stream().findFirst().ifPresent(info->{
                            try {
                                if (info.delta() != null && info.delta().content() != null && !info.delta().content().isEmpty()) {
                                    System.out.println("gpt:"+info.delta().content());

                                    emitter.send(info.delta().content());
                                }
                            } catch (Exception e) {
                                e.printStackTrace();
                            }

                    });

                });
                emitter.complete();
            } finally {
                chunkStream.close();
            }
        });
        return emitter;
    }

}

react前端搭建(未完成,后续有空更新,git仓库有一版本可以自行参考)

封装发送hook

根目录创建hook>usePostSSE.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' // 你封装的 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<SSEStatus>('idle')

  const { stop } = usePostSSE<{ message: string }, GPTStreamResponse>({
    url: '/api/chat',
    body: { message: 'Say something smart' }, // 用户输入的消息
    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="p-4">
      <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>
  )
}

总结

项目未完成,有空继续更新。接口大致可用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值