ADR 0038:连续输入合流——高热群与弹幕
状态:Proposed
关联:#1029 A 部分(B 部分「动态群管工作流」另起 ADR);建立在 #725 会话准入(TurnAdmissionGate)、ADR 0036 审计与硬闸、ADR 0037 群运营闭环之上。#1029 的前置条件(0037 切片 B / C / E)已合入 main:#1027、#1034、#1031。
背景
现在一个会话同时只跑一个回合,后到的消息由 TurnAdmissionGate(packages/im/agent/src/turn/turn-admission-gate.ts)安排:
- 同一个人追加:没回过就停掉重答(
supersede),排着队就并进排队的那条。 - 其他人的消息一律按到达顺序排队,绝不合并进别人的回合——这样回复不会串人,权限也不会继承别人的。
在高热群里,这条规则的代价是:五个人前后脚 @bot,Bot 依次回五条,后几条往往已经过时,刷屏又慢。弹幕平台(B 站直播、抖音)更极端,逐条回复既不可能也不像人。
Fin 2026-10-27 拍板(#1029):
- 多人同时
@bot:一次合并回复,挑重点,并分别@。 - 新消息与在跑的回复:默认不打断;只做发送前过时检查,调过工具 / 走过硬闸的回复照发。
- 范围:高热群聊合流 + 真弹幕 adapter(B 站 / 抖音)都要。
第 1 条与 #725「不合并别人」的不变量直接冲突。本 ADR 的核心就是:合并的是「回几次」,不是「以谁的身份做事」。
决策
0. 总原则
- 收与回拆线:收消息照旧实时入账(会话事件、画像、审计);只有「什么时候、用哪个回合回」被合流改变。
- 权限不合并:合并回合里每个动作都必须能落到某一个具体发送者身上,并按那个人的权限判定;落不到具体人的,按合并内权限最低者判定。
- 主人不被稀释:主人(master)的消息永远单独成回合,不进合并窗口;主人特权既不会借给别人,也不会因为和别人合并而被降级。
- 能单独的照旧单独:指令、取消、审批结算、带媒体 / 引用 / 转发的消息、私聊——行为与现在完全一致。
- 每一步写审计:进窗口、出窗口、丢弃过时回复、照发,均写
ai_decision_events,可按turn_id回放(ADR 0036 §1)。
1. 合并窗口(群聊)
| 参数 | 说明 | 默认 |
|---|---|---|
ai.agent.inboundQueue.merge.enabled | 群聊合流开关 | true(仅群聊;私聊不合流) |
merge.quietMs | 窗口内最后一条消息后安静多久就发车 | 1500 |
merge.maxWaitMs | 窗口从第一条起最多等多久 | 6000 |
merge.maxSenders | 一个合并回合最多容纳几个发送者 | 5 |
- 可进窗口:会唤醒模型的群消息(
@bot、引用 Bot、ambient 判定参与)且是纯文本。 - 不进窗口(照旧走 #725):主人消息;指令前缀(
/、#、ai:等trigger.prefixes/ignorePrefixes);取消类叫停;审批卡结算;带媒体 / 引用 / 转发。 - 会话内最多「1 个在跑 + 1 个合并等待位」。在跑期间到达的可合并消息全部进等待位,在跑回合结束后按窗口规则发车;等待位里同一发送者的多条消息按现有 burst 规则合为一条。
- 超过
maxSenders的发送者留在下一个窗口;maxQueuedTurns/ttlMs照旧对等待位生效。
2. 合并回合的身份与权限
合并回合的 principal 是一个集合(MergedPrincipal),不是某一个人:
- 工具可见性:只暴露所有成员都有权限的工具(角色交集)。主人不在集合里(§0.3),所以交集永远不包含主人专属工具。
- 代谁做:任何有副作用的工具调用必须带
onBehalfOf(合并内某条消息的引用,与现有inspect_conversation_reference同口径),运行时按该发送者的权限复核;缺省或引用不在集合里 → 按集合内权限最低者复核,不够即拒绝。 - 硬闸:危险工具仍走 ADR 0036 硬闸。一个合并回合最多 1 张审批卡;同一动作(工具 + 目标 + 参数摘要)在同会话按哈希去重;每群待审上限
toolHardGate.maxPendingPerScene(默认3),超出 fail closed 并写审计。0037 的ops_cooldown照旧在硬闸之前判定。 - 不扩大:合并只会让回合可用的权限变少或不变,不会变多;有回归测试守住这一条。
3. 回复形态
- 合并回合在 turn context 里收到「本回合合并了 N 位发送者」的结构化列表(每条:引用、发送者、文本),并被要求:挑重点合成一次回复,对回应到的每个人分别
@。 - 结构化回复里以 mention 段
@对应的人;没回应到的人不会被假装回应。审计记录「合并 N 人、回应 M 人」及各自的消息引用。 - 回复条数沿用现有结构化回复上限,不因合并而放宽。
4. 发送前过时检查
默认不打断在跑的回合。发送前判定:
- 在跑回合开始后,它回应的发送者中有人又发了新的可合并消息 → 本次回复视为过时:丢弃,不发送,这些人的新旧消息一起进下一个合并回合。
- 例外照发:本回合调用过有副作用的工具,或走过硬闸(无论批准与否)——已经做过的事必须告诉人。
- 防饿死:同一发送者链最多丢弃 1 次;第二次无论如何照发。
- 丢弃与照发都写审计(见 §6)。
5. 弹幕模式
适用于声明了 liveDanmaku 能力的 adapter(B 站直播、抖音等,见 ADR 0035 评论回复契约):
| 参数 | 说明 | 默认 |
|---|---|---|
inboundQueue.live.mode | pick-one(窗口里挑一条回)/ summary(窗口摘要后回一次) | pick-one |
live.windowMs | 弹幕窗口 | 10000 |
live.minIntervalMs | 两次回复的最小间隔 | 15000 |
- 弹幕窗口不受
maxSenders限制;只回一条,@被选中者(平台支持时)。 - 弹幕回合只暴露只读工具,不触发任何危险工具与审批卡。
- 选中依据(被
@、提问、新观众、重复度低)由模型在窗口列表里判断,审计记录候选数与选中项。
6. 审计扩展
不新增 kind,沿用 inbound_wake / model_skip / outbound,新增 reason_code(追加到 DECISION_REASON_CODES):
| 码 | 何时写 |
|---|---|
merge_window_joined | 消息进入合并等待位 |
merge_window_flushed | 窗口发车,payload 记发送者与消息数 |
merge_reply_stale_discarded | 过时回复被丢弃 |
merge_reply_stale_sent | 过时但因工具 / 硬闸或防饿死照发 |
merge_on_behalf_denied | onBehalfOf 复核不通过 |
danmaku_window_picked | 弹幕窗口选中一条 / 生成摘要 |
hard_gate_scene_pending_cap | 每群待审上限命中 |
7. 实现切片(ADR 合入后按序做,每个切片单独 PR)
| 切片 | 内容 |
|---|---|
| A | TurnAdmissionGate 合并等待位、窗口计时、进 / 不进窗口的判定、配置与审计码 |
| B | MergedPrincipal:工具可见性取交集、onBehalfOf 复核、硬闸「每回合 1 卡 / 哈希去重 / 每群上限」 |
| C | 合并 turn context 与回复形态(逐人 @),以及 eval:chat / eval:agent 合流用例 |
| D | 发送前过时检查与防饿死 |
| E | 弹幕模式(依赖 adapter 声明 liveDanmaku),先接 B 站直播 |
A → B → C 有依赖,D 依赖 A,E 可在 A 之后并行。本 ADR 不含代码。
验收标准
- 同一群 6 秒内三个人
@bot提不同问题:Bot 只回一次,分别@三人;审计merge_window_flushed记三人。 - 合并里有普通成员要求踢人:只有该成员本人有权限时才出审批卡;合并内另有管理员也不能让这次请求获得管理员权限。
- 主人与成员同时
@bot:主人单独一个回合,成员进合并窗口。 - 在跑回复期间被回应者追问:无工具调用的回复被丢弃并重答一次;第二次追问时照发;调过工具的回复照发。
- B 站直播弹幕 10 秒内 50 条:只回一条,无任何审批卡;审计可见候选数与选中项。
- 关闭
merge.enabled后行为与现在(#725)逐条一致。
待 Fin 确认的默认值
- 群聊合流默认开启(
merge.enabled: true),私聊不合流。 - 窗口
quietMs 1500/maxWaitMs 6000/maxSenders 5。 - 过时回复每个发送者链最多丢弃 1 次。
- 每群待审卡上限
3。 - 弹幕默认
pick-one、窗口 10 秒、最小间隔 15 秒。
非目标
- 打断在跑回合(
steer/ 双发取消在跑):本期不做,只做发送前过时检查。 - 跨会话合流;私聊合流。
- 合并回合继承任何人的高权限;主人与他人合并。
- 弹幕回合里的危险动作(禁言、拉黑等走评论审核 / 动态群管工作流,见 #1029 B 与 #971)。
- Console 合流可视化。
结果
- 高热群里 Bot 回得少、回得准、不刷屏,被问到的人都被点名回应。
- #725 的「不串权限」不变量在合并之后依然成立:合并只减权限不加权限,每个副作用落到具体的人。
- 弹幕场景第一次有了像真人主播一样「挑一条回」的形态。