Run、Turn、Step 与运行状态
学习目标
- 用 Run、Turn、Step 三个层次描述 Agent 的时间边界。
- 设计能支持观察、恢复和诊断的运行状态,而不是只保留最后一段文本。
- 理解不同框架对这些词的命名可能不同,源码阅读时如何找到对应生命周期。
前置知识
- 已阅读本章前四篇,理解 Goal、Action、Observation 和 Agent Loop。
- 已了解 State 与 Context 的区别;本文把它们放进一次运行的生命周期中观察。
三层时间边界
Run
Run 是从接受一个 Goal 到交付最终结果、失败或取消的完整运行实例。它应该有稳定的 run_id、开始时间、当前状态和最终结束原因。
一次用户请求通常对应一个 Run,但恢复操作可能继续原 Run,也可能创建带有父 ID 的新 Run。具体策略要由系统定义,不能让重试代码无意中生成无法关联的多个运行。
Turn
Turn 是一次“根据当前 Context 请求 Model 做决定”的交互周期。一个 Turn 可能得到 Final Answer,也可能得到一个或多个 Action。若动作执行后需要重新请求 Model,通常进入下一个 Turn。
不同框架可能把用户消息到最终回答称为一个 Turn,也可能把每次模型调用都称为 Turn。因此源码阅读时不要只依赖名称,应检查 Turn 的开始事件、模型调用次数和结束条件。
Step
Step 是一次可单独记录的状态转移或执行动作,例如解析一次模型结果、执行一次 Tool、写入一次 Observation。一个 Turn 可以包含多个 Step;一个 Run 可以包含多个 Turn。
Step 应有 step_id、输入摘要、输出摘要和结果状态。把整轮工作压成一条“处理中”日志,会丢失最有价值的失败位置和副作用边界。
嵌套关系
常见的心智模型是:
Run
→ Turn(一次模型决策周期)
→ Step(解析、执行、回填等可追踪动作)
但这不是所有框架的固定协议。某些工具调用会被视为独立 Turn,某些框架把模型调用和工具执行都记为 Step。稳定的判断依据是父子 ID、生命周期事件和状态更新,而不是字段名称。
阅读提示:Run 是生命周期容器;RunState 保存可恢复事实,RunStatus 表示当前阶段,StopReason 只在结束或暂停时解释原因。Turn 是模型决策周期,Step 是可诊断的状态转移;具体框架可调整分组,但父子 ID 和生命周期事件不能丢。
运行状态的核心字段
RunStatus
RunStatus 表示当前生命周期阶段。一个最小集合可以是 queued、running、waiting、completed、failed、cancelled。
- queued:已接受但尚未开始执行。
- running:正在进行模型调用、动作校验或工具执行。
- waiting:等待用户审批、外部回调或恢复信号。
- completed:成功满足目标并交付结果。
- failed:无法按当前策略继续。
- cancelled:调用者主动请求停止,或系统执行取消策略。
状态转换应由 Runtime 统一控制。不要允许任意工具直接把 Run 从 running 改成 completed;完成必须经过 Success Criteria 检查。
RunState
RunState 是运行时持有的可恢复事实集合。它通常包含 Goal、当前状态、Turn/Step 计数、最近 Observation、待处理 Action、错误摘要和最终结果引用。
RunState 不必把全部模型输入原样保存,但要足够让系统解释当前处于哪一步、为什么继续或停止。敏感数据应按权限和保留策略存储。
StopReason
StopReason 是结束或暂停的结构化原因,例如 goal_reached、max_steps、timeout、tool_error、awaiting_approval。它不是给用户显示的完整文案,而是程序、日志和监控共享的稳定分类。
多个原因同时发生时,应规定优先级,例如用户取消优先于普通工具失败;不要在异常处理的不同层随意覆盖原始原因。第 06 篇会详细讨论停止条件和失败边界。
Run ID 与父子关系
run_id、turn_id 和 step_id 让日志、事件和结果可以关联。重试、转交或恢复操作应保留 parent_run_id 或 attempt 信息,否则同一个目标的多个执行可能被误认为互不相关。
一个可追踪的运行状态
下面的代码用本地列表记录一个 Run 的 Turn 和 Step,展示状态如何随着“读取状态 → 生成报告”推进;它不调用模型,也不依赖网络。
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
class RunStatus(str, Enum):
QUEUED = "queued"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
@dataclass
class Step:
step_id: int
name: str
status: str
output: str = ""
@dataclass
class Turn:
turn_id: int
steps: list[Step] = field(default_factory=list)
@dataclass
class RunState:
run_id: str
goal: str
status: RunStatus = RunStatus.QUEUED
turns: list[Turn] = field(default_factory=list)
stop_reason: str | None = None
def execute_run(state: RunState) -> str:
state.status = RunStatus.RUNNING
turn = Turn(turn_id=1)
state.turns.append(turn)
turn.steps.append(Step(1, "read_status", "running"))
turn.steps[-1].output = "green"
turn.steps[-1].status = "completed"
turn.steps.append(Step(2, "make_report", "running"))
turn.steps[-1].output = "report: build=green"
turn.steps[-1].status = "completed"
# 状态变化:只有目标结果已产生,Run 才进入 completed。
state.status = RunStatus.COMPLETED
state.stop_reason = "goal_reached"
return turn.steps[-1].output
state = RunState(run_id="run-001", goal="生成 build 报告")
print(execute_run(state))
print(state.status.value, state.stop_reason)
print([(step.name, step.status) for step in state.turns[0].steps])
# 输出:report: build=green
# 输出:completed goal_reached
# 输出:[('read_status', 'completed'), ('make_report', 'completed')]示例把 Step 作为最小诊断单元,把 Turn 作为一组相关 Step,把 Run 作为最终状态所有者。真实运行中每个 Step 还应记录开始和结束时间、错误摘要及必要的输入/输出引用。
状态更新的所有权
Runtime 应是 RunStatus 的主要写入者;Tool 只返回结果,Model 只产生候选决定。若工具直接把状态改成 completed,后续的成功条件、审计和取消都可能被绕过。
状态与事件的关系
State 表示当前事实,事件表示事实如何变化。事件可以追加记录 step_started、step_completed 或 status_changed,状态则可以由事件重放或快照恢复。两者不要混成“每条日志都能恢复”的强保证,除非系统真的实现了完整事件持久化。
快照与恢复
长时间运行的 Run 可以周期性保存 State 快照。恢复时要验证快照版本、待执行 Action 和外部副作用是否已经发生;仅恢复内存计数器而不核对工具结果,可能造成重复写入。
源码阅读时还原生命周期
先找 Run 的入口和出口
搜索 run、execute、invoke 或 runner 等入口,确认什么时候创建 run_id,哪些分支写入 completed、failed 或 cancelled。入口名称不重要,开始和结束状态的写入才是边界。
再找 Turn 的循环位置
观察模型调用前后是否递增 turn_id,工具调用是否启动新的 Turn,或者只是当前 Turn 内的 Step。把实际事件顺序画出来,能避免被抽象名误导。
最后找 Step 的持久化和错误传播
检查每个工具和模型调用是否有开始、成功、失败状态,以及异常如何回到 Run。若某个子任务失败后父 Run 仍显示 completed,应继续追踪父子状态合并逻辑。
与主线项目的对应关系
smolagents
smolagents 的一次 agent 执行可以视为 Run,模型推理和工具执行组成若干 Step。具体是否暴露 Turn/Step 对象取决于版本;阅读时用执行日志和循环变量重建层次。
OpenAI Agents SDK
OpenAI Agents SDK 的 Runner 负责一次 Run 的生命周期,工具调用、转交和 guardrail 结果可能表现为不同的事件或步骤。不要把流式事件名称直接当成统一的 Turn 标准,先确认父 ID 和结束状态。
LangGraph
LangGraph 的一次图执行对应 Run,节点执行和边跳转可以对应 Step;图中一次模型节点及其后续工具节点是否算同一 Turn,由上层应用的事件分组决定。
DeepSeek Harness
DeepSeek Harness 的 Driver、Session 和事件系统可能为 Run 之外增加会话或任务层。阅读时要把 Session 的长期边界与当前 Run 的一次执行分开,避免把会话历史误当成当前状态。
易混点
- Run 不等于一次模型调用:Run 可以包含多个 Turn、工具执行和等待状态。
- Turn 不是固定协议术语:不同框架对 Turn 的分组不同,必须查看实际生命周期。
- Step 不只是“模型思考一步”:解析、工具执行和状态写入都可以是可追踪 Step。
- Status 不等于 StopReason:Status 说明当前阶段,StopReason 说明为何结束或暂停。
- 快照不等于幂等恢复:恢复前仍需确认外部副作用和待执行动作的真实情况。
课后小问
一个 Run 显示 completed,但最后一个工具步骤失败,最可能缺少哪类边界?
答案:缺少由 Runtime 统一执行的结果校验和父子状态合并。
解析:工具失败只能先结束该 Step;只有成功条件成立,父 Run 才能进入 completed。否则应进入 failed 或等待修复。
为什么不能用最后一条日志推断 Run 的当前状态?
答案:日志可能缺少父子关系、覆盖顺序或关键失败事件,且不一定是可恢复的事实来源。
解析:State 由明确所有者更新,事件用于解释变化;可靠系统应定义快照或事件重放契约,而不是依赖文本日志猜测。
本节小结
- Run 是完整运行,Turn 是模型决策周期,Step 是可独立追踪的状态转移或动作。
- RunStatus、RunState 和 StopReason 共同描述运行当前阶段、可恢复事实和结束原因。
- Runtime 应统一拥有父 Run 的状态写入权,工具和模型只提供结果或候选。
- 源码阅读时以生命周期事件和父子 ID 为准,不要假设术语跨框架完全一致。
快速回顾
- 能把一次执行拆成 Run → Turn → Step,并说明每层的开始和结束。
- 能区分 queued、running、waiting、completed、failed、cancelled 的语义。
- 能解释 State、事件、快照和恢复之间的关系。
- 下一篇阅读 停止条件、超时与失败边界,把“何时结束”变成明确的运行契约。
阿源的知识库