本文正在参加「金石计划 . 瓜分6万现金大奖」

最近 ChatGPT 在技能圈子可太火了,票圈也被刷屏。我也决定来凑个热烈,给自己的博客加一个 ChatGPT 对话功用。

先附上体会链接,源码在底部也能够找到。

花1块钱让你的网站支撑 ChatGPT

体会 ChatGPT

ChatGPT 是 Open AI 练习的一个 AI 对话模型,能够支撑在多种场景下进行智能对话。

花1块钱让你的网站支撑 ChatGPT

想体会 ChatGPT,首先要注册账户,可是这个产品在国内网络并不能直接用,需求自行解决网络问题。

花1块钱让你的网站支撑 ChatGPT

搞定网络问题后,注册时会让你供给邮箱验证,

花1块钱让你的网站支撑 ChatGPT

接着要验证手机号,可是很遗憾国内手机号用不了。

花1块钱让你的网站支撑 ChatGPT

你也能够选择用 Google 账号登录,可是终究还是要验证手机号。

所以咱们需求先找一个国外的能接纳短信验证码的手机号,此时能够上SMS-ACTIVATE。

这是一个在这个星球上数以百万计的服务中注册帐户的网站。咱们供给国际上大多数国家的虚拟号码,以便您能够在线接纳带有确认代码的短信。 在咱们的服务中,还有虚拟号码的长期租借,转发衔接,电话验证等等。

SMS-ACTIVATE 上的价格是卢布,咱们需求运用手机号码做短信验证,经过查询能够发现,最便宜的是印度地区的手机号,零售价格是 10.5 卢布。

花1块钱让你的网站支撑 ChatGPT

按照汇率算了一下,大概是1块多RMB。

花1块钱让你的网站支撑 ChatGPT

SMS-ACTIVATE 支撑用某宝充值,我买了一个印度号,就能够收到来自 Open AI 的验证码了。

花1块钱让你的网站支撑 ChatGPT

留意,这个号码仅仅租用,是有期限的,所以咱们要抓紧时间把注册流程搞完,20分钟过了,这个号码就不是你的了。

注册完 Open AI 的账号后,就能够到 ChatGPT 的 Web工作台体会一把 AI 对话了。

花1块钱让你的网站支撑 ChatGPT

经过 API 接入 Open AI 才干

体会完 ChatGPT 之后,对于搞技能的咱们来说,或许会想着怎样把这个才干接入到自己的产品中。

快速上手

ChatGPT 是 Open AI 练习出来的模型,Open AI 也供给了 API 给开发者们调用,文档和事例也比较全面。

机器学习很重要的一个步骤便是调参,但对于前端开发者来说,大部分人肯定是不知道怎样调参的,那咱们就参阅官方供给的最契合咱们需求的事例就好了,这个 Chat 的事例就非常契合咱们的场景需求。

花1块钱让你的网站支撑 ChatGPT

官方有供给一个 nodejs 的 starter,咱们能够基于此快速上手测验一把。

git clone https://github.com/openai/openai-quickstart-node.git

它的中心代码是这么一部分,其中用到的openai是官方封装好的 NodeJS Library。

const completion = await openai.createCompletion({
    model: "text-davinci-003",
    prompt: '发问内容',
    temperature: 0.9,
    max_tokens: 150,
    top_p: 1,
    frequency_penalty: 0,
    presence_penalty: 0.6,
});

在调用 API 之前需求先在你的 Open AI 账户中生成一个 API Key。

现在官方给到的免费额度是 18 刀,超越的部分就需求自己付费了。计费是依据 Token 来算的,至于什么是 Token,能够参阅Key concepts。

花1块钱让你的网站支撑 ChatGPT

咱们把上面那个 Chat 事例的参数拿过来直接用上,根本上也有个七八分 AI 答复问题的姿态了,这个能够自己去试一试作用,并不杂乱。

接着便是研究一下怎样把这个 starter 的关键代码集成到自己的产品中。

产品剖析

我之前有在自己的博客中做过一个简略的 WebSocket 谈天功用,而在 AI 对话这个需求中,前端 UI 部分根本上能够参阅着WebSocket 谈天功用改改,工作量不是很大,主要工作量还是在前后端的逻辑和对接上面。

花1块钱让你的网站支撑 ChatGPT

ChatGPT 的这个产品形式,它不是一个常规的 WebSocket 全双工对话,而是像咱们平常调接口相同,产生用户输入后,客户端发送恳求到服务端,等待服务端响应,最终反应给用户,它仅仅是从界面上看起来像是谈天,实际上不是一个标准的谈天进程。所以前后端交互主要还是靠 HTTP 接口对接。

中心要素 Prompt

openai.createCompletion调用时有一个很重要的参数prompt,它是对话的上下文信息,只要这个信息满意完好,AI 才干正确地做出反应。

举个比如,假设在对话进程中有2个回合。

// 回合1
你:爱因斯坦是谁?
AI: 爱因斯坦(Albert Einstein)是20世纪最重要的物理学家,他被誉为“时空之父”。他发现了相对论,并取得诺贝尔物理学奖。

第一个回合中,传参prompt爱因斯坦是谁?,机器人很好了解,立刻能给出契合实际的回复。

// 回合2
你:他做了什么奉献?
AI: 他为社会做出了许多奉献,例如改善公共卫生、建立教育基础设施、提高农业生产才干、促进经济发展等。

第二个回合传参prompt他做了什么奉献?,看到机器人的答复,你或许会觉得有点离谱,由于这根本便是驴唇不对马嘴。可是仔细想想,这是由于机器人不知道上下文信息,所以机器人不能了解代表的意义,只能经过他做了什么奉献?整句话去推测,所以从成果上看便是契合语言的逻辑,可是不契合咱们给出的语境。

假如咱们把第二个回合的传参prompt改成你: 爱因斯坦是谁?nAI: 爱因斯坦(Albert Einstein)是20世纪最重要的物理学家,他被誉为“时空之父”。他发现了相对论,并取得诺贝尔物理学奖。n你: 他做了什么奉献?nAI:,机器人就能够了解上下文信息,给出接下来的契合逻辑的答复。

// 改善后的回合2
你:他做了什么奉献?
AI: 爱因斯坦对科学有着严重的奉献,他发明晰相对论,改变了人们对国际、物理定律和世界的知道,并为量子力学奠定了基础。他还发现了...

所以,咱们的初步结论是:prompt参数应该包含此次对话主题的较完好内容,才干保证 AI 给出的下一次答复契合咱们的根本认知。

前后端交互

对于前端来说,咱们通常重视的是,我给后端发了什么数据,后端反应给我什么数据。所以,前端重视点之一便是用户的输入,用上面的比如说,爱因斯坦是谁?他做了什么奉献?这两个内容,应该分别作为前端两次恳求的参数。并且,对于前端来说,咱们也不需求考虑后端传给 Open AI 的prompt是不是完好,只要把用户输入的内容合理地传给后端就够了。

对于后端来说,咱们要重视 session 问题,每个用户应该有属于自己和 AI 的私密对话空间,不能和其他的用户对话串了数据,这个能够基于 session 完成。前端每次传过来的信息只要简略的用户输入,而后端要重视与 Open AI 的对接进程,结合用户的输入以及会话中保留的一些信息,合并成一个完好的prompt传给 Open AI,这样才干得到正常的对话进程。

所以根本的流程应该是这个姿态:

花1块钱让你的网站支撑 ChatGPT

咱们依据这个流程输出第一版代码。

后端V1版别代码

router.get('/chat-v1', async function(req, res, next) {
    // 取得用户输入
    const wd = req.query.wd;
    // 结构 prompt 参数
    if (!req.session.chatgptSessionPrompt) {
        req.session.chatgptSessionPrompt = ''
    }
    const prompt = req.session.chatgptSessionPrompt + `n发问:` + wd + `nAI:`
    try {
        const completion = await openai.createCompletion({
            model: "text-davinci-003",
            prompt,
            temperature: 0.9,
            max_tokens: 150,
            top_p: 1,
            frequency_penalty: 0,
            presence_penalty: 0.6,
            stop: ["n发问:", "nAI:"],
        });
        // 调用 Open AI 成功后,更新 session
        req.session.chatgptSessionPrompt = prompt + completion.data
        // 回来成果
        res.status(200).json({
            code: '0',
            result: completion.data.choices[0].text
        });
    } catch (error) {
        console.error(error)
        res.status(500).json({
            message: "Open AI 调用反常"
        });
    }
});

前端V1版别关键代码

const sendChatContentV1 = async () => {
    // 先显示自己说的话
    msgList.value.push({
        time: format(new Date(), "HH:mm:ss"),
        user: "我说",
        content: chatForm.chatContent,
        type: "mine",
        customClass: "mine",
    });
    loading.value = true;
    try {
        // 调 chat-v1 接口,等成果
        const { result } = await chatgptService.chatV1({ wd: chatForm.chatContent });
        // 显示 AI 的答复
        msgList.value.push({
            time: format(new Date(), "HH:mm:ss"),
            user: "Chat AI",
            content: result,
            type: "others",
            customClass: "others",
        });
    } finally {
        loading.value = false;
    }
};

花1块钱让你的网站支撑 ChatGPT

根本的对话才干现已有了,可是最明显的缺陷便是一个回合等得太久了,咱们期望他速度更快一点,至少在交互上看起来快一点。

流式输出(服务器推 + EventSource)

还好 Open AI 也支撑 stream 流式输出,在前端能够合作 EventSource 一起用。

You can also set thestreamparameter totruefor the API to stream back text (asdata-only server-sent events).

根本的数据流是这个姿态的:

花1块钱让你的网站支撑 ChatGPT

后端改造如下:

router.get('/chat-v2', async function(req, res, next) {
    // ...省掉部分代码
    try {
        const completion = await openai.createCompletion({
            // ...省掉部分代码
            // 增加了 stream 参数
            stream: true
        }, { responseType: 'stream' });
        // 设置响应的 content-type 为 text/event-stream
        res.setHeader("content-type", "text/event-stream")
        // completion.data 是一个 ReadableStream,res 是一个 WritableStream,能够经过 pipe 打通管道,流式输出给前端。
        completion.data.pipe(res)
    }
    // ...省掉部分代码
});

前端抛弃运用 axios 建议 HTTP 恳求,而是改用 EventSource。

const sendChatContent = async () => {
    // ...省掉部分代码
    // 先显示自己说的话
    msgList.value.push({
        time: format(new Date(), "HH:mm:ss"),
        user: "我说",
        content: chatForm.chatContent,
        type: "mine",
        customClass: "mine",
    });
    // 经过 EventSource 取数据
    const es = new EventSource(`/api/chatgpt/chat?wd=${chatForm.chatContent}`);
    // 记载 AI 答复的内容
    let content = "";
    // ...省掉部分代码
    es.onmessage = (e) => {
        if (e.data === "[DONE]") {
            // [DONE] 标志数据完毕,调用 feedback 反应给服务器
            chatgptService.feedback(content);
            es.close();
            loading.value = false;
            updateScrollTop();
            return;
        }
        // 从数据中取出文本
        const text = JSON.parse(e.data).choices[0].text;
        if (text) {
            if (!content) {
                // 第一条数据来了,先显示
                msgList.value.push({
                    time: format(new Date(), "HH:mm:ss"),
                    user: "Chat AI",
                    content: text,
                    type: "others",
                    customClass: "others",
                });
                // 再拼接
                content += text;
            } else {
                // 先拼接
                content += text;
                // 再更新内容,完成打字机作用
                msgList.value[msgList.value.length - 1].content = content;
            }
        }
    };
};

从代码中能够发现前端在 EventSource message 接纳完毕时,还调用了一个 feedback 接口做反应。这是由于在运用 Pipe 输出时,后端没有记载 AI 答复的文本,考虑到前端现已处理了文本,这儿就由前端做一次反应,把本次 AI 答复的内容完好回传给后端,后端再更新 session 中存储的对话信息,保证对话上下文的完好性。

feedback 接口的完成比较简略:

router.post('/feedback', function(req, res, next) {
    if (req.body.result) {
        req.session.chatgptSessionPrompt += req.body.result
        res.status(200).json({
            code: '0',
            msg: "更新成功"
        });
    } else {
        res.status(400).json({
            msg: "参数过错"
        });
    }
});

我这儿仅仅给出一种简略的做法,实际产品中或许要考虑的会更多,或许应该在后端自行处理 session 内容,而不是依靠前端的反应。

终究的作用大概是这个姿态:

约束拜访频次

由于 Open AI 也是有免费额度的,所以在调用频率和次数上也应该做个约束,防止被恶意调用,这个也能够经过 session 来处理。我这儿也供给一种比较粗糙的处理方式,详细请往下看。实际产品中或许会写 Redis,写库,加定时使命之类的,这方面我也不够专业,就不多说了。

针对拜访频率,我暂定的是 3 秒内最多调用一次,咱们能够在调用 Open AI 成功之后,在 session 中记载时间戳。

req.session.chatgptRequestTime = Date.now()

当一个新的恳求过来时,能够用当前时间减去上次记载的chatgptRequestTime,判断一下是不是在 3 秒内,假如是,就回来 HTTP 状态码 429;假如不在 3 秒内,就能够持续后边的逻辑。

if (req.session.chatgptRequestTime && Date.now() - req.session.chatgptRequestTime <= 3000) {
    // 不允许在3s里重复调用
    return res.status(429).json({
        msg: "请降低恳求频次"
    });
}

关于恳求次数也是相同的道理,我这儿也写得很简略,实际上还应该有跨天整理等逻辑要做。我这儿偷懒了,暂时没做这些。

if (req.session.chatgptTimes && req.session.chatgptTimes >= 50) {
    // 实际上还需求跨天整理,这儿先偷懒了。
    return res.status(403).json({
        msg: "抵达调用上限,欢迎明天再来哦"
    });
}

同一个论题也不能聊太多,不然传给 Open AI 的 prompt 参数会很大,这就或许会消耗很多 Token,也有或许超越 Open AI 参数的约束。

if (req.session.chatgptTopicCount && req.session.chatgptTopicCount >= 10) {
    // 一个论题聊的次数超越约束时,需求强行重置 chatgptSessionPrompt,换个论题。
    req.session.chatgptSessionPrompt = ''
    req.session.chatgptTopicCount = 0
    return res.status(403).json({
        msg: "这个论题聊得有点深入了,不如换一个"
    });
}

切换论题

客户端应该也有切换论题的才干,不然 session 中记载的信息或许会包含多个论题的内容,或许导致与用户的预期不符。那咱们做个接口就好了。

router.post('/changeTopic', function(req, res, next) {
    req.session.chatgptSessionPrompt = ''
    req.session.chatgptTopicCount = 0
    res.status(200).json({
        code: '0',
        msg: "能够测验新的论题"
    });
});

结语

总的来说,Open AI 开放出来的智能对话才干能够满意根本需求,可是还有很大改善空间。我在文中给出的代码仅供参阅,不保证功用上的完美。

附上源码地址,能够点个 star 吗,球球了[认真脸]。