From 8239c584948ced57a4d1d0ef246e9bd8af60af68 Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Fri, 31 Mar 2023 00:05:04 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E7=9F=A5=E8=AF=86=E5=BA=93=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/model.ts | 6 +- src/constants/common.ts | 33 +- src/hooks/usePagination.tsx | 85 +++++ src/pages/api/chat/chatGpt.ts | 2 +- src/pages/api/chat/vectorGpt.ts | 24 +- src/pages/api/model/data/getSplitData.ts | 35 ++ src/pages/api/model/data/splitData.ts | 2 +- src/pages/chat/components/Empty.tsx | 8 +- src/pages/model/components/ModelDataCard.tsx | 326 ------------------ .../model/detail/components/ModelDataCard.tsx | 79 +++-- .../detail/components/SelectFileModal.tsx | 2 +- src/pages/model/detail/index.tsx | 4 +- 12 files changed, 201 insertions(+), 405 deletions(-) create mode 100644 src/hooks/usePagination.tsx create mode 100644 src/pages/api/model/data/getSplitData.ts delete mode 100644 src/pages/model/components/ModelDataCard.tsx diff --git a/src/api/model.ts b/src/api/model.ts index 0d5cdc1e1..192bfe51c 100644 --- a/src/api/model.ts +++ b/src/api/model.ts @@ -1,8 +1,7 @@ import { GET, POST, DELETE, PUT } from './request'; -import type { ModelSchema, ModelDataSchema } from '@/types/mongoSchema'; +import type { ModelSchema, ModelDataSchema, ModelSplitDataSchema } from '@/types/mongoSchema'; import { ModelUpdateParams } from '@/types/model'; import { TrainingItemType } from '../types/training'; -import { PagingData } from '@/types'; import { RequestPaging } from '../types/index'; import { Obj2Query } from '@/utils/tools'; @@ -39,6 +38,9 @@ type GetModelDataListProps = RequestPaging & { export const getModelDataList = (props: GetModelDataListProps) => GET(`/model/data/getModelData?${Obj2Query(props)}`); +export const getModelSplitDataList = (modelId: string) => + GET(`/model/data/getSplitData?modelId=${modelId}`); + export const postModelDataInput = (data: { modelId: string; data: { text: ModelDataSchema['text']; q: ModelDataSchema['q'] }[]; diff --git a/src/constants/common.ts b/src/constants/common.ts index c6bcb8207..6371c9887 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -17,30 +17,24 @@ export const introPage = ` 4. 进入模型页,创建一个模型,建议直接用 ChatGPT。 5. 在模型列表点击【对话】,即可使用 API 进行聊天。 -### 模型配置 +### 定制 prompt -1. **提示语**:会在每个对话框的第一句自动加入,用于限定该模型的对话内容。 +1. 进入模型编辑页 +2. 调整温度和提示词 +3. 使用该模型对话。每次对话时,提示词和温度都会自动注入,方便管理个人的模型。建议把自己日常经常需要使用的 5~10 个方向预设好。 +### 知识库 -2. **单句最大长度**:每个聊天,单次输入内容的最大长度。 +1. 创建模型时选择【知识库】 +2. 进入模型编辑页 +3. 导入数据,可以选择手动导入,或者选择文件导入。文件导入会自动调用 chatGPT 理解文件内容,并生成知识库。 +4. 使用该模型对话。 - -3. **上下文最大长度**:每个聊天,最多的轮数除以2,建议设置为偶数。可以持续聊天,但是旧的聊天内容会被截断,AI 就不会知道被截取的内容。 -例如:上下文最大长度为6。在第 4 轮对话时,第一轮对话的内容不会被计入。 - -4. **过期时间**:生成对话框后,这个对话框多久过期。 - -5. **聊天最大加载次数**:单个对话框最多被加载几次,设置为-1代表不限制,正数代表只能加载 n 次,防止被盗刷。 - -### 对话框介绍 - -1. 每个对话框以 chatId 作为标识。 -2. 每次点击【对话】,都会生成新的对话框,无法回到旧的对话框。对话框内刷新,会恢复对话内容。 -3. 直接分享对话框(网页)的链接给朋友,会共享同一个对话内容。但是!!!千万不要两个人同时用一个链接,会串味,还没解决这个问题。 -4. 如果想分享一个纯的对话框,请点击侧边栏的分享按键。例如: +注意:使用知识库模型对话时,tokens 消耗会加快。 ### 其他问题 还有其他问题,可以加我 wx: YNyiqi,拉个交流群大家一起聊聊。 + `; export const chatProblem = ` @@ -58,10 +52,9 @@ export const chatProblem = ` `; export const versionIntro = ` -## Fast GPT V2.0 +## Fast GPT V2.2 +* 定制知识库:创建模型时可以选择【知识库】模型, 可以手动导入知识点或者直接导入一个文件自动学习。 * 删除和复制功能:点击对话头像,可以选择复制或删除该条内容。 -* 优化记账模式: 不再根据文本长度进行记账,而是根据实际消耗 tokens 数量进行记账。 -* 文本 QA 拆分: 可以在[数据]模块,使用 QA 拆分功能,粘贴文字或者选择文件均可以实现自动生成 QA。可以一键导出,用于微调模型。 `; export const shareHint = ` diff --git a/src/hooks/usePagination.tsx b/src/hooks/usePagination.tsx new file mode 100644 index 000000000..e88cfa334 --- /dev/null +++ b/src/hooks/usePagination.tsx @@ -0,0 +1,85 @@ +import { useState, useCallback, useMemo } from 'react'; +import type { PagingData } from '../types/index'; +import { IconButton, Flex, Box } from '@chakra-ui/react'; +import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons'; +import { useQuery, useMutation } from '@tanstack/react-query'; +import { useToast } from './useToast'; + +export const usePagination = ({ + api, + pageSize = 10, + params = {} +}: { + api: (data: any) => any; + pageSize?: number; + params?: Record; +}) => { + const { toast } = useToast(); + const [pageNum, setPageNum] = useState(1); + const [total, setTotal] = useState(0); + const maxPage = useMemo(() => Math.ceil(total / pageSize), [pageSize, total]); + + const { + mutate, + data = [], + isLoading + } = useMutation({ + mutationFn: async (num: number = pageNum) => { + try { + const res: PagingData = await api({ + pageNum: num, + pageSize, + ...params + }); + setPageNum(num); + setTotal(res.total); + return res.data; + } catch (error: any) { + toast({ + title: error?.message || '获取数据异常', + status: 'error' + }); + console.log(error); + } + } + }); + + useQuery(['init'], () => { + mutate(1); + return null; + }); + + const Pagination = useCallback(() => { + return ( + + } + aria-label={'left'} + size={'sm'} + onClick={() => mutate(pageNum - 1)} + /> + + {pageNum}/{maxPage} + + } + aria-label={'left'} + size={'sm'} + onClick={() => mutate(pageNum + 1)} + /> + + ); + }, [maxPage, mutate, pageNum]); + + return { + pageNum, + pageSize, + total, + data, + isLoading, + Pagination, + getData: mutate + }; +}; diff --git a/src/pages/api/chat/chatGpt.ts b/src/pages/api/chat/chatGpt.ts index c42f95411..03adca679 100644 --- a/src/pages/api/chat/chatGpt.ts +++ b/src/pages/api/chat/chatGpt.ts @@ -40,6 +40,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } await connectToDatabase(); + let startTime = Date.now(); const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization); @@ -81,7 +82,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // 获取 chatAPI const chatAPI = getOpenAIApi(userApiKey || systemKey); - let startTime = Date.now(); // 发出请求 const chatResponse = await chatAPI.createChatCompletion( { diff --git a/src/pages/api/chat/vectorGpt.ts b/src/pages/api/chat/vectorGpt.ts index 9f0a14d0f..42233b8f2 100644 --- a/src/pages/api/chat/vectorGpt.ts +++ b/src/pages/api/chat/vectorGpt.ts @@ -48,6 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) await connectToDatabase(); const redis = await connectRedis(); + let startTime = Date.now(); const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization); @@ -83,17 +84,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const redisData: any[] = await redis.sendCommand([ 'FT.SEARCH', `idx:${VecModelDataIndex}`, - `@modelId:{${String(chat.modelId._id)}} @vector:[VECTOR_RANGE 0.2 $blob]`, + `@modelId:{${String( + chat.modelId._id + )}} @vector:[VECTOR_RANGE 0.15 $blob]=>{$YIELD_DISTANCE_AS: score}`, // `@modelId:{${String(chat.modelId._id)}}=>[KNN 10 @vector $blob AS score]`, 'RETURN', '1', 'dataId', - // 'SORTBY', - // 'score', + 'SORTBY', + 'score', 'PARAMS', '2', 'blob', binary, + 'LIMIT', + '0', + '20', 'DIALECT', '2' ]); @@ -117,8 +123,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) [2, 4, 6, 8, 10, 12, 14, 16, 18, 20].map((i) => { if (!redisData[i] || !redisData[i][1]) return ''; return ModelData.findById(redisData[i][1]) - .select('text') - .then((res) => res?.text || ''); + .select('text q') + .then((res) => { + if (!res) return ''; + const questions = res.q.map((item) => item.text).join(' '); + const answer = res.text; + return `${questions} ${answer}`; + }); }) ) ).filter((item) => item); @@ -128,7 +139,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) prompts.unshift({ obj: 'SYSTEM', - value: `请根据下面的知识回答问题: ${systemPrompt}` + value: `根据下面的知识回答问题: ${systemPrompt}` }); // 控制在 tokens 数量,防止超出 @@ -150,7 +161,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // 计算温度 const temperature = modelConstantsData.maxTemperature * (model.temperature / 10); - let startTime = Date.now(); // 发出请求 const chatResponse = await chatAPI.createChatCompletion( { diff --git a/src/pages/api/model/data/getSplitData.ts b/src/pages/api/model/data/getSplitData.ts new file mode 100644 index 000000000..2e296cf78 --- /dev/null +++ b/src/pages/api/model/data/getSplitData.ts @@ -0,0 +1,35 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/response'; +import { connectToDatabase, SplitData, Model } from '@/service/mongo'; +import { authToken } from '@/service/utils/tools'; + +/* 拆分数据成QA */ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { modelId } = req.query as { modelId: string }; + if (!modelId) { + throw new Error('参数错误'); + } + await connectToDatabase(); + + const { authorization } = req.headers; + + const userId = await authToken(authorization); + + // 找到长度大于0的数据 + const data = await SplitData.find({ + userId, + modelId, + textList: { $exists: true, $not: { $size: 0 } } + }); + + jsonRes(res, { + data + }); + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/src/pages/api/model/data/splitData.ts b/src/pages/api/model/data/splitData.ts index 379d952e1..0f0e846ff 100644 --- a/src/pages/api/model/data/splitData.ts +++ b/src/pages/api/model/data/splitData.ts @@ -53,7 +53,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) textList }); - // generateQA(); + generateQA(); jsonRes(res, { data: { chunks, replaceText } diff --git a/src/pages/chat/components/Empty.tsx b/src/pages/chat/components/Empty.tsx index 918fef355..d3f3e238a 100644 --- a/src/pages/chat/components/Empty.tsx +++ b/src/pages/chat/components/Empty.tsx @@ -25,14 +25,14 @@ const Empty = ({ intro }: { intro: string }) => { {intro} )} + {/* version intro */} + + +
常见问题
- {/* version intro */} - - - ); }; diff --git a/src/pages/model/components/ModelDataCard.tsx b/src/pages/model/components/ModelDataCard.tsx deleted file mode 100644 index 2dcf1f1b1..000000000 --- a/src/pages/model/components/ModelDataCard.tsx +++ /dev/null @@ -1,326 +0,0 @@ -import React, { useEffect, useCallback, useState } from 'react'; -import { - Box, - TableContainer, - Table, - Thead, - Tbody, - Tr, - Th, - Td, - IconButton, - Flex, - Button, - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - Checkbox, - CheckboxGroup, - ModalCloseButton, - useDisclosure, - Input, - Textarea, - Stack -} from '@chakra-ui/react'; -import type { ModelSchema } from '@/types/mongoSchema'; -import { ModelDataSchema } from '@/types/mongoSchema'; -import { ModelDataStatusMap } from '@/constants/model'; -import { usePaging } from '@/hooks/usePaging'; -import ScrollData from '@/components/ScrollData'; -import { - getModelDataList, - postModelDataInput, - postModelDataSelect, - delOneModelData, - putModelDataById -} from '@/api/model'; -import { getDataList } from '@/api/data'; -import { DeleteIcon } from '@chakra-ui/icons'; -import { useForm, useFieldArray } from 'react-hook-form'; -import { useToast } from '@/hooks/useToast'; -import { useQuery } from '@tanstack/react-query'; -import { customAlphabet } from 'nanoid'; -const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12); - -type FormData = { text: string; q: { val: string }[] }; -type TabType = 'input' | 'select'; -const defaultValues = { - text: '', - q: [{ val: '' }] -}; - -const ModelDataCard = ({ model }: { model: ModelSchema }) => { - const { - nextPage, - isLoadAll, - requesting, - data: modelDataList, - total, - setData, - getData - } = usePaging({ - api: getModelDataList, - pageSize: 20, - params: { - modelId: model._id - } - }); - const { toast } = useToast(); - const { - isOpen: isOpenImportModal, - onOpen: onOpenImportModal, - onClose: onCloseImportModal - } = useDisclosure(); - const { register, handleSubmit, reset, control } = useForm({ - defaultValues - }); - const { - fields: inputQ, - append: appendQ, - remove: removeQ - } = useFieldArray({ - control, - name: 'q' - }); - - const importDataTypes: { id: TabType; label: string }[] = [ - { id: 'input', label: '手动输入' }, - { id: 'select', label: '数据集导入' } - ]; - const [importDataType, setImportDataType] = useState(importDataTypes[0].id); - const [importing, setImporting] = useState(false); - - const updateAnswer = useCallback(async (dataId: string, text: string) => { - putModelDataById({ - dataId, - text - }); - }, []); - - const { data: dataList = [] } = useQuery(['getDataList'], getDataList); - const [selectDataId, setSelectDataId] = useState([]); - - const sureImportData = useCallback( - async (e: FormData) => { - setImporting(true); - - try { - if (importDataType === 'input') { - await postModelDataInput({ - modelId: model._id, - data: [ - { - text: e.text, - q: e.q.map((item) => ({ - id: nanoid(), - text: item.val - })) - } - ] - }); - } else if (importDataType === 'select') { - const res = await postModelDataSelect(model._id, selectDataId); - console.log(res); - } - - toast({ - title: '导入数据成功,需要一段时间训练', - status: 'success' - }); - onCloseImportModal(); - getData(1, true); - reset(defaultValues); - } catch (err) { - console.log(err); - } - setImporting(false); - }, - [getData, importDataType, model._id, onCloseImportModal, reset, toast] - ); - - return ( - <> - - - 模型数据: {total}组 - - - - - - - - - - - - - - - - {modelDataList.map((item) => ( - - - - - - - ))} - -
QuestionTextStatus
- {item.q.map((item, i) => ( - - Q{i + 1}: {item.text} - - ))} - - - {ModelDataStatusMap[item.status]} - } - variant={'outline'} - colorScheme={'gray'} - aria-label={'delete'} - size={'sm'} - onClick={async () => { - delOneModelData(item._id); - setData((state) => state.filter((data) => data._id !== item._id)); - }} - /> -
-
-
- - - - - 选择数据导入 - - {importDataTypes.map((item) => ( - - ))} - - - - - {importDataType === 'input' && ( - <> - 知识点: - @@ -170,7 +173,7 @@ const ModelDataCard = ({ model }: { model: ModelSchema }) => { size={'sm'} onClick={async () => { delOneModelData(item._id); - setData((state) => state.filter((data) => data._id !== item._id)); + refetchData(pageNum); }} /> @@ -179,21 +182,17 @@ const ModelDataCard = ({ model }: { model: ModelSchema }) => { - - + + + + + + {isOpenInputModal && ( - getData(1, true)} - /> + )} {isOpenSelectModal && ( - getData(1, true)} - /> + )} ); diff --git a/src/pages/model/detail/components/SelectFileModal.tsx b/src/pages/model/detail/components/SelectFileModal.tsx index d709b7cd7..daf6a1abb 100644 --- a/src/pages/model/detail/components/SelectFileModal.tsx +++ b/src/pages/model/detail/components/SelectFileModal.tsx @@ -37,7 +37,7 @@ const SelectFileModal = ({ const { File, onOpen } = useSelectFile({ fileType: fileExtension, multiple: true }); const [fileText, setFileText] = useState(''); const { openConfirm, ConfirmChild } = useConfirm({ - content: '确认导入该文件,需要一定时间进行拆解,该任务无法终止!' + content: '确认导入该文件,需要一定时间进行拆解,该任务无法终止!如果余额不足,任务讲被终止。' }); const onSelectFile = useCallback( diff --git a/src/pages/model/detail/index.tsx b/src/pages/model/detail/index.tsx index f1c77c8c9..a702533e8 100644 --- a/src/pages/model/detail/index.tsx +++ b/src/pages/model/detail/index.tsx @@ -11,8 +11,7 @@ import { useGlobalStore } from '@/store/global'; import { useScreen } from '@/hooks/useScreen'; import ModelEditForm from './components/ModelEditForm'; import { useQuery } from '@tanstack/react-query'; -// import dynamic from 'next/dynamic'; -import ModelDataCard from './components/ModelDataCard'; +import dynamic from 'next/dynamic'; const ModelDataCard = dynamic(() => import('./components/ModelDataCard')); @@ -251,7 +250,6 @@ const ModelDetail = ({ modelId }: { modelId: string }) => { {canTrain && model._id && (