今天一觉醒来收到了邮件,OpenAI 释出了 ChatGPT 模型的 API。
之前开放的只有 GPT-3 的模型,它只能完成 Completion 式的会话,现在,我们终于可以进行 Chat 式会话了!
OpenAI 的邮件
接口使用
首先马上去学习了一下如何通过 API 使用这个模型,发现他对于会话式只需要使用你自己的 APIKey 来调用接口,同时携带上下文的数据即可。
一个请求消息体的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"model" : "gpt-3.5-turbo-0301" ,
"messages" : [
{
"role" : "user" ,
"content" : "你好,我在使用 API 调用你!"
},
{
"role" : "assistant" ,
"content" : "\n\n您好,我是 AI 语言模型,很高兴能够为您服务!请问您需要哪些 API 调用呢?"
},
{
"role" : "user" ,
"content" : "我想问问你可以调用网络 API 吗?"
}
]
}
Copy 接口调用结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"id" : "chatcmpl-6pogkJ74asx7oUZHuWN6SxxbGHN1o" ,
"object" : "chat.completion" ,
"created" : 1677807594 ,
"model" : "gpt-3.5-turbo-0301" ,
"usage" : {
"prompt_tokens" : 84 ,
"completion_tokens" : 78 ,
"total_tokens" : 162
},
"choices" : [
{
"message" : {
"role" : "assistant" ,
"content" : "是的,我可以调用网络 API。网络 API 是通过互联网提供服务的接口,可以让您的应用程序访问和处理远程服务器上的数据或功能。请告诉我您想要调用哪个网络 API,我会尽力帮助您。"
},
"finish_reason" : "stop" ,
"index" : 0
}
]
}
Copy 是不是很清晰?
实际调用还是类似于单次会话调用,只是需要把上下文的消息都传递进去,上下文模拟了一次对话,使用 role
来标识出不同的角色,content
来标识出不同的内容。
然后就可以得到 ChatGPT 根据上下文给出的结果了。
搭建一个简易的 ChatGPT
那么,知道了如何使用 API,我们就可以开始搭建一个简易的 ChatGPT 了。
技术选型
首先,我们需要选择一下技术栈,这里我选择了 Next.js + TypeScript。
因为调用 API 需要个人的 APIKey,为了保护这个 APIKey,我们需要在服务端进行调用,所以我们需要一个服务端渲染的框架。用户调用服务端的接口,服务端使用 APIKey 调用 OpenAI 的接口,然后把结果返回给用户。
搭建项目
首先,我们需要创建一个 Next.js 项目,这里我使用的是 Next.js 13 版本,因为并不是生产环境,所以体验一下最新技术。
根据 Next.js Beta 官网 说明使用脚手架创建项目:
1
npx create-next-app@latest --experimental-app
Copy 然后根据提示一步一步创建好项目,然后使用 IDE 打开项目。
开始开发
首先 pnpm i
安装依赖,然后 pnpm dev
启动项目,会发现模版里面已经写了一些东西了。
这里我们可以保留原先的框架内容,做几件事情就好:
/app/globals.css
中仅保留 html
和 body
的样式
/app/pages.tsx
中 Home
组件中 main
中的所有内容删掉
/app/api/
目录清空
然后就可以进行我们的应用开发了。
创建 API
我们在 /app/api/
目录下创建一个 postMsg
文件夹,然后在 postMsg
文件夹下创建 route.ts
文件,这个文件就是我们的 API。在 url 中访问 http://127.0.0.1:3000/api/postMsg
就会访问到这个文件内的逻辑。
在 route.ts
中写入如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export async function POST ( request : Request ) {
// 解析出请求体中的 messages 字段
const { messages } = await request . json ();
// 按照 OpenAI 要求构造请求体
const data = {
messages ,
model : 'gpt-3.5-turbo-0301' , // 使用 ChatGPT 模型
};
// 使用你的 API_KEY 调用 OpenAI 的接口
const response = await fetch ( 'https://api.openai.com/v1/chat/completions' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'Authorization' : `YOUR_API_KEY` ,
},
body : JSON.stringify ( data ),
});
// 解析出 OpenAI 返回的结果并返回给应用
const json = await response . json ();
return new Response ( JSON . stringify ( json ));
}
Copy 这样我们就完成了一个简单的 API 了。在这个简易的应用里也只需要这一个 API。
创建页面
这里我们创建一个简单的页面,用来展示我们的应用。这个就比较简单了,写写样式就好了。这里我主要说一下逻辑。
有一个点需要注意一下,因为做得很简单,交互都在 Home
组件中,所以我们需要使用客户端组件,需要在文件顶部添加 'use client'
。
本地会话缓存
因为简易应用,自己使用,所以我们需要在前端保存会话的数据,这里我保存在了 localStorage
中。如果要实现 ChatGPT
的多会话功能,那么我们肯定需要同时存储多组会话数据,这里给出我的数据结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
// localStorage 中保存两个数据,一个是会话列表,一个是当前会话的 key
// 当前会话的 key
currentKey : string ;
// 会话列表
type RecordItem = {
content : string ;
role : Role ;
};
type RecordList = {
[ key : string ] : RecordItem [];
};
Copy 这里就可以通过 currentKey
来获取当前会话的数据,然后通过 RecordList
来获取所有会话的数据了。
初始化会话
在页面初始化的时候,我们需要初始化会话,这里我们需要判断 localStorage
中是否有会话数据,如果没有,那么我们就创建一个新会话,如果有,那么我们就使用 localStorage
中的数据。
为了统一管理 localStorage
中的数据,我们需要定义一些常量:
1
2
3
// 会话列表的 key
const currentKeyKey = 'currentKey' ;
const recordListKey = 'recordList' ;
Copy
这里开始所有代码都在 Home
组件中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 声明两个变量接收 localStorage 中的数据
let lsCurrentKey = '' ;
let lsRecordList : RecordList = {};
// 这里是为了防止打包时在服务端渲染时报错,需要放在这个判断中
if ( typeof window != 'undefined' ) {
// 如果是第一次使用,那么我们就创建一个新的会话,生成一个随机的 key 并存入 localStorage
lsCurrentKey = window . localStorage . getItem ( currentKeyKey ) ?? '' ;
if ( ! lsCurrentKey ) {
lsCurrentKey = ` ${ new Date (). toLocaleString (). split ( ' ' )[ 0 ] } _ ${ String ( Math . random ()). slice ( 2 , 6 ) } ` ;
window . localStorage . setItem ( currentKeyKey , lsCurrentKey );
}
lsRecordList = JSON . parse ( window . localStorage . getItem ( recordListKey ) || '{}' );
if ( ! lsRecordList [ lsCurrentKey ]) {
lsRecordList [ lsCurrentKey ] = [];
window . localStorage . setItem ( recordListKey , JSON . stringify ( lsRecordList ));
}
}
// 然后将数据保存到 state 中进行响应式管理
const [ currentKey , setCurrentKey ] = useState < string >( lsCurrentKey );
const [ recordList , setRecordList ] = useState < RecordList >( lsRecordList );
// 这里创建了一个当前会话的 state,方便我们在渲染时使用
const [ record , setRecord ] = useState < RecordItem [] >( recordList [ currentKey ]);
Copy 主要逻辑
这里我们需要实现两个功能,发送消息和接收消息,同时更新 localStorage:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 保存到 localStorage 中的方法
const saveToLocalStorage = () => {
console . log ( '存入最新的记录' );
window . localStorage . setItem ( currentKeyKey , currentKey );
window . localStorage . setItem ( recordListKey , JSON . stringify ( recordList ));
};
// 发送消息方法
const sendMsg = async ( msg : string ) => {
if ( msg ) {
setRecordList (( prev ) => {
return {
... prev ,
[ currentKey ] : [... prev [ currentKey ], { role : 'user' , content : msg }],
};
});
} else {
alert ( '请输入内容' );
}
};
// 当 recordList 或者 currentKey 发生变化时,更新 record 使页面重新渲染
useEffect (() => {
saveToLocalStorage ();
setRecord ( recordList [ currentKey ]);
}, [ recordList , currentKey ]);
// 当 record 发生变化时,判断情况进行请求
useEffect (() => {
// 如果当前列表最后一条记录是用户发送的,就发送请求
if ( record [ record . length - 1 ] ? . role === 'user' ) {
// 这里我实现了一个简单的 loading 效果,机器人会先展示一个思考中,然后再展示回复
setLoading ( true );
console . log ( '请求了 ChatGPT' );
// 这里为了防止在请求过程中,用户切换了会话,所以这里需要保存一下当前的 key 以便请求返回结果后存入正确的会话中
const key = currentKey ;
// 发送请求,消息列表放在 body 的 messages 字段中,与 API 中的解析方法一致
fetch ( '/api/postMsg' , {
method : 'POST' ,
body : JSON.stringify ({ messages : record }),
cache : 'no-store' ,
})
. then ( async ( data ) => {
// 根据上面提到过的 API 响应格式解析 ChatGPT 的回复数据
const json = await data . json ();
const newMessage = json . choices [ 0 ]. message ;
// 将新的消息存入会话中
setRecordList (( prev ) => {
return {
... prev ,
[ key ] : [
... prev [ key ],
{ role : newMessage.role , content : newMessage.content },
],
};
});
// 关闭 loading
setLoading ( false );
})
. catch (( err ) => {
console . log ( err );
});
}
}, [ record ]);
Copy 其他功能
这里还有一些其他的功能,比如切换会话,删除会话等,简单放一下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 生成一个随机的 key,确保当前会话列表中没有重复的 key
const generateKey = () : string => {
const key = ` ${ new Date (). toLocaleString (). split ( ' ' )[ 0 ] } _ ${ String (
Math . random ()
). slice ( 2 , 6 ) } ` ;
if ( recordList [ key ]) {
return generateKey ();
}
return key ;
};
// 创建新会话并切换到新会话
const newChat = () => {
const key = generateKey ();
setRecordList (( prev ) => ({ ... prev , [ key ] : [] }));
setCurrentKey ( key );
};
// 删除会话
// 这里我在会话列表中没有给当前会话提供关闭按钮,所以这里只是删除了会话,没有切换到其他会话
// 如果你给当前会话提供删除按钮,那么这里需要对 key === currentKey 的情况进行处理
const closeChat = ( key : string ) => {
const newRecordList = { ... recordList };
delete newRecordList [ key ];
setRecordList ( newRecordList );
};
// 切换会话
const switchChat = ( key : string ) => {
setCurrentKey ( key );
};
Copy 界面展示
以上已经实现了所有的功能,下面我们来看一下效果,相信你可以根据上面的示例和下面的效果图,很快就能实现一个聊天机器人。
多会话聊天界面- PC 多会话聊天界面- 移动
当然还有不少细节我实现了但没有提,比如:
机器人响应是 markdown 格式,如何像上面展示出格式化数据
聊天室中新消息自动滚动到底部
会话列表适配移动端展开收起
机器人响应的 loading 效果(下图)
多会话聊天界面- 移动
总结
因为我是给公司内部搭建的自用项目,对于样式我并没有进行太多的处理,也没有办法提供 demo 给大家试用。
但是相信你现在已经可以自己基于 ChatGPT 实现一个聊天机器人了,样式也可以根据自己的需求进行调整。
就这,拜拜~