文章目录
- 前言
- 一、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 }
首次数据库同步
- 本地项目已经按上述配置好,数据库连接好
- yarn dev 启动项目(必须要先启动项目不然后续无法执行)
- 另起一个窗口npx prisma db push 你的数据库就可以同步进去gpt_chat_history 表
- 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>
)
}
总结
项目未完成,有空继续更新。接口大致可用