終於!我們把 CEO 炒了,讓 ChatGPT 出任 CEO

⚠️ FBI Warning:本文純屬作者自娛自樂,數字人的觀點不代表 CEO 本人的觀點,請大家不要上當受騙!!

哪個公司的 CEO 不想擁有一個自己的數字克隆?

想象🤔一下,如果 CEO 數字克隆上線了,那他是不是就可以一天約見 100 個投資人了?把他接入企業官方公衆號後臺作爲客服,24 小時不喫飯不睡覺不喝水給用戶答疑解惑,想想就很刺激!感覺 CEO 在給我打工

環界雲的 CEO 做到了!先來看看效果:

怎麼樣,你也想擁有一個自己的數字克隆麼?問題不大,跟着我操作。

首先你需要準備自己的語料,我們 CEO 的語料就是來自各種同性交友大會的演講內容,如果你的語料不夠多,那就得自己想辦法了。

當然,本文提供的方法不僅僅適用於數字克隆,你可以基於任意專有知識庫來打造一個私有領域的專家或者客服,然後再對接到公衆號,它不香嗎?

準備工作

已認證的微信公衆號

首先你需要有一個微信公衆號,而且是已經認證的公衆號,因爲公衆號強制要求服務器每次必須在 15s 以內回覆消息,公衆號平臺在發送請求到服務器後,如果 5s 內沒收到回覆,會再次發送請求等候 5 秒,如果還是沒有收到請求,最後還會發送一次請求,所以服務器必須在 15s 以內完成消息的處理。如果超過 15s 還沒有返回怎麼辦?那就超時了,用戶將永遠都收不到這條消息。

如果你想突破 15s 限制怎麼辦?

  • 如果是已認證的公衆號,可以直接使用客服消息進行回覆,它的原理是通過 POST 一個 JSON 數據包來發送消息給普通用戶。客服消息就厲害了,只要在 48 小時以內都可以回覆。具體可查看微信官方文檔
  • 如果是未認證的公衆號,並不能完全解決 15s 限制的問題,但是可以優化。這裏提供一個思路,你可以使用流式響應來緩解這個限制,先與 OpenAI 建立連接,再一個字符一個字符獲取生成的文本,最後將所獲取的文本列表拼接成回覆文本。能緩解請求超時的關鍵在於:建立連接的時間一般情況下不會超過 15s,所以只要在給定的時間內,成功建立連接,基本就能返回內容(15s 之後接收到多少文本就返回多少文本)。雖然有可能會出現回覆內容被截斷的情況,但總比你回覆不了強吧?

本文給出的方法是基於微信客服消息進行回覆,所以需要一個已認證的公衆號。如果是未認證的公衆號,就需要你自己研究流式響應了,本文不做贅述。

FastGPT

其次你需要註冊一個 FastGPT 賬號。它是一個 ChatGPT 平臺項目,目前已經集成了 ChatGPT、GPT4 和 Claude,可以使用任意文本來訓練自己的知識庫

🌐 註冊鏈接:https://fastgpt.run/?inviterId=64215e9914d068bf840141d0

知識庫

註冊完 FastGPT 後,你可以直接填寫自己的 API Key 進行使用,也可以在 FastGPT 平臺充值使用。

接下來點擊側欄的數據庫圖標進入知識庫界面,然後點擊 “+” 號新建一個知識庫。

點擊「導入」,可以看到有 3 種方法來導入知識庫。

如果你有多個文本文件,可以直接選擇「文本/文件拆分」進行導入,模式建議選「QA 拆分」,也可以直接分段。

導入之後,就會開始訓練,訓練完成後的效果:

Laf

最後你還需要一個平臺來開發你的應用,那當然是 Laf 啦。據環界雲 CEO 數字克隆所說👀,Laf 是一個 Serverless 框架,可以用來快速開發具有 AI 能力的分佈式應用,助你像寫博客一樣寫代碼,隨時隨地快速發佈上線應用。真⭕五分鐘上線 CEO 數字克隆!

🌐 Laf 註冊鏈接:https://laf.run

編寫雲函數

一切工作準備就緒後,開始動筆寫億點點代碼。

先新建應用,直接新建免費的進行測試:

點擊「+」新建雲函數:

然後將下面的雲函數代碼直接複製粘貼到 Web IDE 中:

import cloud from '@lafjs/cloud';
import * as crypto from 'crypto';

// 公衆號配置
const appid = 'wxb1833715d8f0809d'
const appsecret = 'fd76ce714a8083112100c2160b2f2c5d'
const wxToken = 'test';
// fastgpt配置
const apikey = "63f9a14228d2a688d8dc9e1b-xsyvfby3cui09tfcvxen3"
const modelId = "642adec15f01d67d4613efdb"

// 創建數據庫連接並獲取Message集合
const db = cloud.database();
const _ = db.command
const Message = db.collection('messages')

// 處理接收到的微信公衆號消息
export async function main(event) {
  // const res = await cloud.fetch.post(` https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${await getAccess_token()}`, {
  //   button: [
  //     {
  //       "type": "click",
  //       "name": "清空記錄",
  //       "key": "CLEAR"
  //     },
  //   ]
  // })
  const { signature, timestamp, nonce, echostr } = event.query;

  // 驗證消息是否合法,若不合法則返回錯誤信息
  if (!verifySignature(signature, timestamp, nonce, wxToken)) {
    return 'Invalid signature';
  }
  // 如果是首次驗證,則返回 echostr 給微信服務器
  if (echostr) {
    return echostr;
  }

  // -------------- 正文開始

  const payload = event.body.xml;
  const sessionId = payload.fromusername[0]

  console.log(payload)
  // 點擊了清空記錄
  if (payload.msgtype[0] === 'event' && payload.eventkey[0] === 'CLEAR') {
    console.log(1111)
    await Message.where({ sessionId: sessionId }).remove({ multi: true })
    await replyBykefu('記錄已清空', sessionId)
    return 'clear record'
  }

  // 僅做文本消息例子
  if (payload.msgtype[0] !== 'text') return 'no text'
  const newMessage = {
    msgid: payload.msgid[0],
    question: payload.content[0].trim(),
    username: payload.fromusername[0],
    sessionId,
    createdAt: Date.now()
  }

  await replyText(newMessage, payload.fromusername[0])

  return 'success'
}

// 處理文本回復消息
async function replyText(message, touser) {
  const { question, sessionId, msgid } = message;

  // 重複的內容,不回覆
  const { data: msg } = await Message.where({ msgid: message.msgid }).getOne()
  if (msg) return

  console.log("收到用戶消息", touser, message)

  // 立即添加一條待回覆記錄 
  await Message.add(message);

  // 回覆提示
  await replyBykefu("🤖機器人正在思考🤔中...", sessionId)
  await changesState(sessionId)

  const reply = await getFastGptReply(question, sessionId);

  const { answer } = reply;

  await Message.where({ msgid: message.msgid }).update({
    answer,
  });

  // return answer;
  await replyBykefu(answer, touser)
}

// 獲取微信公衆號ACCESS_TOKEN
async function getAccess_token() {
  const shared_access_token = await cloud.shared.get("mp_access_token")

  if (shared_access_token && shared_access_token.access_token && shared_access_token.exp > Date.now()) {
    return shared_access_token.access_token
  }
  // ACCESS_TOKEN不存在或者已過期
  // 獲取微信公衆號ACCESS_TOKEN
  const mp_access_token = await cloud.fetch.get(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${appsecret}`)
  mp_access_token.data.access_token && cloud.shared.set("mp_access_token", {
    access_token: mp_access_token.data.access_token,
    exp: Date.now() + 7100 * 1000
  })
  return mp_access_token.data.access_token
}

// 公衆號客服回覆文本消息
export async function replyBykefu(message, touser) {
  // 判斷是否爲中文字符
  function isChinese(char) {
    return /[\u4e00-\u9fa5]/.test(char)  // 判斷是否是中文字符
  }
  // 拆分文本長度
  function splitText(text) {
    let result = []
    let len = text.length
    let index = 0
    while (index < len) {
      let part = ''
      let charCount = 0
      while (charCount < 800 && index < len) {
        let char = text[index]
        charCount++
        part += char
        if (isChinese(char)) charCount++  // 中文字符計數+1
        index++
      }
      result.push(part)
    }
    return result
  }
  // 定義休眠函數
  function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)) };

  const access_token = await getAccess_token()
  let text = splitText(message)
  let len = splitText(message).length

  try {
    for (let i = 0; i < len; i++) {
      let part = text[i]  // 獲取第 i 段
      await sleep(1000)
      // 回覆消息
      const res = await cloud.fetch.post(`https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${access_token}`, {
        "touser": touser,
        "msgtype": "text",
        "text":
        {
          "content": part
        }
      })
    }
  } catch (err) {
    console.log(err)
  }
}

// 修改公衆號回覆狀態
export async function changesState(touser) {
  const access_token = await getAccess_token()
  // 修改正在輸入的狀態
  const res = await cloud.fetch.post(`https://api.weixin.qq.com/cgi-bin/message/custom/typing?access_token=${access_token}`, {
    "touser": touser,
    "command": "Typing"
  })
}

// 校驗微信服務器發送的消息是否合法
export function verifySignature(signature, timestamp, nonce, token) {
  const arr = [token, timestamp, nonce].sort();
  const str = arr.join('');
  const sha1 = crypto.createHash('sha1');
  sha1.update(str);
  return sha1.digest('hex') === signature;
}

// 返回組裝 xml
export function toXML(payload, content) {
  const timestamp = Date.now();
  const { tousername: fromUserName, fromusername: toUserName } = payload;
  return `
      <xml>
        <ToUserName><![CDATA[${toUserName}]]></ToUserName>
        <FromUserName><![CDATA[${fromUserName}]]></FromUserName>
        <CreateTime>${timestamp}</CreateTime>
        <MsgType><![CDATA[text]]></MsgType>
        <Content><![CDATA[${content}]]></Content>
      </xml>
      `
}

// 調用 fastgpt 回答
async function getFastGptReply(question, sessionId) {
  const res = await db.collection('messages')
    .where({ sessionId })
    .get()

  // 獲取最多10組上下文
  const list = res.data.slice(-10)
  const prompts = list.map((item) => [{
    obj: "Human",
    value: item.question || ''
  }, {
    obj: "AI",
    value: item.answer || ''
  }]).concat({
    obj: "Human",
    value: question
  }).flat()

  const config = {
    method: 'post', // 設置請求方法爲POST
    url: 'https://fastgpt.run/api/openapi/chat/chat', // 設置請求地址
    headers: { // 設置請求頭信息
      apikey,
      'Content-Type': 'application/json'
    },
    data: { // 設置請求體數據
      modelId,
      isStream: false,
      prompts
    }
  }
  try {
    const ret = await cloud.fetch(config)
    console.log("fastgpt響應", ret.data)
    return { answer: ret.data.data || ret.data || '' }
  } catch (e) {
    console.log("出錯了", e.response)
    return {
      error: "問題太難了 出錯了. (uДu〃).",
    }
  }
}

整個雲函數的調用流程如下:

❶ 當收到微信公衆號消息時,首先調用 main 函數。在 main 函數中,首先驗證消息是否合法,如果不合法則返回錯誤信息。如果是首次驗證,則返回 echostr 給微信服務器。

❷ 接着根據消息類型進行處理。對於文本消息,調用 replyText 函數進行處理。

❸ 在 replyText 函數中,首先檢查是否爲重複的內容,如果是則不回覆。然後將用戶發送的問題存入數據庫,並回復提示信息給用戶,表示機器人正在思考中。

❹ 接下來調用 getFastGptReply 函數獲取 FastGPT 的回答。在 getFastGptReply 函數中,首先從數據庫中獲取最多 10 組上下文信息,然後將問題和上下文信息一起發送給 FastGPT。接收到 FastGPT 的回答後返回給 replyText 函數。

❺ 回到 replyText 函數,將 FastGPT 返回的回答更新到數據庫中,並通過客服接口將回答發送給用戶。在發送回答之前,會調用 changesState 函數修改公衆號回覆狀態爲正在輸入中。

❻ 調用 replyBykefu 函數通過微信公衆號客服接口發送文本消息給用戶。在 replyBykefu 函數中,首先根據文本長度拆分成多段,並逐段發送給用戶。

先不要改動代碼中的任何內容,後面會告訴你如何修改。

點擊「發佈」:

最後複製已發佈的函數地址:

配置微信公衆號

這一步我們需要在微信公衆號平臺上配置開發者信息,並將服務器地址設置爲部署好的雲函數服務地址。步驟如下:

首先登錄微信公衆平臺,點開左側的「設置與開發」,點擊「基本設置」,然後點擊「服務器配置」,服務器配置那裏點擊修改配置:

將之前的雲函數服務地址複製到「服務器 URL」中,下邊的 Token 與雲函數代碼中的 token 保持一致,下邊的 EncodingAESKey 點擊右側隨機生成就行,然後點擊提交:

返回 token 校驗成功即可。

獲取公衆號的 AppID 和 AppSecret:

這一步的操作請務必不要忘記!!!你需要把 laf.run 的 IP 地址全部添加到 IP 白名單中:

laf.run 域名的 IP 地址可通過以下命令獲取:

$ dig +short laf.run
112.124.8.17
120.26.163.28
112.124.9.83
47.97.22.68
112.124.9.194
114.55.179.67
114.55.177.246
120.27.246.172
120.26.161.248
47.97.5.237

把獲取到的 AppID 和 AppSecret 填寫到 Laf 雲函數中,然後點擊「發佈」:

最後在公衆號平臺點擊「啓用」即可。

配置 FastGPT

接下來開始配置 FastGPT,首先新建一個 API Key:

然後新建一個應用:

然後選擇需要關聯的知識庫:

可以根據自己的需求設置一下溫度、搜索模式和系統提示詞,最終點擊「保存修改」。

獲取應用的 modelId:

將你獲取的 API Key 和 modelId 填寫到 Laf 雲函數中,修改完成後點擊發布:

到公衆號裏測試一下:

完美👀

當然,接入數字 CEO 只是圖個樂呵,演示完了就撤了。目前 Laf 公衆號真正接入的是 Laf 專有模型,可以回答與 Laf 相關的任何問題,感興趣的小夥伴可以去體驗一下,公衆號的名字是:Laf 開發者

QA

如果發送消息後無響應,可以先去 Laf 控制檯的日誌中檢查是否收到用戶消息,有下面的提示代表是正常的(可能需要點下搜索才能刷新出來)。

如果收到了消息,但是沒有回覆,八成是公衆號沒有發送客服消息權限。對應是下圖的權限:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章