今天一觉醒来收到了邮件,OpenAI 释出了 ChatGPT 模型的 API。
之前开放的只有 GPT-3 的模型,它只能完成 Completion 式的会话,现在,我们终于可以进行 Chat 式会话了!
接口使用
首先马上去学习了一下如何通过 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 吗?"
}
]
}
|
接口调用结果:
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
}
]
}
|
是不是很清晰?
实际调用还是类似于单次会话调用,只是需要把上下文的消息都传递进去,上下文模拟了一次对话,使用 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
|
然后根据提示一步一步创建好项目,然后使用 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));
}
|
这样我们就完成了一个简单的 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[];
};
|
这里就可以通过 currentKey
来获取当前会话的数据,然后通过 RecordList
来获取所有会话的数据了。
初始化会话
在页面初始化的时候,我们需要初始化会话,这里我们需要判断 localStorage
中是否有会话数据,如果没有,那么我们就创建一个新会话,如果有,那么我们就使用 localStorage
中的数据。
为了统一管理 localStorage
中的数据,我们需要定义一些常量:
1
2
3
|
// 会话列表的 key
const currentKeyKey = 'currentKey';
const recordListKey = 'recordList';
|
这里开始所有代码都在 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]);
|
主要逻辑
这里我们需要实现两个功能,发送消息和接收消息,同时更新 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]);
|
其他功能
这里还有一些其他的功能,比如切换会话,删除会话等,简单放一下代码:
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);
};
|
界面展示
以上已经实现了所有的功能,下面我们来看一下效果,相信你可以根据上面的示例和下面的效果图,很快就能实现一个聊天机器人。
当然还有不少细节我实现了但没有提,比如:
- 机器人响应是 markdown 格式,如何像上面展示出格式化数据
- 聊天室中新消息自动滚动到底部
- 会话列表适配移动端展开收起
- 机器人响应的 loading 效果(下图)
总结
因为我是给公司内部搭建的自用项目,对于样式我并没有进行太多的处理,也没有办法提供 demo 给大家试用。
但是相信你现在已经可以自己基于 ChatGPT 实现一个聊天机器人了,样式也可以根据自己的需求进行调整。
就这,拜拜~