前置条件
1.微信公众号申请
我们在上网冲浪的时候,去申请一些资源的时候,关注某个微信公众号,发送一串数字就会给一个解锁密码以供后续操作,这个环节需要配合 申请微信公众号。具体的微信公众号的申请请看微信公众号平台教程或者其他总结,在这里不多做赘述。
2.内网穿透
之所以会使用到内网穿透,是因为在配置微信公众号服务器的时候,需要公网的地址,如果你的服务器部署在自己电脑上的话,那么实际上微信公众号是访问不到你的机器的。所以也就是需要将你自己的服务器和公网进行一个应该,在这里推荐 NATAPP这个网站,可以使用免费的,也可以使用付费的,但是付费的还需要额外花几块钱买一个二级域名。
场景
近期我遇到了这么一个场景,我想将chatgpt的能力 和 微信公众号绑定,其实也就是我向一个微信公众号发送问题,那么 后台就会返回我chatgpt的答案,这样,就不需要翻墙,很方便,也是扩展chatgpt的使用场景了。
但是当真正开发完成的时候,发现,微信公众号有一个超时时间5s,也就是如果5s内没有回复就要重试请求,一共三次,如果三次都没有返回请求的话,那么 就会提示 微信公众号不可用。
而通过实践,chatgpt很大概率上不能在5s内完成回复。那么就没办法做到智能问答的效果。
解决思路
1.客服回复
通过查阅各种各样的资料,这个办法一直是大家广泛提起的,其实就是 使用客服回答 接口进行回答即可。但是我们所注册的微信公众号更多的都是个人微信公众号,没办法进行微信认证,所以不能使用客服接口,所以该方法可能对普通人不适用。
2.第一次提问后使用异步线程,然后再次提问获得回答
这个解决思路其实还挺简单的,原理也很简单,就是当发第一次请求的时候,内部启动一个异步线程先回复一个消息 :“请稍等几秒钟再次回复【原来的消息】”,将这个消息存储进一个map中,然后等待异步线程执行结束,将map存储的key中的value补充上,等到下次再次提问的时候,就直接从key里的value取即可。
3.在三次重试中,获取返回,用户只需要提问一次
本质上其实是一个具有超时重试机制的任务处理功能,具体看代码。
public class CallableCallback { private static final Logger logger = LoggerFactory.getLogger(CallableCallback.class); public static final ConcurrentHashMap<Long, Future<String>> map = new ConcurrentHashMap<>(); public static final AtomicInteger atomicCount = new AtomicInteger(); // GPT 任务线程池 public static final ThreadPoolExecutor executor = new ThreadPoolExecutor( 10, 10, 3, TimeUnit.MINUTES, new ArrayBlockingQueue<>(100), (runnable) -> new Thread(runnable, atomicCount.getAndAdd(1) + "号线程"), new ThreadPoolExecutor.AbortPolicy() ); public static void main(String[] args) throws InterruptedException { Long tid = 1L; new Thread(new MyTask(tid, "请求A"), "请求A").start(); Thread.sleep(5000); new Thread(new MyTask(tid, "重试A1"), "重试A1").start(); Thread.sleep(5000); new Thread(new MyTask(tid, "重试A2"), "重试A2").start(); Thread.sleep(5000); executor.shutdown(); } static class MyTask implements Runnable { private Long tid; private String name; /** * GPT 请求耗时 */ private Long gptTime; private TimeUnit gptTimeUnit; /** * 接口超时 */ private final Long outTime = 4800L; private final TimeUnit outTimeUnit = TimeUnit.MILLISECONDS; public MyTask(Long tid, String name) { this.tid = tid; this.name = name; this.gptTime = 12L; this.gptTimeUnit = TimeUnit.SECONDS; } public MyTask(Long tid, String name, Long gptTime, TimeUnit gptTimeUnit) { this.tid = tid; this.name = name; this.gptTime = gptTime; this.gptTimeUnit = gptTimeUnit; } @Override public void run() { long start = System.currentTimeMillis(); Future<String> future = map.get(tid); String res; if (future == null) { // 模拟 GPT 耗时接口 logger.info("<-----线程:{} 开始执行完gpt任务----->", name); future = executor.submit(() -> testGpt(gptTime, gptTimeUnit)); try { res = future.get(outTime, outTimeUnit); long end = System.currentTimeMillis(); logger.info("线程:{} 执行完gpt任务,任务成功返回,耗时:{} 毫秒,结果:{}", name, end - start, res); } catch (InterruptedException | ExecutionException e) { } catch (TimeoutException e) { logger.info("线程:{} 执行完gpt任务,耗时5秒,任务超时", name); // TODO 超时了,任务添加到map,直接返回,不管了,让后面的请求重试 map.put(tid, future); } return; } // 这是重试请求,直接从 Future 获取第一次请求的结果 try { logger.info("线程:{} 开始尝试从 Future 获取第一次请求的结果,任务结果", name); res = future.get(outTime, outTimeUnit); logger.info("线程:{} 在规定时间内从 Future 获取第一次请求的结果:{}", name, res); // 移除阻塞队列 map.remove(tid); } catch (InterruptedException | ExecutionException e) { } catch (TimeoutException e) { // 获取失败 logger.info("线程:{} 获取第一次请求的结果,超时", name); } } /** * 模拟Gpt接口,耗时指定时间 */ private String testGpt(final long time, final TimeUnit timeUnit) { try { timeUnit.sleep(time); } catch (InterruptedException e) { } return "gpt接口返回"; } } }
限制
其实主要是针对第二种 和 第三种方法的限制的,一般来说 chatgpt接口都能在 15s内返回,或者其他任务也是一样的,如果能保证返回,那么就可以使用第三种办法,如果不能保证,还是乖乖使用第二种办法来解决问题。