Agent 跑到一半断了,任务状态该存在哪里?
聊天记录能保存“说了什么”,却不一定能说明“任务做到哪了”。一个 Agent 需要连续调用工具时,仅靠 messages 数组很难处理刷新页面、进程重启和重复执行。
先把三种标识分开
conversation_id 表示一段会话,run_id 表示一次执行,tool_call_id 表示某一次具体的工具调用。同一段会话可以有多次 run;用户点一次“重试”,也不应悄悄复用一次已经失败的执行记录。
type RunStatus =
| 'queued' | 'running' | 'waiting_approval'
| 'cancelling' | 'cancelled' | 'succeeded' | 'failed';
type ToolCallStatus =
| 'pending' | 'executing' | 'succeeded'
| 'failed' | 'unknown';
interface ToolCall {
id: string;
runId: string;
name: string;
arguments: unknown;
status: ToolCallStatus;
result?: unknown;
}这里特意保留 unknown:远端可能已经执行成功,本地却在写入结果前崩溃。把它直接标成 failed 并重试,可能重复创建记录、发送消息或扣减额度。
先记录调用意图,再执行工具
一轮执行可以拆成四步:保存模型提出的工具调用;将调用置为 executing;执行工具;持久化结果后再进入下一轮。数据库里保存的是可恢复的检查点,不需要把每一段临时文本都作为状态依据。
queued → running → succeeded
├──→ waiting_approval → running
├──→ failed
└──→ cancelling → cancelled状态更新要带前置条件。例如取消接口只能把运行中的任务转为 cancelling,不能把已经 succeeded 的任务重新改成取消中。可以通过带 status 条件的 UPDATE 或版本号实现乐观并发控制。
接管任务需要租约,也需要防止旧执行器写入
执行器领取 run 后定期续约,租约过期才允许另一台执行器接管。但单有过期时间还不够:旧进程卡住后恢复,仍可能继续写入。领取时生成递增的 fencing token,并在写入时校验,才能拒绝旧执行器提交的状态。
这个 token 只能约束你能控制的存储。若外部工具不支持它,仍需要工具侧幂等键或结果核对;不能因此承诺所有外部动作都只执行一次。
取消是请求,不是时光倒流
取消可以阻止下一轮模型调用、终止支持 AbortSignal 的请求,或让执行器在检查点退出。已经提交到外部系统的动作不会自动撤销。前端最好显示“正在停止”,等服务端确认后再显示“已停止”。
这个拆分适合需要多步工具执行、刷新后仍可查看进度的应用。只有一次普通文本回答的小功能,可以先用更简单的请求模型。