Steer 中途干预与会话事件模型
SRE-Buddy 人机协同:运行期干预与可回放会话设计
Note
SRE-Buddy 的排障轮次可能持续数分钟并涉及大量工具调用。本文描述运行期人机协同机制(Steer)的语义设计与实现:干预以"带标记的用户消息"在下一轮引擎步进前注入,不打断进行中的执行;会话以统一事件流承载实时传输、历史回放、分支与分享,构成可回放会话的基础。
一、干预语义定义
排障轮次运行期间,模型持续调用工具并在目标机执行命令。若允许用户中途输入,首先需要界定该输入的作用边界:
- 不打断进行中的动作:正在执行的命令可能存在副作用,审批流程不得被运行期输入绕过或撕裂;
- 作用于下一轮思考:干预内容在引擎下一次步进前注入,语义等价于一条带标记的用户消息;
- 可审计、可回放:干预本质是用户在特定时间点的意图表达,应作为事件进入会话流。
上述定义将"运行期干预"约束为消息注入问题,而非执行控制问题——干预通道不获得抢断或绕过权限。
二、会话事件模型
会话内一切过程记录为 session.Event:
Event { ID, RoundID, Type, Content, Data, CreatedAt }
ID:会话内唯一事件游标,用于 SSE 断线续传;Type:事件分类标签,前端依类型决定渲染方式;Data:结构化附加数据(工具名、参数、审批状态等)。
事件类型覆盖:对话(user_message、assistant_delta、assistant)、工具链路(tool_review、tool_result)、审批(approval_requested、approval_decision)、轮次生命周期(run_started / paused / done / error / cancel_requested)、执行细节(execution_connecting / started / stdout / stderr / completed / failed / cancelled / lost)及旁路事件(steer_accepted、compact、knowledge_draft)。
持久化:事件以行锁 + 无缝隙递增序号写入,保证顺序一致性。
同构消费:实时 SSE、历史分页回放、分支与分享快照读取同一事件源,差异仅存在于可见性过滤——compact(上下文压缩块)与 reasoning_content(仅供模型回放的推理内容)不下发至客户端。
分页与续传:会话详情接口支持按事件游标向前/向后翻页,并执行整轮补齐(不截断轮次事件);SSE 流采用"先回放、再订阅、按 event_id 去重"模型,10 秒心跳,客户端携带 Last-Event-ID 重连实现无缝隙续传。
三、Steer 机制
Steer 由 SteerableAgent 接口承载,实现要点为入队而非打断:Steer() 将输入写入队列;注入发生在下一轮循环顶部、引擎步进之前,注入形态为 user_message 事件并携带 Data{"steer": true} 标记。
用户发送运行期输入
→ POST /api/sessions/:id/runs/:roundID/steer(body: {"content": "..."})
→ 校验:内容非空;轮次处于活动状态
→ 入队;返回 202;广播 steer_accepted 事件(实时反馈,不落库)
→ 当前动作继续执行
→ 下一轮循环顶部:队列内容以带标记的 user_message 注入
设计要点:
- 注入形态选择:干预以用户消息进入事件流,而非改写上下文或系统提示词。该形态保证干预内容落库、可审计、可回放,前端可按
steer标记渲染,模型将其作为普通用户输入处理,无需特判; - 返回语义:接口返回 202 Accepted——服务端仅保证接收,不保证即时生效(生效于下一轮步进);
steer_accepted事件仅实时广播,持久化主体为随后注入的user_message,避免重复记录; - 拒绝条件:内容为空返回 400;轮次已终止或无活动轮次返回 409;
- 边界处理:若当前轮仅产生文本输出而未调用工具,且队列非空,则继续驱动一次引擎步进,确保干预内容被消费。
前端侧:轮次运行中输入框占位符切换为干预提示文案,发送文本走 Steer 通道而非新建轮次;发送后立即展示乐观的标记气泡,引擎消费后由后续事件推进。轮次停止或会话切换期间该入口禁用。
四、编排主循环与中断恢复
编排主循环按如下结构反复步进:
排空干预队列 → 引擎步进 → assistant 事件 → 工具批次处理
→ (如需)审批等待 → 执行 → 结果回流 → 回到循环顶部
围绕该循环实现三类恢复机制:
中断补偿(#605):服务重启或故障切换后,可能存在"模型已发起工具调用、结果事件未回传"的断点。恢复时对未决调用补写合成消息("工具执行已中断(服务重启或会话重置)"),使模型获得完整闭环的事实,避免对缺失结果进行推断。
取消链路:取消经 run_cancel_requested 事件触发上下文取消,并经执行器级联至目标机(取消超时 1 秒)。前端 SSE 流配置 25 秒看门狗:超过时限未收到任何消息(含心跳)则重建连接,防止界面假死。
上下文压缩:长会话通过引擎层 summarization 控制上下文规模:可用预算耗尽 90% 时触发压缩;预算按"窗口 − 输出预留 − 工具结果预留 − 安全余量"计算,并按模型档位配置(Claude 200k、DeepSeek、Qwen 128k,默认 128k)。压缩保证可回放性:未决工具调用跳过压缩;压缩产物以 compact 事件落库但不实时广播,回放仅加载最新压缩点之后的事件。
五、分支与分享:快照原语
分支与分享复用同一快照原语:快照定义为某事件序号之前不可变的事件序列。
Fork:用户可从任意轮次创建分支会话。实现为单事务内复制截至指定轮次的事件快照;新会话的下一轮次从 roundID + 1 开始;Origin 记录 {type: "fork", source_round_id};源会话的 fork_count 自增。失败的排查路径由此成为可复用起点。
Share:分享对当前最大事件序号创建只读快照,签发 32 字节随机令牌(服务端仅存哈希,7 天 TTL);被分享方仅可读。"保存到我的会话"将快照克隆为新会话,Origin 标记为 share。结合默认脱敏管道,分享产物为可读过程、不含敏感凭据的档案。
六、前端状态规约
前端以单一 stream_reducer 处理实时与历史两类输入:按 event_id 去重,将事件流规约至 UI 状态,以 persisted 标志区分实时与回放路径。工具卡片状态机完全由事件驱动:
tool_review → 审查中(只读标记、风险等级)
approval_requested → 待审批
approval_decision → approved / rejected
execution_* → connecting → running(流式输出)→ completed / failed / cancelled / lost
tool_result → 最终结果
run_done → 未决审批转为 failed
历史回放与实时直播共用同一状态推导逻辑,保证两者界面一致。
七、不变量小结
| 不变量 | 实现 |
|---|---|
| 干预不打断执行 | 队列化,注入点位于下一轮引擎步进前 |
| 干预可审计 | 以带标记的用户消息进入事件流 |
| 中断可自愈 | 未决工具调用补写合成结果消息 |
| 实时与历史一致 | 同一事件模型与同一状态规约 |
| 长会话可控 | 预算触发压缩,未决调用跳过,产物落库 |
| 分支与分享复用 | 快照 = 事件序号边界 |
小结
Steer 的价值在于对干预行为的边界界定:不获取执行控制权,仅作为消息进入既定事件流。该事件流同时承载实时传输、历史回放、分支与分享及中断恢复,使运行期人机协同与既有审计、回放体系保持一致,而非引入旁路机制。
评论
评论组件加载中…