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}
      />
  );
}

效果:

本文完。

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

参考链接:

相关推荐
小土豆_77717 分钟前
Owl 2.8.1 核心语法速查表(新手专用)
前端·odoo/owl
firstacui23 分钟前
LVS三种模式搭建
前端·chrome
wanzhong233323 分钟前
开发日记13-响应式变量
开发语言·前端·javascript·vue
代码游侠26 分钟前
学习笔记——文件传输工具配置与Makefile详解
运维·前端·arm开发·笔记·学习
踢球的打工仔30 分钟前
typescript-类的静态属性和静态方法
前端·javascript·typescript
匠心网络科技32 分钟前
前端框架-Vue双向绑定核心机制全解析
前端·javascript·vue.js·前端框架
Jinuss32 分钟前
源码分析之React中的FiberRoot节点属性介绍
前端·javascript·react.js
自回归向前看41 分钟前
2020-25 Js ES新增加特性
前端·javascript
wanzhong233344 分钟前
开发日记14-vite配置多环境
服务器·前端·vue
Jinuss1 小时前
源码分析之React中的Fiber节点介绍
前端·javascript·react.js