目前很多公司的业务,都开始加入AI大模型板块,作为前端,第一次碰到这种情况的你,该如何解决?
此时,如果公司内部有自研大模型,那么从前端角度来讲,你只需调用传参即可,调用方式可以采用websocket、SSE等方式,具体要和算法部门后端商议,另外你还要考虑到“流式传输显示”等一系列复杂细节问题。
反之,如果公司没有自研模型,由于项目需要,那么可能会临时内嵌一个外围大模型,用以做对话、助手等功能。
部分人第一次见的话,难免会感到“头疼”,也有点无从下手,实际上只要你了解其原理,那么就很简单了。
把我个人的最佳解决方案记录下来,代码、方法通通放在下面,咱们一步步拆解,看完后,相信你对于接下来的类似业务,定会手到擒来,宝子们可以放心食用!
拜托拜托:您的点赞收藏,是我更新的最大动力,如果能帮助到大家,我就很开森啦,祝好!
一.AI大模型业务的具体调用方法
由于本周参与的项目,目前还未结项,所以为了能100%原汁原味,给大家演示+复盘,晚上做了个1:1复刻demo,力争等比还原!话不多说,咱们开始正题!
前端对接大模型,你要考虑的点,其实就是接口+数据流显示+页面设计的问题,通常情况下,用websocket是不错的选择,而且多数公司,也都会用它来传输、支持各条业务线。
先上图让大家感受一下:(具体AI模型的页面样式设计,看UI看项目,意会即可)
往往这种AI模块,是通过“第三方引入的方式”,从而实现出来的,解决方案有两个比较好,一个是iframe,算是通用的老手解决法,还有一种就是通过“插件”的形式,引入到项目中。
iframe解决方案往上有很多,就不多赘述啦。重点讲一下“插件”引入的实现过程。
名字听着比较唬人,其实就是把你这个“AI模块”,单独起一个项目,然后打包的时候,通过webpack或vite,将项目打包成js文件,最后在主项目中,通过createElement创建script标签,将其挂载即可,这其中要注意挂载+销毁时机的问题。
引入到主项目后,AI大模型插件基本会当作挂件,挂在页面上(如下图):
对于大模型的调用方式,其实方法大同小异,基本类似,为保证接口顺畅,这里我拿月之暗面和gpt这两个,给大家做个演示,就很清晰了。
如果你是用市面上的大模型去做、去对接,那么要按照人家官方文档的方式进行套用,比如:
月之暗面官方文档:这里这里!
GPT接口使用文档:platform(需要登录梯梯)
对接外部AI模型,通常情况下,需要申请账号,并且拥有一个api-key,多内外各厂商都会给这个密钥,设置免费初始额度,让用户来体验+使用,当然,如果额度超出后,想继续使用,那么就要付费啦。
对于各模型的用量限制,你在它的官方文档上,都会找到:
比如月之暗面,会给用户15元的免费额度,当天使用,隔天就可以查询剩余用量:
反之,如果公司有自研大模型的话,那么你不需要输入api-key,只需要传后端要的参数,并且把你每次的问题及答案,都放在一个数组即可。
这么做的意义,也是为了能完成上下文连贯做答。(上一次,上上次等的回答记录,都要积累加在这个数组,然后再给后端返回去)
这里做法有两种,前端去做,后端去做都行,所以到时你们需要商量好。
举个例子,比如这里后端去做的话,首先定义一个空数组变量:
然后后端每次在接收到前端消息时,都会将对应的内容,存入在空数组中,如下:
国内外大模型的传参方式,大多类似,一次对话记录,都会有role(角色)+content(内容)两个参数。
其中role字段,有三种角色,前端作为问题的发起者、使用者,那么角色就是“user"。
后端调用大模型接口,然后把内容返回给前端,那么它可以视为“回答者”,role角色就是“assistant”。
第三个角色通常是大模型预留给用户的参数(用于调参+训练模型),比如市面上多种角色,比如女友AI,数学AI、体育AI等等,都是通过参数来调整实现的,这个角色role可以是“system”。
当然,这里我以月之暗面为例,其余AI模型同理。
所以到了这里,我给大家做个前端的传参演示,你就懂了,我问大模型两个问题,然后你看看它是如何完成上下文衔接的?
刚开始,我问他“写一段定时器代码给我”:
你可以在控制台进行调试,第一步websocket先进行连接,并且发ping包。
随后按照你们后端要求的传参格式,进行传参,比如这里我有两个参数,一个是event,和后端约定俗成的字段,第二个content就是你的问题,随后你会收到流式数据。
那么与此同时,后端这边做了什么?其实就是拿到前端的参数,然后把参数按照“AI大模型官方接口文档”的形式,去请求接口,再把数据拿到,返回给前端。
所以你可以看到role角色字段的变化,user是问题发起人,assiatant是回答者。
接着你问了第二个问题,为了让AI大模型上下文保持连贯,后端把前端的参数,拼接在了同一个数组中,将之全部传给大模型官方api,于是,连贯的上下文就此诞生!
我把这个数组以代码形式放在下面,你只需要看我的注释部分,即可完全理解“上下文的含义”。
this.streamMessages [
1.第一次问问题*****************************************************************
{ role: 'user', content: '写一段定时器代码\n' },
2.AI第一次回答*****************************************************************
{
role: 'assistant',
content: '当然可以。以下是使用 Python 的 `threading` 模块实现的一个简单的定时器代码示例:\n' +
'\n' +
'```python\n' +
'import threading\n' +
'import time\n' +
'\n' +
'def timer(seconds, callback):\n' +
' def timer_thread():\n' +
' time.sleep(seconds)\n' +
' callback()\n' +
' threading.Thread(target=timer_thread).start()\n' +
'\n' +
'def my_callback():\n' +
' print("定时器时间到!")\n' +
'\n' +
'# 设置定时器,5秒后执行 my_callback 函数\n' +
'timer(5, my_callback)\n' +
'print("定时器已启动,5秒后将执行回调函数。")\n' +
'\n' +
'# 保持主线程运行,否则程序会立即退出\n' +
'while True:\n' +
' time.sleep(1)\n' +
'```\n' +
'\n' +
'这段代码定义了一个 `timer` 函数,它接受两个参数:`seconds`(定时器的秒数)和 `callback`(定时器时间到后要执行的回调函数
)。`timer` 函数内部定义了一个 `timer_thread` 函数,该函数在一个新的线程中执行,等待指定的秒数后调用回调函数。\n' +
'\n' +
'`my_callback` 函数是回调函数的示例,当定时器时间到时,它会打印一条消息。\n' +
'\n' +
'最后,我们使用 `timer` 函数设置了一个5秒的定时器,并启动了主线程的无限循环,以保持程序运行,直到定时器触发回调函数。\n' +
'\n' +
'请注意,这个示例代码在实际应用中可能需要根据具体需求进行调整。例如,你可能需要处理异常、添加取消定时器的功能或者在多线
程环境中同步回调函数的执行。'
},
3.前端第二次问问题,用以测试AI的上下文连贯度************************************************
{ role: 'user', content: '解释一下\n' },
4.AI第二次的回答,事实证明,确实实现了上下文连贯********************************************
{
role: 'assistant',
content: '当然,我来解释一下这段代码的工作原理:\n' +
'\n' +
'1. **导入模块**:\n' +
' ```python\n' +
' import threading\n' +
' import time\n' +
' ```\n' +
' 这里我们导入了两个Python标准库模块:`threading` 和 `time`。`threading` 模块允许我们创建和管理线程,而 `time` 模块提
供了各种与时间相关的函数。\n' +
'\n' +
'2. **定义定时器函数**:\n' +
' ```python\n' +
' def timer(seconds, callback):\n' +
' def timer_thread():\n' +
' time.sleep(seconds)\n' +
' callback()\n' +
' threading.Thread(target=timer_thread).start()\n' +
' ```\n' +
' - `timer` 函数接受两个参数:`seconds`(等待的秒数)和 `callback`(当等待时间结束后要调用的函数)。\n' +
' - 在 `timer` 函数内部,我们定义了另一个函数 `timer_thread`,这是一个内部函数,用于在新线程中执行。\n' +
' - `timer_thread` 函数使用 `time.sleep(seconds)` 使当前线程暂停指定的秒数。\n' +
' - 等待结束后,`timer_thread` 调用传入的 `callback` 函数。\n' +
' - `threading.Thread(target=timer_thread).start()` 创建一个新的线程,并指定 `timer_thread` 作为目标函数,然后启动这个
线程。\n' +
'\n' +
'3. **定义回调函数**:\n' +
' ```python\n' +
' def my_callback():\n' +
' print("定时器时间到!")\n' +
' ```\n' +
' - 这是一个简单的函数,当定时器触发时,它会打印一条消息。\n' +
'\n' +
'4. **设置定时器并启动**:\n' +
' ```python\n' +
' timer(5, my_callback)\n' +
' print("定时器已启动,5秒后将执行回调函数。")\n' +
' ```\n' +
' - 这里我们调用 `timer` 函数,设置等待时间为5秒,并指定 `my_callback` 作为回调函数。\n' +
' - 打印一条消息,告知用户定时器已启动。\n' +
'\n' +
'5. **保持主线程运行**:\n' +
' ```python\n' +
' while True:\n' +
' time.sleep(1)\n' +
' ```\n' +
' - 这是一个无限循环,使用 `time.sleep(1)` 每次暂停1秒。\n' +
' - 这个循环的目的是保持主线程活跃,否则程序会立即退出,导致定时器线程无法执行。\n' +
'\n' +
'这段代码的关键在于使用线程来处理定时任务,这样主线程可以继续执行其他任务,而不会阻塞程序的运行。通过这种方式,我们可以
在不干扰主程序流程的情况下执行定时任务。'
}
]
总结一下:
如果公司没有自研大模型,要用外部的,那么api-key是后端去搞得,他会按照AI模型的官方文档,拿到密钥,并且按照人家官方的格式,去调用大模型接口。
后端的作用类似于中转站,他把结果最后会返回给你,所以前端这时要做的就是websocket接收及数据处理就够了。
下面我把整个demo的代码全部奉上,后端基于nestjs,前端基于vue3+ts,对于一些想学后端的前端宝子,亦或是对于即将上手AI模型业务的前端人,会有很大帮助!(完整前后端项目,我会放在github,大家随取随用,随时参考!)
1.前后端对接月之暗面模型(Nestjs+vue3)
为了将对接讲的更清楚,这里我用nestjs先对接月之暗面接口,然后给前端提供模型支持。
后端nestjs这一块,最重要的是websocket部分,原理是先请求月之暗面接口,然后用websocket,将数据返还给前端。
前端其实就是websocket接收,做一个流式显示即可。
nestjs实现部分:
第一步通过node全局安装nestjs脚手架(Nodejs版本>=16)
npm i -g @nestjs/cli
第二步通过命令行创建项目,并命名
nest new project-name
第三步安装websocket和openai(因为月之暗面使用了openai服务)
@nestjs/websockets、ws、openai
这三个安装方式,均为npm/pnpm install即可
第四步,新建文件,命名为ws.gateway.ts,并加入如下代码(里面我都写了注释,可供参考)
// 1.导入各种依赖
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
MessageBody,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { OpenAI } from 'openai'; // 确保已经安装 openai 包
import { Server, WebSocket } from 'ws';
// 2.启动websocket服务,并且这里将端口改为了3004
@WebSocketGateway(3004)
export class WsStartGateway
implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
// 3.定义全局变量和openaiClient
private readonly openaiClient: OpenAI;
private sessions = [];
private streamMessages = [];
constructor() {
// 4.初始化openaiClien,配置你的月之暗面API Key+API基础路径
this.openaiClient = new OpenAI({
apiKey: "*************", // 替换为你的 API Key
baseURL: "https://api.moonshot.cn/v1", // Moonshot API 的基础路径
});
}
handleDisconnect(client: any) {
console.log('mmmmmllllllll', client);
}
handleConnection(client: any, ...args: any[]) {
console.log('mmmmm', client);
// 分配唯一会话 ID(以 WebSocket 对象为标识)
const sessionId = client.id;
// 初始化当前会话的历史记录
this.sessions[sessionId] = [];
console.log(`WebSocket client connected: ${sessionId}`);
}
// 5.定义消息处理函数
@SubscribeMessage('message')
async handleMessage(client: WebSocket, message: { content: string }) {
const sessionId = client.id;
console.log('Received message:', message); // 打印原始消息
try {
// 检查消息是否存在 content 属性
if (!message || !message.content) {
throw new Error('No content found in the message');
}
const content = message.content; // 获取用户输入的内容
console.log('Parsed content:', content); // 打印解析后的内容
this.streamMessages.push({
role: "user", content: content
})
console.log('用户发送完之后', this.sessions)
// 调用 Moonshot AI 的聊天接口
const completion = await this.openaiClient.chat.completions.create({
model: "moonshot-v1-auto",
messages: this.streamMessages,
temperature: 0.3, // 控制回答的随机性
stream: true
});
client.send(JSON.stringify(JSON.stringify({ reply: "", isStreaming: true })))
let fullReply = ""; // 用于记录完整回复
for await (const chunk of completion) {
const delta = chunk.choices[0]?.delta;
if (delta?.content) {
client.send(JSON.stringify({ reply: delta.content }));
fullReply += delta.content;
}
}
this.streamMessages.push({ role: "assistant", content: fullReply });
console.log('kimi回答之后的内容', fullReply)
if (fullReply) {
console.log('fullReply', fullReply);
}
} catch (error) {
console.error('Error:', error); // 打印错误信息
client.send(JSON.stringify({ reply: "Kimi 暂时无法回答您的问题,请稍后再试。" }));
}
}
}
第五步,在app.module.ts引入依赖
只看我画箭头的地方即可,多的删除,缺的补上即可,因为有些内容是我其它的业务代码
import { Module } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { WsStartGateway } from './ws.gateway';
import { OpenaiService } from './OpenAi/openai.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService, PrismaClient, WsStartGateway, OpenaiService],
})
export class AppModule { }
最后启动命令,你的后端服务就写好了:
npm run start:dev
vue3前端实现部分:
首先前端demo我全写在这个文件
代码如下,由于过长,所以你可以直接看注释
<template>
<div class="chat-container">
<div class="top">
<div>JJAI Chat</div>
</div>
<div class="chatBox" v-if="!messages.length">
<div class="boxImg"><img src="@/assets/logo.png" class="robotImg" /></div>
</div>
<!-- 中部内容区 -->
<div class="chatInfor" v-if="!messages.length">
<div class="chatContent">
JJChat is an artificial-intelligence catbot developed by JJAI
</div>
<div class="chatContent">JJAI launched in Noveber 2024</div>
<div class="chatContent">
JJChat is an artificial-intelligence Chatbot developed by JJAI
</div>
<div class="description">This is example that what can I do for you</div>
</div>
<div v-else>
<!-- 显示聊天记录 -->
<div class="boxHeight">
<div class="chat-box">
<div class="messages">
<div
v-for="(message, index) in messages"
:key="index"
class="message-wrapper"
:class="message.role === 'user' ? 'user-message' : 'ai-message'"
>
<div
class="message"
v-html="renderMessageContent(message.content)"
></div>
</div>
</div>
</div>
</div>
</div>
<div class="bottom">
<div class="search">
<div class="input-box">
<textarea
class="textVal"
v-model="userInput"
placeholder="请输入内容开始聊天"
@keyup.enter="sendMessage"
></textarea>
</div>
</div>
<img src="@/assets/send.png" class="robotImg" @click="sendMessage" />
</div>
</div>
</template>
<script setup lang="ts">
import Clipboard from 'clipboard'
import { ref, nextTick, watch, onMounted, onUnmounted } from 'vue'
import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css'
const flag = ref(false)
const messages = ref([])
const userInput = ref('')
let clipboard = null
let isStreaming = false // 是否正在流式接收数据
let streamingMessage = '' // 当前正在接收的消息
var ws = new WebSocket('ws://localhost:3004')
ws.onopen = function (e) {
console.log('websocket连接成功', e)
}
onMounted(() => {
console.log('window', window)
document.addEventListener('DOMContentLoaded', function () {
// 初始化 Clipboard.js
new Clipboard('.copy-btn')
})
nextTick(() => {
clipboard = new Clipboard('.copy-btn')
clipboard.on('success', e => {
console.log('复制成功')
})
clipboard.on('error', e => {
console.log('复制失败')
})
})
})
onUnmounted(() => {
if (clipboard) {
clipboard.destroy()
}
})
const md = new MarkdownIt({
highlight: (code, lang) => {
const linesLength = code.split(/\n/).length - 1
const codeIndex = (
parseInt(Date.now().toString()) + Math.floor(Math.random() * 10000000)
).toString()
let html = `<button class="copy-btn" style="position: absolute;top: 0;right: 0;" type="button" data-clipboard-action="copy" data-clipboard-target="#copy${codeIndex}">复制</button>`
if (lang && hljs.getLanguage(lang)) {
if (linesLength) {
html += '<b class="name">' + lang + '</b>'
}
const highlightedCode = hljs.highlight(code, { language: lang }).value
return `<pre class="hljs" style="overflow-x:auto;position:relative"><code style="display:flex">${html}</code>${highlightedCode}</pre><textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy${codeIndex}">${code.replace(
/<\/textarea>/g,
'</textarea>'
)}</textarea>`
}
return ''
}
})
// 渲染消息内容(支持 Markdown 和普通文本)
const renderMessageContent = content => {
return md.render(content)
}
ws.onmessage = function (e) {
const answer = JSON.parse(e.data)
flag.value = true
console.log('收到数据', answer)
// 如果是流式输出,逐步更新
if (answer == '{"reply":"","isStreaming":true}') {
isStreaming = true
streamingMessage = ''
messages.value.push({ role: 'assistant', content: '' })
console.log('流式第一次加载:', messages.value)
} else if (isStreaming && answer.reply) {
streamingMessage += answer.reply
messages.value[messages.value.length - 1].content = streamingMessage
console.log('最后的messages', messages.value)
} else {
isStreaming = false
}
}
ws.onclose = function () {
console.log('websocket已断开')
}
ws.onerror = function (error) {
console.log('websocket发生错误' + error)
}
watch(
messages,
() => {
nextTick(() => {
document.querySelectorAll('pre code').forEach(el => {
if (el instanceof HTMLElement) {
hljs.highlightElement(el)
}
})
// 重新初始化 Clipboard
if (clipboard) {
clipboard.destroy()
}
clipboard = new Clipboard('.copy-btn')
clipboard.on('success', e => {
console.log('复制成功')
})
clipboard.on('error', e => {
console.log('复制失败')
})
})
},
{ immediate: true }
)
// 发送用户消息
const sendMessage = () => {
if (!userInput.value.trim()) return
// 添加用户输入到消息列表
messages.value.push({ role: 'user', content: userInput.value })
// 通过 WebSocket 发送到后端
ws.send(
JSON.stringify({
event: 'message',
data: {
content: userInput.value
}
})
)
userInput.value = '' // 清空输入框
}
</script>
<style scoped lang="less">
pre.hljs {
padding: 12px 2px 12px 40px !important;
border-radius: 5px !important;
position: relative;
font-size: 14px !important;
line-height: 22px !important;
overflow: hidden !important;
code {
display: block !important;
margin: 10px 10px !important;
overflow-x: auto !important;
}
.line-numbers-rows {
position: absolute;
pointer-events: none;
top: 12px;
bottom: 12px;
left: 0;
font-size: 100%;
width: 40px;
text-align: center;
letter-spacing: -1px;
border-right: 1px solid rgba(0, 0, 0, 0.66);
user-select: none;
counter-reset: linenumber;
span {
pointer-events: none;
display: block;
counter-increment: linenumber;
&:before {
content: counter(linenumber);
color: #999;
display: block;
text-align: center;
}
}
}
b.name {
position: absolute;
top: 2px;
right: 50px;
z-index: 10;
color: #999;
pointer-events: none;
}
.copy-btn {
position: absolute;
top: 2px;
right: 4px;
z-index: 10;
color: #333;
cursor: pointer;
background-color: #fff;
border: 0;
border-radius: 2px;
}
}
.chat-container {
padding-bottom: 30px;
.boxHeight {
height: 78vh;
.chat-box {
height: calc(100% - 60px);
box-sizing: border-box;
padding: 16px;
overflow-y: auto;
background-color: #ffffff;
.messages {
display: flex;
flex-direction: column;
gap: 12px;
}
.message-wrapper {
display: flex;
.message {
overflow-y: auto;
max-width: 70%;
padding: 5px 16px;
border-radius: 18px;
font-size: 20px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
}
.user-message {
justify-content: flex-end;
.message {
background-color: #17c3ce;
color: #ffffff;
text-align: right;
font-size: 14px;
border-bottom-right-radius: 4px;
}
font-size: 20px;
}
.ai-message {
justify-content: flex-start;
.message {
background-color: #f1f0f0;
color: #333333;
text-align: left;
font-size: 14px;
border-bottom-left-radius: 4px;
}
}
}
}
.top {
padding-top: 40px;
padding-bottom: 20px;
text-align: center;
font-size: 24px;
font-weight: 600;
border-bottom: 1px solid #473f3f54;
max-height: 4vh;
}
.boxImg {
margin-top: 100px;
text-align: center;
margin-bottom: 120px;
.robotImg {
width: 160px;
height: 160px;
}
}
.chatInfor {
padding: 0 10px;
.chatContent {
font-size: 14px;
text-align: center;
margin: 20px 0;
padding: 30px 10px;
background-color: #f5f5f5;
color: #bbbbbb;
border-radius: 20px;
}
.description {
font-size: 14px;
color: #bbbbbb;
margin: 20px 0;
text-align: center;
padding: 30px 0px 30px 0;
}
}
.bottom {
// height: 4vh;
display: flex;
.search {
// width: 550px;
// margin-top: 50px;
padding: 4px 10px 5px 10px;
// margin-bottom: 50px;
// margin-right: 5vw;
}
.robotImg {
padding-top: 3px;
// margin-top: 36px;
width: 15vw;
height: 15vw;
}
.input-box {
background-color: #f5f5f5;
width: 70vw;
border-radius: 20px;
// height: 40px;
display: flex;
align-items: center;
gap: 8px;
border-top: 1px solid #e5e5e5;
padding: 4px 10px;
.textVal {
height: 20px;
padding: 10px 0;
font-size: 12px;
}
button {
padding: 5px 20px;
background-color: #0084ff;
color: #ffffff;
border: none;
border-radius: 10px;
font-size: 14px;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 132, 255, 0.3);
transition: background-color 0.3s ease;
}
button:hover {
background-color: #006bbf;
}
button:active {
background-color: #0056a3;
}
textarea {
flex: 1;
padding: 0 10px;
font-size: 10px;
border: none;
resize: none;
font-size: 14px;
background-color: #f5f5f5;
outline: none;
// height: 30px;
}
}
}
:deep(textarea::-webkit-input-placeholder) {
color: #9d9a9a;
font-size: 14px !important;
}
}
</style>
重点讲一下“流式输出的问题”,如果后端给你的返回的数据,是一次全部返还,那么你还要实现“AI几个字几个字持续输出的效果”。
这里我用了MarkdownIt和hljs插件来实现,因为大家要知道,AI输出的内容,它是纯文本形式,可用户一旦想问编程方面的知识,那么内容就很乱了。
所以MarkdownIt插件,就是用来将纯文本转换为“markdown”的插件。而hljs插件,是用来给markdown做代码高亮的插件,两者结合使用,那么效果就是如下情况:(当然你也可以将其设为深色模式,直接在我的demo里面更改即可)
另外你还要考虑到右上角的复制功能,这里我用了Clipboard插件,用以来实现,这个解决方案,在上述demo里面有,而且是可以“直接移植其他项目”的方案,万金油,随取随用!
2.前后端对接GPTgpt-4o-mini模型(Nestjs+vue3)
后端nestjs:
第一步,在src文件夹下建立两个文件,如图:
index.ts文件代码如下:
// openaiClient.js
import OpenAI from 'openai';
// 初始化 OpenAI 客户端
const openai = new OpenAI({
apiKey: 'sk-svcacct-AFXosisZz-rSaZcFpbSfSSeV3xWMX1p1xsEOZ41UaH65GpySKuHDYWMIhlDecpWkrfRAXnn9zTbPuT3BlbkFJGEOOd_jOw8-qswjNANnmI7BYGALk8JSMuUCmuVhKK2R1NYAlPzZlwiRhekuMXw2BjbyUYA_5FpEAA', // 从环境变量中获取 API 密钥
});
// org-rI5kqxr3fGPGgHVHBwb51qHr
// 创建一个函数来生成聊天完成
async function createCompletion(model, messages) {
try {
const completion = await openai.chat.completions.create({
model,
messages,
});
return completion.choices[0].message;
} catch (error) {
console.error('Error creating completion:', error);
throw error;
}
}
// 导出 createCompletion 函数
export default createCompletion;
openaiClient.js代码如下:
import { Injectable } from '@nestjs/common';
import OpenAI from 'openai';
@Injectable()
export class OpenaiService {
private openai: OpenAI;
// 重点在这里,需要你的openai key
constructor() {
this.openai = new OpenAI({
apiKey: '******************', // 确保你已经设置了环境变量
});
}
async createCompletion(model: string, messages: any[]): Promise<string> {
try {
const response = await this.openai.chat.completions.create({
model,
messages,
});
return response.choices[0].message?.content || 'No response';
} catch (error) {
throw error;
}
}
}
第二步,在app.controller.ts文件下,引入你的service:
最后记得在app.module.ts导入进去:
这样gpt的后端服务就写完了,然后你启动一下postman/apifox,试一试:(需要打开梯子)
按照我这个接口信息格式去输入,打开梯子,即可接收到gpt回答。
对应的前端代码还是上面那一套,改一改即可。
最后的最后,奉上全部demo,大家可以把前后端下载使用+参考,有问题随时交流!