初試微信公眾號(hào)開發(fā)與對(duì)接 ChatGPT 接口
上周公眾號(hào)對(duì)接了 ChatGPT 接口,直接在公眾號(hào)內(nèi)即可與 ChatGPT 聊天。本文將整個(gè)對(duì)接的過程分享給大家
由于本人沒有過公眾號(hào)開發(fā)經(jīng)驗(yàn),所以先是直接百度了下「公眾號(hào)對(duì)接 ChatGPT」,果然網(wǎng)上已經(jīng)有簡(jiǎn)單案例了,代碼如下:
import?werobot
import?openai
robot?=?werobot.WeRoBot(token="your_wx_token")
robot.config["HOST"]?=?"0.0.0.0"
robot.config["PORT"]?=?9005
openai.api_key?=?"your_api_key"
@robot.handler
def?handle(messages):
????completion?=?openai.ChatCompletion.create(
????????model="gpt-3.5-turbo",
????????messages=[{"role":?"user",?"content":?messages.content}]
????)
????return?completion.choices[0].message.content
robot.run()
代碼非常簡(jiǎn)潔,使用了 werobot 讓我們可以快速開發(fā)公眾號(hào)被動(dòng)消息回復(fù)邏輯,利用 openai 庫(kù)來調(diào)用調(diào)用 ChatGPT 接口。
代碼跑起來之后,我便迫不及待的給公眾號(hào)發(fā)消息測(cè)試,簡(jiǎn)單測(cè)試后發(fā)現(xiàn)了如下的問題:
-
發(fā)送的所有消息都會(huì)轉(zhuǎn)發(fā)給
ChatGPT接口,會(huì)影響公眾號(hào)后續(xù)的自動(dòng)回復(fù)功能開發(fā)。 - 示例代碼中調(diào)用 ChatGPT 接口只傳遞了單條消息,沒有保持整個(gè)會(huì)話的上下文,這樣無法更好的體驗(yàn) ChatGPT 功能
- 公眾號(hào)被動(dòng)回復(fù)接口需要在 5 秒內(nèi)做出響應(yīng),否則微信將進(jìn)行 2 兩次重試調(diào)用接口,而 ChatGPT API 稍微復(fù)雜點(diǎn)的問題基本都需要半分鐘才能響應(yīng),由于微信的消息重試機(jī)制,一個(gè)問題我重復(fù)調(diào)用了 3 次 ChatGPT 且每一次都會(huì)超時(shí),導(dǎo)致公眾號(hào)無法正常響應(yīng)用戶消息。
解決思路
第一個(gè)問題,我需要有個(gè)入口來開啟聊天會(huì)話,只有開啟了會(huì)話之后的消息才需要被 ChatGPT 處理。
對(duì)于這個(gè)問題,我首先想到的是增加一個(gè)公眾號(hào)菜單,當(dāng)用戶點(diǎn)擊公眾號(hào)菜單「ChatGPT」時(shí)開啟會(huì)話,開啟會(huì)話后的消息讓 ChatGPT 來處理。
很快這個(gè)思路就被否定了,原因是我的公眾號(hào)沒有權(quán)限,無法處理菜單點(diǎn)擊事件,只能設(shè)置回復(fù)公眾號(hào)內(nèi)的文章消息。
最終解決方案: 讓用戶輸入發(fā)送特定的消息來開啟會(huì)話
發(fā)送
/chatgpt指令消息開啟會(huì)話,會(huì)話有效時(shí)常為 5 分鐘,發(fā)送消息可延長(zhǎng)有效期,會(huì)話失效后需要重新啟動(dòng)會(huì)話
第二個(gè)問題,需要保持 ChatGPT 的上下文
通過查閱ChatGPT官方文檔得知,想要保持上下文,需要每次請(qǐng)求的時(shí)候把之前用戶發(fā)送的消息 和 ChatGPT 回復(fù)的消息都放到請(qǐng)求參數(shù)中,通過role來區(qū)分用戶消息(user)和助理消息(assistant),請(qǐng)求參數(shù)JSON示例:
{
??"model":?"gpt-3.5-turbo",
??"messages":?[
????{"role":?"user",?"content":?"第一條用戶消息"},
????{"role":?"assistant",?"content":?"ChatGPT第一條消息回復(fù)"},
????{"role":?"user",?"content":?"第二條用戶消息"}
????//?……
??]
}
ok,當(dāng)用戶開啟會(huì)話后,我們只需要將會(huì)話中所有的消息再下次請(qǐng)求 ChatGPT 時(shí)都攜帶上就可以保持消息上下文了。基于 /chatgpt 指令消息,我們?cè)僭黾觾蓚€(gè)指令:
-
/stop:結(jié)束會(huì)話,結(jié)束后想要再發(fā)消息,需要重新開啟會(huì)話 -
/clear:清空消息上下文,相當(dāng)于執(zhí)行了/stop+/chatgpt開啟了一個(gè)新的會(huì)話
第三個(gè)問題,ChatGPT 接口響應(yīng)慢導(dǎo)致微信接口超時(shí),無法正?;貜?fù)用戶結(jié)果
這個(gè)問題也是最嚴(yán)重的問題,會(huì)直接導(dǎo)致無法與 ChatGPT 進(jìn)行對(duì)話。
image-20230411000326242通過閱讀微信公眾號(hào)開發(fā)文檔,若服務(wù)器無法在 5 秒內(nèi)回復(fù)消息,可以直接回復(fù) success,后續(xù)再將結(jié)果異步推送給用戶。
本以為是找到了完美的解決方案,當(dāng)我試驗(yàn)的時(shí)候才發(fā)現(xiàn)我的公眾號(hào)并沒有「主動(dòng)發(fā)送消息給用戶」的權(quán)限,好吧,只能再想其他辦法了(吐糟下,微信個(gè)人訂閱號(hào)給的權(quán)限太低了,什么都用不了...)
最終解決方案:
-
充分利用微信的重試機(jī)制,在微信首次請(qǐng)求時(shí)異步調(diào)用 ChatGPT,若在當(dāng)前接口調(diào)用超時(shí)前得到 ChatGPT 的回復(fù),則正常響應(yīng)給用戶,否則讓這次接口調(diào)用超時(shí)。當(dāng)微信接口重試時(shí),不再請(qǐng)求 ChatGPT,而是判斷之前的 ChatGPT 有沒有完成回復(fù)并返回給用戶結(jié)果。通過這種方式,將公眾號(hào)正常響應(yīng)的超時(shí)時(shí)間提升到 15 秒!

-
若 15 秒內(nèi) ChatGPT 仍然無法作出回復(fù),則在微信的最后一次接口重試時(shí)返回給用戶一個(gè)網(wǎng)站鏈接,通過可以通過該鏈接查看并等待 ChatGPT 回復(fù)。
開發(fā)過程
1. 技術(shù)與框架選型
由于后續(xù)公眾號(hào)可能會(huì)繼續(xù)開發(fā)消息回復(fù)系統(tǒng) 或 對(duì)接其他的 AI 助手,而 Python 語言我并不擅長(zhǎng)(不喜歡用 Python),所以選擇我最熟悉的 Java 語言進(jìn)行開發(fā)。
Java語言微信開發(fā)框架有很多,如:fastweixin、wechat4j、WxJava 等,經(jīng)過比較源碼的介紹和關(guān)注人數(shù),開發(fā)人數(shù)、活躍程度來看WxJava是比較好的,就用它了。
而對(duì)于 ChatGPT 的接口調(diào)用,其實(shí)隨便哪個(gè)HTTP客戶端都可以,本次則選擇本人比較喜歡的 open-feign 框架。
2. 處理微信重試消息
首先來簡(jiǎn)單了解下WxJava框架處理公眾號(hào)消息的流程
WxJava消息處理流程從上圖中首先由WxPortalController對(duì)外提供HTTP接口,接收微信發(fā)送過來的消息處理請(qǐng)求。然后WxMessageRouter負(fù)責(zé)根據(jù)指定的規(guī)則對(duì) 消息處理器Handler 進(jìn)行路由選擇合適的處理器來處理消息。
然而實(shí)際試驗(yàn)發(fā)現(xiàn),Controller層是可以接收到微信的重試消息的,而 Handler 層卻只會(huì)處理一次,通過閱讀源碼得知,微信重試消息在 WxMessageRouter消息路由層已經(jīng)被過濾了,所以第一步需要做的就是讓 微信重試消息 可以下沉到 Handler層來處理。
//?創(chuàng)建消息路由器
WxMpMessageRouter?newRouter?=?new?WxMpMessageRouter(wxMpService);
//?替換默認(rèn)的「重復(fù)消息校驗(yàn)器」,讓?router?不對(duì)重試消息進(jìn)行過濾處理
newRouter.setMessageDuplicateChecker(v?->?false);
通過這步操作,微信的重試消息已經(jīng)可以到達(dá) Handler 了
Handler 中處理重試消息
抽象出 AbstractReplayHandler來專門負(fù)責(zé)上述的消息重試處理,并預(yù)留抽象方法handlerAsync方法,讓子類正常的進(jìn)行同步處理消息,不用關(guān)注重試邏輯。
定義 ReplayInfo 類記錄接口重試信息
public?class?ReplayInfo?{
????/**?消息Id*/
????private?String?messageId;
????/**?微信消息對(duì)象*/
???private?WxMpXmlMessage?wxMessage;
????/**?請(qǐng)求次數(shù)*/
????private?AtomicInteger?replayCount;
????/**?首次請(qǐng)求時(shí)間戳*/
????private?long?timeMillis?=?System.currentTimeMillis();
????/**?消息處理?CompletableFuture*/
????private?CompletableFuture?completableFuture;
????/**?消息處理異常*/
????private?WxErrorException?exception;
}
ReplayInfo 存儲(chǔ)
private?static?final?Map?REPLAY_CACHE?=?new?ConcurrentHashMap<>();
抽象方法定義
/**
?*?異步處理消息
?*/
public?abstract?WxMpXmlOutMessage?handleAsync(WxMpXmlMessage?wxMessage,?Map?context,?WxMpService?wxMpService,?WxSessionManager?sessionManager) ?throws?WxErrorException;
/**
?*?超時(shí)消息
?*/
public?abstract?WxMpXmlOutMessage?buildTimeoutMessage(WxMpXmlMessage?wxMessage,?Map?context,?WxMpService?wxMpService,?WxSessionManager?sessionManager) ;
核心邏輯處理:handle 方法
//?消息Id
String?messageId?=?getMessageId(wxMessage);
//?首次消息執(zhí)行消息處理并創(chuàng)建?replayInfo,非首次消息則直接獲取
ReplayInfo?replayInfo?=?REPLAY_CACHE.computeIfAbsent(messageId,?key?->?{
????ReplayInfo?dto?=?new?ReplayInfo();
????dto.setMessageId(messageId);
????dto.setWxMessage(wxMessage);
????dto.setReplayCount(new?AtomicInteger());
????//?異步執(zhí)行真正的處理方法
????dto.setCompletableFuture(CompletableFuture.supplyAsync(()?->?{
????????try?{
????????????return?handleAsync(wxMessage,?context,?wxMpService,?sessionManager);
????????}?catch?(WxErrorException?e)?{
????????????//?記錄異常信息
????????????dto.setException(e);
????????????return?null;
????????}
????}));
????return?dto;
});
//?請(qǐng)求次數(shù)
int?replayCount?=?replayInfo.getReplayCount().incrementAndGet();
CompletableFuture?completableFuture?=?replayInfo.getCompletableFuture();
if?(completableFuture?==?null)?{
????throw?new?WxErrorException("Replay消息錯(cuò)誤");
try?{
????//?在單次請(qǐng)求超時(shí)時(shí)間范圍內(nèi)嘗試讀取結(jié)果
????//?n?毫秒超時(shí),則等待?n?-?reservedTime?毫秒,預(yù)留?reservedTime?毫秒時(shí)間響應(yīng)結(jié)果
????WxMpXmlOutMessage?wxMpXmlOutMessage?=?completableFuture.get(timeout?-?reservedTime,?TimeUnit.MILLISECONDS);
????if?(completableFuture.isDone())?{
????????//?已經(jīng)完成響應(yīng),刪除?replayInfo
????????REPLAY_CACHE.remove(messageId);
????????if?(replayCount?>?1)?{
????????????log.info("{}?消息第?{}?次重試請(qǐng)求成功處理!",?messageId,?replayCount);
????????}
????}
????//?判斷是否異常
????if?(replayInfo.getException()?!=?null)?{
????????return?exceptionCaught(wxMessage,?context,?wxMpService,?sessionManager,?replayInfo.getException());
????}
????//?返回結(jié)果
????return?wxMpXmlOutMessage;
}?catch?(TimeoutException?e)?{
????//?獲取結(jié)果超時(shí)
????//?判斷是否達(dá)到請(qǐng)求重試上限
????if?(replayCount?>=?maxReplayCount)?{
????????//?已經(jīng)達(dá)到上限,返回請(qǐng)求超
????????log.info("{}?消息到達(dá)重試上限?{}?次仍未處理完成,服務(wù)降級(jí)處理",?messageId,?replayCount);
????????return?buildTimeoutMessage(wxMessage,?context,?wxMpService,?sessionManager);
????}
????//?未到重試上限,sleep?2倍預(yù)留時(shí)間,讓這個(gè)請(qǐng)求也超時(shí)
????log.info("{}?消息第?{}?次請(qǐng)求超時(shí),等待請(qǐng)求重試",?messageId,?replayCount);
????try?{
????????Thread.sleep(reservedTime?*?2L);
????}?catch?(InterruptedException?ignored)?{
????}
????return?null;
}?catch?(InterruptedException?|?ExecutionException?e)?{
????return?exceptionCaught(wxMessage,?context,?wxMpService,?sessionManager,?new?WxErrorException(e));
}
AbstractReplayHandler關(guān)鍵邏輯大致如此,繼承了AbstractReplayHandler的處理器則可以將消息處理的有效時(shí)常延長(zhǎng)到 15 秒!
3. ChatMsgHandler 消息處理
處理器路由匹配規(guī)則
若當(dāng)前用戶已經(jīng)開始會(huì)話,則所有消息都需要進(jìn)行處理。否則只處理定義好的指令消息:/chatgpt、/clear、/stop 等
public?boolean?match(WxMpXmlMessage?wxMessage)?{
??//?首先判斷當(dāng)前用戶是否有開啟聊天會(huì)話
??String?chatSessionId?=?userSessionMappingStorage.getUserSessionId(wxMessage.getFromUser());
??if?(StringUtils.isNotBlank(chatSessionId))?{
????//?檢測(cè)會(huì)話是否有效
????String?sessionCacheKey?=?WxChatConstants.getSessionCacheKey(chatSessionId);
????boolean?exists?=?redissonClient.getBucket(sessionCacheKey).isExists();
????if?(exists)?{
??????return?true;
????}
??}
??String?content?=?wxMessage.getContent();
??if?(StringUtils.isBlank(content))?{
????return?false;
??}
??switch?(content.toLowerCase())?{
????case?CMD_START_SESSION:
????case?CMD_STOP_AND_NEW_SESSION:
????case?CMD_STOP_SESSION:
????case?CMD_SHOW_HISTORY:
??????return?true;
????default:
??????return?false;
??}
}
消息處理
考慮到文章長(zhǎng)度,就不挨個(gè)列舉指令和聊天消息的處理代碼了,有興趣可以找作者領(lǐng)取源碼!
public?WxMpXmlOutMessage?handleAsync(WxMpXmlMessage?wxMessage,?Map?context,?WxMpService?wxMpService,?WxSessionManager?sessionManager) ?throws?WxErrorException?{
??switch?(wxMessage.getContent().toLowerCase())?{
????case?CMD_START_SESSION:
??????return?handleStartSession(wxMessage,?context,?wxMpService,?sessionManager);
????case?CMD_STOP_AND_NEW_SESSION:
??????return?handleStopAndNewSession(wxMessage,?context,?wxMpService,?sessionManager);
????case?CMD_STOP_SESSION:
??????return?handleStopSession(wxMessage,?context,?wxMpService,?sessionManager);
????case?CMD_SHOW_HISTORY:
??????return?new?TextBuilder().build("點(diǎn)擊下方鏈接查看歷史會(huì)話:\n"?+?buildUserHistoryUrl(wxMessage),?wxMessage,?wxMpService);
????default:
??????return?handleChatMsg(wxMessage,?context,?wxMpService,?sessionManager);
??}
}
4. ChatGPT 接口調(diào)用
定義請(qǐng)求與響應(yīng)數(shù)據(jù)結(jié)構(gòu)
public?class?ChatCompletionsRequest?implements?Serializable?{
??/**?AI模型*/
??private?String?model;
??/**?會(huì)話消息*/
??private?List?messages;
}
public?class?ChatMessage?implements?Serializable?{
??/**?消息角色?use、assistant?*/
??private?String?role;
??/**?消息內(nèi)容*/
??private?String?content;
}
public?class?ChatCompletionsResult?implements?Serializable?{
??//?忽略部分字段……
??private?List?choices;
??public?static?class?ChoicesDTO?{
????private?ChatMessage?message;
??}
}
定義 FeignClient
@FeignClient(
????????name?=?"ChatCompletionsV1Api",
????????url?=?"${chat-gpt.base-url:https://api.openai.com}"
)
public?interface?ChatV1ApiClient?{
????String?BASE_URL?=?"/v1/chat";
????/**
?????*?ChatGTP
?????*
?????*?@param?request?req
?????*?@return?resp
?????*/
????@PostMapping(
??????value?=?BASE_URL?+?"/completions",
??????headers?=?{"Authorization=Bearer?${${chat-gpt.api-key:}"}
????)
????ChatCompletionsResult?completions(@RequestBody?ChatCompletionsRequest?request);
}
定義了ChatV1ApiClient之后變可以直接@Autowired來使用啦~
最后
簡(jiǎn)單記錄了下本人第一次開發(fā)微信公眾號(hào)與對(duì)接ChatGPT的過程,將遇到的問題以及解決思路、代碼分享給大家,如果大家還有更好的方法可以私信我,非常感謝~
還望大家?guī)臀尹c(diǎn)點(diǎn)“在看“或轉(zhuǎn)發(fā),您的舉手之勞是對(duì)我莫大的鼓勵(lì)。謝謝!

