Deepseek接入微信公众号简单实践
最近在经营养斗鱼的事情,感觉和鱼友交流的压力有点繁重。
这不巧了吗,有Deepseek(下文简称DS)这个好东西。于是就萌生搞个带AI顾问的公众号的想法。
https://www.bilibili.com/video/BV1fjNmeNEXS
其实技术上的实现难度几乎没有,手搓代码可能也就1、2小时能完成。但我还是选择了相信DS,口述了我的需求,让它来完成代码的部分。结果而言完成的很好。以下是流水账和遇到的一些问题和解决思路。
准备
- 需要一个DS的 APIKey, 我是上官网开的,第三方现在很多选择了,只要是满血的671b模型的表现应该都是一致的。
- 上 https://mp.weixin.qq.com/ 注册一个公众号(以前叫订阅号)。注册为个人号,要求的门槛最低,但功能也是最少限制最多的。这引致了后面的一点小问题。
- 自备一个服务器(可以是云主机、虚拟主机、云应用),有已备案域名,能跑 PHP/MySQL 就行。
思路
部署一个常规的微信公众号响应脚本(用户发消息,接收后被动回复),调用SD 的 API 生成回复内容。
实施
公众号的注册和开发配置过程略过,按照微信文档帮助处理即可。验证开发设置URL直接用微信提供的PHP demo配置上自己的 appId appKey即可
响应公众号的php脚本:
<?php
/**
* wechat php
*
* 公众号的响应脚本
* 所在路径
*
*/
//no cache
header('Cache-Control:no-cache,must-revalidate');
header('Pragma:no-cache');
//define your token
define("TOKEN", "your_token");
$wechatObj = new wechatCallbackapiTest();
if(isset($_GET["echostr"])){
$wechatObj->valid();
}else{
$wechatObj->responseMsg();
}
class wechatCallbackapiTest
{
private $err = "init";
public function error(){
return $this->err;
}
public function valid()
{
$echoStr = $_GET["echostr"];
//验证微信响应脚本URL
//valid signature , option
if($this->checkSignature()){
echo $echoStr;
exit;
}
}
public function responseMsg()
{
$postStr = file_get_contents('php://input', 'r');
//extract post data
if (!empty($postStr)){
$postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
$msgType = $postObj->MsgType;
$event = $postObj->Event;
$fromUsername = $postObj->FromUserName;
$toUsername = $postObj->ToUserName;
$keyword = trim($postObj->Content); //只有text类型消息才会有Content
$time = time();
$textTpl = "<xml>
<ToUserName><![CDATA[%s]]></ToUserName>
<FromUserName><![CDATA[%s]]></FromUserName>
<CreateTime>%s</CreateTime>
<MsgType><![CDATA[%s]]></MsgType>
<Content><![CDATA[%s]]></Content>
<FuncFlag>0</FuncFlag>
</xml>";
if(!empty( $keyword )) //关键词答复
{
$key = strtolower($keyword);
switch($key){
case 'hi':
die($this->xmlText($fromUsername,$toUsername,MSG_WELCOME));
break;
case 'tips':
case '提示':
$msg = "欢迎,这里是斗鱼繁育小助手(已接入Deepseek),可以查询饲养斗鱼的常见问题。
输入【提示】,显示本消息。你还可以尝试以下其他关键词或直接提问相关的问题。
【新鱼到家】【养水】【混养】【过冬】【常见品种】【常见疾病】【如何繁殖】【鱼苗开口】
";
die($this->xmlText($fromUsername,$toUsername,$msg));
break;
case '新鱼到家': //普通的关键词词典命中直接返回预设答案
$msg = "小鱼刚到家时,由于路途颠簸和不熟悉新环境及温度,可能会出现颜色变浅的现象,这是正常情况,通常适应1天后颜色会逐渐恢复。建议过温过水再入缸。为了帮助小鱼缓解应激,建议在到家第一天不要投喂,并将其放置在安静、光线柔和或偏暗的环境中。";
die($this->xmlText($fromUsername,$toUsername,$msg));
break;
default: //其他问题 交给 AI尝试回答
//注意事项,由于AI响应较慢,微信要求5秒内回应,不回应则则重试两次。
//1.检查db是否已经具有此答案
$s = $this->db();
$sql = "SELECT `reply`,`createtime` FROM `chatlog` WHERE `ask` = '".$s->escape($keyword)."' ORDER BY `id` DESC LIMIT 1";
$rs = $s->getLine($sql);
if($rs){
//超过一个小时还未得到答案
if(!$rs["reply"] && time() > (strtotime($rs["createtime"]) + 3600)){
// 定期清理超过一定时间未回答的记录
$sql = "DELETE FROM `chatlog` WHERE `reply` = '' AND `createtime` < DATE_SUB(NOW(), INTERVAL 1 HOUR)";
$s->runSql($sql);
unset($rs);
}
}
if($rs){
//可能上一个请求还在处理,尚未有答案,轮询等待
$waitcount = 0;
while($waitcount < 3 && !$rs["reply"]){
$waitcount++;
sleep(1);
$rs = $s->getLine($sql);
}
if($rs["reply"]){ //db 已有答案
$feedback = $rs["reply"];
die($this->xmlText($fromUsername,$toUsername, $feedback));
}else{ //本轮查询还未得到答案,放弃回答
die($this->xmlText($fromUsername,$toUsername, "AI仍在思考中,1分钟后再复制黏贴问这个问题试试。"));
}
}else{ //没有这个问题
// 新问题,记录到数据库(初始无回答)
$sql = "INSERT INTO `chatlog`(`ask`, `reply`) VALUES (
'".$s->escape($keyword)."',
''
)";
$s->runSql($sql);
$qaid = $s->lastId();
$start_time = microtime(true); // 记录开始时间
$feedback = $this->ask_deepseek($keyword); // 发起API请求
$elapsed_time = microtime(true) - $start_time; // 计算耗时
//大概率首次回答会被微信后台抛弃重试,只需要专注于更新db
if($feedback == '0'){
$feedback = "问题【{$keyword}】似乎与斗鱼饲养繁殖无关,其它问题推荐到微信或抖音粉丝群与其他网友交流。";
}elseif(strstr($feedback,"err:")){ //接口错误
//写入日志
file_put_contents("api.log", "\n[API ERROR] ".feedback, FILE_APPEND);
}else{} //无异常
//写库
// 更新数据库中的回答
$sql = "UPDATE `chatlog` SET `reply` = '".$s->escape($feedback)."' WHERE `id` = {$qaid} LIMIT 1;";
$s->runSql($sql);
die($this->xmlText($fromUsername,$toUsername, $feedback));
}
break;
}
}else{
//处理关注事件
if($event=="subscribe"){ //关注
$msg = "欢迎,这里是斗鱼繁育小助手(已接入Deepseek),可以查询饲养斗鱼的常见问题。
输入【提示】,显示本消息。你还可以尝试以下其他关键词或直接提问相关的问题。
【新鱼到家】【养水】【混养】【过冬】【常见品种】【常见疾病】【如何繁殖】【鱼苗开口】";
die($this->xmlText($fromUsername,$toUsername,$msg));
}else{ //debug
die($this->xmlText($fromUsername,$toUsername,"event push:".$event."\n".$postStr)); //尝试原样返回为支持的时间raw信息
}
echo "";
}
}else {
echo "well? empty request";
exit;
}
}
private function checkSignature()
{
$signature = $_GET["signature"];
$timestamp = $_GET["timestamp"];
$nonce = $_GET["nonce"];
$token = TOKEN;
$tmpArr = array($token, $timestamp, $nonce);
sort($tmpArr,SORT_STRING);
$tmpStr = implode( $tmpArr );
$tmpStr = sha1( $tmpStr );
//debug
file_put_contents('wx.access.log',date("\nY-m-d H:i:s - ")."(_GET)".print_r($_GET,true),FILE_APPEND);
file_put_contents('wx.access.log',date("\nY-m-d H:i:s - ")."(checkSignature){$tmpStr}:{$signature}",FILE_APPEND);
if( $tmpStr == $signature ){
return true;
}else{
return false;
}
}
/**
* wechatCallbackapiTest::xmlText()
* 组织文本消息 xml
* @param String $fromUsername
* @param String $toUsername
* @param String $contentStr
* @return String $xml
*/
private function xmlText($fromUsername,$toUsername,$contentStr){
$time = time();
$textTpl = "<xml>
<ToUserName><![CDATA[%s]]></ToUserName>
<FromUserName><![CDATA[%s]]></FromUserName>
<CreateTime>%s</CreateTime>
<MsgType><![CDATA[%s]]></MsgType>
<Content><![CDATA[%s]]></Content>
<FuncFlag>0</FuncFlag>
</xml>";
$msgType = "text";
$resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
return $resultStr;
}
private function ask_deepseek($question) {
$api_key = 'sk-{你的deepseek API Keys}'; // 替换为你的API密钥
$api_url = 'https://api.deepseek.com/chat/completions'; // 确认实际API地址
// 系统角色设定(关键部分)
$system_message = <<<PROMPT
你是一个专门解答泰国斗鱼养殖问题的百科全书AI助手,包括以下范畴:
1. 泰国斗鱼的饲养方法
2. 鱼缸设备与水质管理
3. 鱼类疾病防治
4. 繁殖技术与品种改良
5. 水族造景与生态系统维护
请严格遵守以下规则:
1. 如果问题超出以上范畴,直接返回数字0,不要任何解释或附加内容。
2. 即使问题涉及其他水生生物(如龟类、水草)但无关鱼类养殖也返回0。
3. **回答内容必须为纯文本格式,禁止使用任何Markdown标签(如 ```、**、*、# 等)**。
4. 确保回答内容简洁、清晰,适合直接显示在文本消息中。
重要提示:回答内容必须为纯文本格式,禁止使用任何形式的 Markdown 标签或特殊符号。
PROMPT;
// 构造请求数据
$data = [
'model' => 'deepseek-chat', // 确认实际模型名称
'messages' => [
['role' => 'system', 'content' => $system_message],
['role' => 'user', 'content' => $question]
],
'max_tokens' => 2000,
'temperature' => 0.3
];
// 发送API请求
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $api_url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $api_key
],
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_TIMEOUT => 45, // 设置总超时时间为45秒
CURLOPT_CONNECTTIMEOUT => 10 // 设置连接超时时间为10秒
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
$error_msg = curl_error($ch);
curl_close($ch);
return 'err:API请求错误: ' . $error_msg;
}
curl_close($ch);
// 解析响应
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return 'err:API响应解析错误';
}
//$answer = $result['choices'][0]['message']['content'] ?? '';//?
//debug
if (isset($result['choices'][0]['message']['content'])) {
$answer = $result['choices'][0]['message']['content'];
} else {
file_put_contents("api.log", "\n[STRUCTURE ERROR] ".print_r($result,true), FILE_APPEND);
}
// 严格检测0响应
//手动清除markdown标签
$patterns = [
'/\*\*(.*?)\*\*/' => '$1', // 加粗
'/\*(.*?)\*/' => '$1', // 斜体
'/`(.*?)`/' => '$1', // 代码块
'/#+\s*/' => '', // 标题
'/\[(.*?)\]\(.*?\)/' => '$1', // 链接
];
$answer = preg_replace(array_keys($patterns), array_values($patterns), $answer);
return (trim($answer) === '0') ? '0' : $answer;
}
//数据库连接
private function db(){
if(isset($this->s)){
return $this->s;
}else{
include_once("./fmysql.class.php");
$this->s = new FMYSQL();
return $this->s;
}
}
}
?>
代码有点长,而且上线调试的时候遭遇了一点问题进行了调整优化,我们拆解来看
private function ask_deepseek($question)
是调用DS API的用法,它会在上文微信用户发来的消息没有匹配到静态答案的时候触发。
遇到的问题&优化
最初的代码没有这么复杂,但遇到了一个矛盾:
微信平台要求响程序本必须 5秒内响应,如果5秒未能输出结果,会主动断开连接然后再次重试,最多重试3次。
但deepseek在大部分情况下是需要一些时间思考后才能输出答案的。实测下来通常这个时间需要10-30秒左右。
按照正常的思路去解决这个问题,应该是做异步处理。用另一个微信公众号接口:客服消息。这个接口允许在用户向公众号提问的48小时内,程序主动向用户发消息,但是!这个接口要求公众号进行企业认证不对个人号开放,堵死了这个方向的解决途径。那么迂回地解决这个问题,重新思考,利用响应机制的5秒重试。来实现异步处理。
同时,使用数据库MySQL 建一个表存储AI对话的问题和答案,作用有三。一方面是作为问题的回答进度查询,另一方面是缓存答案,避免微信端重试的时候反复查询同一个问题,节省Token消耗,最后一方面是记录历史的问题和答案以便跟踪调优AI的调用。
-- 表的结构 `chatlog`
--
CREATE TABLE `chatlog` (
`id` int(11) NOT NULL,
`ask` varchar(100) NOT NULL,
`reply` text NOT NULL,
`createtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- Indexes for table `chatlog`
--
ALTER TABLE `chatlog`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `ask` (`ask`);
--
-- 使用表AUTO_INCREMENT `chatlog`
--
ALTER TABLE `chatlog`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;
上文代码引用了一个fmysql.class的数据库类简化代码,没有的话自己替换成常规mysql处理语句替代 $s->getLine()
和 $s->runSql()
即可
这个变通的异步机制如下。
用户在微信公众号向AI提问 =>
微信服务器首次请求 => 触发 ask_deepseek() 调用API向AI求助,并开始计时。
=> 此时先把问题写入数据库。
=> AI思考需要时间,这个时间大概率会超过5秒。链接被微信后台断开,但程序会继续执行把得到的答案写入数据库。
微信服务器第二次 => 查询数据库,发现这个问题已经存在。如果 reply
为空,表示AI还未返回答案,隔1秒查询一次,5秒内还未得到答案,响应微信,告知用户:AI还在思考;
当用户1分钟后再次提问相同问题或微信服务器再次查询时已有答案,则把答案返回给用户。
感想
在AI普及的未来,你不一定要掌握具体的技巧和经验,但一定要对这个世界保持谦卑、好奇确保脑子一直转动有用不完的想法。
附带的彩蛋
让AI又写了个直接调用查看AI对话历史的页面
https://2.ruoyuzhilian.site/douyubot/
相关链接
