最近看到ant-design出了个pro-chat组件,再加上之前申请google gemini的api key。所以突然想写个个人的大模型对话应用,由于pro-chat是基于react的,所以这边想到了使用umi
来作为前端框架。
初始化项目
sql
pnpm dlx create-umi@latest
不用多说了,umijs官方的脚手架。
安装其他依赖:
bash
pnpm i @ant-design/pro-chat @google/generative-ai -S
开发过程
pro-chat组件:
javascript
import { ProChat } from "@ant-design/pro-chat";
export default function HomePage() {
const requestGemini = () => {
return 'hello'
}
return (
<ProChat
helloMessage={"新的一天,有什么我可以帮你的~~"}
displayMode={"docs"}
request={requestGemini}
/>
);
}
页面如下:
不过现在这个机器人它只会回答你hello,那是因为组件中的request中的函数的返回值设置了hello,我们下面要改写这个函数。
对接上google gemini
首先要去申请一个apikey,地址
根据google gemini文档地址,我们这边要创建一个多轮聊天,且为了传输速度我们要使用流式传输。
在umi项目下创建util目录,里面新建一个request.ts。
ini
// util/request.ts
import { GoogleGenerativeAI } from "@google/generative-ai";
const API_KEY = '个人apikey';
const genAI = new GoogleGenerativeAI(API_KEY!);
// 多轮聊天
export type multiChatParams = {
history: Array<{
role: "user" | "model";
parts: string;
}>;
currentParts: string;
};
export async function multipleChatRequest(params: multiChatParams) {
const model = genAI.getGenerativeModel({ model: "gemini-pro" });
const { history, currentParts } = params;
const chat = model.startChat({
history,
});
const result = await chat.sendMessageStream(currentParts);
return result.stream;
}
multiChatRequest是我们的请求函数,它的history参数是个对象数组,它记录了我们之前和大模型聊天的内容。其中role参数有两个枚举值(user-是我们,model-是对话的大模型这边就是gemini),parts就是聊天的内容。
另一个参数currentParts就是本次聊天的内容。
最后的返回值是result.stream,是个流式数据。我们稍后也要特殊处理它。
下面我们就要到pro-chat中准备请求数据,组件request属性方法中的参数,它的ts类型如下:
typescript
/**
* 表示具有可选泛型额外数据的聊天消息对象。
*/
export interface ChatMessage<T extends Record<string, any> = Record<string, any>> {
role: ModelRoleType | string; // 发送消息者的角色。
content: ReactNode; // 消息内容,可以呈现为ReactNode。
error?: any; // 与消息相关的可选错误信息。
model?: string; // 与消息关联的模型。
name?: string; // 发送消息者的名称。
parentId?: string; // 如果这是对另一条消息的回复,则为父级消息的ID。
createAt: number; // 消息创建时间戳。
id: string; // 消息的唯一标识符。
updateAt: number; // 消息最后更新时间戳。
extra?: T; // 消息关联的可选泛型额外数据。
}
其中,role对应了上面role,content就是每次请求的内容。可以看个例子:
在我询问'你是谁'后,request中的形参是这样:
css
[ { "content": "hello", "createAt": 1706176574453, "id": "oDSo0Yrw", "role": "user", "updateAt": 1706176574453, "meta": { "avatar": "😀" } }, { "content": "hello", "createAt": 1706176574594, "id": "6tnf7nD0", "parentId": "oDSo0Yrw", "role": "assistant", "updateAt": 1706176574847, "extra": { "fromModel": "gpt-3.5-turbo" }, "meta": { "avatar": "🤖" } }, { "content": "你是谁", "createAt": 1706176580611, "id": "gtDsDyZL", "role": "user", "updateAt": 1706176580611, "meta": { "avatar": "😀" } }]
所以我们只需要取出对应数组中的role,content来组成我们的请求参数。
ini
// 获取历史消息和本次消息
const multiChatContent = (messages || []).reduce(
(pre: multiChatParams, cur, index) => {
const { content, role } = cur;
if (index === messages.length - 1) {
// 当前消息
pre.currentParts = content;
} else {
// 历史消息
pre.history.push({
role: role === "user" ? "user" : "model",
parts: content,
});
}
return pre;
},
{
history: [],
currentParts: "",
},
);
请求参数有了下面发送请求,然后处理返回参数。
ini
let res = await multipleChatRequest(multiChatContent);
处理流式数据
首先简单介绍下流(stream)
:
流的核心思想是一种"分隔并攻克"大量数据的模式:当我们将大量数据分割为一些小的部分并一次处理一部分,我们可以处理它。
Node.js 提供多种类的流,举例来说:
- readable streams(可读流) 是可以读取数据的流。换句话说,它们是数据的来源。一个例子是可读文件流,可以让我们读取文件的内容。
- writable streams(可写流) 是可以写数据的流。换句话说,它们是数据的水池。一个例子是可写文件流,可以让我们向文件写数据。
- transform stream(转换流) 是同时可读和可写的流。作为可写流,它提取数据,并转换(改变或丢弃)它们,然后作为可读流输出它们。
这边gemini返回给我们的是一个readableStream,我们要从中读取内容显示到前端。
我们通过异步迭代的方式读取gemini返回流中的数据,
javascript
const encoder = new TextEncoder();
const readableStream = new ReadableStream({
async start(controller) {
for await (const chunk of res) {
try {
const chunkText = chunk.text();
controller.enqueue(encoder.encode(chunkText));
} catch (err) {
console.error("读取流中的数据时发生错误", err);
controller.error(err);
}
}
controller.close();
},
});
return new Response(readableStream);
完整的代码如下:
typescript
import { ProChat } from "@ant-design/pro-chat";
import { multiChatParams, multipleChatRequest } from "../util/request";
export default function HomePage() {
const requestBard = async (
messages: Array<{ content: string; [x: string]: string }>,
) => {
// 获取历史消息和本次消息
const multiChatContent = (messages || []).reduce(
(pre: multiChatParams, cur, index) => {
const { content, role } = cur;
if (index === messages.length - 1) {
// 当前消息
pre.currentParts = content;
} else {
// 历史消息
pre.history.push({
role: role === "user" ? "user" : "model",
parts: content,
});
}
return pre;
},
{
history: [],
currentParts: "",
},
);
try {
let res = await multipleChatRequest(multiChatContent);
const encoder = new TextEncoder();
const readableStream = new ReadableStream({
async start(controller) {
for await (const chunk of res) {
try {
const chunkText = chunk.text();
controller.enqueue(encoder.encode(chunkText));
} catch (err) {
console.error("读取流中的数据时发生错误", err);
controller.error(err);
}
}
controller.close();
},
});
return new Response(readableStream);
} catch (e) {
console.warn(e);
return new Response("呜呜呜,出错了,请稍后再试~~");
}
};
return (
<ProChat
helloMessage={"新的一天,有什么我可以帮你的~~"}
displayMode={"docs"}
request={requestBard}
/>
);
}
效果:
本文完。
上面内容如果有错误,请欢迎指出~~☺️
参考链接: