⚠️ 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 控制檯的日誌中檢查是否收到用戶消息,有下面的提示代表是正常的(可能需要點下搜索才能刷新出來)。
如果收到了消息,但是沒有回覆,八成是公衆號沒有發送客服消息權限。對應是下圖的權限: