最近在折腾大模型应用,发现市面上的 QQ 机器人大多是简单的"一问一答"套壳,缺乏连贯的记忆和拟真感。作为一个经常熬夜写代码的独立开发者,我突发奇想,想在 QQ 里养一个 AI 机器人。于是我决定:从零手搓了一个具备长期记忆、多模态识图、语音合成以及物理设备联动的 AI Agent。
今天,我将从架构设计、多线程并发控制、多模态路由以及"将 Bug 变成 Feature"的踩坑经历,全方位复盘这个项目。

一、通信基建:基于 AtomicLong 的版本号打断机制
底层采用 NapCatQQ 作为协议端,通过 WebSocket 与 Java 后端建立长连接。这个环节最大的技术难点不在连接,而在 "并发打断" 。
痛点 :真人聊天时,如果你手速很快连发了两句话,机器人往往会针对第一句回一次,针对第二句再回一次,显得极度机械。
解决方案:引入用户维度的版本号控制。每次收到消息,用户版本号自增。异步线程在调用大模型前,必须校验自己的版本号是否最新。
csharp
// 核心逻辑:版本号自增
long myVersion = userVersions.computeIfAbsent(userId, k -> new AtomicLong(0)).incrementAndGet();
// 异步线程处理时校验:如果在思考期间你又发了消息,旧线程直接终止,绝不乱花 Token!
if (userVersions.get(userId).get() != myVersion) return;
这个机制完美实现了"如果你连发消息,她会立刻放弃之前的思考,重新组织语言",体验极其丝滑。
二、多模态双脑接力:如何让大模型"看图说话"?
文本大模型无法直接识别 QQ 里的 CQ 码图片。这里我设计了"双脑接力"架构:
- 识别阶段:正则提取图片 URL,调用视觉模型(如 GLM-4V)。
- 降级防爆:如果图片过大(Base64 超长),会导致 Token 爆炸或被风控拦截。代码里必须先进行下载、压缩转码,并限制在 5MB 以内。
- 人格化:提取视觉模型的客观描述("一碗红色的泡面"),将它和用户问题打包,丢给文本模型(GLM-5.3)去生成傲娇回复。
ini
// 核心伪代码:视觉提取 -> 文本生成
String imageDesc = callVisionModel(imgUrl); // 返回:一碗泡面
String prompt = "【视觉情报】主人发了一张图,内容是:" + imageDesc + "。请结合人设回怼。";
String aiReply = callTextModel(prompt); // 返回:好啊!本小姐在辛苦打扫,你居然背着我偷偷吃泡面?

三、记忆的物理篡改:打破大模型的"精神分裂"
在大模型角色扮演中,最头疼的是"指令不遵循"。比如你让她发张自拍,她文字回复"死也不发"。
为了让她言行一致,我在 Java 层写了极其暴力的物理外挂:既然她嘴硬,代码就帮她发;既然她不承认,代码就篡改她的记忆。
拦截机制:正则表达式匹配"要照片"的意图。一旦命中:
- 强行在末尾追加发图 CQ 码
[img:selfie],让底层网络直接发图。 - 覆写 Redis 上下文 。删除她刚刚"嘴硬"的回复,强行塞入一条
【已发送照片】的新记忆。
dart
if (isRequestingPhoto) aiReply += " [img:selfie]";
// 篡改 Redis 记忆
List<Map<String, String>> history = ...;
history.remove(history.size() - 1); // 删掉她嘴硬的回复
history.add(Map.of("role", "assistant", "content", "(本小姐刚刚发送了一张绝美自拍)"));
redisTemplate.opsForValue().set(redisKey, mapper.writeValueAsString(history));
下次对话,大模型读取到这段被篡改的历史,会坚信自己发了图。这就实现了"身体很诚实,嘴上也服软"的完美人设。

四、突破风控:异常兜底与借口生成
国内大模型的安全审核(风控)非常严格。一旦聊天中出现敏感词,API 会直接抛出 1301 异常。如果直接返回"大脑短路",用户体验会瞬间出戏。
我的做法是:将报错包装成傲娇的借口,并写入短期记忆。API 崩溃时,随机抽一句"切,网线刚才被本小姐踢掉了",存入 Redis。等下次模型恢复,它会看到自己的借口,从而顺理成章地继续往下演。
结语:代码的浪漫与下一步
技术从来不只是冰冷的 0 和 1。当你把 WebSocket、多模态、Redis 拼凑在一起,赋予她性格时,代码原来也可以这么"鲜活"。
不过说实话,目前的这套架构离我心目中的完美还差得很远。随着聊天的深入,瓶颈越来越明显:上下文越来越臃肿导致 Token 燃烧,长对话时她偶尔会忘记自己是个傲娇,也缺乏真正的状态延续(比如心情、好感度)。
但独立开发者的乐趣,就在于不断填坑、不断重构。下一步,我打算对她的底层记忆和状态系统进行一次彻底的升级。
下一篇,我会重点聊聊这次重构里的几个硬核取舍:为什么个人项目最终放弃了向量数据库?在 Java 里怎么用 Redis 原语优雅地解决 State 层的并发与数值防通胀?
至于她还会进化成什么样?我们下篇见分晓。