从零构建 Coding Agent
English

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[];
};

每个 tool batch 结束后,内核先检查 steering。若存在 steering,就把它们合并成新的用户消息,并继续当前任务的下一轮。只有当当前任务 stop 后,才取 follow-up 队列启动新任务。

合并 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.

事件与 UI

队列变化也应该是事件:

queue_updated steering=1 followUp=0
steering_applied count=1
follow_up_started id=...

UI 需要告诉用户“已排队,当前工具结束后应用”,而不是沉默。否则用户会重复输入,导致模型收到多条相同指令。

失败模式

最危险的失败是把 steering 直接追加到 messages,同时当前 assistant 仍在流式输出。这样下一轮上下文可能出现半条 assistant、用户插话、再加后续 tool result 的交错序列。很多 provider 对这种顺序不宽容,模型也会困惑。

第二个失败是 follow-up 抢占当前任务。用户说“完成后再更新文档”,结果 Agent 还没修 bug 就去写文档,任务顺序被破坏。Follow-up 的重点是“after”,不是“also now”。

练习

给 Agent 增加 steer(text)followUp(text)

验收标准:

  • 模型流式输出期间调用 steer 不会直接修改正在发送的请求。
  • 当前 tool batch 结束后,steering 会进入下一轮上下文。
  • 当前任务 stop 后,follow-up 才启动。
  • UI 或事件订阅者能看到队列长度变化。
  • 用户 abort 后,steering 和 follow-up 的处理策略明确:保留、清空或询问用户,不能隐式丢失。