07. 中断、插话与后续任务
真实用户不会等 Agent 完成后才说话。他们会在模型输出时补充信息,在工具执行时发现方向错了,或者在当前任务快结束时追加下一件事。Agent 需要区分三类输入:停止、插话和后续任务。把它们都当成新的 user prompt,会破坏运行时状态。
三种语义
停止是取消当前工作。它应该触发 AbortSignal,停止 provider 请求或工具执行,并把 aborted 状态写入日志。
Steering 是“当前方向需要调整”。它不是立刻杀掉所有工具,而是在安全时机插入一条新的用户消息,让下一轮模型看到。典型例子是“别改测试,修实现”。
Follow-up 是“当前任务完成后再做这个”。它应该排队等待当前任务自然结束,然后作为新用户消息启动下一轮。典型例子是“修完后顺便更新 changelog”。
这三种语义不同,UI 文案、队列行为、日志记录也应该不同。
为什么不立刻打断工具批次
假设模型已经发起两个工具:读取文件和运行测试。用户此时 steering:“先别跑测试”。如果你立刻杀掉正在执行的工具,可能留下半条日志、半个进程或未完成的文件写入。更稳妥的策略是:允许当前工具批次到达一致边界,然后把 steering 注入下一轮。
一致边界通常是:
- 当前 assistant 消息已经完整。
- 已经启动的工具都结束或被可控取消。
- tool result 已经写入日志。
- 下一次 model request 尚未开始。
这个时机还有一个额外好处:下一轮模型请求里同时包含工具结果和用户插话。模型看到“测试失败了”加上“先别跑测试”,可以一次性调整计划,而不是在信息不全的状态下被打断。这不是响应最快的策略,但它让恢复、审计和测试更可靠。对 Coding Agent 来说,可恢复性通常比毫秒级插话更重要。
双队列模型
运行时可以维护两个队列:
type QueuedInput = {
id: string;
text: string;
createdAt: string;
};
type AgentQueues = {
steering: QueuedInput[];
followUp: QueuedInput[];
};
loop 在两个固定检查点消费队列:每个 turn 结束后(tool result 已入历史、下一次模型请求前)检查 steering 队列;只有当“没有更多 tool call 且 steering 队列为空”、任务即将自然结束时,才检查 follow-up 队列。若 follow-up 存在,run 不结束,而是以它作为新的用户消息继续。两个检查点的先后关系保证了 follow-up 的“after”语义:只要任务还在推进,follow-up 永远排在后面。
消费策略本身也值得暴露成配置。成熟系统提供两种 drain 模式:“一次取一条”和“一次取全部”。一次取一条让模型按顺序逐个处理用户输入,每条都得到完整的推理回合;一次取全部则节省请求次数,适合用户连续快速输入的场景。默认取一条更稳妥——两条方向相反的插话同时注入时,模型的行为很难预测。
合并 steering 时要保留用户原文和时间。不要把多条用户输入压成一句模糊总结,否则会丢失意图。可以采用这种注入方式:
User provided steering while you were working:
1. Do not edit tests.
2. Keep the public API unchanged.
Continue from the current state and adjust your plan.
还有一个实现细节容易踩坑:如果 prompt() 本身是把用户消息放进队列再启动 loop 的,loop 的第一次队列检查就不能再消费一遍这条消息,否则同一条输入会被处理两次。无论用什么机制(跳过首次轮询、区分初始消息和插话),都要在测试里覆盖这个场景。
事件与 UI
队列变化也应该是事件:
queue_updated steering=1 followUp=0
steering_applied count=1
follow_up_started id=...
UI 需要告诉用户“已排队,当前工具结束后应用”,而不是沉默。否则用户会重复输入,导致模型收到多条相同指令。交互式界面通常在输入框上方显示排队中的消息,机器可读模式(JSON、RPC)则把 steer 和 followUp 作为独立命令暴露——同一个内核队列,多种输入入口。
失败模式
最危险的失败是把 steering 直接追加到 messages,同时当前 assistant 仍在流式输出。这样下一轮上下文可能出现半条 assistant、用户插话、再加后续 tool result 的交错序列。很多 provider 对这种顺序不宽容,模型也会困惑。
第二个失败是 follow-up 抢占当前任务。用户说“完成后再更新文档”,结果 Agent 还没修 bug 就去写文档,任务顺序被破坏。Follow-up 的重点是“after”,不是“also now”。
第三个失败是 abort 时隐式丢弃队列。用户排了三条 follow-up,然后中止当前任务——那三条输入应该保留、清空还是询问?没有正确答案,但必须有明确策略并让用户看到。默默清空的队列会让用户以为任务还会继续。
练习
给 Agent 增加 steer(text) 和 followUp(text)。
验收标准:
- 模型流式输出期间调用
steer不会直接修改正在发送的请求。 - 当前 tool batch 结束后,steering 会进入下一轮上下文,且和 tool result 出现在同一次请求里。
- 当前任务自然收束后,follow-up 才启动,且不触发新的
agent_start(仍属于同一个 run)或有明确的新 run 语义,两者选一并写下理由。 - 连续多次 steer 时,“一次取一条”和“一次取全部”两种模式都可用且行为符合定义。
- UI 或事件订阅者能看到队列长度变化。
- 用户 abort 后,steering 和 follow-up 的处理策略明确:保留、清空或询问用户,不能隐式丢失。