
Spock 的群聊调度与回应设计
Spock 是我做的一个常驻群聊 Agent,可以参与聊天,也可以接下任务。群里直接部署过 OpenClaw,当时一个很明显的问题是它经常什么都回,或者在别人聊天时插话。单看回复未必有问题,放在那段对话里却有些突兀。
我希望 Spock 能区分这些情况:有人明确找它,需要回应;几个人正在互相聊天,就不必参与。除了回答内容,什么时候回复、用文字还是表情包,也会影响它待在群里的感觉。
调度器是这部分最有效果的设计。它先判断一条消息是否需要 Spock 处理,再决定要不要唤醒负责回答和执行任务的主模型。
明确寻址与普通群消息
只允许被 @ 时回复,会漏掉直接叫名字、接着上文追问和发图片的情况。但仅凭名字或发言时间判断,又容易认错对话对象。
调试时,群里有人说“其实就是星际迷航来的”,Agent 把它当成了对 Spock 的追问。我纠正过一次:这句实际在回复另一个人。Spock 刚参与过相关话题,也不能据此认定后续消息都在找它。
触发条件因此分成两条路径:
- 明确 @ Spock、直接回复它的消息,以及私聊,由程序直接唤醒主模型。
- 其余群消息交给较便宜的 Copilot(副驾模型),判断是否需要参与。
副驾的输入包括最近五条原文、当前消息批次、Spock 上次发言时间和群聊的简短状态。“刚说过话”只是判断追问的参考,不能单独触发唤醒。“然后呢”这样的短句,也要结合前面的消息判断对象。
未唤醒主模型的消息仍然进入最近消息缓冲,并尝试保存脱敏后的记录。下次判断时,副驾能够看到这段时间发生的聊天,不会只剩上次唤醒时的背景。
副驾只判断是否参与,资料查询、任务执行和回答仍由主模型处理。这样可以减少普通闲聊触发完整执行流程的次数;副驾本身仍有调用成本,实际开销取决于群聊量、唤醒频率和模型选择。
唤醒理由与日志
最早的副驾只返回 wake 或 ignore。我后来要求增加 reason,让主模型知道为什么被唤醒,也方便检查误判。输出包含两个字段:
type WakeDecision = {
action: "wake" | "ignore";
reason: string;
};
程序解析 action,将 reason 与消息一起提供给主模型。普通群消息在输出格式不合法或副驾调用失败时,默认不唤醒。明确 @、私聊和直接回复走前面的规则,不依赖副驾。
日志分别保留副驾的决定与理由,以及主模型的处理结果。Spock 没有回复时,可以区分副驾主动忽略、副驾调用失败、主模型主动忽略和执行异常。发生多余插话时,也能回看它依据什么判断需要参与。

reason 提供的是判断依据,主模型仍需读取消息决定动作。多人同时聊天时,副驾依然可能认错对象,记录理由主要是为了定位和调整这些误判。
主模型的 ignore
主模型被唤醒后,仍然可以选择不回应。我为它增加了 ignore(reason) 工具,只记录忽略理由,不向群里发送消息。明确交给它的任务则不能用 ignore 跳过。
如果模型只在内部生成文字,没有调用发送工具,群里同样看不到输出。这与主动选择不回复的结果相似,运行时却需要分别处理。因此,每个被唤醒的回合都要有明确动作:发送消息、添加表情回应、发送表情包,或调用 ignore。缺少动作时,程序会提示模型完成这一轮。
Typing(正在输入)提示也由非 ignore 工具动作触发。主模型刚开始运行时还不确定是否回复,立即显示提示,可能出现群员看见“正在输入”、最后却没有消息的情况。纯 ignore 回合不显示这个状态。
这些检查能确认一次执行有没有正常结束。模型是否该沉默、是否把明确任务当成闲聊忽略,仍然要结合实际消息检查。
收集和使用群里的表情包
Spock 还会“偷”群里的表情包:收到后保存文件和描述,需要时再检索、发送。群里的梗图不断增加,素材也会随聊天积累,不需要预先准备固定的一套。
配置识图模型后,第一次收到某个资源时识别并保存描述。再次遇到同一个资源标识,直接复用已有结果。需要发送时,先通过 list_stickers 查找已收录的内容,再调用 send_sticker。发送工具检查资源是否确实存在,避免模型编造图片标识。
收集与回复是独立的过程,收到一张表情包可以先保存,不必立即回一张。对同一资源标识的缓存也只负责复用识别结果,不等于不同上传一定能识别为同一张图。
图片还暴露过唤醒判断的问题。副驾最初只看文字,纯图片消息在输入中近似空消息,主模型看到图片之前就可能被忽略。后来把识图前置到唤醒判断之前,描述同时交给副驾和主模型;识图需要等待上限和超时回退,避免阻塞整批消息。
表情包与短回复
表情包加入后,提示词也要允许它作为完整回复。轻松的调侃可以只回一张图,明确的问题和任务仍然需要回答或执行。
一次群聊测试后的反馈是:“对于挑衅和直接的辱骂,spock 以表情包回复,效果很不错。”同一条反馈中,我也记下了回复仍有大段文字和 Markdown 的问题。表情包回应已经有不错的效果,文字长度和措辞还需要调整。
我也问过:“‘我看看’‘在看’这种程度的消息算什么,spock 现在是永远都不会说出这种干练的话吗。”如果提示词一律禁止过程消息,这种表示开始处理的短确认也会被限制。群聊里的确认和正式交付中的过程说明,需要分别考虑。
调度器和表情包让 Spock 有些时候更像群里的一员,但认错多人对话关系、受陈旧状态影响继续跟进旧事、回复过长等问题仍然存在。wake reason、ignore reason 和动作日志为这些问题保留了具体的检查入口。
后记
在编写这篇文章时,TypeSafe 的 Jev 已经发布有一段时间了。它接收上下文和预先定义的问题,直接返回结构化判断,不生成自由文本,适合用在分类、路由等需要程序继续处理结果的地方。
我在其他场景中使用过 Jev,也想试试把它用在 Spock 的调度器里。副驾主要判断是否需要参与对话,后续回答和执行任务仍然交给主模型。从这项工作的形式来看,Jev 是个值得尝试的选择;如果判断的延迟和开销能进一步降低,也更适合频繁到来的群消息。
不过,wake reason 需要重新考虑。副驾给出的一段理由,既能向主模型补充背景,也方便排查误判。Jev 可以通过 Choice(从固定选项中选择) 返回“承接追问”“群员互聊”等理由类别,但无法像自由文本那样解释具体语境。保留自然语言理由就仍然需要能生成文本的模型,调用成本和延迟也会随之变化。
因此,要不要保留 wake reason,也是更换调度模型时的一项取舍。实际效果还需要放到群聊里比较:是否更少误唤醒和漏唤醒,理由简化后又是否仍然方便检查。
评论
评论加载中…