数据截至 (上游 commit 6a6b6383eb6a)
决策层:单步 Agent 循环与动作规划
30 秒导读: 上一章(感知层)把网页压成了 LLM 能读的元素清单和截图。这一章讲决策:
skyvern_v1引擎下,Skyvern 怎么下"一步棋"——把页面事实和目标拼成一段 prompt,调一次 LLM,让它回一批动作,解析成强类型对象,执行,再判定该收工、该重试还是该继续。动作具体怎么落到真实元素留给执行层(第 3 章),CUA 引擎细节留给第 4 章。
1. 这节讲什么(先建直觉)
一次浏览器任务不是一锤子买卖,而是一步一步走:看一眼页面 → 决定这一步做什么 → 做 → 再看一眼 → 再决定……直 到目标达成或放弃。
Skyvern 把"走一步"这件事拆成两个函数,职责分明:
| 函数 | 白话职责 | 位置 |
|---|---|---|
execute_step | step 的生命周期管家:查取消/超时、算步数上限、调用真正干活的那个、根据结果决定收尾还是递归走下一步 | skyvern/forge/agent.py:1719 |
agent_step | 真正下一步棋的人:感知 → 拼 prompt → 调 LLM → 解析动作 → 逐个执行 → 标记 step 状态 | skyvern/forge/agent.py:2500 |
一句话直觉:execute_step 是循环的骨架和守卫,agent_step 是循环体里那颗跳动的心脏。 本章几乎全部篇幅在讲这颗心脏,以及它和 LLM 之间那份"你必须回这种 JSON"的契约。
这一步棋的输入 / 输出很干净:
- 输入: 当前页面的元素树 + 截图(页面事实)、用户的导航目标 / 数据抽取目标、之前几步的动作历史。
- 输出: 一批带元素 ID 的强类型动作(点这个、往那个框输文字、选这个下拉项、或者 COMPLETE/TERMINATE),加上"这一步 step 是完成了、失败了还是要继续"的判定。
2. 顶层全景(一步棋怎么转)
怎么读这张图: 从上到下是一次 agent_step 的时间线;左边一列是它依赖的外部部件。命中 COMPLETE/失败会拐出主线去"善后"。
用户目标 / 抽取目标
│
▼
┌───────────────────────────────────────────────┐
│ execute_step (骨架 + 守卫) │ agent.py:1719
│ · 任务取消 / 工作流取消 / 超时 → 直接收尾 │
│ · 算 max_steps_per_run(默认 10) │
│ · 调 agent_step;拿回 step 状态后决定下一步 │
└───────────────────┬───────────────────────────┘
│ 调用
▼
┌───────────────────────────────────────────────┐
│ agent_step (核心决策管线) agent.py:2500│
│ │
│ ① 感知 build_and_record_step_prompt ───────┼──▶ 感知层(ch01)
│ scrape 页面 + 拼 extract-action prompt │
│ │
│ ② 决策 llm_api_handler(prompt, 截图) ───────┼──▶ LLM
│ 拿回一坨 actions JSON │
│ │
│ ③ 解析 parse_actions(JSON) ──▶ [Action,...] │──▶ parse_actions.py
│ JSON → 强类型、非法动作丢弃 │
│ │
│ ④ 执行 for action: ActionHandler.handle ─────┼──▶ 执行层(ch03)
│ 逐个落到真实元素;失败即停 │
│ │
│ ⑤ 标记 update_step(completed / failed) │
└───────────────────┬───────────────────────────┘
│ step 状态
┌───────────┴────────────┐
▼ ▼
handle_completed_step handle_failed_step
agent.py:7450 agent.py:6931
· 目标达成? → 任务完成 · 达到重试上限? → 任务失败
· TERMINATE? → 任务终止 · 否则 → 新建 retry_index+1 的 step
· 到步数上限? → 失败
· 否则 → 新建下一 step
主线走一遍(高层,不进代码):
execute_step先做一圈守卫检查(任务/工作流是否已取消、超时),没问题才往下。agent_step感知当前页面,把元素树、截图、目标、历史拼成extract-actionprompt。- 把 prompt + 截图丢给 LLM,拿回一段
actionsJSON。 parse_actions把 JSON 逐条变成强类型Action;非法的丢弃。- 逐个执行动作。任一动作硬失败就停下,把这一 step 标
failed。 - 全部成功就标
completed。 - 回到
execute_step,根据 step 是completed还是failed,分别走handle_completed_step/handle_failed_step,决定:任务完成 / 任务失败 / 新建下一 step(然后递归调回execute_step,agent.py:2139和:2156)。
3. execute_step:循环的骨架与守卫
这节讲:为什么"下一步棋"之前要先过一堆检查。
execute_step 本身不做决策,它的价值在"别让 agent 在不该跑的时候跑"。真正的守卫有三类:
① 取消 / 超时 —— 随时可能被叫停。 每进一次 execute_step,它都重新查一遍状态:
- 工作流被取消 / 超时(
agent.py:1760、:1776)→ 把 step 标canceled,任务标对应状态,直接返回。 - 任务本身被取消(
agent.py:1798)→ 同样收尾走人。
这不是一次性检查,而是每一步开头都查——因为一个任务可能跑几十步,用户中途点了停止,必须尽快感知到。
② 步数上限 —— 别无限走下去。 agent.py:1820-1826 按优先级算出这一趟最多走几步:
# 示意,非源码:上限来源的优先级链
max_steps = (
context.max_steps_override # 运行时临时覆盖
or task.max_steps_per_run # 任务级配置
or organization.max_steps_per_run # 组织级配置
or settings.MAX_STEPS_PER_RUN # 兜底默认 = 10
)
真实兜底值是 MAX_STEPS_PER_RUN: int = 10(skyvern/config.py:155)。这个上限不在 execute_step 里直接拦,而是传到后面 handle_completed_step 判"到顶了就把任务标失败"(见 §7)。
③ 特殊短路 —— 纯 GOTO_URL 任务。 如果任务既没导航目标、也没抽取目标、也没完成/终止判据(agent.py:1867),那多半只是个"跳到某 URL"的块,直接 goto 完就把 step 和任务标完成,不惊动 LLM。
守卫过了,才调用心脏(agent.py:1965):
# 示意,非源码:execute_step 里调用 agent_step 的关键一行
step, detailed_output = await self.agent_step(
task, step, browser_state,
engine=engine, # 默认 skyvern_v1
complete_verification=complete_verification,
...
)
拿回结果后按 step 状态分流(agent.py:2055 起):failed → handle_failed_step;completed → handle_completed_step;然后若需要继续,用返回的 next_step 递归调回自己(agent.py:2133-2151 是重试路径,:2156 是正常推进路径)。整个"多步循环"就是这条递归尾调用撑起来的。
4. agent_step:核心决策管线
这是本章的主角。它现在拆成一条主干(agent.py:2500 到 :2721)加三个帮手——动作来源分支 _generate_step_actions(agent.py:3174)、执行循环 _execute_step_actions(agent.py:2722)、收尾 _finalize_step_execution(agent.py:3044)——骨架仍是全景图里那五步:感知 → 决策 → 解析 → 执行 → 标记。下面逐步拆。
4.1 感知 + 拼 prompt(build_and_record_step_prompt)
agent_step 第一件事是拿到"页面事实 + 一段现成的 prompt"(agent.py:2594-2599):
# 示意,非源码
(scraped_page, extract_action_prompt, use_caching, prompt_name) = \
await self.build_and_record_step_prompt(task, step, browser_state, engine)
build_and_record_step_prompt(真身 agent.py:4682)干两件事:
- 抓页面。 按
SCRAPE_TYPE_ORDER = [NORMAL, NORMAL, RELOAD](skyvern/constants.py:118)依次尝试:先普通抓两次,还失败就 reload 再抓(agent.py:4778)。这是一条三级容错的抓取链,一次抓不到不至于整步崩。 - 拼 prompt。 非 CUA 引擎才拼(
agent.py:4842),委托给_build_extract_action_prompt(agent.py:5150)。
_build_extract_action_prompt 会按任务类型选模板(agent.py:5176-5181):
| 任务类型 | 用的模板 | 说明 |
|---|---|---|
general | extract-action(本章主角) | 常规导航/操作任务 |
validation | decisive-criterion-validate | 只判断"判据是否满足" |
action | 先 infer-action-type 推断,再选 single-click/input/upload/select-action | 单动作任务 |
模板选定后,把一堆运行时变量塞进去 render(agent.py:5492 的 load_prompt_with_elements):导航目标、用户资料 payload、当前 URL、数据抽取目标、动作历史、错误码映射、当前时间、以及一串控制"提供哪些动作类型"的开关(多标签页、新规划器动作等)。元素树本身由 scraped_page 提供(可能是精简版 lean tree,agent.py:5290)。
一个巧思:动作历史进 prompt。 变量
action_history(agent.py:5372)把前几步做过什么、结果如何塞回 prompt,并在模板里明确提示"上一步没起作用就换招"(extract-action.j2:70)。这让无状态的 LLM 调用之间有了"记忆",避免在同一个坑里反复横跳。
4.2 决策:调一次 LLM 拿动作 JSON
skyvern_v1(默认引擎,非 CUA、非注入、非纯抽取)走 agent.py:3267 的 else 分支,核心就一次调用(agent.py:3284-3290):
# 示意,非源码
llm_api_handler = LLMAPIHandlerFactory.get_override_llm_api_handler(...)
json_response = await llm_api_handler(
prompt=extract_action_prompt,
prompt_name=prompt_name, # "extract-actions"
step=step,
screenshots=scraped_page.screenshots, # 截图一起喂
system_prompt=task.workflow_system_prompt,
)
拿回的 json_response["actions"] 就是 LLM 给的"这一步该做什么"。注意几个在调 LLM 前后的分叉(都在 agent.py 的这段 if/elif 里),说明不是所有 step 都真的问 LLM:
| 分叉 | 触发条件 | 位置 | 做什么 |
|---|---|---|---|
| 注入动作 | prepare_step_execution 预置了动作(如主动解验证码) | agent.py:3206 | 跳过 LLM,直接用注入的 |
| CUA 引擎 | openai_cua / anthropic_cua / ui_tars / yutori | agent.py:3214-3254 | 走各自的 CUA 生成器(见 ch04) |
| 纯抽取任务 | 有抽取目标、无导航目标 | agent.py:3257 | 直接造一个 ExtractAction(见 §6) |
| PDF 页面 | 抓到 <embed>/iframe 是 PDF | agent.py:3301-3369 | 造 DownloadFileAction 下载,不解析 LLM 动作 |
| 常规 | 以上都不是 | agent.py:3379 | 调 parse_actions 解析 LLM 的 JSON |
4.3 解析:parse_actions 把 JSON 变强类型
见 §5 单独讲。产出一批 Action 对象。
若解析后动作数为 0(agent.py:2633),这一步没事可做,直接标 failed 返回——会触发上层重试。
4.4 执行:逐个动作落地
拿到动作列表后进入执行循环(_execute_step_actions,agent.py:2722;循环本体从 :2793 起)。这里的逻辑很密,但决策层只需理解它的判定规则,具体怎么点/怎么输是执行层(ch03)的事:
- 执行前先建"元素 ID → 动作"的链表(
agent.py:2775-2789),用来处理同一元素被多个动作命中的情况。 - 过滤 WAIT。 如果动作列表里既有 WAIT 又有实际动作,跳过 WAIT(
agent.py:2753-2762)——因为 WAIT 被当成失败信号,会挡住后面的动作。 - 逐个 交给执行层(
agent.py:2901):
# 示意,非源码
results = await ActionHandler.handle_action(
scraped_page=scraped_page, task=task, step=step,
page=current_page, action=action,
)
- 失败即停的判定(
agent.py:2964起)——这是执行循环的核心规则:
最后一个 result 成功?
├─ 是 → 记成功;若 result.skip_remaining_actions → 提前收尾
└─ 否 ┬─ 是 DecisiveAction(COMPLETE/TERMINATE)? → 记警告但不停不重试
├─ result.stop_execution_on_failure == False? → 记警告,继续下一个
├─ 该元素还有后续重复动作节点? → 继续(换个动作试同一元素)
└─ 否则 → 把 step 标 failed,return,触发重试 (agent.py:3034)
一句话:普通动作硬失败 → 整个 step 失败重来;决定性动作或"软失败"不打 断整步。
每个动作执行后还会异步补录制品(截图等,agent.py:2950 的 record_artifacts_after_action),页面级 SCROLL 例外(怕破坏滚动驱动的 JS 状态,agent.py:2933-2941)。
4.5 标记:这一步 step 收尾
全部动作跑完没硬失败,就标 completed(agent.py:3165):
# 示意,非源码
completed_step = await self.update_step(
step=step, status=StepStatus.completed,
output=detailed_agent_step_output.to_agent_step_output(),
)
有一个特例要在标完成前处理:任务同时有导航目标和抽取目标、且这一步已达成目标(agent.py:3148),就顺手补跑一次抽取(create_extract_action + handle_action,agent.py:3159-3160),把数据抓下来再收工。
5. LLM 契约:extract-action.j2 的动作 JSON schema
这节讲最要紧的一份约定:prompt 里逐字告诉 LLM"你必须回成这个形状"。真身在 skyvern/forge/prompts/skyvern/extract-action.j2(模板名常量 EXTRACT_ACTION_TEMPLATE = "extract-action",agent.py:238;调用时的 prompt_name = "extract-actions",agent.py:362)。
模板开头先给 LLM 立规矩(extract-action.j2:3-8):只用给定 ID 的元素、别凭空想象元素、必须输出合法 JSON(不许有注释、尾逗号、多余引号)。然后要求回这样一个对象:
5.1 顶层字段
| 字段 | 类型 | 含义 |
|---|---|---|
user_goal_stage | str | 对"目标是否达成"的推理说明 |
user_goal_achieved | bool | 目标是否已完成 |
action_plan | str | 这一批动作的计划摘要;若目标达成,须在 actions 里放 COMPLETE |
actions | array | 动作数组,每个元素结构见下 |
(user_goal_stage/action_plan 在精简输出模式 slim_output 为 safe/terse 时会被省掉,extract-action.j2:16-19。)
5.2 单个 action 的关键字段(extract-action.j2:21-61)
| 字段 | 用途 |
|---|---|
reasoning | 为什么选这个动作类型、为什么选这个元素(须与具体用户数据无关) |
user_detail_query / user_detail_answer | "把动作意图写成一道 Jeopardy 问答":query 问需要什么信息(不含具体用户数据),answer 才填具体值。这是 Skyvern 把"意图"和"隐私数据"分离的巧招 |
confidence_float | 0.0–1.0 的信心值 |
action_type | 动作类型枚举(见 5.3) |
id | 要操作的元素 ID,必须来自元素清单;无目标动作(WAIT/COMPLETE 等)填 null(extract-action.j2:27) |
text | 仅 INPUT_TEXT 用 |
option | 仅 SELECT_OPTION 用:{label, index, value} |
key / repeat | 仅 KEYPRESS 用(允许 Enter/Tab/Escape/箭头等) |
direction | 仅 SCROLL 用(up/down) |
file_url | UPLOAD_FILE 必填;CLICK 触发上传时也可有 |
click_context | 仅 CLICK:single_option_click 标记"是否唯一选择" |
context | INPUT_TEXT/SELECT_OPTION:字段名、是否必填、是否搜索框、是否日期、日期格式等 |
5.3 action_type 枚举与 COMPLETE / TERMINATE 语义
枚举值随开关动态增删(extract-action.j2:26),基础集合:CLICK / HOVER / INPUT_TEXT / UPLOAD_FILE / SELECT_OPTION / WAIT / SOLVE_CAPTCHA / COMPLETE / TERMINATE / KEYPRESS / SCROLL;开了 mini-goal 改进还会加 PASTE_TEXT(整块填表格);开了新规划器动作还会加 GOTO_URL / RELOAD_PAGE / EXTRACT_INFORMATION,开了多标签页加 NEW_TAB / SWITCH_TAB / CLOSE_PAGE。
两个决定性动作的语义是整份契约的关键(extract-action.j2:26):
COMPLETE:当"完成判据满足"或"用户目标达成"、且(若有抽取目标)能从当前页拿到数据时才返回。模板反复强调"目标没达成绝不返回 COMPLETE"。TERMINATE:判断目标不可能达成时用它带失败终止。一旦返回 TERMINATE,同批其它动作全被忽略。模板提醒"如果等待可能推进目标,就别 TERMINATE"。
为什么这份契约值得逐字读: 它是 Skyvern 全部智能的"最后一公里"——模型再聪明,只要吐的 JSON 不符合这里的字段名和枚举,下游
parse_actions就接不住。模板里那些絮絮叨叨的约束("captcha 令牌 2 分钟过期,所以先填表最后再 SOLVE_CAPTCHA"、extract-action.j2:26)都是踩坑攒出来的。
6. parse_actions:JSON → 强类型 Action
这节讲:LLM 的 JSON 怎么变成程序能安全执行的对象,以及"垃圾进、别让垃圾崩"。
入口是 parse_actions(复数,skyvern/webeye/actions/parse_actions.py:360),它遍历 actions 数组,对每条调 parse_action(单数,parse_actions.py:73),再补上 task_id/step_id/action_order 等归属字段。
核心设计:逐条容错,坏一条不牵连全体。 每条解析都包在 try 里(parse_actions.py:377-421):
| 异常 | 处理 |
|---|---|
UnsupportedActionType | 记 error,跳过这条 |
ValidationError / ValueError | 记 warning,跳过这条 |
其它 Exception | 记 error,跳过这条 |
也就是说 LLM 回了 5 条动作、其中 1 条非法,另外 4 条照样执行。这比"整批作废"鲁棒得多。
parse_action 内部就是一棵大 if/elif 树,按 action_type 造对应类型(parse_actions.py:128 起)。几个关键规整动作值得点名:
- 大小写 + 别名归一(
parse_actions.py:119-127):action_type.upper();老名PRESS_ENTER映射到KEYPRESS;prompt 里对外叫EXTRACT_INFORMATION,内部映射回EXTRACT。 - 非 web 动作强制清空 element_id(
parse_actions.py:130-134):LLM 常给 WAIT/COMPLETE 也塞个元素 ID,这里统一抹掉(SCROLL 例外,它需要容器 ID)。 - KEYPRESS 白名单(
parse_actions.py:245-251):只允许Enter/Tab/Escape/ArrowDown/ArrowUp和单个字符,其余键直接降级成NullAction。 - URL 类动作安全校验(GOTO_URL
parse_actions.py:302-321、NEW_TAB:328-343):验证 URL 合法、且拦截私网/回环/内网主机(is_blocked_host)——因为导航目标可能被页面内容操纵,这是一道注入防线。 - 元素缺失检查(
parse_actions.py:437-448):解析完扫一遍所有 element_id,凡是不在scraped_page.id_to_element_hash里的记 warning(代码注释自嘲"这段可能不需要")。
7. Action 模型族:强类型的动作对象
这节讲:解析产物长什么样、类型层级怎么组织。定义在 skyvern/webeye/actions/actions.py。
根基类 Action(actions.py:131) 是个胖 Pydantic 模型,把所有动作可能用到的字段都摊平在一起(element_id、text、option、file_url、errors、reasoning、intention 等,actions.py:143-213)。子类只是给 action_type 定死默认值、并声明自己真正在乎的字段。
关键的类型分层(决定执行循环怎么对待它):
Action (actions.py:131 一切动作的基类)
├── WebAction (actions.py:289 element_id 必填 —— 要落到具体元素)
│ ├── ClickAction (actions.py:318)
│ ├── InputTextAction (actions.py:332)
│ ├── SelectOptionAction (actions.py:404)
│ ├── UploadFileAction (actions.py:374)
│ ├── CheckboxAction (actions.py:419)
│ └── HoverAction (actions.py:432)
├── DecisiveAction (actions.py:293 带 errors —— "拍板"类)
│ ├── CompleteAction (actions.py:442 verified 字段)
│ └── TerminateAction (actions.py:437 failure_categories)
├── ExtractAction (actions.py:448 抽取,无目标元素)
├── ScrollAction / KeypressAction / WaitAction / SolveCaptchaAction ...
└── GotoUrlAction / NewTabAction / SwitchTabAction / ClosePageAction ...
两条为什么这么分很重要:
WebAction(actions.py:289)把element_id收紧成必填——凡是要"落到某个真实元素"的动作都归它,执行循环里正是靠isinstance(action, WebAction)来建元素链表、查重复元素(agent.py:2781、:2836)。DecisiveAction(actions.py:293)代表"这一步拍板了"——COMPLETE/TERMINATE。执行循环对它特殊对待:即便它"失败"也不打断、不重试(agent.py:2991),因为它的失败本身就是一种结论。
动作类型的总枚举在 skyvern/webeye/actions/action_types.py 的 ActionType(StrEnum),其中 is_web_action() 定义了哪些算 web 动作(CLICK/INPUT_TEXT/UPLOAD_FILE/SELECT_OPTION/CHECKBOX/HOVER 等)。
执行结果用另一族模型 ActionResult(skyvern/webeye/actions/responses.py:8)表达:ActionSuccess(:65)、ActionFailure(:86)、ActionAbort(:110)。关键字段是 success、stop_execution_on_failure、skip_remaining_actions——正是 §4.4 那套"失败即停"判定读的字段。
纯抽取动作的特殊造法: 当任务只有抽取目标时,不走 LLM 动作规划,而是 create_extract_action(agent.py:8057)直接造一个 ExtractAction,reasoning 取自一次"抽取摘要"预取(prefetched_summary_task),confidence_float=1.0。
8. 善后:判定这一步的命运(完成 / 失败 / 继续)
agent_step 只负责标 step 是 completed 还是 failed。"任务级"的命运判定回到 execute_step,分两条路。
8.1 step 完成 → handle_completed_step
真身 agent.py:7450,返回 (是否任务完成, 末步, 下一步) 三元组。判定优先级:
handle_completed_step (agent.py:7450)
│
├─ 需要 complete 验证 且 目标尚未标达成?
│ → 走并行验证 _handle_completed_step_with_parallel_verification (agent.py:6600)
│
├─ step.is_goal_achieved()? (models.py:114)
│ → 任务 completed,抓 extracted_information (agent.py:7516)
│
├─ step.is_terminated()? (models.py:153)
│ → 任务 terminated + 失败分类 (agent.py:7535)
│
├─ 是 ActionBlock 且 step 成功?
│ → 任务 completed(ActionBlock 无 COMPLETE 动作)(agent.py:7574)
│
├─ 工作流运行级步数预算到顶?
│ → 任务 terminated(按 workflow_run 总预算收尾) (agent.py:7591)
│
├─ step.order + 1 >= max_steps_per_run?
│ → 任务 failed(到步数上限) (agent.py:7599)
│
└─ 否则 → 新建 order+1、retry_index=0 的下一 step (agent.py:7648)
complete verification(完成校验)是这里的精华。 光靠 LLM 在动作里回一个 COMPLETE 并不可信——它可能幻觉"我完成了"。所以当开了 complete_verification、且这一步没有决定性动作时(agent.py:3124-3139 先埋 enable_parallel_verification),善后阶段会再单独问一次 LLM"目标到底达成没有":
_handle_completed_step_with_parallel_verification(agent.py:6600)并行做两件事:一边check_user_goal_complete校验目标(agent.py:4316),一边投机性地预抓下一步(_speculate_next_step_plan),赌"没完成"以省一次抓取延迟。check_user_goal_complete内部调complete_verify(agent.py:4143)重新抓页面、再问 LLM。返回CompleteVerifyResult,其三态语义定义在skyvern/webeye/actions/actions.py:69-105(is_complete/is_terminate/is_continue三个 property,分别对应complete/terminate/continue)。- 校验说"达成" → 造一个
verified=True的CompleteAction(agent.py:4352),任务收工;说"该终止" →TerminateAction;说"没完成" → 返回None,用预抓的数据接着走下一步。
is_goal_achieved(models.py:114)本身也有讲究:它只认"最后一个动作是 COMPLETE 或 EXTRACT 且成功";而且带导航目标时,一个中途的 EXTRACT 不能算完成,必须同一步里存在成功的 COMPLETE(models.py:132-141)——防止规划器随手 EXTRACT 就误判整个导航任务完成。
8.2 step 失败 → handle_failed_step 与 retry_index
真身 agent.py:6931,返回"下一 step 或 None":
handle_failed_step (agent.py:6931)
│
├─ 有用户自定义的终止级错误(terminal_user_errors)?
│ → 任务直接 failed,不重试 (agent.py:6942)
│
├─ step.retry_index >= max_retries_per_step? (agent.py:6974)
│ → 任务 failed,总结 失败原因 + 分类
│ (兜底 MAX_RETRIES_PER_STEP = 5, config.py:185)
│
└─ 否则 → 新建同 order、retry_index + 1 的 step (agent.py:7046)
这个 step 被 execute_step 递归执行(agent.py:2139)
关键区分 order 与 retry_index(都在 Step 模型,models.py:58):
| 字段 | 含义 | 谁 +1 |
|---|---|---|
order | 第几步棋(推进) | handle_completed_step 建下一步时 +1(agent.py:7648) |
retry_index | 同一步棋的第几次重试 | handle_failed_step 重试时 +1(agent.py:7046) |
所以"走 10 步"和"某步重试 5 次"是两个独立预算:前者受 max_steps_per_run(默认 10)管,后者受 max_retries_per_step(默认 5)管。一步失败先原地重试,重试到顶才让整个任务失败。
9. 贯穿始终的数据模型
这节把前面反复出现的两个模型收在一处。
Step(skyvern/forge/sdk/models.py:58) 是决策循环的状态单元