关于这篇稿子里的 AI,我没打算含糊。 骨架是我把几天日志和口述捋出来的,AI 帮着整理成结构;句子被它改短过,数字和日志被它逐条比对过,查 API 和库用法它出过力。(用的模型:
deepseek-v4.1-flash+ 若干辅助工具。)但内核不是它给的:「限制 AI 说话的能力,比让它会说话的能力重要十倍」这个结论,是我自己在真机运行里撞出来的。229 条群聊对 9 条私聊、51 次主动沉默、445 项测试全绿,都是我自己机器上跑出来的原始数字。五个事故我真踩过,七条设计我真改过。
事实和引文我逐条核过一遍,AI 说错的、编的地方都删了;写歪的地方算我的。
脱敏说明 :文中所有群友一律用「群友X」这类代号(群友C、群友E...)。真实昵称与聊天原文不在本文出现------这跟项目本身的做法一致(真昵称只落本地,发给模型的一律是代号)。
第一部分:为什么「能回话」不是难点
为什么非得是微信
如果你做过 Slack 机器人、Discord 机器人,它们都有一条共同的路:官方开放接口、Webhook 回调、标准事件格式。
微信没有。
但这不只是「微信没接口」这么简单------更准确的说法是:官方给机器人的口子,恰好都不在我要去的那个地方。
我把微信生态里跟「机器人」沾边的通道都查过一遍,它们的边界是这样的:
| 通道 | 能进普通微信群吗 | 能被 @ 触发对话吗 |
|---|---|---|
| 个人号的官方 AI 通道(ClawBot / 龙虾) | ❌ 官方明文「禁止被拉入群组」 | ❌ |
| 企业微信「群机器人」(Webhook) | ❌ 只能单向往群里推消息 | ❌ 收不到群里的消息 |
| 企业微信「智能机器人」 | ⚠️ 能进群,但只限企业内部群 | ✅ |
| 企业微信客服 | ❌ 不在群里(是独立的 1v1 窗口) | ❌ |
这些不是我猜的,是官方自己说的:
- 微信开放社区答复:「目前不支持接入普通微信及群聊」
- 企业微信技术支持:「智能机器人目前仅支持内部群添加,互联群、上下游群、外部群暂不支持」
- 而个人号那条唯一的官方 AI 通道(2026 年 3 月推出的 ClawBot 插件,接「龙虾」用的),微信方面对媒体的说法是:「仅支持一对一私聊模式,禁止被拉入群组,其他用户无法添加或查看」
注意最后一条的用词:不是「暂不支持」,是「禁止」。这是设计上就不让,不是还没做。
再补一个:微信自己内置的 AI(「小微」)也一样------实测报道里它只能被动响应指令,无法主动发送群消息。
连腾讯自家的 AI 在群里都没有发言权。
看明白这些,就明白问题在哪了:不是能力不够,是官方在微信群里根本没给机器人留位置。个人号那条通道被定位成「个人私密 AI 助手」;企业微信那条路要企业认证,进的是企业内部群------它服务的对象是企业员工,不是我这群高中同学。
工作消息在微信、家人群在微信、缴费通知在微信、高中同学群当然也在微信。 一个住在 Discord 里的群宠,功能再全,我们这群人也不会为它搬过去。
所以这不是「哪条路能力更强」的比较------接口那几条路,能力确实强(发图、发表情包、引用回复、话题线程、点表情回应,全是官方一等公民,还不用占电脑、不怕锁屏)。但它们进不了我要去的地方。
这里还有个更本质的差别,是我做完才想明白的:
| 官方机器人接口 | 我这套 | |
|---|---|---|
| 它是什么 | 一个挂在群上的服务 | 它就是一个账号 |
| 怎么进群 | 需要被「添加」(而普通群不给添加) | 那个号本来就在群里 |
| 算不算群成员 | 不算,是插件 | 算,就是群友 |
我不是「把一个机器人拉进群」,我是让群里本来就有的一个号,自己会说话了。 这也顺带解释了它为什么能看群历史、能接梗、能「在场」------因为对它来说,那不是一个「被接入的会话」,那就是它的日常。
所以第一道坎不是模型,是通道:先要有人在,才谈得上让它住进去。
我给的是一台「人工操作位」
这里有个关键选择,我要说清楚:我没有去接微信的接口,我给模型的是一个真微信号 + 一块屏幕 + 一双手。
它的每一条消息,都是这么发出去的:
css
① 找到微信窗口 → 切到前台
② 定位输入框 → 点一下
③ 把回复文本写进系统剪贴板
④ Ctrl+V 粘贴
⑤ Ctrl+A → Delete 清空(怕有残留)
⑥ 按回车
⑦ 回读输入框,确认真的清空了才认为发成功
这就是 CV 工程------复制、粘贴。 没有接口,没有协议,就是一个程序在替你点鼠标敲键盘。
完整的能力清单:
| 能力 | 实现方式 |
|---|---|
| 看 | 本地视觉模型(Ollama)读图,理解群友发的截图和表情 |
| 听 | 本地语音转写(faster-whisper)把语音消息变文字 |
| 说 | 剪贴板 + Ctrl+V + 回车 |
| 发图 | 本地图片走剪贴板粘贴,微信把它当图片消息插进去 |
| 读引用 | 解析群友引用消息里的被引原文(只能读,我还不会引用回复) |
通俗说:它不是一个「接了 API 的机器人」,它是坐在电脑前那个用鼠标点微信的人。
为什么非得是这一步
先声明:这几样接口也都做得到。下面这四条不是「接口做不到」,是「在这个没有接口的地方,我硬把它们做出来了」:
- 能看见图(不只是文字)------接口路线一行代码传图;我这边是本地视觉模型认图再拼进 prompt
- 能听见语音(群里很多人懒得打字)------接口路线收到就是文件;我这边得从本地库里把 SILK 挖出来解码再转写
- 能发表情包(这是群聊的通用语)------Telegram 有
sendSticker;我这边得先攒一个图池、再教模型在什么时候配哪张 - 发出去的那条消息,在群友眼里没有任何可分辨的机械特征------接口路线会挂个标签(Discord 名字旁一个「APP」、Telegram 是
xxx_bot加机器人标记),而这一条才是我真正换来的东西
⚠️ 说「和真人完全一样」就是吹牛了。真人能撤回、能翻聊天记录、能在锁屏时也回你------这些它都不行。 真正站得住的只有一句:在那条消息被看到的瞬间,看不出是机器发的。
顺带一句公道话:那个标签不是缺陷,是人家有意做的信息披露。 我这条路能把它绕掉,靠的不是技术更强,而是微信本来就没给这个场景留口子 。 我知道这话说出来不好听------绕开标签本身就是这件事里我最不确定该不该做的地方。 我能给的交代只有两条:只在自己的小号和熟人小群里跑,以及一旦收到平台警告就立刻停机。
代价也很实在------占一台电脑的桌面,而且有一堆「物理」约束(下面细说)。
这也是为什么光把它跑通就拖了三天: 它不是一个纯软件问题,它是一个「如何让程序伪装成一个坐在电脑前的人」的问题。
而且这条路很脆。窗口被最小化、被别的窗口盖住、输入框位置变了------都可能失败。所以流程里才有那么多「回读校验」:发完要确认,失败要重试,不然回复就悄悄丢了。
几个只有这条路才会踩的坑
① 微信窗口不能最大化。
因为它算的是相对坐标------按窗口尺寸的比例去定位输入框。窗口最大化到 2590px 宽时,比例算出来的位置就偏了,点不到输入框,然后卡满 180 秒超时。
把窗口调成 1100×759 左右,同样的操作 3 秒完成。
② 它会抢你的剪贴板。
因为是「复制→粘贴」,它发消息的瞬间,你剪贴板里的内容会被替换掉。
③ 窗口必须在前台。
被别的窗口盖住就点不到。所以它干活的时候,那台机器基本不能干别的。
④ 锁屏就全废。
屏幕一锁,GUI 操作没有目标,什么都做不了。
这四条加起来说明一件事:这不是一个「跑在服务器上的服务」,它是一个需要有人在场的东西。 后来我也接受了这件事------它本来就是个「群友」,不是「服务」。
为什么是「群」,不是「私聊」
这一条是我做了之后才想明白的:群聊比私聊好养太多了。
一开始我以为私聊更简单------一对一,不用抢话、不用管配额、没有围观压力。实际上完全相反。
私聊最大的问题:找不到话题。
群里不需要找话题。群里的话题是现成的------有人在吐槽课表、有人在发午饭照片、有人在喊开黑。它只要接得住就行。
私聊呢?两个人干瞪眼。你先说什么?「在干嘛」?那是最像客服的开场白。
| 维度 | 群聊 | 私聊 |
|---|---|---|
| 话题从哪来 | 现成的,别人抛的 | 得凭空造 |
| 不说话会不会尴尬 | 不会,群里本来就吵 | 会,对着屏幕沉默很怪 |
| 回错了 | 有人跟着起哄,气氛还在 | 一对一冷场,就结束了 |
| 节奏 | 短句连发,能插得上 | 你来我往,像面试 |
| 错了的代价 | 大家当梗玩 | 对方可能觉得「这什么鬼」 |
真实数据也印证了这一点------跑五天:
| 它说过的条数 | |
|---|---|
| 群聊 | 229 条 |
| 私聊 | 9 条(其中 7 条还是刚加好友那天) |
差了 25 倍。
而且那 9 条里,绝大多数还是对方先开口它才回的。它主动在私聊里起话题的情况------几乎为零。
所以结论是:这东西天然属于群。 群给了它一个「可以在场但不说话」的位置,而私聊没有这个位置------ 一对一里,不说话就是一种失礼。
这也是为什么文章开头那句「群友不想要助手,他们想要一个能接梗的活物」------ 接梗这件事,需要有个梗。而梗是群里长出来的。
环境逼出来的四个设计
因为走的是这条路,后面所有的设计都被迫换了思路:
| 有开放接口的世界 | 它所在的世界 |
|---|---|
| 事件来了就处理 | 得主动去轮询,还得自己去重 |
| 消息带完整上下文 | 只有裸文本,上下文要自己拼 |
| 发消息是幂等的 | 发出去就收不回,发错就是真错 |
| 有 users API | 谁是谁都得自己认 |
| 有 rate limit 头 | 没有任何反馈,只能靠延迟自己控制节奏 |
「谁是谁都得自己认」这条,直接决定了我后面一半的设计。
另外它得能看能听
群里聊天有一半不是文字。只会读文字的 AI,在群里等于半个瞎子。
所以它接了两个本地模型:
| 能力 | 用什么 | 成本 |
|---|---|---|
| 看图 | 本机 Ollama 跑视觉模型 | 约 5~15 秒 / 张,占 4GB 显存 |
| 听语音 | 本机 faster-whisper 转写 | 约 0.5 秒 / 条,占 1GB |
为什么坚持本地:群友发的东西什么都可能有------家里的照片、身份证、截图、随手拍的乱七八糟。这些不该出你这台电脑。
而且视觉模型吃显存。它是为了「偶尔看一眼图」常驻着的,所以还做了个闲时自停:一段时间没用就自动卸载,把显存让出来。
这一块带来的最大感受:多模态不是加个功能,是换一种听人说话的方式。 群友发一张「食堂烧鸭饭」的照片配一句「巨好吃」------只读文字你只知道好吃; 能看图,你才知道他在夸自己学校。
回到接话本身
技术上就三步:
- 监听消息
- 拼个 prompt 发给模型
- 把回复发出去
半天就能跑通。然后你会发现------它一开口就像机器人。
具体表现:
| 现象 | 为什么不像人 |
|---|---|
| 秒回 | 真人要看到消息、想一下、打字,最快也有几秒延迟 |
| 一次说一大段 | 群里是短句连发的节奏 |
| 谁说话都接 | 真人不这样,真人有亲疏 |
| 什么都答得上来 | 真人有不知道的事 |
| 每句都带表情 | 真人是偶尔用 |
「不像机器人」的真正难点不是它会不会说话,是它知不知道什么时候不说。
第二部分:五个真实事故
先说结论:我踩的坑,八成不在「怎么说话」,而在「话怎么没发出去」。
先列个清单,方便你对照自己的情况:
| # | 事故 | 一句话 |
|---|---|---|
| 1 | 同一句话被排了两次队 | 看着像并发 bug,其实是跑了两个实例 |
| 2 | 回了一条 86 分钟前的消息 | 回老消息比不回更糟 |
| 3 | 排了 186 秒才发出去 | 好回复在排队中已经过期了 |
| 4 | 好回复被新消息顶掉 | 最隐蔽------你根本不知道丢了什么 |
| 5 | 它不知道自己没说出去 | 最严重------它会反过来说「我不是说过了吗」 |
后面每一节都是真事,带日志、带数字。
事故 1 · 同一句话被排了两次队
现象:群里出现两条一模一样的话。
第一反应 :查去重逻辑。查了,没问题------decideInbound() 之后立刻标记已处理,同进程内第二次进来一定命中「已处理过」,还有测试钉着这个顺序。
真正的解释:同时跑了两个实例。
两个进程各自读同一个数据库、各自入队、各自到点发送。证据是两边队列长度都从 0 变 1------所以每边此后都走「连发保护」不再重复,最终正好两条。这个形状单实例造不出来。
修法:单实例锁。
js
// 判据是「持有者进程还活着」,不是「文件存在」
process.kill(pid, 0)
几个细节值得说:
- 锁文件里存
pid/startedAt/channel,排障时一眼看出是谁占着 - 第二个实例不抢占、不崩溃,安静停下并说清是谁占了锁
- 陈旧锁自动接管:持锁进程已死就覆盖。不能因为一次崩溃就让插件再也起不来------那比重复发送更难查
这一条的反直觉之处:读起来像并发 bug,实际是部署问题。查代码方向就错了。
事故 2 · 回了一条 86 分钟前的消息
现象:群里 18:18 @ 了它,它 19:44 才回。
那 86 分钟里,群里已经聊过「原来是 AI」「大肥鱼是什么鬼」。此时它突然冒一句「在。咋了」------最典型的机器人犯傻。
关键认知:回一条老消息,比不回更糟。
而且这是高频场景------笔记本合盖休眠几小时再唤醒,群里积压几十条,全是过期的。
修法:过期消息闸。
yaml
staleMessageMs: 600000 # 10 分钟,0 = 关闭
细节:
| 细节 | 为什么 |
|---|---|
| 放在「深夜静默」之后、「配额」之前 | 它和「今天回了几条」不是一个维度,但和「现在几点」同族 |
ts 必须真是数字才判 |
Number(null) === 0 是有限数,直接转换会把它当成 1970 年 → 每条都判过期 |
| 返回码 + 人话 | 日志里要能读出「过期了多久」,不能只留一个代号 |
| 阈值 10 分钟而非 2 分钟 | 正常群里隔十几分钟回一句完全正常,不能误杀 |
和「队列满时挤掉最旧的那条」是互补的两层 :那层管排不排得上队 (容量满了挤旧的),这层管这条还值不值得回(时间太久了直接不吭声)。
事故 3 · 排了 186 秒才发出去
现象:日志里出现「排队:186s 后发到」。
原因:几条消息各自占「最小间隔」档期,叠出了三分钟的排队。
而这引出了一个更痛的问题------
事故 4 · 好回复被新消息顶掉
这是最隐蔽的一种损失。
排队本身还好,至少你知道它在等。
真正可怕的是:一条很好的回复,被后来的消息挤掉了,而你完全不知道。
事情是这样的:队列有容量上限。满了之后,新来的消息该怎么办?
旧的做法是「丢掉新的」------理由是「队列里已经排了两条,这条先不要了」。
听起来合理,但结果是荒谬的:
ini
[队列] 1~3 分钟前生成的两条回复,还在等档期
[新来] 有人 @ 它,它想了一句特别好的回复
→ 队列满了 → 这条不错的回复被丢掉
[群里] 它答的是上一轮的话,这一句理都不理
你看不见那条被丢掉的回复有多好。日志里只有一句「队列已满,跳过」。
我后来才想明白:这跟真人的行为完全相反。 真人忙的时候不会这样。他会把过期的想法咽回去,回你最新那句。 因为「答非所问」比「不答」更糟------前者是傻,后者只是没空。
修法:从「丢新的」改成「挤掉最旧的」。
arduino
① 队列满了 → 挤掉最旧的那条(它本来就过时了)
② 把最新这条排进去
③ 例外:如果新来的是"主动插话",而队列里全是"有人叫我"的 → 让它自己让掉
④ 被点名的回复:永远不挤,单独算一个更高的上限
第 ③ 和第 ④ 条其实是同一条原则的两个面:
- 有人叫我 > 我自己想说话
- 被叫了几次就该应几次(真人也是这样)
所以现在队列里的形状是:
css
[队列]
├─ 被点名的回复 ×N ← 优先保住,上限更高
└─ 主动插话 ×M ← 满了先挤自己
事故 5 · 它以为自己说了
这是最严重的一种。
上面这些修完,还有一层更深的坑------我一开始完全没想到。
模型的回复是「生成」出来的,生成完就写进了会话历史。它自己会觉得:
「我刚才说过这句话了。」
但发送是后面一步的事。那条消息可能:
- 队列满了被挤掉
- 排队中过期了
- 发送失败
- 甚至在「等档期」的路上
而会话历史里,它已经「说过」了。
结果就是:下次群里再提这事,它会说「我前面不是说过了吗」------但你根本没看到那条。
这比「没说」糟糕得多:
| 情况 | 群里看到 | 严重度 |
|---|---|---|
| 没回 | 它没吭声 | 正常,真人也会漏看 |
| 以为自己回了 | 它反过来质疑你没看见 | 社交性伤害 |
一个 AI 坚信自己说过一句谁都没见过的话------这不是技术问题,是「人设崩了」的问题。
我目前的观察是:这需要把「说什么」和「发出去」分成两笔账------只有真正发出去的,才算进「它说过的话」。
我在队列被挤掉的地方看到了 evictOneQueued------它会释放档期、写日志、把消息从等待集里摘掉,但没有任何一步去告诉会话层「这条没发」。
所以严格说,这一条我还没有真正解决,只是定位到了。
我把它写出来,是因为它比前面三个事故都更隐蔽: 前三个你至少能从日志里看见,这一个只能靠「它说胡话」反推。
有一点我现在很确定:
模型的世界和群里的世界,必须保持一致。 不一致的地方,它就会开始说胡话------而且说得特别有自信。
顺带一提 :挤掉的时候会写日志(新消息更要紧)。
因为「丢了什么」这件事,必须留下痕迹------否则你永远发现不了自己损失过东西。
第三部分:真正让「不像机器人」的几条设计
事故修完,才是这套东西有意思的地方。核心原则一句话:
限制它说话的能力,比让它会说话的能力重要十倍。
1. 延迟不是 bug,是特征
真人打字要时间。所以回复不是立即发,而是随机延迟(30 秒 ~ 2 分钟)。
但下限必须守------秒回是最强的机器人特征。
这里改过一版:原先是 45 秒~5 分钟,结果实测日志出现「排队 218 秒后发到」,
加上读消息/决策耗时,群里体感就是「问了半天不理人」。
长尾是体验杀手。 所以砍掉上游的长尾,但保留 30 秒下限。
分布形状也有讲究------不是均匀分布,而是截断对数正态。因为真人回复间隔是长尾的:多数很快,偶尔很久。
2. 连发要像人
群里常见的不是「一条完整的话」,是短句连发:
我打算翘了
全勤是什么东西
所以让它能用换行表示「这里断一下」,代码把换行变成真的多发几条,中间隔几秒。
但上限是三条。超过就是刷屏;每条都拆,又是另一种不像人。
3. 判断「信息给全了没有」
这是我觉得最有意思的一条设计。
问题:真人在群里说话,很少一句话说完。常见的是这样:
甲:那个比赛
甲:就是上次说的那个
甲:你报不报
如果 AI 收到第一条就急着回,它会回一个「哪个比赛?」------而答案两秒后就来了。这种抢答,是最典型的机器人特征。
但也不能傻等------等太久,对方会以为你没反应。
所以它要能判断:这条消息是「说完了」还是「还有下半句」。
三层处理
第一层:等他打完字(防抖)
同一个人连续发的消息,会被合并成一批再处理:
| 情况 | 等待 |
|---|---|
| 被 @ 了 | 1.5 秒(他在等你,别拖) |
| 普通聊天 | 6 秒(等他把话说完) |
有人接着发下一条,计时器重置------直到他停下来。
第二层:被并掉的也要记住
这里踩过一个坑,我觉得值得单说:
甲:那个比赛 ← 被并掉
甲:就是上次说的那个 ← 被并掉
甲:你报不报 ← 这条触发了回复
表面上看,「合并」就是把前两条丢掉、只处理最后一条。但那样它就只看到「你报不报」,完全不知道在说哪个比赛。
所以并掉的消息必须补进背景板。原文注释写得很直接:
「等他发完再回」如果变成「只看得到最后一句」------比逐条处理更糟。
第三层:把握时机,但别错过时机
还有种情况:消息本身没问题,但现在不是说话的时候(比如它刚说完一句,群里还没消化)。
这时它不丢弃消息,而是约个时间再说:
节奏未到,92s 后重新考虑
最多顺延两次。而且有个天然上限------如果拖得太久,这条消息自己就过期了(就是前面说的过期闸),不会无限拖着。
顺带一提:它只带 20 条上下文
发送时它的「背景板」不是全部聊天记录,而是最近 20 条。
为什么不是 100 条?两个原因:
- 成本:每条都进 prompt,一个月下来是天文数字
- 重点:20 条足够理解「现在在聊什么」(约群里十几分钟),更早的都过时了
这跟它的「记忆」是两回事:20 条是「现在的语境」,记忆是「长期的人和事」。
(这里从 8 条改成了 20 条:8 条在活跃群里只有一两分钟,模型看不见话题从哪来, 只能对着当前那句孤立回答------这就是「没理解上下文就回答」的直接原因。)
4. 表情包要配额
表情包是群里起哄的东西。所以:
- 每天最多 8 张(一开始只给 2 张,实测一天只出得了 1 张------配额还没用完就被别的限制拦下了,后来才放开)
- 两张间隔至少 15 分钟(原来卡 1 小时,结果一天轮不到几张)
- 只在群聊发,私聊不发表情包
最后一条是踩坑加的:它曾经在一对一私聊里发了张卡通表情,对方直接问「你是不是发错人了」。
5. 「谁是谁」要自己认
这是我前面说的那条------微信群不给 users API,所以每个人是谁,得靠它自己从聊天里读出来。
所以它给每个群友建了一份档案:
json
{
"alias": "群友C",
"displayName": "(真昵称只落本地,不进 prompt)",
"traits": ["爱调戏AI", "嘴很损", "爱拆台"],
"bond": "经常拿鱼设定逗它",
"callsMe": "大肥鱼",
"facts": [...]
}
注意 alias 和 displayName 是两个字段------真实昵称只存在本地文件里,发给模型的一律是「群友C」这种代号。
这不是我事后补的,是一开始就定的:群友的隐私不该因为「技术上方便」就被送进模型。而且顺带解决了一个工程问题------模型不需要知道真名,只需要知道「这是同一个人」。
跑了几天下来,它自己总结出来的东西,比我预想的准:
| 群友 | 它自己写出来的特征 |
|---|---|
| 群友C | 爱调戏AI、嘴很损、爱拆台 |
| 群友E | 爱起哄、爱玩梗、爱发表情包 |
| 群友G | 嘴损、爱怼人、爱管大肥鱼 |
| 群友H | 吃货、爱问吃什么、爱打王者 |
| 群友K | 补作业型学生、爱吐槽、爱玩梗 |
注意第三行那个「爱管大肥鱼」(大肥鱼是它的群昵称)------它把这个人的行为模式读出来了。这不是我写的,是它自己归纳的。
踩过的坑:档案里曾经混进过一个叫「?」的幽灵成员,唯一记录是「某某拍了拍某某」。还有一次,它把自己也记成了群友。
修法是在数据进模型之前就过滤:系统事件(拍了拍/撤回/入群)不收,自己发的也不收,?、我、未知 这类名字不许当昵称存。
这类「谁是真人、谁是噪音」的判断,在任何群里都是必须的,只是微信逼你更早面对它。
6. 记忆要分「特征」和「事实」
给每个群友建档,但字段分两类:
| 类型 | 例子 | 会不会变 |
|---|---|---|
| 特征(traits) | 爱熬夜、嘴很损、学计算机 | 不怎么变 |
| 事实(facts) | 想考深大、家在广州 | 可能变,新说法覆盖旧的 |
关键设计:每条事实必须带证据原话 + 时间 。核验时拿原话去聊天记录里逐字比对,对不上就丢弃。宁缺。
这是防幻觉最有效的一招------不是靠 prompt 说「不要编」,是靠事后逐字核对。
7. 雷区宁可不写
档案里还有个「雷区」字段------什么话题不能碰。
但我给它加了条硬规则:
只认明确信号,禁止泛泛猜测。写错了,会让它在真该关心的时候绕开人。宁可不写。
实测下来,这个字段绝大多数人是空的------符合预期。群里没人流露过雷区,就该是空。
第四部分:给想做同类东西的人
踩过的几个坑,顺手写出来
① 输出额度和超时,必须一起调。
用的推理模型,它会先「想」再「答」,而这两件事共用同一条输出额度。我一开始给总结任务 4096,结果它把额度全花在思考上,正文返回空串------报错看着像模型坏了,其实是被截断。
于是我把额度提到 16K。然后立刻报了超时------因为超时还是给闲聊设的 60 秒,而 16K 额度的思考要几分钟。
改一个参数,必然带出另一个。 这两件事得一起设。

② 日志里「沉默」比「说话」更需要留痕。
这个坑我印象最深。
有天群里有人说「AI 是不是卡了」,我去查------查了半小时,最后只能靠排除法推断。因为最高频的结果(它选择沉默)恰恰没有记录。控制台早滚没了,落盘文件里一个字都没有。
补上之后才看清:它没卡,是它自己决定不说话。
③ 测试写得比源码厚,不丢人。
7000 行源码配了 6500 行测试。听起来过度,但改闸门规则的时候,我是真敢改------因为知道有几百个用例盯着。
哦对,还有一条
真实世界比任何测试用例都脏。 我遇到过:
- 昵称叫「?」的幽灵成员(昵称解析失败)
- 微信系统消息被当成「某人说过的话」
- 消息时间戳是
null,直接被当成 1970 年 → 每条都判过期
这些在写代码的时候一个都想不到,全是被群里真实数据揍出来的。
跑起来之后,数据长这样
放了几天,翻日志统计了一下。
说清楚一个前提:「它说的」这一列是从日志里数出来的,精确到条 ;「群里消息」那一列是我按量级估的------刷得快,没逐条数。所以这两列的精度不一样,看个量级关系就行:
| 日期 | 群里消息(估) | 它说的(精确) |
|---|---|---|
| 第 1 天 | 一百来条 | 7 条 |
| 第 2 天 | 两百多条 | 65 条 |
| 第 3 天 | 约 210 条 | 95 条 |
| 第 4 天 | 一百多条 | 25 条 |
| 第 5 天 | 一百多条 | 46 条 |
它大概只说群里十分之一到三分之一的话------具体比例取决于那天群有多活跃。群越冷清,它说话占比越高;群一旦炸起来,它就退到后面去了。
而这部分,是它认为「该说」的。剩下的它都憋住了------日志里「没说话」的原因,排第一的就是它自己决定沉默(51 次):看完了消息、判断过「这话我接不上或不该接」,主动不出声。排在后面的才是机制层的拦截:去重(27 次「这条已经处理过了」)和防自激(10 次「这是我自己发的」)。
我最满意的不是它说了多少,是它有 51 次明确决定不说。
说回代价和边界
前面全在讲怎么让它像人,这一节讲这条路本身的代价------因为每个看完的人第一个问题都是「这不封号吗」,我干脆自己先说。
这事到底有没有风险
有。我不打算粉饰。
微信的个人号使用规则里,自动化操作不是你被欢迎做的事。我没法拍胸脯说「绝对安全」,只能说我自己的实际边界:
- 只用小号。不是一个正常在用的主号------出事了损失的只是一个号,不是我的社交关系
- 只在熟人小群里跑。十来个人的高中同学群,不对外扩散、不加陌生人、不做群控
- 一有官方提醒就立刻停。收到任何安全提示,先停机,冷却几天,不做「换个工具再试一遍」这种操作------那是加重特征
- 不追求规模和时长。这东西的定位就是「我们这几个人群里的一只宠物」,不是要上线服务谁
跑了几天,没有收到过任何提醒。但这是「目前还没发生」,不是「不会发生」。这个区别我分得清。
技术上它是什么、不是什么
这一点我很在意,因为它直接决定这算不算「外挂」:
| 它不是 | 它是 |
|---|---|
| 不解析、不伪造微信的网络协议 | 一个读写屏幕、点鼠标、按键盘的桌面程序 |
| 不破解客户端、不注入进程 | 用的是最普通的 GUI 自动化 |
| 不改数据库、不伪造登录态(只读) | 操作的是我自己登录着的那个客户端 |
| 不调用任何「微信机器人接口」------微信也没有这东西 | 截图、复制、粘贴、回车------和你自己用鼠标没区别 |
说白了:它就是一个替你坐在电脑前点鼠标的程序。你能用手做的事,它也能做;你不能用手做的事,它一样做不到。它没有得到任何「人以外的权限」------这也是它为什么那么脆(窗口被盖住就瞎了、锁屏就全废)。
一个我必须承认的矛盾
前面我说过:接口路线会挂个「APP/机器人」标签,我这路没标签------当时我说「这是唯一真正换来的东西」。
但那个标签不是缺陷,是人家有意做的信息披露。我能把它绕掉,靠的不是技术更强,是微信本来就没给这个场景留口子。
所以绕过去这件事,我一直有点心虚。我能给的交代只有上面那四条边界,加上一句:如果哪天这个做法在群里造成任何人的不适,我会直接停掉,不会去优化它继续跑。
技术上有意思的事很多,不值得为它让身边的人不舒服。
结语
回到最开始那个目的:我只是想让群里热闹一点。
写完这套东西,回头看我做得最多的事------不是让它会说话,是让它知道什么时候不说。
群里 @ 它,它可以不回; 消息过了 10 分钟,它就不该回; 有人连着发三条,它得等; 谁都没理它,它也不用硬凑上去。
这些全都是「不」的动作。
但就是这些「不」,才让它从「一个会接话的机器人」变成「群里那个有点意思的东西」。
让它像人的,不是它会说什么,是它忍住不说什么。
另外一个我做了才明白的道理:
它需要的是「在场」,不是「服务」。
群聊给了它一个位置------可以听着、可以偶尔插一句、也可以一整天不说话。这个位置是群聊特有的。一对一里没有「在场」这回事,只有「在服务」或者「不在线」。
所以它私下找我聊天的时候,总显得有点干------因为那不是我需要它的场景。
它需要的不是「用户」,是一个热闹的地方。
本文部分内容包含 AI 辅助创作。