umijs + pro-chat + gemini 搭建个人对话大模型

最近看到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}
      />
  );
}

效果:

本文完。

上面内容如果有错误,请欢迎指出~~☺️

参考链接:

相关推荐
栈老师不回家40 分钟前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙1 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds1 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
阿伟来咯~2 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app
帅比九日3 小时前
【HarmonyOS Next】封装一个网络请求模块
前端·harmonyos
bysking3 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js