景泰拾伍.
首页 / Agent 工程

Agent 跑到一半断了,任务状态该存在哪里?

状态机 / 持久化   ·   整理于 2026-10-30

聊天记录能保存“说了什么”,却不一定能说明“任务做到哪了”。一个 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 的请求,或让执行器在检查点退出。已经提交到外部系统的动作不会自动撤销。前端最好显示“正在停止”,等服务端确认后再显示“已停止”。

这个拆分适合需要多步工具执行、刷新后仍可查看进度的应用。只有一次普通文本回答的小功能,可以先用更简单的请求模型。

← 返回文章列表