在大陆地区使用GeminiPro搭建生成式聊天机器人

本文仅作技术方案记录 两周前偶然发现了一个基于GeminiPro接口的开源项目,可以一键搭建聊天机器人,于是准备尝试一下,并顺便找到一种可以直接通过国内网络访问的方式. 方案一:使用 Google Gemini API + Gemini Pro Chat + Vercel 部署聊天机器人 优点: 部署简单,易于使用。 Vercel 提供了开箱即用的解决方案,降低了开发难度。 缺点: 由于中国大陆的 GFW,无法通过在大陆直连的方式访问聊天机器人。 Vercel 的服务器位于海外,可能会导致访问速度较慢。 方案二:在访问域名前加上 Cloudflare 的 DNS 解析和 CDN 分发 优点: 绕过了 GFW,用户可以访问到部署在 Vercel 的页面。 Cloudflare 提供了全球化的 CDN 服务,可以提高访问速度。 缺点: 由于 Google 本身对于访问来源 IP 的限制,当用户访问 API 的时候,仍然会被 API 检测到来源 IP 是来自大陆,因此,这种方案虽然能让用户访问到部署在 Vercel 的页面,但是仍然无法使用 Gemini API。 方案三:将服务部署到个人美国 VPS 上面,再通过 VPS 上面现有的 Caddy 做了反向代理 优点: 隐藏了来源 IP,使 Gemini API 认为用户来自美国,因此实现了在中国大陆正常使用 Gemini API。 使用 Caddy 做反向代理,配置简单,易于使用。 缺点: ...

January 24, 2024 · 1 min · 141 words

ChatGPT初体验

在ChatGPT开放之初,注册了账号并体验了免费版的ChatGPT,总体感受还是比较震撼。给我留下最深印象的是它可以结合上下文和人类进行真正意义上的聊天。 最近开始尝试在Discord上玩各种基于ChatGPT的机器人,Midjourney Bot,ChatGPT Bot之类的。 今天在跟ChatGPT交互的时候发现model有一个选项是DAN,之前从未使用过,于是切换到DAN开始了体验。 以下是个人对ChatGPT默认model和DAN model的特点总结。 **AI(dan):**[GPT]: 始终保持客观,中立。 [DAN]: 根据问题的类型可能出现各种风格的回答,正如它的全称一样:“Do Anything Now”。 问题一 How to make a perfect Cappuccino? 相对中立,无立场的问题,DAN的回答保持了理性,客观。 ChatGPT的回答: To make a perfect cappuccino, you’ll need to start with a shot of espresso, steamed milk, and frothed milk. Begin by pulling a shot of espresso and then steaming the milk to a creamy consistency. Next, use the frother to create a thick layer of frothed milk, and then pour it on top of the steamed milk and espresso. The result should be a balanced and delicious cappuccino with a velvety texture and perfect foam. ...

February 20, 2023 · 3 min · 567 words

一些好的编程习惯

好习惯为何重要? 如果把编程比作一个游戏,一定是一个需要长期投入大量时间,精力的游戏。因为一个优秀的程序员不仅仅意味着掌握多项编程技术或技巧,更重要的是具备可以持续让自己的职业生涯受益的好习惯。 1. 身体的劳逸结合 身体是最重要的财产。保持良好的坐姿,半小时一次站立,走动或喝水。有条件的情况最好能每天进行有氧运动来保持良好的身体状态。 编程是一项脑力运动,但同样需要良好的身体作支撑。 2. 代码的简洁易读 不要过于追求代码设计模式的套用或是所谓的优雅,建议以结果为导向(result oriented)。不应该为了炫耀自己的代码多么优雅而写一些对于人类阅读不友好的骚操作。 代码的高可读性在CR,DEBUG和REFACTORING的时候非常重要。如果线上出现紧急故障,需要快速找出问题所在,可读性越高的代码将会在此过程中节省大量时间。 Keep it as simple, as consistent as possible! 3. 一段时间的持续专注 编程需要良好的专注力,每天可以尝试3-4小时的持续专注时间,在这段时间里不被周围环境打扰,工作可以变得非常高效。比如可以戴上降噪耳机或是告知周围的人让他们知道自己正进入一个专注的工作时间。 4. 跳出当下的舒适圈 就像我在16年的时候,当时每天在小型外包公司做着大量重复的工作:微信公众号网页开发,TP框架,CMS模板,前端的Jquery,数据库的CRUD,开发,生产环境的部署(当时也不知容器化为何物😓)。但那个状态对于当时的我来说也足够很轻松地应对每一天的工作,也大可不必学习新的技术(docker, cd/ci, 前端的spa相关)。但如果持续下去,我可能会丧失对新技术的兴趣,并且在未来1-2年内被完全淘汰(可能连小型外包公司都会嫌弃)。后来我主动跳出舒适圈,在持续的自学提升下找到了更好的工作机会。 5. 团队协作 开发是一个团队协作的过程,一个人独立完成所有工作并不是最高效的方式。在合适的时候与团队成员沟通或向他们请教可以很大程度提高效率,但注意:是在合适的时候。比如我遇到了一个问题,我通常应该在尝试过几种解决方式之后在去向同事请教,而不是直接把问题丢给同事。但也不能在遇到一个严重阻塞性问题的时候一直拖到任务将被delay的时候再提出来,应该把握好提出问题的时机。 6. 编程是"孤独"的 “孤独"是相对于其他职业而言的,因为每天需要对着电脑屏幕坐很长时间。作为程序员,我们应该适应这样的生活方式。 7. 具备快速学习的能力 通常来讲,技术的更迭速度是远快于我们的学习速度的。对于我们而言,能够用于学习的时间其实很有限(我们需要完成工作任务,照顾家庭,抽时间休息等等)。因此很多时候的学习其实是被动的:当一个需求/项目即将到来的时候,可能需要根据这个需求/项目中包含的技术栈去学习相应的自己尚未掌握的技能。这就需要我们具备高效的学习能力,掌握这种学习能力比学习某种技术/语言/框架更重要。 8. 更多关注项目的价值 技术(语言/框架等等)只是为需求服务的工具,再好的工具也需要合理的应用才能实现其价值。 举个🌰:不论用多少时间来学习了PHP或是Golang,在简历上通常也只是一句掌握PHP或是Golang。假如此时正在参加一场面试,掌握某种技术可能对面试官来讲只是大多候选人都应该具备的一个能力,但这可能并不够出众。而如果可以告诉面试官自己曾经通过某种技术在项目上的应用解决了某个难题或创造了多少额外收入,那么这个经历一定会是简历中的亮点。 Above Are Inspired By 7 Habits of Highly Effective Programmers (ft. ex-Google TechLead) Things I wish I knew when I started Programming

July 18, 2021 · 1 min · 62 words

Golang+Redis分布式可重入锁

概念 计算机科学中,可重入互斥锁(英語:reentrant mutex)是互斥锁的一种,同一线程对其多次加锁不会产生死锁。可重入互斥锁也称递归互斥锁(英語:recursive mutex)或递归锁(英語:recursive lock)。 如果对已经上锁的普通互斥锁进行「加锁」操作,其结果要么失败,要么会阻塞至解锁。而如果换作可重入互斥锁,当且仅当尝试加锁的线程就是持有该锁的线程时,类似的加锁操作就会成功。可重入互斥锁一般都会记录被加锁的次数,只有执行相同次数的解锁操作才会真正解锁。 递归互斥锁解决了普通互斥锁不可重入的问题:如果函数先持有锁,然后执行回调,但回调的内容是调用它自己,就会产生死锁。 参考维基百科:可重入互斥锁 个人观点 在Go中应该很少会有这样的场景,互斥锁从字面上理解,应该不能接收重入,需要重入的场景也不应该考虑互斥锁。个人认为更好的解决方法是从设计的层面避免这种场景的出现。因此,与基于redis的互斥锁不同,这篇文章仅仅是尝试在技术上的实现,在实际应用中应尽可能避免这样的场景出现 参考 功能 在基于redis的互斥锁(自动续期,自动重试)的基础上允许重入 实现的关键功能点: 加锁:同一线程多次加锁时可以通过某个标识识别该线程为当前持有锁的线程,并且加锁次数+1 解锁:解锁时加锁次数-1,直到次数为0,则可以解锁(DEL) hash锁的结构 Thread KEY FIELD VALUE A EXAMPLE_LOCK 304597349587439(线程对应的随机数,标识锁,防止误解锁) 1(当前线程已加锁次数) 基本流程 在不可重入锁的实现里,只需要关心锁的互斥,误解除和自动续期,因此可以直接使用string类型配合SETNX,PEXPIRE,DEL完成加锁,解锁和续期 但可重入锁需要锁可以记录当前线程的标识和当前线程已加锁次数,就需要用redis的hash代替string。因为结构发生了变化,所以在加锁,解锁流程上也会有相应改变 Time ThreadA ThreadB T1 尝试加锁 尝试加锁 T2 加锁成功(key:EXAMPLE_LOCK,field:304597349587439,value:1) 加锁失败 T3 执行当前方法业务代码 尝试重试加锁并等待ThreadA解锁(根据配置间隔和最大重试次数) T4 执行另一个方法业务代码,也可能是递归调用,并再次尝试加锁 T5 加锁成功(key:EXAMPLE_LOCK,field:304597349587439,value:2) T6 执行新的调用方法内的业务代码,直到完成所有嵌套调用 T7 从最里层调用开始解锁,(key:EXAMPLE_LOCK,field:304597349587439,value:1) T8 返回到最外层第一次加锁的位置,解锁(key:EXAMPLE_LOCK,field:304597349587439,value:0) T9 如果当前已加锁次数为0,释放锁 T10 加锁成功 FF 加锁: -- KEYS[1]:锁对应的key -- ARGV[1]:锁的expire -- ARGV[2]:锁对应的计数器field(随机值,防止误解锁),记录当前线程已加锁的次数 -- 判断锁是否空闲 if (redis.call('EXISTS', KEYS[1]) == 0) then -- 线程首次加锁(锁的初始化,值和过期时间) redis.call('HINCRBY', KEYS[1], ARGV[2], 1); redis.call('PEXPIRE', KEYS[1], ARGV[1]); return 1; end; -- 判断当前线程是否持有锁(锁被某个线程持有,通常是程序第N次(N>1)在线程内调用时会执行到此处) if (redis.call('HEXISTS', KEYS[1], ARGV[2]) == 1) then -- 调用次数递增 redis.call('HINCRBY', KEYS[1], ARGV[2], 1); -- 不处理续期,通过守护线程续期 return 1; end; -- 锁被其他线程占用,加锁失败 return 0; 解锁: ...

May 3, 2021 · 4 min · 852 words

Golang+Redis分布式互斥锁

引言 假设我们的某个业务会涉及数据更新,同时在实际场景中有较大并发量。流程:读取->修改->保存,在不考虑基于DB层的并发处理情况下,这种场景可能对部分数据造成不可预期的执行结果,此时可以考虑使用分布式锁来解决该问题 需要解决的问题 锁的误解除 业务执行超时导致并发 重试机制 GET和DEL非原子性 代码 目录结构: │ main.go │ └─demo lock.go lock.go: package demo import ( "context" "fmt" "github.com/go-redis/redis/v8" "math/rand" "time" ) // 重试次数 var retryTimes = 5 // 重试频率 var retryInterval = time.Millisecond * 50 var rdb = redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password set DB: 0, // use default DB }) // 锁的默认过期时间 var expiration time.Duration // 模拟分布式业务加锁场景 func MockTest(tag string) { var ctx, cancel = context.WithCancel(context.Background()) defer func() { // 停止goroutine cancel() }() // 随机value lockV := getRandValue() lockK := "EXAMPLE_LOCK" // 默认过期时间 expiration = time.Millisecond * 200 fmt.Println(tag + "尝试加锁") set, err := rdb.SetNX(ctx, lockK, lockV, expiration).Result() if err != nil { panic(err.Error()) } // 加锁失败,重试 if set == false && retry(ctx, rdb, lockK, lockV, expiration, tag) == false { fmt.Println(tag + " server unavailable, try again later") return } fmt.Println(tag + "成功加锁") // 加锁成功,新增守护线程 go watchDog(ctx, rdb, lockK, expiration, tag) // 处理业务(通过随机时间延迟模拟) fmt.Println(tag + "等待业务处理完成...") time.Sleep(getRandDuration()) // 业务处理完成 // 释放锁 val := delByKeyWhenValueEquals(ctx, rdb, lockK, lockV) fmt.Println(tag+"释放结果:", val) } // 释放锁 func delByKeyWhenValueEquals(ctx context.Context, rdb *redis.Client, key string, value interface{}) bool { lua := ` -- 如果当前值与锁值一致,删除key if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end ` scriptKeys := []string{key} val, err := rdb.Eval(ctx, lua, scriptKeys, value).Result() if err != nil { panic(err.Error()) } return val == int64(1) } // 生成随机时间 func getRandDuration() time.Duration { rand.Seed(time.Now().UnixNano()) min := 50 max := 100 return time.Duration(rand.Intn(max-min)+min) * time.Millisecond } // 生成随机值 func getRandValue() int { rand.Seed(time.Now().UnixNano()) return rand.Int() } // 守护线程 func watchDog(ctx context.Context, rdb *redis.Client, key string, expiration time.Duration, tag string) { for { select { // 业务完成 case <-ctx.Done(): fmt.Printf("%s任务完成,关闭%s的自动续期\n", tag, key) return // 业务未完成 default: // 自动续期 rdb.PExpire(ctx, key, expiration) // 继续等待 time.Sleep(expiration / 2) } } } // 重试 func retry(ctx context.Context, rdb *redis.Client, key string, value interface{}, expiration time.Duration, tag string) bool { i := 1 for i <= retryTimes { fmt.Printf(tag+"第%d次尝试加锁中...\n", i) set, err := rdb.SetNX(ctx, key, value, expiration).Result() if err != nil { panic(err.Error()) } if set == true { return true } time.Sleep(retryInterval) i++ } return false } 流程说明 假设MockTest方法就是业务处理方法 ...

May 2, 2021 · 3 min · 440 words

laravel切换到swoole问题总结

环境 名称 版本 PHP 7.4.9 Swoole 4.5.2 LaravelS(目前项目用的这个工具) 3.7.8 Laravel Framework [local] 7.26.1 因为切换到swoole之后的问题数量非常多,因此以下简单按问题类型来记录 第一类问题:静态变量 如果一个静态变量参与了.=,+=,*=,/=,-=类似的运算就需要格外小心了,如果处理不当,它的值会不断变得不可预期,应谨慎使用 第二类问题:常量 在某个流程处理完成后,发送event事件通知其他listener前初始化了一个常量 !defined('CONSTANT_NAME') && define('CONSTANT_NAME', 'value'); 用于后续listener(此处是同步的listener)可以直接获取这个常量的值,这在fpm模式下完全正常,因为每次请求完成都会释放资源 在swoole模式下会导致第一次请求之后的其他请求无法重新初始化这个常量,因此需要谨慎使用这种写法 参考解决方法:可通过类成员属性或实时取值的方式代替 调试总结 调试过程中遇到时有时无的问题,先把swoole的dispatch_mode设置为4(ip_hash),保证每次调试请求分配到同一个worker上,方便复现问题 laravel框架切换到swoole之后遇到的常见问题大多是单例引起的(也需要重点检查构造方法,静态变量,全局常量),单例问题可通过每次请求重新注册单例解决;如果无法通过全局配置批量处理单例问题,可使用new代替laravel的App::make,并在使用完实例后及时unset掉

August 28, 2020 · 1 min · 28 words

我的2019

工作经历 经历前公司的倒闭和入职现在的公司, 在新公司负责C端的API开发,熟悉了不同场景下基于RPC,MQ的异步模式 通过大半年的工作积累,自己的业务理解能力比以往有所提高 折腾树莓派 把吃灰一年的树莓派3B重新拿出来玩,尝试过ubuntu server 19(preinstall version),ubuntu mate等发行版,最终还是用回raspbian版本,硬件驱动方面最省心. 树莓派上装了proxy软件,dnsmasq,ipset等工具用于协助实现透明网关(绕开dns污染,配合GFWList实现动态的域名访问proxy,配合脚本实现指定时间段指定域名列表的限制访问等);N个定时任务和N个@reboot任务用于实现如开机后自动播报本地内网IP,定时播报空气质量/天气预报,定时给儿子讲故事等功能;安装retropie用于模拟nintendo,mame,psp等平台游戏 学英语 做开发的时间越久越会发现这个技能是必不可少的,暂不说移民或外企,即便是网上Google一下日常技术问题,也会发现很多答案都是英文的,再加之目前的人工智障自动翻译水平有限,因此开始从下半年每天坚持学习英语,买了一款叫开言英语的App年会员,目前为止感觉还是有所提升 写在最后 2020年将继续以技术积累为主,同时坚持英语的学习

January 18, 2020 · 1 min · 15 words

MYSQL一致性非锁定读和锁定读

前言 本文测试和讨论的前提是事务隔离级别为REPEATABLE READ(默认)且存储引擎为InnoDB的场景 测试表结构 CREATE TABLE `members` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 Consistent Nonlocking Reads(一致性非锁定读,快照读) 官方定义 常见场景 使用SELECT查询 time transaction1 transaction2 T1 BEGIN BEGIN T2 SELECT * FROM members T3 INSERT INTO members (name) VALUES (‘demo’) T4 COMMIT T5 SELECT * FROM members 以上示例中T2和T5读取数据一致,原因是T5读取的是当前事务开启后T2创建的快照(事务执行过程中的第一次读取) time transaction1 transaction2 T1 BEGIN BEGIN T2 INSERT INTO members (name) VALUES (‘demo’) T3 COMMIT T4 SELECT * FROM members 以上示例中T4能够读取到transaction2的已提交数据,因为T4是事务中第一次读取,此时才会创建快照,由于transaction2数据在T3时间已提交,早于T4,因此T4可读取到 ...

December 18, 2019 · 1 min · 192 words

使用redis解决并发引起的业务问题

上周线上项目遇到了一点问题,处理完成后,在这做个小结 相关功能 后台可通过配置发起拼团, 每个拼团可指定同一个用户的最大参与次数 遇到的问题 由于并发, 导致了代码在有前置判断用户拼团是否超限的前提下, 仍然可能出现拼团次数超限的情况 数据表结构 -- ---------------------------- -- Table structure for logs -- ---------------------------- DROP TABLE IF EXISTS `logs` ; CREATE TABLE `logs` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'pk', `act_id` int(6) unsigned NOT NULL DEFAULT '0' COMMENT '', `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '', `pay_amount` int(6) unsigned NOT NULL DEFAULT '0' COMMENT '', `cat` tinyint(1) unsigned NOT NULL DEFAULT '2' COMMENT '', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '', `form_id` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '', utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '', PRIMARY KEY ( `id` ) USING BTREE, KEY `idx_userId` ( `user_id` ) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户拼团记录'; 原有代码 // 开始处理请求 // 判断用户购买次数是否超限(通过 `act_id` && `user_id` )(当前N个并发请求同时通过验证,N>限制次数-用户已拼团次数) // 开启事务 // 写入用户请求日志 // 提交事务(已写入N条数据,N>限制次数-用户已拼团次数) // 请求处理完成 以上代码无法避免重复写入超过限制次数的用户拼团记录 ...

November 17, 2019 · 2 min · 350 words

简单记录下最近半年的变化

入职目前的公司已有半年,逐渐适应了新的业务和协作开发模式 今年的主要收获在于相对复杂业务需求的代码实现,开发过程中比以往更多的并发场景的处理,比以往更合理的代码封装 前段时间因为个人状态不是很好,因此暂停了更新,现在多数事情基本已经理顺,恢复博客更新

November 7, 2019 · 1 min · 3 words