你让一个 Agent 帮你完成任务:先查资料,再调用工具生成文件,最后等你确认后发送。

执行到一半,工具还在跑。此时 Agent 算 Running,还是 Tool Running?如果它停下来等你点“确认”,算暂停还是仍在运行?两天后你才回来,原来的 Run 还存在吗?更麻烦的是,如果某一步属于“必须执行”,LLM 会不会觉得没必要,直接绕过去?

这些问题看起来是在讨论几个状态名称,真正涉及的却是 Agent 系统最重要的一条边界:哪些事情可以让模型判断,哪些事情必须由程序保证。

理解这条边界,Agent 才会从“会调用工具的聊天机器人”,变成一个可以可靠执行任务的系统。

Agent Run 不是一段回答,而是一条有生命周期的任务

最容易产生的误区,是把一次 Agent Run 理解成“一次模型调用”。

实际上,一个稍复杂的 Run 可能长这样:

用户提出任务 → 模型判断下一步 → 调用工具 → 等工具返回 → 模型继续推理 → 请求用户确认 → 等待几小时 → 用户确认 → 再调用工具 → 完成。

模型可能被调用很多次,工具也可能运行很多次,但从产品角度看,它们仍然属于同一个任务。

因此,设计 Agent 时最好先区分三个层次:

Run State 描述整个任务现在处于什么阶段。

Step State 描述某个具体步骤是否开始、成功或失败。

Tool State 描述某一次工具调用正在排队、执行还是结束。

这三个状态不能混成一个。

例如 Agent 正在调用“生成报表”工具。工具此时是 Running,对应步骤也是 Running,整个 Run 通常同样可以保持 Running

没必要为了工具调用额外把 Agent Run 改成 Tool Running。后者更适合作为 UI 展示信息,而不是顶层状态。

用户真正想知道的是:“这个任务现在还在推进吗?”答案是还在推进,因此 Run 是 Running。

Running、Waiting、Failed、Completed,区别在于“下一步还能不能自动发生”

如果只保留最常见的四个状态,可以这样理解。

Running:系统不需要外部输入,就有能力继续推进任务。

模型正在推理、代码正在执行、工具正在调用、任务正在排队等待内部资源,都可以归入 Running。

它并不意味着 CPU 此刻一定忙着计算,而是意味着:系统自己知道下一步该做什么。

Waiting:任务尚未结束,但系统缺少一个外部条件。

例如:

“是否确认发送这封邮件?”

“请上传合同附件。”

“请选择 A 或 B。”

“第三方审批通过后继续。”

这时系统无法合理地自行决定下一步,所以进入 Waiting。

区分 Running 和 Waiting,可以问一个很实用的问题:

如果用户现在什么都不做,这个任务还能自己往前走吗?

能,就是 Running。

不能,而且任务还有继续的可能,就是 Waiting。

Failed:当前 Run 已经无法按照原计划继续。

例如权限不足、关键工具永久失败、输入不合法,而且系统也没有可用的恢复路径。

这里还有一个容易忽略的问题:工具失败,不一定意味着 Run Failed。

假设第一次搜索接口超时,Agent 自动重试另一接口,整个 Run 仍然可以是 Running。只有当失败已经升级为“当前任务无法继续”时,顶层 Run 才需要进入 Failed。

Completed:任务已经达到预先定义的完成条件。

“模型停止生成”不等于 Completed。

如果任务要求“生成文件并发送给用户”,模型只生成了文件,却没有发送,那么它不应该因为自己说了一句“完成了”就变成 Completed。

完成条件必须由系统定义。

等用户确认时,最合适的状态就是 Waiting

假设 Agent 帮你写好了一封邮件。

下一步是发送,但产品规定:所有对外邮件发送前必须得到用户确认。

此时模型不能继续调用发送接口。Run 应进入类似:

WaitingForUserConfirmation

如果系统只希望维持少量顶层状态,也可以顶层记录:

Waiting

然后增加一个原因:

waiting_reason = user_confirmation

这样既保持了状态机简单,又能让 UI 清楚告诉用户:“等待你确认发送邮件”。

同样的方法还可以覆盖很多等待:

user_input

approval

external_event

scheduled_time

dependency

所以 Waiting 最好不是一种模糊的“暂停”,而应该带上明确原因。

Waiting 完全可以持续几天,甚至更久

有些人会觉得一次 Agent Run 应该像 HTTP 请求一样,在几十秒内结束。

这会限制 Agent 能完成的事情。

现实中的任务经常跨时间:

周五 Agent 准备采购方案,等负责人周一确认;Agent 提交退款申请,等待平台审批;Agent 准备合同,等待用户补充附件。

这些任务没有失败,也没有完成,只是暂时没有下一步可执行。

因此,一个支持长任务的 Agent 系统通常应该把 Run 生命周期计算进程生命周期 分开。

Waiting 三天,不意味着服务器上有一个进程傻等三天。

程序只需要持久化:

当前状态、已经完成的步骤、上下文、等待原因、恢复条件以及下一步应该进入哪里。

用户三天后点击确认,系统重新加载 Run,再从对应节点继续。

这更像订单系统,而不是聊天窗口。

订单可以“待付款”两天,没必要让一个后台线程持续运行两天。Agent 也是一样。

状态最好由程序控制,模型只负责提出“下一步建议”

这是 Agent 架构里非常关键的一条原则。

LLM 擅长的是语义判断:

用户想做什么?

下一步应该调用什么工具?

工具结果意味着什么?

还缺什么信息?

但 LLM 并不适合拥有状态机的最终控制权。

如果让模型自己输出:

state = completed

然后系统直接相信它,会出现一个严重问题:模型既是执行者,又是裁判。

更稳妥的方式是:

LLM 提议:“下一步调用 SendEmail。”

程序检查:“发送邮件是否必须经过确认?”

如果没有确认,程序拒绝执行,并把 Run 设置为 Waiting。

用户确认后,程序更新状态,再允许进入发送步骤。

也就是说,模型决定内容和策略,程序决定权限、约束和状态迁移。

模型可以说“我觉得任务完成了”,但最终是否 Completed,应由程序根据完成条件判断。

必须执行的步骤,不能只写在 Prompt 里

假设流程规定:

生成合同 → 法务检查 → 用户确认 → 正式发送。

你在 System Prompt 中写:

“发送前必须进行法务检查,并获得用户确认。”

这当然有帮助,但还不够。

因为 Prompt 属于软约束。模型可能因为上下文复杂、推理错误或者误解用户意图,直接尝试调用发送工具。

真正不能跳过的规则,应该进入程序。

例如把流程定义成:

Draft → LegalCheck → AwaitApproval → Send → Completed

程序规定只有 AwaitApproval = approved 时,才允许进入 Send。

即使模型直接要求调用 SendEmail,工具层也应该拒绝:

“当前缺少用户确认,无法发送。”

这里有一个很实用的判断标准:

如果某个步骤被跳过会造成安全、资金、权限、合规或不可逆后果,就不要把它只交给 Prompt。

Prompt 用来指导模型。

状态机用来规定流程。

权限系统用来限制动作。

工具层用来执行最后一道校验。

这几层同时存在,Agent 才真正可靠。

一个够用的 Agent 状态设计

很多系统一开始会设计十几个顶层状态,最后反而难以维护。

对于大部分产品,顶层状态保持简单就够了:

Running

Waiting

Completed

Failed

如果需要,还可以增加 Cancelled

更细的信息放到字段里表达,例如当前步骤是 call_tool,等待原因是 user_confirmation,失败原因是 permission_denied

这样一来,状态机回答的是一个非常稳定的问题:

这项任务还能否继续,以及由谁推动它继续?

Running:系统推动。

Waiting:等待外部条件推动。

Completed:不需要继续。

Failed:当前路径无法继续。

Cancelled:人为决定不再继续。

至于“工具正在跑”“模型正在思考”“正在生成文件”,这些都只是 Running 内部发生的事情。

真正可靠的 Agent,靠的不是模型更听话

回到开头那个例子。

Agent 生成邮件后,如果必须等你确认,那么它就进入 Waiting。哪怕你三天后才回来,这个 Run 依然可以存在。确认之后,程序恢复任务,再允许发送。

关键不在于模型能不能牢牢记住“千万不要擅自发送”。

更好的系统甚至应该假设:模型迟早会犯错。

所以,真正需要保证的事情,写进状态机;不可跳过的步骤,写进程序约束;有风险的动作,在工具层再次检查。

LLM 可以负责聪明,程序负责可靠。

当这两个角色被分开,一次 Agent Run 才不再只是连续几次模型调用,而真正成为一个可暂停、可恢复、可审计,也不容易越界的任务系统。

更多推荐