这个项目最早的想法很直接:把 QQ 和微信消息收进来,让模型整理一份摘要。
Demo 很快能做出来。读一段文本,拼 Prompt,拿回 Markdown。真正接上日常聊天后,问题全变了:私信一页装不下,微信消息散在多个数据库里,模型会超时,也会引用输入里根本不存在的消息。更麻烦的是,程序看起来正常运行,并不能证明消息真的收全了。
我后来花的时间,大多不在 Prompt 上。
采集和摘要分成两个进程
现在的数据流大致是这样:
QQ / 微信数据源 → 采集进程 → SQLite(WAL) ← MCP 查询进程 ← 本地 Agent
采集进程负责标准化和入库,MCP 进程负责查询、分页和组织摘要。模型调用即使卡住,采集也可以继续;MCP 重启后,已经写进 SQLite 的消息和检查点还在。
这里有一条不能放松的约束:消息和下一检查点必须放在同一个事务里提交。
如果先推进检查点,随后写消息失败,这一段会永久漏掉。反过来,消息写成了但检查点没动,恢复后只会重放。消息表有稳定唯一键,重复写入会被忽略,所以重放可以接受,漏消息不行。
微信桌面数据库还可能有多个消息分片。查询时要分别取候选消息,再按 (sort_seq, local_id) 做全局排序和交界去重。只打开第一个数据库最危险的地方在于它通常不会报错,只会安静地少一截内容。
私信要明确告诉调用者“还没拉完”
摘要拆成了两步:
prepare_digest 本地整理私信
prepare_group_analysis 生成群聊分析包
prepare_digest 会返回 direct_complete 和 next_direct_cursor。只要 direct_complete 还是 false,调用者就得继续翻页。第一页看起来已经有不少消息,也不能据此说“今天的私信都总结完了”。
分开两个入口还有隐私上的好处。私信留在本机,由本地 Agent 阅读;只有明确选中的群聊,才会进入配置的外部模型分析链路。这个边界写进调用方式后,比依赖一段“请勿上传私信”的说明可靠得多。
微信数据源需要维护只读解密缓存,同一个工作目录只允许一个采集器占用。第二个实例拿不到锁就直接退出。两个进程同时碰这些中间文件,未必马上报错,反而可能互相覆盖,留下很难复现的损坏。
模型给的内容先当成待校验数据
模型生成的每个摘要条目都要带 evidence_message_ids。本地服务会检查这些 ID 是否属于本批输入。
某条证据不合法,只丢掉或降级对应条目,不牵连同批其他有效结果。整批无法解析时则记为失败,未读游标不动。生成了一段语句通顺的摘要,和成功处理完这一批消息,是两回事。
未读游标也不会在预览时推进。只有摘要成功,并且用户明确选择非预览模式,才会写入新的位置。调试和查看历史范围用的是只读查询,不应该顺手改变下次的未读起点。
长时间范围允许部分完成
全天消息可能横跨很多群。系统会按群数和消息数分批,限制并发、单批等待时间和输出 token。模型因长度上限截断时不自动重试,已经成功的批次也不会因为另一批超时而作废。
返回结果会写清楚成功、部分成功和失败的批次数,还会标出输出截断、证据修正以及实际送入模型的群数。跨批结果在本地精确去重并稳定排序,不再多调一次模型做“大总结”。少一次调用,也少一个把已有结果全部卡住的地方。相似但写法不同的话题可能保留成两条,这个代价目前可以接受。
日志只记耗时、批次数和输入字节量,不记群名、正文、证据值或密钥。异常在写日志和返回 MCP 之前统一脱敏,因为泄漏往往来自第三方库的异常对象,不一定是业务代码主动打印。
现在这个 Agent 仍会遇到数据源离线和模型超时,只是失败的位置能看见,也能从已经提交的检查点继续。对聊天摘要来说,这比偶尔生成一份很漂亮、却说不清漏了多少消息的报告更有用。