Agent Run 的状态机:别把智能体交给 LLM 自己管
你让一个 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 才不再只是连续几次模型调用,而真正成为一个可暂停、可恢复、可审计,也不容易越界的任务系统。
更多推荐




所有评论(0)